Compare commits

..

No commits in common. "master" and "feat/godot" have entirely different histories.

138 changed files with 1053 additions and 16077 deletions

3
.gitignore vendored
View file

@ -1,3 +1,2 @@
node_modules
manyfold/usr
manyfold/.env
manyfold/usr/public/webxr/node_modules

3
.gitmodules vendored
View file

@ -0,0 +1,3 @@
[submodule "godot"]
path = godot
url = ../xrforge-godot.git

View file

@ -1,9 +0,0 @@
https://ip:443 {
reverse_proxy localhost:8080
}
https://ip:5566 {
reverse_proxy localhost:6566
}
https://ip:5577 {
reverse_proxy localhost:6577
}

View file

@ -2,30 +2,13 @@
> 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"/>
<img src="manyfold/usr/src/app/public/assets/badges.png" height="35" style="height:35px;display:inline-block"/>
![](https://i.imgur.com/0NA7MMg.png)
<br><br>
![](xrforge.jpg)
* View [index.html](https://coderofsalvation.codeberg.page/xrforge) for the official docs
* Click [here](manyfold/README.md) for backend-installation instructions.
## Try it now
```bash
$ podman run -p 8080:3214 -p 5566:5566 -p 5577:5577 xrforge
```
<center>
<img src="manyfold/usr/src/app/public/assets/xrforge_term.svg" style="border-radius:7px; width:100%; max-width:350px"/>
</center>
Click [here](manyfold/README.md) for backend-installation instructions.
Powered by:
* [NIX](https://nixos.org) for reproducibility and reliability
* [JanusWeb](https://github.com/jbaicoianu/janusweb) for browsing JML / JanusXR immersive web-layer
* [XR Fragments](https://xrfragment.org) for interoperability between XR experiences
* [Manyfold](https://manyfold.app) for curating & collaborating XR experiences
* [Godot](https://godot.org) & [Blender](https://blender.org) for editing XR experiences

1
godot Submodule

@ -0,0 +1 @@
Subproject commit 79e8669ca609782aa6fa9eeed259d9621697f460

3836
index.html

File diff suppressed because one or more lines are too long

View file

@ -16,20 +16,6 @@ $ mkdir mnt config
$ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./mnt:/mnt -v ./config:/config
```
> To scan all (mounted) libraries run this once: `$ docker exec xrforge /manyfold/cli/manyfold.sh scan_libraries`
# ports
By default the following services are running in the docker:
| port | HTTP PATH | info |
|-------|----------------------------------------------|
| 3214 | / | [Manyfold](https://manyfold.app) |
[ 3214 | /view | [janusweb](https://github.com/meetecho/janus-gateway) the XR viewer |
| 5566 | | [janus-server](https://github.com/janusvr/janus-server) for chat + syncing avatar positions |
| 5577 | / | [cors-anywhere](https://github.com/Rob--W/cors-anywhere) for a deep immersive browsing via janusweb |
| *6379 | | Manyfold REDIS-server (* = never expose this port) |
# Build & Run the container-image
> **NOTE**: [nix](https://nixos.org) is used to promote reproducability-over-repeatability
@ -38,10 +24,10 @@ By default the following services are running in the docker:
$ git clone --recurse-submodules --depth 1 https://codeberg.org/coderofsalvation/xrforge.git
$ cd xrforge
$ docker load < $(nix-build nix/docker.nix)
$ docker run $(manyfold/cli/manyfold run) # generates a dockercmd with sane env-flags
$ manyfold/cli/manyfold run
[xrforge] podman detected..starting OCI container
+ /usr/bin/podman run -p 8080:3214 -p 8791:3215 --name xrforge -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l -e DATABASE_ADAPTER=sqlite3 -e SUDO_RUN_UNSAFELY=enabled -e MULTIUSER=enabled -e FEDERATION=enabled -e THEME=vapor -e HOMEPAGE=/models -e FEDERATE_DRIVE_CACHE=5s -v ./xrfragment/assets:/mnt/assets/xrfragment/#1 --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse xrforge
+ /run/current-system/sw/bin/podman run -p 8790:3214 -p 8791:3215 --name xrforge -e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l -e DATABASE_ADAPTER=sqlite3 -e SUDO_RUN_UNSAFELY=enabled -e MULTIUSER=enabled -e FEDERATION=enabled -e THEME=vapor -e HOMEPAGE=/models -e FEDERATE_DRIVE_CACHE=5s -v ./xrfragment/assets:/mnt/assets/xrfragment/#1 --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse xrforge
[xrforge] booting...
[xrforge] applying filesystem overlay
sending incremental file list
@ -50,11 +36,11 @@ sending incremental file list
> NOTE: if you don't want the default XR Fragments asset-library, omit `--recurse-submodules` in the git cmd
To preserve your uploaded models and db, add these docker volume-flags, for example:
To preserve your uploaded models, add the `experiences` as a docker volume-flag, for example:
```
$ mkdir experiences config
$ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/config -v ./experiences:/mnt/experiences
$ mkdir experiences
$ manyfold/cli/manyfold run -v ./experiences:/mnt/experiences
```
@ -62,34 +48,29 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c
| environment variable | default | info |
|-----------------------|--------------|------------------------|
| `APPNAME` | `XRForge` | manyfold instance name |
| `APPNAME` | `manyfold` | manyfold instance name |
| `HOMEPAGE` | `/models` | show '/models' URL as homepage (use `/` for manyfold default) |
| `THEME` | `default` | bootstrap theme |
| 'IMPORT_INSTANCES' | `1` | integrate cherrypicked content from other JanusXR instances (see [manyfold/root/instances](root/instances) |
| `AFRAME_VERSION` | `1.7.0` | AFRAME version |
| `GODOT_VERSION` | `4.4.1-stable`| godot editor version |
| `GODOT_TEMPLATE_ZIP` | `` | godot template zip URL or file (default is empty godot project) |
| `RUNTESTS` | `0` | set to `1` to run XRForge related [/test](test) scripts |
| `DEV` | `` | enable development mode (disables caching, sets `bin/dev` as entrypoint) |
| `NO_PUBLIC_ONLY` | `` | disable public only models |
| `NO_OVERLAYFS` | `` | disable the filesystem overlay mechanism |
| `NO_DEFAULTDB` | `` | disable the default xrforge db (activates manyfold installer) |
| `NO_ASSETS` | `` | disable downloading assets from xrfragment.org repository |
| `NO_DELETEBIGFILES` | `` | disable deleting big files which are older than 5 days and bigger than ($currentyear-2020) MB's |
| `NO_PACKAGEALL` | `` | don't package all experiences every hour to /usr/src/app/public/experiences.zip |
| `RCLONE_REMOTE` | `` | specify **single** rclone remote name (without semicolon) to mount (default: mount all rclone remotes)|
| `UPLOAD_PATH` | `/mnt/experiences`| specify default library where user-files are uploaded (regular dir or mounted rclone path) |
| `FEDERATE_SERVERS` | see info | allowed servers to share remote content with |
| | | `"https://janusxr.org","https://web.janusxr.org", "https://vesta.janusxr.org", "https://xrfragment.org", "https://isvery.ninja", "https://xrhf.isvery.ninja", "https://xrforge.isvery.ninja"` |
| `FEDERATE_DRIVE_HOST` | `http://localhost:8081` | host adress which other hosts can use to access the federate drive |
| `FEDERATE_DRIVE_HOST` | `http://localhost:3215` | host adress which other hosts can use to access the federate drive |
| `FEDERATE_DRIVE_PATH` | `/mnt` | serve path over HTTP (so other instances can add it as a remote). Specify `0` to disable |
| `FEDERATE_DRIVE_PORT` | `8081` | specify port |
| `FEDERATE_DRIVE_PORT` | `3215` | specify default library where user-files are uploaded (regular dir or mounted rclone path) |
| `FEDERATE_DRIVE_USER` | `` | specify HTTP AUTH credentials (`user` e.g.) for restricted sharing |
| `FEDERATE_DRIVE_PW` | `` | specify HTTP AUTH credentials (`pass` e.g.) for restricted sharing |
| `FEDERATE_DRIVE_CACHE`| `1m0s` | specify interval to re-check all models/directories |
| `FEDERATE_DRIVE_KEY` | `` | specify path to TLS PEM private key file (`-v ./key.pem:/key.pem -e FEDERATE_DRIVE_KEY=/key.pem` dockerflag e.g.) |
| `FEDERATE_DRIVE_CERT` | `` | specify path to TLS PEM public key certificate/CA/intermediate file (`-v ./cert.pem:/cert.pem -e FEDERATE_DRIVE_KEY=/cert.pem` dockerflag e.g.) |
| `SERVER_CORS` | `http://localhost:5577`| CORS-ANYWHERE server for JanusWeb |
| `SERVER_JANUS` | `http://localhost:5566`| JANUS presence server for JanusWeb |
> NOTE: if you have nix installed, you can easily try out environment-flags by running: `docker load < $(nix-build nix/docker.nix) && manyfold/cli/manyfold run -e RUNTESTS=1` e.g.
@ -101,40 +82,14 @@ $ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./config:/c
> please modify the password in the settings screen of the webinterface.
It's also possible to enforce a default sqlite3 db via the `-v ./manyfold.sql:/manyfold/manyfold.sql` container flag.
# Filesystem overlay-mechanism
The server-image will boot `manyfold/cli/manyfold.sh boot` and check for directory `/manyfold` (in the container).
When found, it uses the files in there instead (`/manyfold/usr/src/app/public/404.html` instead of `/usr/src/app/public/404.html` e.g.).
# Federation
# Federated libraries
Federation is possible on many levels:
1. Outbound: cors-anywhere
By default [cors-anywhere](https://github.com/Rob--W/cors-anywhere), so XRForge can source remote content from anywhere.
> It is running inside the docker on port 5577, and can be disabled by env-var `SERVER_CORS=''`
1. Inbound: restrict via env-var `FEDERATE_SERVERS`
This way you can only allow trusted servers to use your content (iframes e.g.):
| usecase | env-var value |
|--------------------|---------------|
| every server | `FEDERATE_SERVERS='"*"'` (default) |
| only this instance | `FEDERATE_SERVERS=':self'` |
| trusted servers | `FEDERATE_SERVERS='"https://janusxr.org", https://web.janusxr.org", "https://vesta.janusxr.org", "https://xrfragment.org", "https://isvery.ninja", "https://xrhf.isvery.ninja", "https://xrforge.isvery.ninja"' |
* ActivityPub: each author and experiences can be followed
Next experiences and authors, an '@' address is shown.<br>
You can enter these in mastodon to follow updates.
* Federating network-drives via `FEDERATE_DRIVE_HOST`
Network-drives:
Besides ActivitPub, XRForge allows federating manyfold libraries too, which allows manyfold libraries to:
* be mounted as a network drive on their desktop-machine (3D editor export-to-library)
* scale horizontally across instances:
@ -166,7 +121,7 @@ To enable rclone to mount **readonly** network drives (=remotes), the container
The quickest way is:
1. create directory `./manyfold/root/.config` outside of the container
2. add `-v ./manyfold/root/.config:/root/.config --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse -p 8081:8081 -e FEDERATE_DRIVE_PORT=8081 -e FEDERATE_DRIVE_HOST=http://localhost:8081` to the docker cmd
2. add `-v ./manyfold/root/.config:/root/.config --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse` to the docker cmd
3. now federate XRForge libraries by running `docker exec -it xrforge rclone config create myhttp http url=https://xrforgeinstanceB.com user=myuser pass=$(rclone obscure mypassword)` in a running container
4. profit!
@ -175,9 +130,8 @@ The quickest way is:
* TIP2: use env-var `RCLONE_REMOTE` to mount only one specific remote (in case of a [combined](https://rclone.org/combine/) or [union](https://rclone.org/union/) rclone remote e.g.).
* TIP2: use **alphanumeric** names for rclone remotes (manyfold libraries choke on dot- or other special-characters)
By default environment-flag `FEDERATE_DRIVE_PATH` will share path `/mnt/experiences` as an open web directory.<br>
Make sure that the URL (and credentials if configure) of step 3 are setup properly, so it matches your reverse proxy/ or SSL configuration (via `FEDERATE_DRIVE_CERT` and `FEDERATE_DRIVE_KEY` flags).<br>
For more info see the SSL section below.
By default environment-flag `FEDERATE_DRIVE_PATH` will share path `/mnt/experiences` as an open web directory.
Make sure that the URL (and credentials if configure) of step 3 are setup properly, so it matches your reverse proxy/ or SSL configuration (via `FEDERATE_DRIVE_CERT` and `FEDERATE_DRIVE_KEY` flags)
# Git libraries
@ -230,49 +184,3 @@ For a quick dev-environment run:
$ mkdir /dev
$ manyfold/cli/manyfold.sh run -e DEV=1
```
# JanusXR
By default the xrforge docker will run an opensource [JanusXR](https://janusxr.org) stack in the background:
* [janusweb](https://github.com/meetecho/janus-gateway) the viewer using the above services
* [janus-server](https://github.com/janusvr/janus-server) at port `5566`, for chat + syncing avatar positions
* [cors-anywhere](https://github.com/Rob--W/cors-anywhere) at port `5577`for a hasslefree deep immersive web
* TODO: [janus-gateway](https://github.com/meetecho/janus-gateway) for video/audio
For SSL, you need to configure your reverse proxy as following:
* wss://presence.foo.bar.com => localhost:5566
* https://cors.foo.bar.com > localhost:5577
> This assumes environment-var `FEDERATE_DRIVE_HOST` is set to `https://foo.bar.com` (`presence` / `cors` subdomain is automatically prefixed when running xrforge over https)
# Developing
```
$ podman load < $(nix-build nix/docker.nix) # build xrforge-overlay on manyfold-image
$ $(./manyfold/cli/manyfold.sh run -e DEV=1) # start docker
```
> Profit! now point your browser to 'https://localhost:8080'
### With SSL (for WebXR/JanusXR testing)
> NOTE: WebXR requires SSL, so we need ngrok or [caddy](https://caddyserver.com) to run the docker over SSL:
```
$ $(CADDY=10.171.13.61 ./manyfold/cli/manyfold.sh run -e DEV=1)
```
make sure you have caddy installed before running this as root:
```
# /manyfold/cli/manyfold caddy 10.171.13.61
```
> Profit! now point your browser to `https://<external_ip>`
you can edit/copy files via:
* `podman exec -it xrforge vim /manyfold/cli/manyfold.sh` e.g.
* `podman cp xrforge:/foo .` and vice versa `podman cp foo xrforge:/.` e.g.

View file

@ -1,27 +1,10 @@
#!/bin/sh
oci=$(which podman || which docker)
test -n "$APPNAME" || export APPNAME=XRForge
test -n "$TAGLINE" || export TAGLINE="Publish single-file XR experiences to immersive networks"
test -n "$UPLOAD_PATH" || export UPLOAD_PATH=/mnt/experiences
test -n "$THEME" || export THEME=slate
test -n "$HOMEPAGE" || export HOMEPAGE=/models
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 "$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 "$CADDY" && {
export HTTPS_ONLY=enabled
export SERVER_CORS=https://$CADDY:5577
export SERVER_JANUS=https://$CADDY:5566
}
test -n "$APPNAME" || APPNAME=xrforge
test -n "$UPLOAD_PATH" || UPLOAD_PATH=/mnt/experiences
test -n "$THEME" || THEME=slate
test -n "$HOMEPAGE" || HOMEPAGE=/models
test -n "$GODOT_VERSION" || GODOT_VERSION=4.4.1-stable
db=/config/manyfold.sqlite3
# utility funcs
@ -35,35 +18,22 @@ create_config(){
}
run(){
{
test -x ${oci} || { echo "warning: not running manyfold OCI container [install podman/docker/etc]"; exit; }
${oci} rm -f xrforge
} 1>&2
#-e NO_OVERLAYFS=true \
#-e NO_DEFAULTDB=true \
#-e PUBLIC_HOSTNAME=localhost \
#-e PUBLIC_PORT=80 \
#-e JANUSXR=1 \
ports="-p 8080:3214 -p 8081:8081 -p 5566:5566 -p 5577:5577"
# shift ports with 1000 if caddy is used as SSL reverse proxy
test -n "$CADDY" && {
ports='-p 8080:3214 -p 9081:8081 -p 6566:5566 -p 6577:5577'
}
echo ${oci} run "$@" ${ports} --name xrforge \
test -x ${oci} || { echo "warning: not running manyfold OCI container [install podman/docker/etc]"; exit; }
echocolor "[$APPNAME]" "$(basename ${oci}) detected..starting OCI container"
${oci} rm -f xrforge
#-e NO_OVERLAYFS=true \
#-e NO_DEFAULTDB=true \
#-e PUBLIC_HOSTNAME=localhost \
#-e PUBLIC_PORT=80 \
debug ${oci} run "$@" -p 8790:3214 -p 8791:3215 --name xrforge \
-e SECRET_KEY_BASE=lkjwljlkwejrlkjek34k234l \
-e DATABASE_ADAPTER=sqlite3 \
-e FEDERATE_DRIVE_HOST=http://localhost:8081 \
-e FEDERATE_DRIVE_PORT=8081 \
-e PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME \
-e SERVER_JANUS=$SERVER_JANUS \
-e SERVER_CORS=$SERVER_CORS \
-e FEDERATE_DRIVE_HOST=http://localhost:8791 \
-e SUDO_RUN_UNSAFELY=enabled \
-e MULTIUSER=enabled \
-e FEDERATION=enabled \
-e THEME=$THEME \
-e HOMEPAGE=$HOMEPAGE \
-e UPLOAD_PATH=$UPLOAD_PATH \
-e GODOT_VERSION=4.4.1-stable \
-e FEDERATE_DRIVE_CACHE=5s \
--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
@ -74,14 +44,8 @@ run(){
overlayfs(){
test -d /manyfold || return 0; # nothing to override
echocolor "[$APPNAME]" "applying filesystem overlay"
# compose overlays
cd /
mkdir /usr/src/app/public/view || true
rsync -ravuzi /nix/store/*-janus/janusweb/* /usr/src/app/public/view/.
cd /manyfold
rsync -rvziL * /.
ln -fs /usr/src/app/public/view /mnt/view # /mnt is one big zippable runnable backup
#apply_patches
rsync -rvzi * /.
}
# cron-like function using sleep (./manifold.sh infinite 3600 zip -r /backup.zip /)
@ -106,11 +70,10 @@ hook(){
logger "$ hook $*"
cmd=$1
shift
test -d ~/hook.d/$cmd && {
find -L ~/hook.d/$cmd/ -type f -executable -maxdepth 1 | sort -V | while read hook; do
find -L ~/hook.d/$cmd/ -type f -executable -maxdepth 1 | while read hook; do
logger " |+ hook $hook $*"
{ $hook "$@" 2>&1 || true; } | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
{ $hook "$@" || true; } 2>&1 | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
done
}
}
@ -120,29 +83,10 @@ start_hook_daemon(){
# 86400 secs = 1 day 3600 = 1 hour
$0 infinite 86400 hook daily &
$0 infinite 3600 hook hourly &
# trigger hooks when files change in /mnt
#find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
# echocolor "[$APPNAME]" "listening to inotify events in $dir"
# # scan for '/mnt/experiences/creatorname/#234/ MODIFY foo.glb' e.g.
# # scan for '/mnt/experiences/creatorname/#234/ MOVED_TO foo.glb' e.g.
# inotifywait -r -m $dir | awk '/.*/ { print $0 }; $2 ~ /(CREATE|MODIFY|MOVED_TO|DELETE)/ && $3 ~ /datapackage/ { system("'$0' hook datapackage_"$2" "$1""$3) }' &
#done
## force-trigger processing hooks in /mnt
#find /mnt | grep datapackage | xargs -n1 $0 hook inotify_MODIFY
}
apply_patches(){
echocolor "[$APPNAME]" "applying patches"
for patch_file in /manyfold/patches/*.patch; do
if patch -p1 -N --forward < "$patch_file"; then
echo "✅ Successfully applied **$(basename "$patch_file")**"
else
echo "🛑 Failed to apply **$(basename "$patch_file")**"
echo "Aborting script. Please inspect the failed patch and resolve conflicts."
# Use 'exit' to stop the script on the first failure
exit 1
fi
# trigger hooks when files change in /mnt/experiences
find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
echocolor "[$APPNAME]" "listening to inotify events in $dir"
inotifywait -r -m $dir | awk '$2 ~ /(CREATE|MODIFY|MOVED_TO|DELETE)/ { system("'$0' hook inotify_"$2" "$1""$3) }' &
done
}
@ -173,15 +117,15 @@ set_upload_path(){
echocolor "[$APPNAME]" "configuring upload library"
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
add_lib_to_db $UPLOAD_PATH
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
set_global default_library "replace('--- $id\\n','\\n',char(10))"
sqlite3 $db "UPDATE settings set value = $id where var = 'default_library';"
}
mount_dir(){
find /mnt -type d -mindepth 1 -maxdepth 1 | grep -vE '(janusweb|view)' | while read dir; do
find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
echocolor "[$APPNAME]" "mounting $dir as library"
add_lib_to_db "$dir"
ln -s "$dir" /usr/src/app/public/.
done
}
@ -208,30 +152,15 @@ mount_rclone(){
test -n "$RCLONE_REMOTE" || libraries
}
set_global(){
echocolor "[$APPNAME]" "setting $1 to '$2'"
debug sqlite3 /config/manyfold.sqlite3 'CREATE TABLE IF NOT EXISTS "settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "var" varchar NOT NULL, "value" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);'
debug sqlite3 /config/manyfold.sqlite3 "
INSERT OR REPLACE INTO settings (id, var, value, created_at, updated_at)
VALUES (
(SELECT id FROM settings WHERE var = '$1'),
'$1',
$2,
COALESCE((SELECT created_at FROM settings WHERE var = '$1'), datetime('now')),
datetime('now')
);
"
set_theme(){
test -z "$THEME" && return 0 # nothing to do
echocolor "[$APPNAME]" "setting theme to '$THEME'"
debug sqlite3 /config/manyfold.sqlite3 "UPDATE settings SET value = '"$THEME"' WHERE var == 'theme';"
}
set_admin(){
echocolor "[$APPNAME]" "adding xrforge admin"
read -r -d '' QUERY <<EOF
INSERT INTO users VALUES(1,'xrforge@localhost','\$2a\$12\$u/j8LRzbPiJRHmi1eV/fvOXXiKxN2vBGtNd.Pt28w.wOnq3rnfpzO','2025-07-25 10:52:56.989975','2025-07-25 12:46:27.338917','xrforge','{"models":true,"creators":true,"collections":true,"per_page":12}','{"grid_width":200,"grid_depth":200,"show_grid":true,"enable_pan_zoom":false,"background_colour":"#000000","object_colour":"#ffffff","render_style":"original"}','{"threshold":2,"heatmap":true,"keypair":true,"sorting":"frequency"}','{"missing":"danger","empty":"info","nesting":"warning","inefficient":"info","duplicate":"warning","no_image":"silent","no_3d_model":"silent","non_manifold":"warning","inside_out":"warning","no_license":"silent","no_links":"silent","no_creator":"silent","no_tags":"silent"}','{"hide_presupported_versions":true}',NULL,'2025-07-25 12:46:27.325287',NULL,NULL,0,NULL,NULL,NULL,NULL,'71863vkppj6k',1,1,1);
EOF
debug sqlite3 /config/manyfold.sqlite3 "$QUERY"
debug sqlite3 /config/manyfold.sqlite3 'INSERT INTO users_roles VALUES(1,1);'
#debug sqlite3 /config/manyfold.sqlite3 'INSERT INTO users_roles VALUES(1,4);'
#debug sqlite3 /config/manyfold.sqlite3 "INSERT INTO sqlite_sequence VALUES('users',1);"
set_modelpath(){
echocolor "[$APPNAME]" "enforcing modelpath"
debug sqlite3 /config/manyfold.sqlite3 "UPDATE settings SET value = replace('--- \"{creator}/{modelId}\"\n','\n',char(10)) WHERE var == 'model_path_template';"
}
set_homepage(){
@ -243,26 +172,10 @@ 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
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"
sed -i 's|Model|Experience|g' "$dir"
#sed -i 's|model|experience|g' "$dir"
#sed -i 's|models|experiences|g' "$dir"
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
}
sed -i 's|powered_by_html:.*|powered_by_html: Radically opensource-powered by <a href="https://forgejo.isvery.ninja/coderofsalvation/xrforge">XR Forge</a>, <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a> and <a href="https://nixos.org" target="_blank">NIX</a>|g' /usr/src/app/config/locales/*.yml
federate_servers(){
# allow iframes + other federated servers by adjusting manyfold's strict CSP policies
local servers="$FEDERATE_SERVERS"
test -n "$FEDERATE_DRIVE_HOST" && servers="${servers}, \"$FEDERATE_DRIVE_HOST\""
test -n "$SERVER_CORS" && servers="${servers}, \"$SERVER_CORS\""
test -n "$SERVER_JANUS" && servers="${servers}, \"$SERVER_JANUS\""
sed -i "s|:self,|:self, $servers,|g" /usr/src/app/app/controllers/application_controller.rb
sed -i "s|content_security_policy.connect_src(\*origins)|content_security_policy.connect_src(\*origins);content_security_policy.connect_src( $servers )|g" /usr/src/app/app/controllers/application_controller.rb
sed -i 's|Models|Experiences|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
sed -i 's|Model|Experience|g' /usr/src/app/config/locales/*.yml /usr/src/app/config/locales/*/*.yml
}
start_syslog(){
@ -273,18 +186,10 @@ start_syslog(){
}
scan_libraries(){
sleep 10 # wait for manyfold/redis to boot first
cd /usr/src/app
echocolor "scanning libraries"
BOOT_SCAN=$BOOT_SCAN flock -n /tmp/.scanlibrary bin/manyfold libraries scan
rm /tmp/.scanlibrary
}
scan_experience(){
# don't do this when all libraries are already being scanned
test -f /tmp/.scanlibrary && return 0
id=$1
cd /usr/src/app
rails_query 'Model.find('$id').add_new_files_later(include_all_subfolders:false)'
bin/manyfold libraries scan
}
rails_query(){
@ -298,77 +203,43 @@ force_public(){
infinite 60 rails_query 'Model.find_each { |it| it.grant_permission_to("view", nil) }' &
}
import_assets(){
get_xrfragment_assets(){
test -n "$NO_ASSETS" && return 0 # nothing to do here
if test -n "$FEDERATE_DRIVE_HOST"; then
# mount because rclone does support symlinks outside of the served folder
mkdir /mnt/templates && mount --bind /nix/store/*-xrfragments/xrf/templates /mnt/templates
mkdir /mnt/assets && mount --bind /nix/store/*-xrfragments/xrf/assets /mnt/assets
else
ln -s /nix/store/*-xrfragments/xrf/templates /mnt/templates
ln -s /nix/store/*-xrfragments/xrf/assets /mnt/assets
fi
add_lib_to_db /mnt/assets
add_lib_to_db /mnt/templates
}
janusxr(){
start_server(){
while sleep 2s; do
flock -n "$@"
echocolor "[$1]" "'$2 $3 $4' exited (why?)...restarting"
done
test -d /mnt/asset || {
echocolor "fetching XR Fragments asset & templates"
mkdir -p /mnt/asset/xrfragment /mnt/templates/xrfragment
cd /tmp
timeout 50 wget "https://codeberg.org/coderofsalvation/xrfragment/archive/main.zip"
unzip main.zip
cp -r xrfragment/assets/library /mnt/asset/xrfragment/\#1
cp -r xrfragment/assets/template /mnt/templates/xrfragment/\#2
}
cd /root/corsanywhere
npm install
PORT=${SERVER_CORS/*:/} start_server ~/.run-corsanywhere node server.js &
cd /root
chmod +x janus_server-linux
PORT=${SERVER_CORS/*:/} start_server ~/.run-janus-server ./janus_server-linux &
}
init_database(){
test -f ${db}.xrforgeinit && exit 0 # already inited
sleep 3
test -z "$THEME" || set_global theme "'$THEME'"
set_admin
set_global site_name "'$APPNAME'"
set_global site_tagline "'$TAGLINE'"
set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))"
set_global model_path_template "replace('--- \"{creator}/{modelId}\"\\n','\\n',char(10))"
set_upload_path
#set_global about "$ABOUT"
import_assets
mount_dir
BOOT_SCAN=1 scan_libraries &
touch ${db}.xrforgeinit
}
caddy(){
cat Caddyfile | sed 's|ip:|'$1':|g' > /tmp/Caddyfile
set -x
cat /tmp/Caddyfile
cd /tmp
$(which caddy) run
add_lib_to_db /mnt/asset
add_lib_to_db /mnt/templates
}
# The new entrypoint of the docker
boot(){
echocolor "[$APPNAME]" "booting..."
test -z "$NO_OVERLAYFS" && overlayfs
test -z "$NO_DEFAULTDB" && db default
start_syslog
rename_app
federate_servers
set_homepage
set_theme
set_modelpath
mount_rclone
set_upload_path
force_public
get_xrfragment_assets
mount_dir
start_hook_daemon
mount_rclone
janusxr
force_public &
scan_libraries &
hook boot # emit unixy hook-event (/root/hook.d/boot/* scripts)
# enable development mode (disables template caching etc)
test -n "$DEV" && {
sed -i 's|^exec.*|exec s6-setuidgid $PUID:$PGID bin/dev|g' /usr/src/app/bin/docker-entrypoint.sh
sed -i 's|config.enable_reloading = false|config.enable_reloading = true|g' /usr/src/app/config/environments/production.rb
apk add vim
}
@ -383,10 +254,9 @@ is_inside_container(){
usage(){
echocolor "Usage:" manyfold.sh "<cmd>"
echocolor "Cmds:"
echocolor " " "run [-d] " "# prints OCI container cmd (needs podman/docker)"
echocolor " " "run [-d] " "# runs a OCI container (needs podman/docker)"
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 -n "$1" && "$@"
test -n "$1" || usage

362
manyfold/manyfold.sql Normal file
View file

@ -0,0 +1,362 @@
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "data_migrations" ("version" varchar NOT NULL PRIMARY KEY);
INSERT INTO data_migrations VALUES('20221214230757');
INSERT INTO data_migrations VALUES('20221220223040');
INSERT INTO data_migrations VALUES('20230221174212');
INSERT INTO data_migrations VALUES('20230308006000');
INSERT INTO data_migrations VALUES('20230612080306');
INSERT INTO data_migrations VALUES('20230613134254');
INSERT INTO data_migrations VALUES('20230617222353');
INSERT INTO data_migrations VALUES('20230628194944');
INSERT INTO data_migrations VALUES('20240319155526');
INSERT INTO data_migrations VALUES('20240322150022');
INSERT INTO data_migrations VALUES('20240615085913');
INSERT INTO data_migrations VALUES('20240731165647');
INSERT INTO data_migrations VALUES('20240802094448');
INSERT INTO data_migrations VALUES('20240805111500');
INSERT INTO data_migrations VALUES('20240830121749');
INSERT INTO data_migrations VALUES('20240830151650');
INSERT INTO data_migrations VALUES('20240904152358');
INSERT INTO data_migrations VALUES('20240909100000');
INSERT INTO data_migrations VALUES('20240923114515');
INSERT INTO data_migrations VALUES('20241013215000');
INSERT INTO data_migrations VALUES('20241021125322');
INSERT INTO data_migrations VALUES('20241118155027');
INSERT INTO data_migrations VALUES('20250121164452');
CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);
INSERT INTO schema_migrations VALUES('20210130201037');
INSERT INTO schema_migrations VALUES('20210130203243');
INSERT INTO schema_migrations VALUES('20210205230918');
INSERT INTO schema_migrations VALUES('20210210153744');
INSERT INTO schema_migrations VALUES('20210212143001');
INSERT INTO schema_migrations VALUES('20210212143002');
INSERT INTO schema_migrations VALUES('20210212143003');
INSERT INTO schema_migrations VALUES('20210212143004');
INSERT INTO schema_migrations VALUES('20210212143005');
INSERT INTO schema_migrations VALUES('20210212143006');
INSERT INTO schema_migrations VALUES('20210220204055');
INSERT INTO schema_migrations VALUES('20210225232756');
INSERT INTO schema_migrations VALUES('20210225232841');
INSERT INTO schema_migrations VALUES('20210301230017');
INSERT INTO schema_migrations VALUES('20210302161202');
INSERT INTO schema_migrations VALUES('20210314185822');
INSERT INTO schema_migrations VALUES('20210318202638');
INSERT INTO schema_migrations VALUES('20210321164508');
INSERT INTO schema_migrations VALUES('20210330215825');
INSERT INTO schema_migrations VALUES('20220105233138');
INSERT INTO schema_migrations VALUES('20220106220519');
INSERT INTO schema_migrations VALUES('20220612220115');
INSERT INTO schema_migrations VALUES('20220612220116');
INSERT INTO schema_migrations VALUES('20220612220117');
INSERT INTO schema_migrations VALUES('20220614210023');
INSERT INTO schema_migrations VALUES('20220614211256');
INSERT INTO schema_migrations VALUES('20220614213902');
INSERT INTO schema_migrations VALUES('20220617122809');
INSERT INTO schema_migrations VALUES('20220619174307');
INSERT INTO schema_migrations VALUES('20220626152444');
INSERT INTO schema_migrations VALUES('20220702202932');
INSERT INTO schema_migrations VALUES('20221128165903');
INSERT INTO schema_migrations VALUES('20221210001132');
INSERT INTO schema_migrations VALUES('20221219204414');
INSERT INTO schema_migrations VALUES('20221220223340');
INSERT INTO schema_migrations VALUES('20230202210000');
INSERT INTO schema_migrations VALUES('20230202210001');
INSERT INTO schema_migrations VALUES('20230203150000');
INSERT INTO schema_migrations VALUES('20230221180921');
INSERT INTO schema_migrations VALUES('20230222155910');
INSERT INTO schema_migrations VALUES('20230303150029');
INSERT INTO schema_migrations VALUES('20230305180823');
INSERT INTO schema_migrations VALUES('20230307215826');
INSERT INTO schema_migrations VALUES('20230308004237');
INSERT INTO schema_migrations VALUES('20230308005021');
INSERT INTO schema_migrations VALUES('20230313000000');
INSERT INTO schema_migrations VALUES('20230313000001');
INSERT INTO schema_migrations VALUES('20230316184012');
INSERT INTO schema_migrations VALUES('20230324000000');
INSERT INTO schema_migrations VALUES('20230615135601');
INSERT INTO schema_migrations VALUES('20230628195018');
INSERT INTO schema_migrations VALUES('20230707082403');
INSERT INTO schema_migrations VALUES('20230710102250');
INSERT INTO schema_migrations VALUES('20230711111009');
INSERT INTO schema_migrations VALUES('20240122114207');
INSERT INTO schema_migrations VALUES('20240127143358');
INSERT INTO schema_migrations VALUES('20240131134832');
INSERT INTO schema_migrations VALUES('20240208143319');
INSERT INTO schema_migrations VALUES('20240209125409');
INSERT INTO schema_migrations VALUES('20240228130246');
INSERT INTO schema_migrations VALUES('20240306095646');
INSERT INTO schema_migrations VALUES('20240319155251');
INSERT INTO schema_migrations VALUES('20240319155903');
INSERT INTO schema_migrations VALUES('20240322143621');
INSERT INTO schema_migrations VALUES('20240410221112');
INSERT INTO schema_migrations VALUES('20240410222922');
INSERT INTO schema_migrations VALUES('20240418112821');
INSERT INTO schema_migrations VALUES('20240423102250');
INSERT INTO schema_migrations VALUES('20240610120318');
INSERT INTO schema_migrations VALUES('20240614085913');
INSERT INTO schema_migrations VALUES('20240701142651');
INSERT INTO schema_migrations VALUES('20240703103707');
INSERT INTO schema_migrations VALUES('20240703160732');
INSERT INTO schema_migrations VALUES('20240727113215');
INSERT INTO schema_migrations VALUES('20240727113216');
INSERT INTO schema_migrations VALUES('20240727113217');
INSERT INTO schema_migrations VALUES('20240731165646');
INSERT INTO schema_migrations VALUES('20240827155630');
INSERT INTO schema_migrations VALUES('20240904151944');
INSERT INTO schema_migrations VALUES('20240924122004');
INSERT INTO schema_migrations VALUES('20240926162407');
INSERT INTO schema_migrations VALUES('20241004194445');
INSERT INTO schema_migrations VALUES('20241007182824');
INSERT INTO schema_migrations VALUES('20241009122540');
INSERT INTO schema_migrations VALUES('20241015090803');
INSERT INTO schema_migrations VALUES('20241017093301');
INSERT INTO schema_migrations VALUES('20241017113112');
INSERT INTO schema_migrations VALUES('20241021124608');
INSERT INTO schema_migrations VALUES('20241025110218');
INSERT INTO schema_migrations VALUES('20241105121830');
INSERT INTO schema_migrations VALUES('20241122121621');
INSERT INTO schema_migrations VALUES('20241128162213');
INSERT INTO schema_migrations VALUES('20241128162214');
INSERT INTO schema_migrations VALUES('20250114105808');
INSERT INTO schema_migrations VALUES('20250121105010');
INSERT INTO schema_migrations VALUES('20250122171731');
INSERT INTO schema_migrations VALUES('20250222000000');
INSERT INTO schema_migrations VALUES('20250305171530');
INSERT INTO schema_migrations VALUES('20250407160721');
INSERT INTO schema_migrations VALUES('20250407160722');
INSERT INTO schema_migrations VALUES('20250408111644');
INSERT INTO schema_migrations VALUES('20250408145956');
INSERT INTO schema_migrations VALUES('20250409120155');
INSERT INTO schema_migrations VALUES('20250409125753');
INSERT INTO schema_migrations VALUES('20250423094525');
INSERT INTO schema_migrations VALUES('20250425104619');
INSERT INTO schema_migrations VALUES('20250520111046');
INSERT INTO schema_migrations VALUES('20250609210440');
INSERT INTO schema_migrations VALUES('20250620141805');
INSERT INTO schema_migrations VALUES('20250621223410');
INSERT INTO schema_migrations VALUES('20250629212656');
INSERT INTO schema_migrations VALUES('20250716093106');
INSERT INTO schema_migrations VALUES('20250724094951');
INSERT INTO schema_migrations VALUES('20250806142734');
CREATE TABLE IF NOT EXISTS "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
INSERT INTO ar_internal_metadata VALUES('environment','production','2025-07-25 10:52:31.380052','2025-07-25 10:52:31.380054');
CREATE TABLE IF NOT EXISTS "libraries" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "path" varchar NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "notes" varchar, "caption" varchar, "name" varchar, "tag_regex" text, "icon" text, "storage_service" varchar DEFAULT 'filesystem' NOT NULL, "s3_endpoint" varchar DEFAULT NULL, "s3_region" varchar DEFAULT NULL, "s3_bucket" varchar DEFAULT NULL, "s3_access_key_id" varchar DEFAULT NULL, "s3_secret_access_key" varchar DEFAULT NULL, "public_id" varchar, "s3_path_style" boolean DEFAULT 1 NOT NULL);
INSERT INTO libraries VALUES(6,'/mnt/experiences','2025-08-14','2025-08-14','','','experiences',NULL,'','filesystem','','','','','','experiences',1);
CREATE TABLE IF NOT EXISTS "tags" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "created_at" datetime, "updated_at" datetime, "taggings_count" integer DEFAULT 0);
CREATE TABLE IF NOT EXISTS "taggings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "tag_id" integer, "taggable_type" varchar, "taggable_id" integer, "tagger_type" varchar, "tagger_id" integer, "context" varchar(128), "created_at" datetime, CONSTRAINT "fk_rails_9fcd2e236b"
FOREIGN KEY ("tag_id")
REFERENCES "tags" ("id")
);
CREATE TABLE IF NOT EXISTS "links" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" varchar, "linkable_type" varchar, "linkable_id" integer, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "synced_at" datetime(6));
INSERT INTO links VALUES(1,'https://xrfragment.org','Creator',1,'2025-08-14 12:57:19.685425','2025-08-14 12:57:19.685425',NULL);
CREATE TABLE IF NOT EXISTS "settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "var" varchar NOT NULL, "value" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
INSERT INTO settings VALUES(1,'default_library','6','2025-07-25 10:58:00.004576','2025-07-28 13:30:03.381376');
INSERT INTO settings VALUES(2,'site_name',replace('--- XRForge\n','\n',char(10)),'2025-07-25 10:59:04.496016','2025-07-25 10:59:04.496016');
INSERT INTO settings VALUES(3,'site_tagline',replace('--- Self-sovereign XR Experiences based on 3D files & URLs\n','\n',char(10)),'2025-07-25 10:59:04.519264','2025-07-25 10:59:04.519264');
INSERT INTO settings VALUES(4,'theme','vapor','2025-07-25 10:59:04.522670','2025-07-28 13:47:54.690364');
INSERT INTO settings VALUES(5,'about',replace('--- ''''\n','\n',char(10)),'2025-07-25 10:59:04.527612','2025-07-25 10:59:04.527612');
INSERT INTO settings VALUES(6,'rules',replace('--- ''''\n','\n',char(10)),'2025-07-25 10:59:04.531378','2025-07-25 10:59:04.531378');
INSERT INTO settings VALUES(7,'support_link',replace('--- https://forgejo.isvery.ninja/coderofsalvation/xrforge\n','\n',char(10)),'2025-07-25 10:59:04.533678','2025-07-25 10:59:04.533678');
INSERT INTO settings VALUES(8,'site_icon',replace('--- ''''\n','\n',char(10)),'2025-07-25 10:59:04.536228','2025-07-25 14:19:06.192651');
INSERT INTO settings VALUES(9,'model_path_template',replace('--- "{creator}/{modelId}"\n','\n',char(10)),'2025-07-28 15:57:18.598798','2025-07-28 15:57:18.598798');
INSERT INTO settings VALUES(10,'parse_metadata_from_path',replace('--- true\n','\n',char(10)),'2025-07-28 15:57:18.624917','2025-07-28 15:57:18.624917');
INSERT INTO settings VALUES(11,'safe_folder_names',replace('--- true\n','\n',char(10)),'2025-07-28 15:57:18.627224','2025-07-28 15:57:18.627224');
INSERT INTO settings VALUES(12,'model_ignored_files',replace('---\n- !ruby/regexp /^\.[^\.]+/\n- !ruby/regexp /.*\/@eaDir\/.*/\n- !ruby/regexp /__MACOSX/\n','\n',char(10)),'2025-07-28 15:57:18.633182','2025-07-28 15:57:18.633182');
INSERT INTO settings VALUES(13,'model_tags_filter_stop_words',replace('--- true\n','\n',char(10)),'2025-07-28 15:57:18.637858','2025-07-28 15:57:18.637858');
INSERT INTO settings VALUES(14,'model_tags_tag_model_directory_name',replace('--- false\n','\n',char(10)),'2025-07-28 15:57:18.640351','2025-07-28 15:57:18.640351');
INSERT INTO settings VALUES(15,'model_tags_stop_words_locale',replace('--- en\n','\n',char(10)),'2025-07-28 15:57:18.643185','2025-07-28 15:57:18.643185');
INSERT INTO settings VALUES(16,'model_tags_custom_stop_words',replace('---\n- png\n- jpeg\n- jpg\n- jpe\n- pjpeg\n- gif\n- bmp\n- tiff\n- tif\n- svg\n- webp\n- threeds\n- 3ds\n- amf\n- ldr\n- mpd\n- scad\n- dwg\n- dxf\n- threemf\n- 3mf\n- gltf\n- glb\n- iges\n- igs\n- mtl\n- obj\n- step\n- stp\n- stl\n- collada\n- dae\n- draco\n- drc\n- vrml\n- wrl\n- abc\n- blend\n- brep\n- cheetah3d\n- jas\n- fbx\n- fcstd\n- f3d\n- f3z\n- ipt\n- iam\n- maya\n- ma\n- mb\n- mix\n- modo\n- lxo\n- ply\n- sketchup\n- skp\n- sldprt\n- hfp\n- speedtree\n- spm\n- x3d\n- gcode\n- bgcode\n- lychee\n- lys\n- lyt\n- chitubox\n- ctb\n- mpeg\n- mpg\n- mpe\n- webm\n- mp4\n- m4v\n- html\n- xhtml\n- text\n- txt\n- pdf\n- md\n- doc\n- docx\n- bin\n- gbr\n- gerber\n- geb\n- gb\n- gbrjob\n- drl\n- kicad_pro\n- pro\n- kicad_mod\n- kicad_pcb\n- kicad_sym\n- kicad_sch\n- sch\n- kicad_wks\n- zip\n- gzip\n- gz\n- rar\n- sevenz\n- 7z\n- bz2\n','\n',char(10)),'2025-07-28 15:57:18.645648','2025-07-28 15:57:18.645648');
INSERT INTO settings VALUES(17,'model_tags_auto_tag_new',replace('--- "!new"\n','\n',char(10)),'2025-07-28 15:57:18.648518','2025-07-28 15:57:18.648518');
CREATE TABLE IF NOT EXISTS "problems" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "problematic_type" varchar, "problematic_id" integer, "category" integer, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "note" varchar DEFAULT NULL, "ignored" boolean DEFAULT 0 NOT NULL, "public_id" varchar, "in_progress" boolean DEFAULT 0 NOT NULL);
CREATE TABLE IF NOT EXISTS "favorites" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "favoritable_type" varchar NOT NULL, "favoritable_id" integer NOT NULL, "favoritor_type" varchar NOT NULL, "favoritor_id" integer NOT NULL, "scope" varchar DEFAULT 'printed' NOT NULL, "blocked" boolean DEFAULT 0 NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS "flipper_features" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS "flipper_gates" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "feature_key" varchar NOT NULL, "key" varchar NOT NULL, "value" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS "roles" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "resource_type" varchar, "resource_id" integer, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
INSERT INTO roles VALUES(1,'administrator',NULL,NULL,'2025-07-25 10:52:35.066502','2025-07-25 10:52:35.066502');
INSERT INTO roles VALUES(2,'moderator',NULL,NULL,'2025-07-25 10:52:35.079751','2025-07-25 10:52:35.079751');
INSERT INTO roles VALUES(3,'contributor',NULL,NULL,'2025-07-25 10:52:35.081798','2025-07-25 10:52:35.081798');
INSERT INTO roles VALUES(4,'member',NULL,NULL,'2025-07-25 10:52:35.083068','2025-07-25 10:52:35.083068');
CREATE TABLE IF NOT EXISTS "users_roles" ("user_id" integer, "role_id" integer);
INSERT INTO users_roles VALUES(1,4);
INSERT INTO users_roles VALUES(1,1);
CREATE TABLE IF NOT EXISTS "users" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar DEFAULT '' NOT NULL, "encrypted_password" varchar DEFAULT '' NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "username" varchar NOT NULL, "pagination_settings" json DEFAULT '{"models":true,"creators":true,"collections":true,"per_page":12}', "renderer_settings" json DEFAULT '{"grid_width":200,"grid_depth":200,"show_grid":true,"enable_pan_zoom":false,"background_colour":"#000000","object_colour":"#ffffff","render_style":"original"}', "tag_cloud_settings" json DEFAULT '{"threshold":2,"heatmap":true,"keypair":true,"sorting":"frequency"}', "problem_settings" json DEFAULT '{"missing":"danger","empty":"info","nesting":"warning","inefficient":"info","duplicate":"warning","no_image":"silent","no_3d_model":"silent","non_manifold":"warning","inside_out":"warning","no_license":"silent","no_links":"silent","no_creator":"silent","no_tags":"silent"}', "file_list_settings" json DEFAULT '{"hide_presupported_versions":true}', "reset_password_token" varchar, "remember_created_at" datetime(6), "reset_password_sent_at" datetime(6), "interface_language" varchar, "failed_attempts" integer DEFAULT 0 NOT NULL, "locked_at" datetime(6), "auth_provider" varchar, "auth_uid" varchar, "sensitive_content_handling" varchar DEFAULT NULL, "public_id" varchar, "approved" boolean DEFAULT 1 NOT NULL, "quota" integer DEFAULT 1 NOT NULL, "quota_use_site_default" boolean DEFAULT 1 NOT NULL);
INSERT INTO users VALUES(1,'xrforge@localhost','$2a$12$u/j8LRzbPiJRHmi1eV/fvOXXiKxN2vBGtNd.Pt28w.wOnq3rnfpzO','2025-07-25 10:52:56.989975','2025-07-25 12:46:27.338917','xrforge','{"models":true,"creators":true,"collections":true,"per_page":12}','{"grid_width":200,"grid_depth":200,"show_grid":true,"enable_pan_zoom":false,"background_colour":"#000000","object_colour":"#ffffff","render_style":"original"}','{"threshold":2,"heatmap":true,"keypair":true,"sorting":"frequency"}','{"missing":"danger","empty":"info","nesting":"warning","inefficient":"info","duplicate":"warning","no_image":"silent","no_3d_model":"silent","non_manifold":"warning","inside_out":"warning","no_license":"silent","no_links":"silent","no_creator":"silent","no_tags":"silent"}','{"hide_presupported_versions":true}',NULL,'2025-07-25 12:46:27.325287',NULL,NULL,0,NULL,NULL,NULL,NULL,'71863vkppj6k',1,1,1);
CREATE TABLE IF NOT EXISTS "federails_followings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "actor_id" integer NOT NULL, "target_actor_id" integer NOT NULL, "status" integer DEFAULT 0, "federated_url" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "uuid" varchar DEFAULT NULL, CONSTRAINT "fk_rails_2e62338faa"
FOREIGN KEY ("actor_id")
REFERENCES "federails_actors" ("id")
, CONSTRAINT "fk_rails_4a2870c181"
FOREIGN KEY ("target_actor_id")
REFERENCES "federails_actors" ("id")
);
CREATE TABLE IF NOT EXISTS "federails_activities" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "entity_type" varchar NOT NULL, "entity_id" integer NOT NULL, "action" varchar NOT NULL, "actor_id" integer NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "uuid" varchar DEFAULT NULL, CONSTRAINT "fk_rails_85ef6259df"
FOREIGN KEY ("actor_id")
REFERENCES "federails_actors" ("id")
);
INSERT INTO federails_activities VALUES(1,'Federails::Actor',2,'Create',1,'2025-08-14 12:57:19.593979','2025-08-14 12:57:19.737880','22e24c27-09ac-4f61-8897-1e176f16fcf7');
CREATE TABLE IF NOT EXISTS "caber_relations" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "subject_type" varchar, "subject_id" integer, "permission" varchar, "object_type" varchar NOT NULL, "object_id" integer NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
INSERT INTO caber_relations VALUES(1,'User',1,'own','Creator',1,'2025-08-14 12:57:19.679313','2025-08-14 12:57:19.679313');
INSERT INTO caber_relations VALUES(2,'Role',4,'view','Creator',1,'2025-08-14 12:57:19.762509','2025-08-14 12:57:19.762509');
CREATE TABLE IF NOT EXISTS "federails_moderation_reports" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "federated_url" varchar, "federails_actor_id" integer, "content" varchar, "object_type" varchar, "object_id" integer, "resolved_at" datetime(6), "resolution" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, CONSTRAINT "fk_rails_a5cda24d4c"
FOREIGN KEY ("federails_actor_id")
REFERENCES "federails_actors" ("id")
);
CREATE TABLE IF NOT EXISTS "federails_moderation_domain_blocks" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "domain" varchar NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS "comments" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "public_id" varchar NOT NULL, "commenter_type" varchar, "commenter_id" integer, "commentable_type" varchar NOT NULL, "commentable_id" integer NOT NULL, "comment" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "system" boolean DEFAULT 0 NOT NULL, "sensitive" boolean DEFAULT 0 NOT NULL, "federated_url" varchar, "federails_actor_id" integer, CONSTRAINT "fk_rails_3a181ceff0"
FOREIGN KEY ("federails_actor_id")
REFERENCES "federails_actors" ("id")
);
CREATE TABLE IF NOT EXISTS "oauth_applications" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "uid" varchar NOT NULL, "secret" varchar NOT NULL, "redirect_uri" text, "scopes" varchar DEFAULT '' NOT NULL, "confidential" boolean DEFAULT 1 NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "owner_id" bigint, "owner_type" varchar);
CREATE TABLE IF NOT EXISTS "oauth_access_grants" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "resource_owner_id" integer NOT NULL, "application_id" integer NOT NULL, "token" varchar NOT NULL, "expires_in" integer NOT NULL, "redirect_uri" text NOT NULL, "scopes" varchar DEFAULT '' NOT NULL, "created_at" datetime(6) NOT NULL, "revoked_at" datetime(6), CONSTRAINT "fk_rails_b4b53e07b8"
FOREIGN KEY ("application_id")
REFERENCES "oauth_applications" ("id")
, CONSTRAINT "fk_rails_330c32d8d9"
FOREIGN KEY ("resource_owner_id")
REFERENCES "users" ("id")
);
CREATE TABLE IF NOT EXISTS "oauth_access_tokens" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "resource_owner_id" integer, "application_id" integer NOT NULL, "token" varchar NOT NULL, "refresh_token" varchar, "expires_in" integer, "scopes" varchar, "created_at" datetime(6) NOT NULL, "revoked_at" datetime(6), "previous_refresh_token" varchar DEFAULT '' NOT NULL, CONSTRAINT "fk_rails_732cb83ab7"
FOREIGN KEY ("application_id")
REFERENCES "oauth_applications" ("id")
, CONSTRAINT "fk_rails_ee63f25419"
FOREIGN KEY ("resource_owner_id")
REFERENCES "users" ("id")
);
CREATE TABLE IF NOT EXISTS "federails_actors" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "federated_url" varchar, "username" varchar, "server" varchar, "inbox_url" varchar, "outbox_url" varchar, "followers_url" varchar, "followings_url" varchar, "profile_url" varchar, "entity_id" integer, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "entity_type" varchar DEFAULT NULL, "public_key" text, "private_key" text, "uuid" varchar, "extensions" json, "local" boolean DEFAULT 0 NOT NULL, "actor_type" varchar, "tombstoned_at" datetime(6));
INSERT INTO federails_actors VALUES(1,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2025-07-25 10:52:57.029315','2025-07-25 10:52:57.029315','User',NULL,NULL,'eb64d114-1bc7-4cb3-8be6-350d23ccfb3e',NULL,1,NULL,NULL);
INSERT INTO federails_actors VALUES(2,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,'2025-08-14 12:57:19.669133','2025-08-14 12:57:19.669133','Creator',NULL,NULL,'a544a58c-52be-4013-9415-440a9014b4c8',NULL,1,NULL,NULL);
CREATE TABLE IF NOT EXISTS "model_files" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "filename" varchar, "model_id" integer NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "presupported" boolean DEFAULT 0 NOT NULL, "y_up" boolean DEFAULT 0 NOT NULL, "digest" varchar, "notes" text, "caption" text, "size" bigint, "presupported_version_id" integer, "attachment_data" json, "public_id" varchar, "filename_lower" varchar GENERATED ALWAYS AS (LOWER(filename)) STORED, "previewable" boolean DEFAULT 0 NOT NULL, CONSTRAINT "fk_rails_b5ac05b6e3"
FOREIGN KEY ("presupported_version_id")
REFERENCES "model_files" ("id")
, CONSTRAINT "fk_rails_8e378ff647"
FOREIGN KEY ("model_id")
REFERENCES "models" ("id")
);
CREATE TABLE IF NOT EXISTS "altcha_solutions" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "algorithm" varchar, "challenge" varchar, "salt" varchar, "signature" varchar, "number" integer, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS "models" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "path" varchar NOT NULL, "library_id" integer NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "preview_file_id" integer, "creator_id" integer, "notes" text, "caption" text, "collection_id" integer, "slug" varchar, "license" varchar, "public_id" varchar, "name_lower" varchar GENERATED ALWAYS AS (LOWER(name)) STORED, "sensitive" boolean DEFAULT 0 NOT NULL, "indexable" varchar, "ai_indexable" varchar, CONSTRAINT "fk_rails_cdf016e15c"
FOREIGN KEY ("collection_id")
REFERENCES "collections" ("id")
, CONSTRAINT "fk_rails_aaa717f5bb"
FOREIGN KEY ("library_id")
REFERENCES "libraries" ("id")
, CONSTRAINT "fk_rails_3b8b50d3f3"
FOREIGN KEY ("creator_id")
REFERENCES "creators" ("id")
);
CREATE TABLE IF NOT EXISTS "creators" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "notes" text, "caption" text, "slug" varchar, "public_id" varchar, "name_lower" varchar GENERATED ALWAYS AS (LOWER(name)) STORED, "indexable" varchar, "ai_indexable" varchar);
INSERT INTO creators VALUES(1,'xrfragments','2025-08-14 12:57:19.593979','2025-08-14 12:57:19.593979','XR Fragments is an open specification for hyperlinking & deeplinking 3D fileformats .\nTurn 3D files into linkable AR/VR websites .\n3D files with XR Fragments enable interoperable, networkable and interactions via so-called extras and promote URL standards','deeplinking and adressing 3D objects using URI''s','xrfragments','bdb7rrdjwtdm','yes',NULL);
CREATE TABLE IF NOT EXISTS "collections" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "notes" text, "caption" text, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "collection_id" integer, "slug" varchar, "public_id" varchar, "name_lower" varchar GENERATED ALWAYS AS (LOWER(name)) STORED, "creator_id" integer, "indexable" varchar, "ai_indexable" varchar, CONSTRAINT "fk_rails_63724415e9"
FOREIGN KEY ("collection_id")
REFERENCES "collections" ("id")
, CONSTRAINT "fk_rails_ab2fec83b3"
FOREIGN KEY ("creator_id")
REFERENCES "creators" ("id")
);
CREATE TABLE IF NOT EXISTS "fasp_client_providers" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "uuid" varchar, "name" varchar, "base_url" varchar, "server_id" varchar, "public_key" varchar, "ed25519_signing_key" varchar, "status" integer, "capabilities" json, "privacy_policy" json, "sign_in_url" varchar, "contact_email" varchar, "fediverse_account" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);
INSERT INTO sqlite_sequence VALUES('taggings',0);
INSERT INTO sqlite_sequence VALUES('users',1);
INSERT INTO sqlite_sequence VALUES('comments',0);
INSERT INTO sqlite_sequence VALUES('oauth_access_grants',0);
INSERT INTO sqlite_sequence VALUES('oauth_access_tokens',0);
INSERT INTO sqlite_sequence VALUES('federails_actors',2);
INSERT INTO sqlite_sequence VALUES('model_files',0);
INSERT INTO sqlite_sequence VALUES('models',0);
INSERT INTO sqlite_sequence VALUES('creators',1);
INSERT INTO sqlite_sequence VALUES('collections',0);
INSERT INTO sqlite_sequence VALUES('roles',4);
INSERT INTO sqlite_sequence VALUES('libraries',6);
INSERT INTO sqlite_sequence VALUES('settings',17);
INSERT INTO sqlite_sequence VALUES('caber_relations',2);
INSERT INTO sqlite_sequence VALUES('links',1);
INSERT INTO sqlite_sequence VALUES('federails_activities',1);
CREATE UNIQUE INDEX "index_tags_on_name" ON "tags" ("name");
CREATE UNIQUE INDEX "taggings_idx" ON "taggings" ("tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type");
CREATE INDEX "taggings_taggable_context_idx" ON "taggings" ("taggable_id", "taggable_type", "context");
CREATE INDEX "index_taggings_on_taggable_type" ON "taggings" ("taggable_type");
CREATE INDEX "index_taggings_on_context" ON "taggings" ("context");
CREATE INDEX "index_taggings_on_tagger_id_and_tagger_type" ON "taggings" ("tagger_id", "tagger_type");
CREATE INDEX "taggings_idy" ON "taggings" ("taggable_id", "taggable_type", "tagger_id", "context");
CREATE INDEX "index_links_on_linkable" ON "links" ("linkable_type", "linkable_id");
CREATE UNIQUE INDEX "index_settings_on_var" ON "settings" ("var");
CREATE INDEX "index_problems_on_problematic" ON "problems" ("problematic_type", "problematic_id");
CREATE INDEX "index_favorites_on_favoritor" ON "favorites" ("favoritor_type", "favoritor_id");
CREATE INDEX "index_favorites_on_scope" ON "favorites" ("scope");
CREATE INDEX "index_favorites_on_blocked" ON "favorites" ("blocked");
CREATE INDEX "fk_favorites" ON "favorites" ("favoritor_id", "favoritor_type");
CREATE INDEX "fk_favoritables" ON "favorites" ("favoritable_id", "favoritable_type");
CREATE UNIQUE INDEX "uniq_favorites__and_favoritables" ON "favorites" ("favoritable_type", "favoritable_id", "favoritor_type", "favoritor_id", "scope");
CREATE UNIQUE INDEX "index_problems_on_category_and_problematic_id_and_type" ON "problems" ("category", "problematic_id", "problematic_type");
CREATE UNIQUE INDEX "index_flipper_features_on_key" ON "flipper_features" ("key");
CREATE UNIQUE INDEX "index_flipper_gates_on_feature_key_and_key_and_value" ON "flipper_gates" ("feature_key", "key", "value");
CREATE INDEX "index_roles_on_resource" ON "roles" ("resource_type", "resource_id");
CREATE INDEX "index_users_roles_on_role_id" ON "users_roles" ("role_id");
CREATE INDEX "index_roles_on_name_and_resource_type_and_resource_id" ON "roles" ("name", "resource_type", "resource_id");
CREATE INDEX "index_users_roles_on_user_id_and_role_id" ON "users_roles" ("user_id", "role_id");
CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email");
CREATE UNIQUE INDEX "index_users_on_username" ON "users" ("username");
CREATE UNIQUE INDEX "index_users_on_reset_password_token" ON "users" ("reset_password_token");
CREATE INDEX "index_federails_followings_on_target_actor_id" ON "federails_followings" ("target_actor_id");
CREATE UNIQUE INDEX "index_federails_followings_on_actor_id_and_target_actor_id" ON "federails_followings" ("actor_id", "target_actor_id");
CREATE INDEX "index_federails_activities_on_entity" ON "federails_activities" ("entity_type", "entity_id");
CREATE INDEX "index_federails_activities_on_actor_id" ON "federails_activities" ("actor_id");
CREATE INDEX "index_caber_relations_on_subject" ON "caber_relations" ("subject_type", "subject_id");
CREATE INDEX "index_caber_relations_on_object" ON "caber_relations" ("object_type", "object_id");
CREATE UNIQUE INDEX "idx_on_subject_id_subject_type_object_id_object_typ_a279b094be" ON "caber_relations" ("subject_id", "subject_type", "object_id", "object_type");
CREATE INDEX "index_problems_on_public_id" ON "problems" ("public_id");
CREATE INDEX "index_libraries_on_public_id" ON "libraries" ("public_id");
CREATE UNIQUE INDEX "index_federails_activities_on_uuid" ON "federails_activities" ("uuid");
CREATE UNIQUE INDEX "index_federails_followings_on_uuid" ON "federails_followings" ("uuid");
CREATE INDEX "index_users_on_public_id" ON "users" ("public_id");
CREATE INDEX "index_users_on_approved" ON "users" ("approved");
CREATE INDEX "index_federails_moderation_reports_on_federails_actor_id" ON "federails_moderation_reports" ("federails_actor_id");
CREATE INDEX "index_federails_moderation_reports_on_object" ON "federails_moderation_reports" ("object_type", "object_id");
CREATE UNIQUE INDEX "index_federails_moderation_domain_blocks_on_domain" ON "federails_moderation_domain_blocks" ("domain");
CREATE UNIQUE INDEX "index_comments_on_public_id" ON "comments" ("public_id");
CREATE INDEX "index_comments_on_commenter" ON "comments" ("commenter_type", "commenter_id");
CREATE INDEX "index_comments_on_commentable" ON "comments" ("commentable_type", "commentable_id");
CREATE INDEX "index_comments_on_federails_actor_id" ON "comments" ("federails_actor_id");
CREATE UNIQUE INDEX "index_oauth_applications_on_uid" ON "oauth_applications" ("uid");
CREATE INDEX "index_oauth_access_grants_on_resource_owner_id" ON "oauth_access_grants" ("resource_owner_id");
CREATE INDEX "index_oauth_access_grants_on_application_id" ON "oauth_access_grants" ("application_id");
CREATE UNIQUE INDEX "index_oauth_access_grants_on_token" ON "oauth_access_grants" ("token");
CREATE INDEX "index_oauth_access_tokens_on_resource_owner_id" ON "oauth_access_tokens" ("resource_owner_id");
CREATE INDEX "index_oauth_access_tokens_on_application_id" ON "oauth_access_tokens" ("application_id");
CREATE UNIQUE INDEX "index_oauth_access_tokens_on_token" ON "oauth_access_tokens" ("token");
CREATE UNIQUE INDEX "index_oauth_access_tokens_on_refresh_token" ON "oauth_access_tokens" ("refresh_token");
CREATE INDEX "index_oauth_applications_on_owner_id_and_owner_type" ON "oauth_applications" ("owner_id", "owner_type");
CREATE UNIQUE INDEX "index_federails_actors_on_federated_url" ON "federails_actors" ("federated_url");
CREATE UNIQUE INDEX "index_federails_actors_on_entity" ON "federails_actors" ("entity_type", "entity_id");
CREATE UNIQUE INDEX "index_federails_actors_on_uuid" ON "federails_actors" ("uuid");
CREATE INDEX "index_model_files_on_model_id" ON "model_files" ("model_id");
CREATE INDEX "index_model_files_on_digest" ON "model_files" ("digest");
CREATE INDEX "index_model_files_on_presupported_version_id" ON "model_files" ("presupported_version_id");
CREATE UNIQUE INDEX "index_model_files_on_filename_and_model_id" ON "model_files" ("filename", "model_id");
CREATE INDEX "index_model_files_on_public_id" ON "model_files" ("public_id");
CREATE INDEX "index_model_files_on_filename_lower" ON "model_files" ("filename_lower");
CREATE INDEX "index_links_on_linkable_id_and_linkable_type_and_url" ON "links" ("linkable_id", "linkable_type", "url");
CREATE UNIQUE INDEX "index_altcha_solutions" ON "altcha_solutions" ("algorithm", "challenge", "salt", "signature", "number");
CREATE INDEX "index_models_on_library_id" ON "models" ("library_id");
CREATE INDEX "index_models_on_creator_id" ON "models" ("creator_id");
CREATE INDEX "index_models_on_preview_file_id" ON "models" ("preview_file_id");
CREATE INDEX "index_models_on_collection_id" ON "models" ("collection_id");
CREATE INDEX "index_models_on_slug" ON "models" ("slug");
CREATE UNIQUE INDEX "index_models_on_path_and_library_id" ON "models" ("path", "library_id");
CREATE INDEX "index_models_on_public_id" ON "models" ("public_id");
CREATE INDEX "index_models_on_name_lower" ON "models" ("name_lower");
CREATE UNIQUE INDEX "index_creators_on_name" ON "creators" ("name");
CREATE UNIQUE INDEX "index_creators_on_slug" ON "creators" ("slug");
CREATE INDEX "index_creators_on_public_id" ON "creators" ("public_id");
CREATE INDEX "index_creators_on_name_lower" ON "creators" ("name_lower");
CREATE INDEX "index_collections_on_collection_id" ON "collections" ("collection_id");
CREATE UNIQUE INDEX "index_collections_on_name" ON "collections" ("name");
CREATE UNIQUE INDEX "index_collections_on_slug" ON "collections" ("slug");
CREATE INDEX "index_collections_on_public_id" ON "collections" ("public_id");
CREATE INDEX "index_collections_on_name_lower" ON "collections" ("name_lower");
CREATE INDEX "index_collections_on_creator_id" ON "collections" ("creator_id");
CREATE INDEX "index_models_on_created_at" ON "models" ("created_at");
CREATE INDEX "index_models_on_updated_at" ON "models" ("updated_at");
CREATE INDEX "index_creators_on_created_at" ON "creators" ("created_at");
CREATE INDEX "index_creators_on_updated_at" ON "creators" ("updated_at");
CREATE INDEX "index_collections_on_created_at" ON "collections" ("created_at");
CREATE INDEX "index_collections_on_updated_at" ON "collections" ("updated_at");
CREATE INDEX "index_links_on_url" ON "links" ("url");
COMMIT;

View file

@ -1,92 +0,0 @@
var fs = require('fs');
module.exports = {
/* Socket port to listen on */
port: 5566,
/* SSL configurations */
/*
************************************************************************
*** The following options REQUIRE a redis database to function ! ***
************************************************************************
*/
multiprocess: {
enabled: false, // requires redis for IPC
processes: 1
},
partyList: false,
redis: {
host: "127.0.0.1",
port: 6379,
//password: null
},
/*
************************************************************************
*** The following options REQUIRE a MySQL database to function ! ***
************************************************************************
*/
//// If you want to track how many users are online, set UserList: true.
//Userlist: false,
///* Controls how many results a request for 'users_online' receives. */
//maxUserResults: 100,
///* MySQL database connection info for janus-mysql-auth and janus-mysql-userlist */
//MySQL_Hostname: 'localhost',
//MySQL_Database: 'janusvr',
//MySQL_Username: 'janusvr',
//MySQL_Password: 'janusvr',
///* Authentication mode:
// 'none' - Will not attempt to authenticate users,
// anyone can connect with any unused userId.
// 'optional' - Anyone can connect, but if userId has been registered
// a password must be provided.
// 'required' - Only users with userids and passwords are allowed to connect.
//*/
//authMode: "none",
//
//popularRooms: {
// halfLife: 7 * 24 * 60 * 60 * 1000, // set halflife to 7 days
// updateInterval: 3000, // interval between weight updates on the popular rooms
// masterToken: "changethis"
//},
/*
************************************************************************
*** The previous options REQUIRE a MySQL database to function ! ***
************************************************************************
*/
/* Plugins must be installed from npm, or manually created in node_module/ to be loaded. */
/* hookPlugins are called while parsing messages */
hookPlugins: {
logon: {
plugins: [
//" janus-mysql-auth"
]
},
enter_room: {
plugins: [
// "janus-mysql-popular"
]
}
},
/* methodPlugins add new commands to the server */
methodPlugins: {
// ping: { plugin: "janus-method-ping" }
},
/* intervalPlugins are called in intervals specified in seconds. */
intervalPlugins: [
//{ plugin: "janus-mysql-userlist-official", interval: 6 }
],
};

View file

@ -1,76 +0,0 @@
#!/bin/sh
test -n "$2" || { echo "Usage: $0 <input.jpg> <output.jpg> [blurtimes]"; exit 0; }
# --- Configuration ---
# Define the size of the rectangle to crop (Width x Height)
CROP_SIZE="128x128"
CROP_WIDTH=128
CROP_HEIGHT=128
BLURTIMES=0
test -n "$3" && BLURTIMES=$3
# Input and Output filenames
INPUT_FILE="$1" # <-- **CHANGE THIS** to your actual input file name
OUTPUT_FILE="$2"
# --- Pre-Check: Ensure the input file exists ---
if [ ! -f "$INPUT_FILE" ]; then
echo "🚨 Error: Input file '$INPUT_FILE' not found."
echo "Please update the INPUT_FILE variable in the script."
exit 1
fi
# --- 1. Get the dimensions of the input image ---
# The 'identify' command returns image info; we use awk/cut to extract just the dimensions.
# Example output format: "1920x1080"
IMAGE_GEOMETRY=$(identify -format "%wx%h" "$INPUT_FILE")
IMAGE_WIDTH=$(echo "$IMAGE_GEOMETRY" | cut -dx -f1)
IMAGE_HEIGHT=$(echo "$IMAGE_GEOMETRY" | cut -dx -f2)
echo "Input Image: $INPUT_FILE (${IMAGE_GEOMETRY})"
echo "Crop Area: $CROP_SIZE"
# --- 2. Calculate the maximum possible starting coordinates ---
# To ensure the 16x16 crop doesn't go off the edge:
# Max_X_Start = Image_Width - Crop_Width
# Max_Y_Start = Image_Height - Crop_Height
MAX_X=$((IMAGE_WIDTH - CROP_WIDTH))
MAX_Y=$((IMAGE_HEIGHT - CROP_HEIGHT))
# Check if the image is too small to crop
if [ $MAX_X -lt 0 ] || [ $MAX_Y -lt 0 ]; then
echo "🚨 Error: Image dimensions ($IMAGE_GEOMETRY) are smaller than the crop size ($CROP_SIZE)."
exit 1
fi
# --- 3. Generate Random Coordinates ---
# $RANDOM generates a pseudo-random integer (0 to 32767).
# We use the modulo operator (%) to limit it to the required range (0 to MAX_X or MAX_Y).
# Random X-coordinate (0 to MAX_X)
RANDOM_X=$((RANDOM % (MAX_X + 1)))
# Random Y-coordinate (0 to MAX_Y)
RANDOM_Y=$((RANDOM % (MAX_Y + 1)))
# --- 4. Assemble the ImageMagick Geometry String ---
# The final geometry string is: <Width>x<Height>+<X_Offset>+<Y_Offset>
CROP_GEOMETRY="${CROP_SIZE}+${RANDOM_X}+${RANDOM_Y}"
echo "Random Start Coordinate: X=${RANDOM_X}, Y=${RANDOM_Y}"
echo "ImageMagick Crop String: ${CROP_GEOMETRY}"
# --- 5. Execute the ImageMagick Command ---
# 'convert' is the core command.
# -crop: Specifies the geometry string for cropping.
# -quality 100: (Optional) Ensures best quality for the output JPEG.
test -f $OUTPUT_FILE && rm $OUTPUT_FILE
magick "$INPUT_FILE" \
-crop "$CROP_GEOMETRY" \
-blur 0x$BLURTIMES \
-quality 100 \
"$OUTPUT_FILE"
echo "✅ thumbnail generated"

View file

@ -1,20 +0,0 @@
#!/bin/sh
test -z "$AFRAME_VERSION" && exit 0 # nothing to do
mkdir /usr/src/app/public/aframe || true
wget "https://aframe.io/releases/${AFRAME_VERSION}/aframe.min.js" -O /usr/src/app/public/aframe/aframe.min.js
test -f /usr/src/app/public/aframe/index.html || echo '<html>
<head>
<script src="/aframe/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>
' > /usr/src/app/public/aframe/index.html

View file

@ -2,5 +2,5 @@
test -z "$GODOT_VERSION" && exit 0 # nothing to do
wget "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_web_editor.zip" -O /root/godot.zip
cd /usr/src/app/public/godot
unzip -o /root/godot.zip
unzip /root/godot.zip
cp index.override.html index.html # make index.html autoload a manyfold zip-file (passed via HTTP query)

View file

@ -1,9 +1,8 @@
#!/bin/sh
test -z "$FEDERATE_DRIVE_HOST" && FEDERATE_DRIVE_HOST=http://localhost:8081
test -z "$FEDERATE_DRIVE_HOST" && FEDERATE_DRIVE_HOST=http://localhost:3215
test -z "$FEDERATE_DRIVE_PATH" && FEDERATE_DRIVE_PATH=/mnt
test -z "$FEDERATE_DRIVE_PORT" && FEDERATE_DRIVE_PORT=8081
test -z "$FEDERATE_DRIVE_CACHE" && FEDERATE_DRIVE_CACHE=0s
test -n "$DEV" && FEDERATE_DRIVE_CACHE=0s
test -z "$FEDERATE_DRIVE_PORT" && FEDERATE_DRIVE_PORT=3215
test -z "$FEDERATE_DRIVE_CACHE" && FEDERATE_DRIVE_CACHE=1m0s
test "$FEDERATE_DRIVE_PATH" = 0 && exit 0 # nothing to do (disabled)
@ -18,6 +17,5 @@ test -n "$FEDERATE_DRIVE_CERT" && test -m "$FEDERATE_DRIVE_KEY" && {
set -x
rclone serve http \
--links --exclude .xrforge \
--allow-origin '*' --poll-interval $FEDERATE_DRIVE_CACHE --dir-cache-time $FEDERATE_DRIVE_CACHE \
--poll-interval $FEDERATE_DRIVE_CACHE \
--addr 0.0.0.0:$FEDERATE_DRIVE_PORT ${AUTH} ${SSL} $FEDERATE_DRIVE_PATH &> /var/log/rclone.log &

View file

@ -1,116 +0,0 @@
#!/usr/bin/env ruby
require 'yaml' # For reading input
require 'json' # <--- REQUIRED FOR OUTPUT
require 'fileutils'
require 'pathname'
require 'net/http'
require 'uri'
require 'erb'
if ! ENV['IMPORT_INSTANCES']
exit 0 # nothing to do
end
INPUT_ROOT_DIR = '/root/instances'
OUTPUT_ROOT_DIR = '/mnt/instances'
FileUtils.mkdir_p(OUTPUT_ROOT_DIR)
puts "scanning yaml-files in /root/instances"
# 2. Iterate over all 'room.yaml' files recursively
Dir.glob(File.join(INPUT_ROOT_DIR, '**', 'room.yaml')).each do |input_file_path|
begin
input_data = YAML.load_file(input_file_path)
rescue StandardError => e
puts " ❌ Error reading or parsing YAML: #{e.message}"
next
end
relative_path = Pathname.new(input_file_path).relative_path_from(Pathname.new(INPUT_ROOT_DIR))
path_components = relative_path.to_s.split(File::SEPARATOR)
unless path_components.length >= 4
puts " ⚠️ Skipping: Path structure too shallow. Expected SERVER/AUTHOR/ROOM_NAME/room.yaml"
next
end
server_name = path_components[0]
author_name = path_components[1]
room_name = path_components[2]
begin
u = input_data['url']
r = Net::HTTP.new(URI.parse(u).host, URI.parse(u).port)
r.use_ssl = (URI.parse(u).scheme == 'https')
a = r.request(Net::HTTP::Head.new(URI.parse(u).request_uri))
if a.code.to_i != 200
puts " ⚠️ Skipping:HTTP code #{a.code} received :/"
next
end
# 5. Construct the final data structure (Hash)
output_data = {}
output_data['homepage'] = input_data['url']
output_data['name'] = input_data['title']
output_data['description'] = input_data['description']
output_data['image'] = input_data['thumbnail']
output_data['tags'] = [server_name]
output_dir = File.join(OUTPUT_ROOT_DIR, author_name, room_name)
output_file_path = File.join(output_dir, 'datapackage.json')
begin
url = URI(input_data['thumbnail'])
output_path = output_dir+"/.xrforge/thumbnail.jpg"
Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == "https") do |http|
request = Net::HTTP::Get.new(url)
http.request(request) do |response|
raise "Download failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
File.open(output_path, "wb") do |file|
response.read_body do |chunk|
file.write(chunk)
end
end
end
end
if a.code.to_i != 200
puts " ⚠️ Skipping:HTTP code #{a.code} receivedfor thumbnail :/"
next
end
# write datapackage.json
FileUtils.mkdir_p(output_dir) # Create directory (including parents)
File.write(output_file_path, JSON.pretty_generate(output_data))
puts " ✅ HTTP 200: Wrote #{output_file_path}"
# write janusxr.html + scene.jml
templateDir = File.dirname(__FILE__)+"/../../templates/"
FileUtils.mkdir_p(output_dir+"/.xrforge") # Create directory (including parents)
data = output_data
data['title'] = output_data['name']
$links = []
janusweb_src = ENV['DEV'] ? "/mnt/janusweb/janusweb.js" : "/mnt/janusweb/janusweb.min.js"
jml = ERB.new( File.read( templateDir+"JML/portalAR.erb" ) ).result(binding)
html = ERB.new( File.read( templateDir+"JML/client.html") ).result(binding)
File.write(output_dir+"/.xrforge/scene.jml", jml )
puts " ✅ Wrote #{output_dir}/.xrforge/scene.jml"
File.write(output_dir+"/.xrforge/janusxr.html", html )
puts " ✅ Wrote #{output_dir}/.xrforge/janusxr.html"
rescue StandardError => e
puts " ❌ Error writing output file: #{e.message}"
end
rescue StandardError => e
# Optionally handle error quietly or output e.message
end
end

View file

@ -1,2 +0,0 @@
#!/bin/sh
/manyfold/cli/manyfold.sh init_database

View file

@ -1,3 +1,13 @@
#!/bin/sh
test -n "$RUNTESTS" || exit 0
exec /test/runtests.sh
test -z "$RUNTESTS" && exit 0 # nothing to do
echo ""
echo "[!] RUNTESTS=1 was set "
echo "[.] running tests in /test/*"
echo ""
find -L /test/* -type f -executable -maxdepth 1 | while read testscript; do
echo "[.] test: "$testscript
$testscript "$@" 2>&1 | awk '{ print " | "$0 }'
done

View file

@ -1,3 +0,0 @@
#!/bin/sh
# remove succesful tasks
ts | awk '$4 == 0 { print $1 }' | xargs -n1 ts -r

View file

@ -1,6 +0,0 @@
#!/bin/sh
dir="$(dirname $1)/.xrforge"
mkdir -p "$dir" || true
cd "$dir"
echo "[v] reset log.txt"
date > log.txt

View file

@ -1,82 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
require 'base64'
require 'json'
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( dir )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
data['resources'].each do |resource|
ext = File.extname(resource['path'])
filenameWithoutExt = File.basename(resource['path'], ext)
if ! XRForge::MODEL_EXT.any?( ext )
next # skip unknown 3d files
end
if ! resource['path'].match(/^[a-zA-Z0-9]/)
next # skip filenames like '_generated.glb' e.g. (produced by hooks)
end
XRForge.log("✅ unpacking textures", logfile)
gltf_path = ".xrforge/#{filenameWithoutExt}.gltf"
system("assimp export #{resource['path']} #{gltf_path} --embed-textures")
Dir.chdir("#{dir}/.xrforge")
system("assimp extract #{filenameWithoutExt}.gltf | sed 's|/.*/||g'")
Dir.chdir(dir)
# Read and parse the GLTF JSON
gltf = JSON.parse(File.read(gltf_path))
# Ensure images section exists
unless gltf['images'] && gltf['images'].is_a?(Array)
puts "✅ No 'images' array found in #{gltf_path}"
next
end
# Iterate through the images array
gltf['images'].each_with_index do |img, i|
imgExts = ["jpg","png"]
new_filename = "#{dir}/#{filenameWithoutExt}_img#{i}"
old_filename = "#{dir}/.xrforge/#{filenameWithoutExt}_img#{i}"
imgExts.each do |imgExt|
# move file to modeldirectory (but dont overwrite if user overwrite it)
if File.exist?("#{old_filename}.#{imgExt}") && !File.exist?("#{new_filename}.#{imgExt}")
puts "✅ Renaming #{old_filename}.#{imgExt} -> #{new_filename}.#{imgExt}"
File.rename( "#{old_filename}.#{imgExt}", "#{new_filename}.#{imgExt}" )
else
if File.exist?("#{new_filename}.#{imgExt}")
puts "✅ Not overwriting (useruploaded) #{new_filename}.#{imgExt}"
end
end
end
end
end
XRForge.log(" ", logfile)
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,80 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
require 'base64'
require 'json'
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( dir )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
update = false
data['resources'].each do |resource|
ext = File.extname(resource['path'])
filenameWithoutExt = File.basename(resource['path'], ext)
if ! XRForge::MODEL_EXT.any?( ext )
next # skip unknown 3d files
end
if ! resource['path'].match(/^[a-zA-Z0-9]/)
next # skip filenames like '_generated.glb' e.g. (produced by hooks)
end
XRForge.log("✅ compiling textures", logfile)
gltf_path = ".xrforge/#{filenameWithoutExt}.gltf"
# Read and parse the GLTF JSON
gltf = JSON.parse(File.read(gltf_path))
# Ensure images section exists
unless gltf['images'] && gltf['images'].is_a?(Array)
XRForge.log("✅ No 'images' array found in #{gltf_path}",logfile)
next
end
# Iterate through the images array
gltf['images'].each_with_index do |img, i|
# move file to modeldirectory (but dont overwrite if user overwrite it)
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)
img['uri'] = new_filename # NOTE: editing uri will cause assimp to drop image['name'] when exporting :/
update = true
end
end
end
if update
File.write( gltf_path, JSON.pretty_generate(gltf) )
XRForge.log("✅ writing #{resource['path']}",logfile)
system("assimp export #{gltf_path} #{resource['path']} --embed-textures")
end
end
XRForge.log(" ", logfile)
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,52 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
require 'base64'
require 'json'
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
ext = File.extname(filename)
filenameWithoutExt = File.basename(filename, ext)
if ! XRForge::MODEL_EXT.any?( ext ) || ext == ".gltf"
exit(0) # not a 3d file
end
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ generating gltf", logfile)
gltf_path = ".xrforge/scene.gltf"
system("assimp export #{filename} #{gltf_path} --embed-textures")
# tag it!
if ! data['keywords'].include?('gltf')
data['keywords'].push('gltf')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
XRForge.log(" ", logfile)
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,61 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/datapackage.json>"
exit 1
end
filename = ARGV[0]
begin
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
exit 0
end
# Change the directory
dir = File.dirname(filename)
dirPublic = dir.gsub("/mnt/","").gsub("#","%23")
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting generating audiovisual experience", logfile)
# Check if a model file was found after the loop
#model_file = XRForge.getExperienceFile(data,dir,logfile)
#if ! model_file
# XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile)
# exit
#end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
autogenerate = true
if ! data['description']
end
XRForge.log("✅ generated audiovisual experience", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('audiovisual')
data['keywords'].push('audiovisual')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,10 +0,0 @@
#!/bin/sh
dir="$(dirname $1)"
cd "$dir"
echo "[package_godot_zip.sh] zipping godot.zip"
# overwrite empty godot template project-zip with given URL
test -n "$GODOT_TEMPLATE_ZIP" && timeout 50 wget "$GODOT_TEMPLATE_ZIP" -O ~/templates/template_godot.zip
cp ~/templates/template_godot.zip .xrforge/godot.zip
zip .xrforge/godot.zip *.glb *.usdz *.obj

View file

@ -1,210 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require 'erb'
require 'cgi'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/datapackage.json>"
exit 1
end
filename = ARGV[0]
JMLHeuristic = /<fireboxroom>.*?<\/fireboxroom>/im
metadataRegex= /(^<!--|<fireboxroom).*/im
templateDir = File.dirname(__FILE__)+"/../../templates/"
$links = []
def addLink(url)
$links.push(url)
return url
end
begin
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
exit 0
end
# Change the directory
dir = File.dirname(filename)
dirPublic = dir.gsub("/mnt/","").gsub("#","%23")
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
#if data['keywords'].empty? || data['keywords'].include?('janusxr')
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting build janusXR XR scene", logfile)
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Check if a model file was found after the loop
if ! model_file
XRForge.log("❌ No suitable 3D file found for JanusXR-compatible experience", logfile)
end
autogenerate = true
janusweb_src = ENV['DEV'] ? "/janusweb/janusweb.js" : "/janusweb/janusweb.min.js"
if data['description'] && data['description'].match(JMLHeuristic)
if ! data['description'].match(/<room.* autogenerate.*>/i)
XRForge.log("✅ autogenerate attribute found in JML..keeping this JML",logfile)
autogenerate = false
jmlMatch = data['description'].match(JMLHeuristic)
jml = jmlMatch[0]
end
end
if autogenerate
assets = ""
objects = ""
startpos = ""
audiovisual = false
showavatar = "showavatar=\"false\""
data['keywords'] = data['keywords'] || []
data['description'] = data['description'] || ""
if data['description'].strip().length == 0
data['description'] = "Click the edit-button to change this description, or add the 'nodescription' tag to hide this."
end
# tags to JML rooms
use_local_asset = ""
use_local_asset = data['keywords'].include?('room1') ? "room1" : use_local_asset
use_local_asset = data['keywords'].include?('room2') ? "room2" : use_local_asset
use_local_asset = data['keywords'].include?('room3') ? "room3" : use_local_asset
use_local_asset = data['keywords'].include?('room4') ? "room4" : use_local_asset
use_local_asset = data['keywords'].include?('room5') ? "room5" : use_local_asset
use_local_asset = data['keywords'].include?('room6') ? "room5" : use_local_asset
use_local_asset = data['keywords'].include?('room1_pedestal') ? "room1_pedestal" : use_local_asset
use_local_asset = data['keywords'].include?('room2_pedestal') ? "room2_pedestal" : use_local_asset
use_local_asset = data['keywords'].include?('room2_narrow') ? "room3_narrow" : use_local_asset
# private room or not
if data['keywords'].include?('singleuser')
private = "private=\"true\""
data['keywords'] = data['keywords'] || []
showavatar = "showavatar=\"false\""
end
if data['keywords'].include?('showavatar')
showavatar = "showavatar=\"false\""
end
# detect audio track sidecarfile as per XR Fragment spec: https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
xrf_ext = model_file ? File.extname(model_file) : ""
xrf_base = model_file ? File.basename(model_file, xrf_ext) : ""
# remove the metadata from the description
description = CGI.escapeHTML(
data['description'].gsub(metadataRegex,"")
.strip()
)
data['resources'].each do |resource|
ext = File.extname(resource['path'])
if ext.match(/(mp3|ogg)/i)
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
soundtrackUrl = addLink("#{resource["path"]}")
assets += " <assetsound id=\"soundtrack\" src=\"#{soundtrackUrl}\"/>\n"
objects += "<sound id=\"soundtrack\" loop=\"true\"/>\n"
end
if ext.match(/(png|jpg|jpeg|gif)/i)
imgbase = File.basename(resource['path'])
if File.basename( data['image'] || "") != imgbase && # ignore thumbnail
xrf_base != imgbase # ignore XR Fragments thumbnail
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
videoSrc = addLink("#{resource["path"]}")
assets += "<assetimage id=\"image\" src=\"#{videoSrc}\" loop=\"true\" auto_play=\"true\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"image\" lighting=\"false\" pos=\"0 6.5 5.5\" scale=\"4 4 1\" image_id=\"image\"/>\n"
end
end
if ext.match(/mp4/i) || ext.match(/webm/i)
audiovisual = model_file ? false : true
base = File.basename(resource['path'], ext)
videoSrc = addLink("#{resource["path"]}")
assets += "<assetvideo id=\"video\" src=\"#{videoSrc}\" loop=\"true\" auto_play=\"true\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"video\" lighting=\"false\" pos=\"0 6.5 5.5\" scale=\"4 4 1\" video_id=\"video\"/>\n"
end
end
if audiovisual
# TODO: copy files from a heuristic-based folder instead hardcoding it here
startpos = "pos=\"0 0 -10\""
use_local_asset = "room5"
rplaneSrc = addLink("roundedplane.glb")
system("cp /root/templates/audiovisual/roundedplane.glb #{dir}/.")
assets += " <assetobject id=\"rplane\" src=\"#{rplaneSrc}\"/>\n"
objects += " <Object cull_face=\"none\" id=\"rplane\" js_id=\"text_rplane\" lighting=\"false\" pos=\"2.69 1.84 -3.37\" rotation=\"0 45 0\" scale=\"1.6 1 1\"/>\n"
if ! data['keywords'].include?( use_local_asset )
data['keywords'].push( use_local_asset )
end
end
# generate thumbnail if no thumbnail
if data['resources'].length == 1
thumbfile = "thumbnail.jpg"
if model_file
thumbfile = "#{xrf_base}.jpg"
end
system("/root/bin/random_thumbnail.sh","/root/templates/thumbnailseed.jpg","#{dir}/#{thumbfile}","6","2>&1 | sed 's|/root/template||g'")
data['image'] = thumbfile
XRForge.log("✅ generated thumbnail", logfile)
end
if description && !data['keywords'].include?('nodescription')
objects += " <paragraph pos=\"2.8 1 -3.21\" fwd=\"0 0 1\" lighting=\"false\" rotation=\"180 -45 180\" col=\"0.5 0.8 0.5\" scale=\"2 2 2\" font_size=\"50\" locked=\"false\">#{description}</paragraph>\n"
end
if model_file
objectSrc = "#{model_file.gsub("#","%23")}"
if ! $links.include?(objectSrc)
objectSrc = addLink(objectSrc)
assets += " <assetobject id=\"experience\" src=\"#{objectSrc}\"/>\n"
objects += " <object pos=\"0 0 5\" rotation=\"-180 0 180\" lighting=\"false\" collision_id=\"experience\" id=\"experience\" />"
end
end
use_local_asset = use_local_asset ? "use_local_asset=\"#{use_local_asset}\"" : ""
jml = ERB.new( File.read( templateDir+"JML/room.erb" ) ).result(binding)
data['description'] = data['description'] ? data['description'].gsub(metadataRegex,"") : ""
data['description'] += "\n\n" + ERB.new( File.read( templateDir+"JML/header.erb" ) ).result(binding)
data['description'] += jml
File.write("datapackage.json", JSON.pretty_generate(data) )
end
html = ERB.new( File.read( templateDir+"JML/client.html") ).result(binding)
File.write('.xrforge/janusxr.html', html)
File.write('.xrforge/scene.jml', jml)
XRForge.log("✅ generated scene.jml", logfile)
XRForge.log("✅ generated janusxr.html", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('janusxr')
data['keywords'].push('janusxr')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,66 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
exit 0
end
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
#if data['keywords'].empty? || data['keywords'].include?('mml')
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting build mml XR scene", logfile)
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Check if a model file was found after the loop
if ! model_file
XRForge.log("❌ No suitable 3D file found for MML-compatible experience", logfile)
exit 0
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
# 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>
MML
File.write('.xrforge/scene.mml', mml)
XRForge.log("✅ generated scene.mml", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('mml')
data['keywords'].push('mml')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,60 +0,0 @@
#!/usr/bin/env ruby
require 'json'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( dir )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting XR fragments check", logfile)
model_file = XRForge.getExperienceFile(data,dir,logfile)
# Check if a model file was found after the loop
if ! model_file
XRForge.log("❌ No suitable 3D file found for XR Fragments-compatible experience", logfile)
exit 0
end
# Check if a model file was found after the loop
if model_file
XRForge.log("✅ Final model file: '#{model_file}'", logfile)
# update datapackage
data['main'] = model_file
if ! data['keywords'].include?('xrfragments')
data['keywords'].push('xrfragments')
end
File.write("datapackage.json", JSON.pretty_generate(data) )
system("assimp export #{model_file} .xrforge/scene.gltf")
else
XRForge.log("❌ No suitable 3D file found for XR Fragments-compatible experience", logfile)
# update datapackage
if data['keywords'].include?('xrfragments')
data['keywords'].delete('xrfragments')
File.write("datapackage.json", JSON.pretty_generate(data) )
end
end
XRForge.log("✅ updating xrfragment tag", logfile)
XRForge.log(" ", logfile)
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -1,93 +0,0 @@
#!/usr/bin/env ruby
require 'rss'
require 'json'
require 'open-uri'
require 'cgi'
require_relative './../../xrforge.rb'
# Check if a filename is provided
if ARGV.length != 1
puts "Usage: #{$0} <path/to/experience/somefile.xxx>"
exit 1
end
filename = ARGV[0]
# dont run for each file-update
if ! filename.end_with?("datapackage.json")
exit 0
end
def generate(aphandle,filename,dir,data,logfile)
XRForge.log("✅ starting mastodon-post for #{aphandle}", logfile)
parts = aphandle.split("@")
server = parts[2].sub(/@/,"")
rssUrl = "https://#{server}/@#{parts[1]}.rss"
XRForge.log("✅ checking #{rssUrl}", logfile)
feed = RSS::Parser.parse(URI.open(rssUrl, 'User-Agent' => 'Ruby-RSS-Client'))
puts feed.channel.title
puts feed.channel.link
first_item = feed.items.first
if first_item
description = CGI.unescapeHTML(first_item.description.to_s)
.split("\n")[0,4].join("\n") # max 5 lines
.gsub(/<[^>]*>/, '') # remove other tags
#.gsub(/<br\s*\/?>/i, "\n") # preserve linebreaks
#.gsub(/<\/p>/i, "\n") # preserve linebreaks
description = description.length > 130 ? description = description[0,130] + " (..)" : description
else
XRForge.log("❌ did not find post", logfile)
exit 0
end
puts description
# look for first image
media_regex = /<media:content url=['"]([^'"]+)['"]/
img = feed.to_s.match(media_regex)
if img and img[1] and img[1].match(/(png|jpg)/)
imgurl = img[1]
end
# generate the final .glb
system("/root/templates/mastodon-post/mastodon-post.sh", description, feed.channel.title, feed.channel.link, aphandle, "#{dir}/#{aphandle}.glb")
end
begin
# Change the directory
dir = File.dirname(filename)
Dir.chdir( dir )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
data['keywords'].each do |tag|
puts tag
if tag.match?(/^@.*@.*\./) # scan for activitypub handle (@foo@mastodon.online e.g.)
generate(tag,filename,dir,data,logfile)
end
end
XRForge.log(" ", logfile)
rescue OpenURI::HTTPError => e
# Handle HTTP errors (e.g., 404 not found, 403 forbidden)
puts "Error fetching feed: #{e.message}"
exit
rescue => e
# Handle other parsing or connection errors
puts "An error occurred: #{e.message}"
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -0,0 +1 @@
../hourly/placeholder.sh

View file

@ -0,0 +1,3 @@
#!/bin/sh
test -f "$1".zip && rm "$1".zip
echo "[cleanup_package.sh] deleting $dir.zip"

View file

@ -0,0 +1,6 @@
#!/bin/sh
echo "$1" | grep datapackage || exit 0 # nothing to do
dir=$(dirname $1)
cd "$dir"
echo "[package_experience.sh] zipping $dir.zip"
zip -r "$dir".zip $dir/*

View file

@ -0,0 +1,11 @@
#!/bin/sh
echo "$1" | grep datapackage || exit 0 # nothing to do
dir=$(dirname $1)
cd "$dir"
echo "[package_experience.sh] zipping $dir.zip"
# overwrite empty godot template project-zip with given URL
test -n "$GODOT_TEMPLATE_ZIP" && timeout 50 wget "$GODOT_TEMPLATE_ZIP" -O ~/template_godot.zip
cp ~/template_godot.zip package_godot.zip
zip package_godot.zip *.glb *.usdz *.obj

View file

@ -0,0 +1 @@
inotify_MODIFY

View file

@ -1,4 +0,0 @@
url: https://vesta.janusxr.org/spyduck/lains-bedroom-serial-experiments-lain
title: "Lain's Bedroom"
description: "Recreation of the protagonist's bedroom from Serial Experiments Lain"
thumbnail: https://vesta.janusxr.org/assets/thumbs/spyduck/c59d083f4c3e9fcde3f525d28d8acd69.jpg

View file

@ -1,62 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title><%=data['title']%> - JanusXR</title>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, maximum-scale=1.0, width=device-width" />
<meta name="apple-mobile-web-app-capable" content="yes">
<!--
<meta name="theme-color" content="#2f363b">
<meta http-equiv="origin-trial" data-feature="WebVR" data-expires="2017-06-12" content="Avy/Fo2QM5trR+WVHnaVz0t3LVltGxx3yvpSYSCC2oklwuDEYUEK6YdnxYv4p687MJGB61q//htZUvSIZPg93goAAABOeyJvcmlnaW4iOiJodHRwczovL3dlYi5qYW51c3ZyLmNvbTo0NDMiLCJmZWF0dXJlIjoiV2ViVlIiLCJleHBpcnkiOjE0OTczMTIwMDB9">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@bai0" />
<meta name="twitter:title" content="JanusWeb" />
<meta name="twitter:description" content="The World Within The Web" />
<meta name="twitter:image" content="https://janusxr.org/backgrounds/wallpapers/generic.png" />
<meta name="twitter:image:alt" content="JanusWeb" />
<meta property="og:app_id" content="1197654320349894" />
<meta property="og:url" content="https://web.janusxr.org/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="JanusWeb" />
<meta property="og:description" content="The World Within The Web" />
<meta property="og:image" content="https://janusxr.com/backgrounds/wallpapers/generic.png" />
-->
</head>
<body>
<janus-viewer homepage="/" autostart="false" showavatar="false">
<%=jml%>
</janus-viewer>
<!-- archive.org hints -->
<% $links.each do |link| %>
<a href="<%=link%>"></a>
<% end %>
<!-- map URL query args to room-attributes -->
<script>
args = new URLSearchParams(document.location.search)
for( const [k,v] of args.entries()) document.querySelector("Room").setAttribute(k,v)
</script>
<script src="<%=janusweb_src%>"></script>
<script>
let serveruri = "ws://"+document.location.hostname+":5566"
let corsproxy = document.location.origin.replace(/:[0-9].*/,'')+":5577/"
if( document.location.protocol.match(/https/) ){
corsproxy = corsproxy.replace(/\/\//,"//cors.").replace(/:[0-9].*/,"/")
serveruri = serveruri.replace(/ws:\/\//,"wss://presence.").replace(/:[0-9].*/,"")
}
elation.config.set("engine.assets.corsproxy", corsproxy )
elation.config.set("janusweb.network.host", serveruri )
elation.janusweb.init({
uiconfig: `/janusweb/media/assets/webui/${ args.get("networking") == "false" ? "preview" : "default"}.json`,
// for more opts see getClientArgs() in
// https://github.com/jbaicoianu/janusweb/blob/master/scripts/client.js
})
.then( (client) => {
elation.janusweb.init = function(){} // prevent multi-inits
})
</script>
</body>
</html>

View file

@ -1,9 +0,0 @@
<!-- Hi there! Below is autogenerated JanusXR Markup (JML). -->
<!-- If you want to tweak it, then first disable autogeneration -->
<!-- How? remove the 'autogenerate' attribute from the <Room> tag below -->
<!-- Please note: the tags will **no longer** reflect this JML -->
<!-- -->
<!-- JML info: -->
<!-- https://coderofsalvation.github.io/janus-guide/#/examples/markup -->
<!-- https://janusxr.org/docs/build/introtojml/index.html -->

View file

@ -1,8 +0,0 @@
<fireboxroom>
<Assets>
</Assets>
<Room pos="0 0 0" skybox="true" showavatar="false" use_local_asset="room_plane">
<link url="<%=data['url']%> col="1.0 0.0 1.0" title="<%=data['title']%> pos="0 0 5" scale="1.8 3.2 1"/>
<Paragraph pos="2.8 1 -3.21" fwd="0 0 1" lighting="false" rotation="180 -45 180" col="0.5 0.8 0.5" scale="2 2 2" font_size="50" locked="false"><%=data['description']%></Paragraph>
</Room>
</fireboxroom>

View file

@ -1,10 +0,0 @@
<FireBoxRoom>
<Assets>
<%=assets%>
<assetimage id="home" src="/view/home.png"/>
</Assets>
<Room autogenerate="true" <%=use_local_asset%> <%=private%> <%=showavatar%> <%=startpos%>>
<%=objects%>
<link url="/models" col="1.0 0.0 1.0" title="back to home" pos="0 0 -5" scale="1.8 3.2 1" image_id="home"/>
</Room>
</FireBoxRoom>

View file

@ -1,10 +0,0 @@
<FireBoxRoom>
<Assets>
<%=assets%>
<assetimage id="home" src="/view/home.png"/>
</Assets>
<Room autogenerate="true" <%=use_local_asset%> <%=private%> <%=showavatar%> <%=startpos%>>
<%=objects%>
<link url="/models" col="1.0 0.0 1.0" title="back to home" pos="0 0 -5" scale="1.8 3.2 1" image_id="home"/>
</Room>
</FireBoxRoom>

View file

@ -1,15 +0,0 @@
<fireboxroom>
<assets>
<assetimage id="image" src="lobby.png"/>
<assetvideo auto_play="true" id="video" loop="true" src="video.mp4"/>
<assetobject id="rplane" src="roundedplane.glb"/>
</assets>
<room use_local_asset="room5" pos="0 0 -10">
<Object cull_face="none" id="rplane" js_id="video" lighting="false" pos="0 6.5 5.5" scale="4 4 1" video_id="video"/>
<Object cull_face="none" id="rplane" js_id="text_rplane" lighting="false" pos="2.69 1.84 -3.37" rotation="0 45 0" scale="1.6 1 1"/>
<Paragraph pos="2.8 1 -3.21" fwd="0 0 1" lighting="false" rotation="180 -45 180" col="0.5 0.8 0.5" scale="2 2 2" font_size="50" locked="false">
This is a template for audiovisual experiences. Use it for beautiful spatial music/video releases, by just upload/replacing an audio- or video-file.
</Paragraph>
</room>
</fireboxroom>
<title>Audiovisual</title>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,154 +0,0 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v4.4.56",
"version":"2.0"
},
"extensionsUsed":[
"KHR_materials_unlit"
],
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"mastodon-post",
"rotation":[
0.70710688829422,
0,
0,
0.7071066498756409
],
"scale":[
2.471118211746216,
2.471118450164795,
2.471118450164795
]
}
],
"materials":[
{
"doubleSided":true,
"extensions":{
"KHR_materials_unlit":{}
},
"name":"unlit.001",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.9
}
}
],
"meshes":[
{
"name":"Plane",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"mastodon-post",
"uri":"mastodon-post.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":20,
"max":[
1,
0,
1
],
"min":[
-1,
0,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":20,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":20,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5123,
"count":54,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":240,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":240,
"byteOffset":240,
"target":34962
},
{
"buffer":0,
"byteLength":160,
"byteOffset":480,
"target":34962
},
{
"buffer":0,
"byteLength":108,
"byteOffset":640,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":748,
"uri":"mastodon-post.bin"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 B

View file

@ -1,34 +0,0 @@
#!/bin/sh
test -n "$5" || { echo "usage: ./mastodon-post.sh <text> <title> <link> <handle> </abs/path/to/out.glb>"; exit 0; }
out="$5"
tmpdir=/tmp/.mastodon-post
outdir="$(dirname "$out")"
mydir="$(dirname $(readlink -f $0))"
bgfile="$outdir"/mastodon-post-bg.png
cd $mydir
set -x
# create tmp workspace
test -d $tmpdir && rm -rf $tmpdir
mkdir $tmpdir
cp * $tmpdir/.
# use template PNG from modeldir
test -f "$bgfile" || cp $tmpdir/mastodon-post-bg.png "$bgfile"
cp "$bgfile" $tmpdir/.
rm $tmpdir/mastodon-post.png # IMPORTANT: prevent newer/older imagemagick -clobber (non)existence
magick "$bgfile" \
\( -size 580x -background none -pointsize 32 -interline-spacing 10 -fill \#FFF -font Montserrat-SemiBold.ttf "caption:$1" \) \
-gravity center -composite \
-pointsize 20 \
-fill \#FFFFFF -gravity NorthWest -annotate +152+130 "$4" \
-fill \#9c9cc9 -gravity NorthWest -annotate +152+160 "$2" \
-fill \#9c9cc9 -gravity NorthWest -annotate +52+460 "$3" \
-define png:compression-level=9 \
-define png:compression-filter=5 \
-define png:compression-strategy=1 \
$tmpdir/mastodon-post.png
cd $tmpdir
assimp export mastodon-post.gltf $out --embed-textures

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,148 +0,0 @@
require 'rexml/document'
require 'rexml/formatters/pretty'
module XRForge
MODEL_EXT = ['.glb', '.gltf', '.blend', '.usdz', '.obj', '.dae', '.x3d']
def self.log(message, filename)
# Append the log entry to the log file
File.open(filename, 'a') do |file|
file.write("#{message}\n")
end
puts("#{message}\n")
end
# ==============================================================================
# 1. JSON (Ruby Hash) to XML String
# ==============================================================================
# Internal recursive helper for converting Hash/Array structure to REXML.
# Handles attribute conversion (@key -> attribute), hash nesting, and arrays
# (which create multiple sibling elements of the same name).
#
# @param data [Hash, Array, String] The data fragment.
# @param p [REXML::Element] The parent REXML element.
def self.j2x_rec(data, p)
# Check if data is a Hash (for attributes and children)
if data.is_a?(Hash)
data.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_rec(v, p.add_element(k)) }
# Check if data is an Array (for repeated elements)
elsif data.is_a?(Array)
# The parent in this context (e.g., <obj>) is already created. For each array item,
# we need to create a new element with the same name as the current parent's *name*
# and attach it to the parent's *parent*.
# This requires looking up the parent's parent, which breaks terseness.
# We must use a simpler recursive structure for compactness.
# We assume array structure requires us to create a new element named by the
# parent, and assign attributes/children recursively.
data.each { |v| j2x_rec(v, p) } # Recursive call to continue processing children
# Handle text content
else
p.text = data.to_s
end
end
# The entry point for converting Ruby Hash to XML string.
#
# @param data [Hash] The root hash structure.
# @return [String] The resulting compact XML string.
def self.json2xml(data)
# Corrected recursive logic for arrays that are values (e.g., "obj" => [..])
j2x_map = lambda do |d, p_name, p|
if d.is_a?(Hash)
d.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_map.call(v, k, p.add_element(k)) }
elsif d.is_a?(Array)
# If the value is an array, we iterate, create a sibling element for each, and recurse
d.each { |v| j2x_map.call(v, p.name, p.parent.add_element(p.name)) }
p.remove # Remove the placeholder element created before hitting the array
else
p.text = d.to_s
end
end
# Initialize Document and process the single root key
doc = REXML::Document.new
data.each { |k, v| j2x_map.call(v, k, doc.add_element(k)) }
# Output with 2-space indentation
o = ""; f = REXML::Formatters::Default.new; f.indentation = 2; f.write(doc.root, o); o.strip
end
# ==============================================================================
# 2. XML String to JSON (Ruby Hash)
# ==============================================================================
# Internal recursive helper for converting REXML Element to Hash structure.
# Handles attribute conversion (attribute -> @key), text, and array creation for siblings.
#
# @param e [REXML::Element] The XML element.
# @return [Hash, String] The resulting hash or simple text value.
def self.x2j_rec(e)
h = {}; e.attributes.each { |k, v| h["@#{k}"] = v } # Attributes first
e.elements.each do |c| # Iterate children
v = x2j_rec(c)
if h.key?(c.name)
h[c.name] = [h[c.name]] unless h[c.name].is_a?(Array)
h[c.name] << v
else
h[c.name] = v
end
end
# Check for text content if no children were processed
t = e.get_text.to_s.strip; return t if t && !t.empty? && h.empty?; return h
end
# The entry point for converting XML string to Ruby Hash.
#
# @param xmlstr [String] The XML string.
# @return [Hash] The resulting Ruby hash structure.
def self.xml2json(xmlstr)
doc = REXML::Document.new(xmlstr)
root = doc.root
{ root.name => x2j_rec(root) }
end
def self.getExperienceFile(datapackage, dir, logfile)
model_file = nil # Initialize model_file to nil
base_name = ""
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = datapackage['image']
if thumb_file && ! thumb_file.empty?
# HEURISTIC: XR Fragments sidecarfile https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
# Loop over the list of extensions
MODEL_EXT.each do |ext|
# Construct the filename with the current extension
filename = "#{base_name}#{ext}"
# Check if the file exists
if File.exist?(filename)
XRForge.log("✅ 3D file '#{filename}' detected (heuristic: XRFragment sidecarfile)", logfile)
model_file = "#{dir.gsub("/mnt/","")}/#{filename}" # Store the found filename
break # Stop the loop once a file is found
else
# Log a message for the file that was not found, but don't stop
XRForge.log("⚠️ 3D file '#{filename}' not detected", logfile)
end
end
else
# HEURISTIC: get first suitable 3D file
datapackage['resources'].each do |resource|
ext = File.extname(resource['path'])
if MODEL_EXT.include?( ext )
model_file = resource['path']
XRForge.log("✅ 3D file '#{model_file}' detected (heuristic: first-found)", logfile)
break
end
end
end
return model_file ? File.basename(model_file) : model_file
end
end

View file

@ -1,5 +0,0 @@
#!/bin/sh
which rclone &>/dev/null || { echo "❌ rclone not installed"; exit 1; }
test -d /mnt/experiences || { echo "❌ /mnt/models does not exist"; exit 1; }

View file

@ -1,24 +0,0 @@
#!/bin/sh
experience=/mnt/templates/xrfragments/\#4
dir=/tmp/hook-extract-textures
xrfdir=$dir/.xrforge
which assimp || {
echo "❌ assimp was not installed";
exit 1;
}
mkdir -p $xrfdir || true
rm $experience/*_img*.* || true
/root/hook.d/experience_updated/*-extract-textures.rb $experience/datapackage.json 2>&1 \
| grep "Wrote texture 0" || {
echo "$experience/murial3D_img0.png was not written";
exit 1;
}
test -f $experience/murial3D_img0.png || {
echo "$experience/murial3D_img0.png was not written";
exit 1;
}
rm -rf $dir
exit 0

View file

@ -1,18 +0,0 @@
#!/bin/sh
experience=/mnt/templates/xrfragments/\#4
dir=/tmp/hook-compile-textures
xrfdir=$dir/.xrforge
which assimp || {
echo "❌ assimp was not installed";
exit 1;
}
mkdir -p $xrfdir || true
/root/hook.d/experience_updated/*-compile-textures.rb $experience/datapackage.json 2>&1 | grep "wrote output" || {
echo "$experience/murial3D.glb was not updated";
exit 1;
}
rm -rf $dir
exit 0

View file

@ -1,12 +0,0 @@
#!/bin/sh
dir=/tmp/11-hook
xrfdir=$dir/.xrforge
mkdir -p $xrfdir
echo 1 > $xrfdir/log.txt
/root/hook.d/experience_updated/*-reset-log.sh $dir/datapackage.json
test "$(cat $xrfdir/log.txt)" = "1" && {
echo "❌ log.txt was not reset";
exit 1;
}
rm -rf $dir
exit 0

View file

@ -1,12 +0,0 @@
#!/bin/sh
dir=/tmp/hook-package_godot_zip
xrfdir=$dir/.xrforge
mkdir -p $xrfdir
touch $xrfdir/foo.glb
/root/hook.d/experience_updated/*-package_godot_zip.sh $dir/datapackage.json
test -f $xrfdir/godot.zip || {
echo "❌ godot.zip was not created";
exit 1;
}
rm -rf $dir
exit 0

View file

@ -1,36 +0,0 @@
#!/bin/sh
id=janusxr
dir=/tmp/hook-package_$id
xrfdir=$dir/.xrforge
mkdir -p $xrfdir
testjml(){
/root/hook.d/experience_updated/*-package_$id.rb $dir/datapackage.json
test -f $xrfdir/scene.jml || {
echo "❌ scene.jml was not created";
exit 1;
}
test -f $xrfdir/janusxr.html || {
echo "❌ janusxr.html was not created";
exit 1;
}
}
echo "### test xrfragment heuristic"
touch $dir/foo.png
touch $dir/foo.glb
echo '{
"image":"foo.png",
"resources": [{"path":"foo.glb"}]
}' > $dir/datapackage.json
testjml
echo "### test first-suitable-3d-file heuristic"
rm $dir/foo.png
echo '{
"resources": [{"path":"foo.glb"}]
}' > $dir/datapackage.json
testjml
rm -rf $dir
exit 0

View file

@ -1,15 +0,0 @@
#!/bin/sh
test $(ps aux | grep run-janus-server | wc -l) = 2 || {
echo "❌ janus-server is not running"
exit 1;
}
echo "✅ janus-server is running"
test $(ps aux | grep run-corsanywhere | wc -l) = 2 || {
echo "❌ corsanywhere is not running"
exit 1;
}
echo "✅ corsanywhere is running"

5
manyfold/test/rclone.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
which rclone &>/dev/null || { echo "[!] rclone not installed"; exit 0; }
test -d /mnt/models || { echo "[!] /mnt/models does not exist"; exit 0; }

View file

@ -1,16 +0,0 @@
#!/bin/sh
echo ""
echo "running tests in /test/*"
echo ""
error(){ echocolor "❌" "$*"; exit 1; }
ok(){ echocolor "✅" "$*"; return 0; }
echocolor(){ printf "\033[96m%s\033[0m \033[95m%s\033[0m %s\n" "$1" "$2" "$3"; }
find -L /test/* -type f -executable -maxdepth 1 | grep -v runtests | sort -V | while read testscript; do
echo "🛠 $testscript"
{
$testscript "$@" 2>&1 && ok "$testscript" || error "$testscript"
} | awk '{ print " | "$0 }'
done

View file

@ -105,22 +105,14 @@ class Components::ModelCard < Components::Base
def actions
div class: "card-footer" do
div class: "row" do
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
div class: "col" do
#open_button
#whitespace
status_badges @model
end
div class: "col col-auto" do
whitespace
BurgerMenu do
#DropdownItem(icon: "app", label: "Open in Godot Web" , path: "/godot/?url="+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/package_godot.zip", aria_label: translate("components.model_card.edit_button.label", name: @model.name), target: "_blank" )
DropdownItem(icon: "app", label: "Open in Godot Web" , path: "/godot/?url="+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/package_godot.zip", aria_label: translate("components.model_card.edit_button.label", name: @model.name), target: "_blank" )
DropdownItem(icon: "pencil", label: t("components.model_card.edit_button.text"), path: model_path(@model), aria_label: translate("components.model_card.edit_button.label", name: @model.name))
DropdownItem(icon: "trash", label: t("components.model_card.delete_button.text"), path: model_path(@model), method: :delete, aria_label: translate("components.model_card.delete_button.label", name: @model.name), confirm: translate("models.destroy.confirm")) if policy(@model).destroy?

View file

@ -1,87 +0,0 @@
# frozen_string_literal: true
class Components::PreviewFrame < Components::Base
include Phlex::Rails::Helpers::ImageTag
register_value_helper :policy_scope
def initialize(object:)
@object = object
end
def before_template
@file = @object.is_a?(Model) ? @object.preview_file : policy_scope(@object.models).first&.preview_file
end
def view_template
if @file
a href: "/view/index.html?profile=preview#janus.url="+model_url(@file.model), target: "_blank" do
local
end
elsif @object.remote?
remote
else
empty
end
end
private
def local
if @file.is_image?
image model_model_file_path(@file.model, @file, format: @file.extension, derivative: "preview"), @file.name
elsif @file.is_renderable?
div class: "card-img-top #{"sensitive" if needs_hiding?}" do
Renderer file: @file
end
else
empty
end
end
def remote
preview_data = @object.federails_actor&.extensions&.dig("preview")
case preview_data&.dig("type")
when "Image"
image preview_data["url"], preview_data["summary"]
when "Document"
div class: "card-img-top #{"sensitive" if needs_hiding?}" do
iframe(
scrolling: "no",
srcdoc: safe([
"<html><body style=\"margin: 0; padding: 0; aspect-ratio: 1\">",
preview_data["content"],
"</body></html>"
].join),
title: preview_data["summary"]
)
end
else
empty
end
end
def needs_hiding?
return false unless current_user.nil? || current_user.sensitive_content_handling.present?
case @object.class
when Model
@object.sensitive
when Collection
@file.model.sensitive
else
false
end
end
def empty
div class: "preview-empty" do
p { t("components.model_card.no_preview") }
end
end
def image(url, alt)
div class: "card-img-top card-img-top-background", style: "background-image: url(#{url})"
image_tag url, class: "card-img-top image-preview #{"sensitive" if needs_hiding?}", alt: alt, style: "position:absolute; top:0"
end
end

View file

@ -1,157 +0,0 @@
# app/controllers/cors_proxy_controller.rb
class CorsProxyController < ApplicationController
skip_after_action :verify_authorized
after_action :verify_policy_scoped
HOP_BY_HOP_HEADERS = %w[
connection keep-alive proxy-authenticate proxy-authorization
te trailers transfer-encoding upgrade
].freeze
# --- Main endpoint ---
def proxy
skip_authorization
return handle_options if request.options?
return handle_head if request.head?
puts "proxy: #{fetch_target_url}"
# GET only
target = fetch_target_url
return render status: 400, plain: "Missing target URL" unless target
uri = URI.parse(target)
upstream_response = forward_get(uri)
set_cors_response_headers
copy_headers_from_upstream(upstream_response)
render plain: upstream_response.body,
status: upstream_response.code.to_i
rescue URI::InvalidURIError
render status: 400, plain: "Invalid target URL"
end
private
# --- Allow only GET, HEAD, OPTIONS ---
def validate_allowed_method
allowed = %w[GET HEAD OPTIONS]
unless allowed.include?(request.method)
render status: 405, plain: "Method Not Allowed (only GET, HEAD, OPTIONS)"
end
end
# --- HEAD just returns headers with no body ---
def handle_head
target = fetch_target_url
return render status: 400, plain: "Missing target URL" unless target
uri = URI.parse(target)
upstream = forward_head(uri)
set_cors_response_headers
copy_headers_from_upstream(upstream)
head upstream.code.to_i
end
# --- OPTIONS preflight ---
def handle_options
set_cors_response_headers
headers['Access-Control-Allow-Methods'] = "GET, HEAD, OPTIONS"
headers['Access-Control-Allow-Headers'] =
request.headers['Access-Control-Request-Headers'] ||
"Origin, X-Requested-With, Content-Type, Accept"
headers['Access-Control-Max-Age'] = '86400'
head :no_content
end
# --- Extract target URL ---
def fetch_target_url
if params[:url].present?
params[:url]
elsif params[:target].present?
if request.query_string.present?
"#{params[:target]}?#{request.query_string}"
else
params[:target]
end
end
end
# --- Forward GET ---
def forward_get(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.open_timeout = 10
http.read_timeout = 30
req = Net::HTTP::Get.new(uri.request_uri)
copy_request_headers(req)
puts "URI:: #{uri.request_uri}"
http.request(req)
end
# --- Forward HEAD ---
def forward_head(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
req = Net::HTTP::Head.new(uri.request_uri)
copy_request_headers(req)
http.request(req)
end
# --- Copy incoming headers to outbound request ---
def copy_request_headers(req)
request.headers.each do |k, v|
next if k.start_with?('rack.') || k.start_with?('action_dispatch.')
name = normalize_header_name(k)
next if HOP_BY_HOP_HEADERS.include?(name.to_s.downcase)
next if name.to_s.downcase == 'host'
req[name] = v.to_s rescue nil
end
req['User-Agent'] ||= "Rails-CorsProxy"
end
def normalize_header_name(k)
return nil unless k
k = k.sub(/^HTTP_/, '') if k.start_with?("HTTP_")
k.split('_').map(&:capitalize).join('-')
end
# --- Copy upstream headers back to Rails ---
def copy_headers_from_upstream(up)
up.each_header do |k, v|
next if HOP_BY_HOP_HEADERS.include?(k.downcase)
response.headers[k] = v
end
end
# --- CORS headers ---
def set_cors_response_headers
origin = request.headers["Origin"] || "*"
headers["Access-Control-Allow-Origin"] = origin
headers["Vary"] = "Origin"
headers["Access-Control-Expose-Headers"] = "Content-Length, Content-Type, ETag"
end
# --- Origin validation (optional, can disable) ---
def validate_origin
# Always allow for now; add checks if needed
end
# --- Prevent open proxy usage (recommended) ---
def validate_target!
url = fetch_target_url
return if url.blank?
uri = URI.parse(url)
unless uri.scheme =~ /\Ahttps?\z/
render status: 400, plain: "Only http/https allowed"
end
end
end

View file

@ -1,141 +0,0 @@
module SupportedMimeTypes
def self.image_types
Mime::LOOKUP.filter { |k, v| is_image_mime_type?(v) }.values
end
def self.image_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_image_mime_type?(v) }.keys
end
def self.audio_types
Mime::LOOKUP.filter { |k, v| is_audio_mime_type?(v) }.values
end
def self.audio_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_audio_mime_type?(v) }.keys
end
def self.video_types
Mime::LOOKUP.filter { |k, v| is_video_mime_type?(v) }.values
end
def self.video_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_video_mime_type?(v) }.keys
end
def self.document_types
Mime::LOOKUP.filter { |k, v| is_document_mime_type?(v) }.values
end
def self.document_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_document_mime_type?(v) }.keys
end
def self.archive_types
Mime::LOOKUP.filter { |k, v| is_archive_mime_type?(v) }.values
end
def self.archive_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_archive_mime_type?(v) }.keys
end
def self.model_types
Mime::LOOKUP.filter { |k, v| is_model_mime_type?(v) }.values
end
def self.model_extensions
Mime::EXTENSION_LOOKUP.filter { |k, v| is_model_mime_type?(v) }.keys
end
def self.loadable
Mime::EXTENSION_LOOKUP.slice(
*Assimp.extension_list.to_s.delete("*.").split(";")
).values.map(&:to_sym)
end
def self.can_load?(type)
loadable.include? type
end
def self.exportable
Mime::EXTENSION_LOOKUP.slice(
*(0...Assimp.aiGetExportFormatCount).map { |it| Assimp.aiGetExportFormatDescription it }.map(&:file_extension)
).values.map(&:to_sym)
end
def self.can_export?(type)
exportable.include? type
end
def self.indexable_types
image_types + model_types + video_types + document_types + archive_types + audio_types
end
def self.indexable_extensions
image_extensions + model_extensions + video_extensions + document_extensions + archive_extensions + audio_extensions
end
class << self
private
def is_image_mime_type?(type)
exclusions = [
"image/vnd.dxf",
"image/vnd.dwg"
]
type.to_s.start_with?("image/") && exclusions.exclude?(type.to_s)
end
def is_audio_mime_type?(type)
type.to_s.start_with?("audio/")
end
def is_video_mime_type?(type)
type.to_s.start_with?("video/")
end
def is_document_mime_type?(type)
[
"application/pdf",
"text/markdown",
"text/plain",
"text/html",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/octet-stream",
"application/x-gerber",
"application/x-gerber-job",
"application/x-excellon",
"application/x-kicad-project",
"application/x-kicad-footprint",
"application/x-kicad-pcb",
"application/x-kicad-symbol",
"application/x-kicad-schematic",
"application/x-kicad-worksheet"
].include?(type.to_s)
end
def is_model_mime_type?(type)
extras = [
"text/x-gcode",
"application/x-openscad",
"image/vnd.dxf",
"image/vnd.dwg",
"application/x-3ds",
"application/x-amf",
"application/x-ldraw"
]
type.to_s.start_with?("model/") || extras.include?(type.to_s)
end
def is_archive_mime_type?(type)
[
"application/zip",
"application/gzip",
"application/vnd.rar",
"application/x-7z-compressed",
"application/x-bzip2"
].include?(type.to_s)
end
end
end

View file

@ -1,132 +0,0 @@
<nav class="navbar navbar-expand-md bg-primary" data-bs-theme="dark">
<div class='container-fluid'>
<a class="navbar-brand ms-2" href="<%= root_path %>" aria-label="<%= translate ".home" %>">
<%= image_tag site_icon, alt: site_name, height: "40px", class: "me-2" %>
<span class="d-md-none"><%= site_name %></span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbarTogglerDemo01" aria-expanded="false" aria-label="<%= translate ".navbar.toggler.label" %>">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse row" id="navbar">
<ul class="navbar-nav col ps-4 ps-md-0 align-self-start">
<li class="nav-item">
<%= nav_link "box", Model.model_name.human(count: 100), models_path(@filter&.to_params), text_style: "d-md-none d-lg-inline" %>
</li>
<li class="nav-item">
<%= nav_link "people", Creator.model_name.human(count: 100), creators_path(@filter&.to_params(except: :creator)), text_style: "d-md-none d-lg-inline" %>
</li>
<li class="nav-item">
<%= nav_link "collection", Collection.model_name.human(count: 100), collections_path(@filter&.to_params(except: :collection)), text_style: "d-md-none d-lg-inline" %>
</li>
<% if SiteSettings.show_libraries %>
<% policy_scope(Library).find_each do |library| %>
<li class="nav-item">
<%= nav_link(
library.icon.presence || "boxes",
library.name,
models_path({library: library}.merge(@filter&.to_params(except: :library) || {})),
title: library.caption
) %>
</li>
<% end %>
<% end %>
<li class="nav-item">
<a class="nav-link " href="/about">
<span><i class="bi bi-question-circle" role="img" title="About"></i></span>
<span class="d-md-none d-lg-inline">About</span>
</a>
</li>
</ul>
<ul class="navbar-nav col-auto pe-4 align-self-start justify-content-end">
<li class="nav-item">
<% if policy(:scan).create? %>
<% if @scan_in_progress %>
<%= nav_link "", t(".scanning"), activity_path, nofollow: true, style: "btn btn-outline-warning btn-sm me-1 mt-lg-1", icon_style: "spinner-border spinner-border-sm" %>
<% else %>
<div class="btn-group">
<button type="button" data-bs-toggle="dropdown" aria-expanded="false"
class="btn btn-warning btn-sm mt-1 me-1 dropdown-toggle">
<%= render Components::Icon.new(icon: "radar") %>
<%= t ".scan" %>
</button>
<ul class="dropdown-menu">
<li><%= link_to t(".scan_changes"), scans_path, method: :post, nofollow: true, class: "dropdown-item" %></li>
<li><%= link_to t(".check_existing"), scans_path(type: :check), method: :post, nofollow: true, class: "dropdown-item" %>
<li><%= link_to t(".check_results"), scans_path({type: :check}.merge(@filter.to_params)), method: :post, nofollow: true, class: "dropdown-item" if @filter&.any? %>
</ul>
</div>
<% end %>
<% end %>
</li>
<li class="nav-item">
<% if policy(:upload).index? %>
<div class="btn-group">
<%= link_to "Create", new_model_path, nofollow: true, class: "btn btn-secondary", style: "margin-right:7px" %>
<!--
<button type="button" data-bs-toggle="dropdown" aria-expanded="false"
class="btn btn-warning btn-sm mt-1 me-1 dropdown-toggle">
<%= render Components::Icon.new(icon: "plus-circle") %>
<%= t ".add_models" %>
</button>
<ul class="dropdown-menu">
<li><%= link_to t(".upload"), new_model_path, nofollow: true, class: "dropdown-item" %></li>
<li><%= link_to t(".import_url"), new_import_path, nofollow: true, class: "dropdown-item" %>
</ul>
-->
</div>
<% end %>
</li>
<%- if current_user %>
<% if policy(Problem).index? && Problem.visible(problem_settings).count > 0 %>
<li class="nav-item">
<% severity = max_problem_severity(policy_scope(Problem).all) %>
<%= nav_link problem_icon(severity),
Problem.model_name.human(count: 100),
problems_path,
title: translate("problems.severities.#{severity}"), # rubocop:todo I18n/RailsI18n/DecorateStringFormattingUsingInterpolation
icon_style: "link-#{severity}",
text_style: "d-md-none",
aria_label: Problem.model_name.human(count: 100) %>
</li>
<% end %>
<% if policy(:activity).index? %>
<li class="nav-item">
<%= nav_link "activity", t(".activity"), activity_path, nofollow: true, text_style: "d-md-none" %>
</li>
<% end %>
<% if policy(:settings).index? %>
<li class="nav-item">
<%= nav_link "gear", t(".settings"), settings_path, nofollow: true, text_style: "d-md-none" %>
</li>
<% end %>
<% if policy(:user).index? and not policy(:settings).index? %>
<li class="nav-item">
<%= nav_link "people", t(".moderator_settings"), settings_users_path, nofollow: true, text_style: "d-md-none" %>
</li>
<% end %>
<% end %>
<%- unless current_page?(root_path) %>
<li id="nav-search" class="nav-item ms-1 me-3" data-turbo-permanent>
<%= form_with url: models_path, method: :get, role: "search" do |f| %>
<%= f.search_field :q, class: "form-control", placeholder: translate(".search"), aria_label: translate(".search"), aria_describedby: "button-search", value: @query || params[:q] %>
<% end %>
</li>
<% end %>
<%- if current_user %>
<li class="nav-item">
<%= nav_link "sliders", t(".account"), edit_user_registration_path, nofollow: true, text_style: "d-md-none" %>
</li>
<% end %>
<%- if SiteSettings.multiuser_enabled? %>
<li class="nav-item">
<%- if current_user %>
<%= nav_link "box-arrow-right", t(".log_out"), destroy_user_session_path, method: :delete, title: translate(".log_out"), text_style: "d-md-none" %>
<% else %>
<%= nav_link "box-arrow-in-right", t(".log_in"), new_user_session_path, title: translate(".log_in"), nofollow: true, style: "btn btn-secondary" %>
<% end %>
</li>
<%- end %>
</ul>
</div>
</div>
</nav>

View file

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="<%= I18n.locale %>" data-controller="i18n">
<head>
<title><%= @title || site_name %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= tag.meta name: "csp-nonce", content: content_security_policy_nonce if content_security_policy_nonce %>
<%= favicon_link_tag "roundel.svg" %>
<%= tag.link rel: "apple-touch-icon", href: asset_path("square-180.png") %>
<%= tag.meta name: "apple-mobile-web-app-title", content: site_name %>
<%= javascript_include_tag "application", nonce: true, defer: true %>
<%= stylesheet_link_tag "themes/#{SiteSettings.theme}", nonce: true %>
<%= stylesheet_link_tag "/assets/xrforge.css" %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= tag.meta name: "robots", content: @indexing_directives if @indexing_directives.presence %>
<%= yield :head %>
</head>
<body>
<%= skip_link "content", t(".skip_to_content") %>
<%= render "application/navbar" %>
<%= yield :breadcrumbs %>
<main class="container-fluid" id="content">
<div>
<% if notice %>
<p class="alert alert-info">
<%= icon "info-circle-fill", t(".alert.info") %>
<%= notice %>
</p>
<% end %>
<% if alert %>
<p class="alert alert-danger">
<%= icon "x-octagon-fill", t(".alert.danger") %>
<%= alert %>
</p>
<% end %>
</div>
<div class="pt-3">
<%= yield %>
</div>
</main>
<%= render "application/footer" %>
</body>
</html>

View file

@ -1,17 +0,0 @@
<%= yield :page_header %>
<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>
<%= yield :items %>
</div>
<div class="col-md-3" id="sidebar">
<% action_content = yield :actions %>
<%= card(:secondary, t(".actions_heading"), class: "action-card") { action_content } if action_content.present? %>
<%= yield :sidebar %>
</div>
</div>
<% parent_layout "application" %>

View file

@ -49,11 +49,6 @@
<%= icon "bar-chart-line-fill", ModelFile.human_attribute_name(:presupported) %>
<% end %>
<%= problem_icon_tag(file.problems) if policy(Problem).show? %>
<!-- archive.org JanusXR verbatim asset hints -->
&nbsp;
<a href="<%=model_path(@model)+"/model_files/"+file.filename%>"><i class="bi bi-link-45deg"></i></a>
</div>
<div class="col col-auto">
<div class="float-end">

View file

@ -1,52 +0,0 @@
<%= form_with model: @model do |form| %>
<%= text_input_row form, :name %>
<%= render "tags_edit", form: form, name: "model[tag_list]", value: (@model.tags.order(taggings_count: :desc, name: :asc).map { |tag| tag.name }).join(","), label: translate(".tags"), tags: @available_tags %>
<div style="margin-left: 97px; max-height: 333px; overflow: scroll; margin-bottom: 50px; border: 1px solid #CCC; border-radius: 5px; margin-right:10px; background:black; padding:20px;">
<b>Available tags:</b><br>
<%= render 'models/tags_info' %>
</div>
<%= collection_select_input_row form, :preview_file, @model.valid_preview_files, :id, :name, help: t(".preview_file.help") %>
<%= collection_select_input_row form,
:creator, @creators, :id, :name_with_domain,
include_blank: true,
selected: @default_creator&.id,
button: (if policy(:creator).new?
{
path: new_creator_path,
label: t("creators.general.new")
}
end) %>
<% if SiteSettings.show_libraries || current_user.is_administrator? %>
<%= unless @model.contains_other_models?
collection_select_input_row form,
:library, policy_scope(Library).all, :id, :name,
include_blank: true
end %>
<% end %>
<%= collection_select_input_row form,
:collection, @collections, :id, :name_with_domain,
include_blank: true,
button: (if policy(:collection).new?
{
path: new_collection_path,
label: t("collections.general.new")
}
end) %>
<%= render "links_form", form: form %>
<%= text_input_row form, :caption %>
<%= rich_text_input_row form, :notes, help: t(".notes.help_html") %>
<%= select_input_row form, :license, license_select_options(selected: @model.license), include_blank: true %>
<%= checkbox_input_row form, :sensitive %>
<%= select_input_row form, :indexable, indexable_select_options(form.object) %>
<%= select_input_row form, :ai_indexable, ai_indexable_select_options(form.object) if SiteSettings.allow_ai_bots %>
<%= render "caber_relations_form", form: form %>
<%= form.submit "Save", class: "btn btn-primary" %>
<% end %>

View file

@ -1,33 +0,0 @@
<!-- JanusXR JML markup -->
<% content_for :items do %>
<% if ! @models.empty? %>
<fireboxroom>
<assets>
<assetscript src="/view/element/layout.js"/>
<assetobject src="/view/element/janus-script-peertube/asset/button.glb" id="button"/>
<assetobject src="/view/element/janus-script-peertube/asset/button.icon.glb" id="icon"/>
<assetsound src="/view/element/janus-script-peertube/asset/button.click.mp3" id="button-click"/>
<assetsound src="/view/element/janus-script-peertube/asset/button.hover.mp3" id="button-hover"/>
<% @models.each_with_index do |model, i| %>
<% @file = model.is_a?(Model) ? model.preview_file : policy_scope(model.models).first&.preview_file %>
<% if @file&.filename? %>
<assetimage id="thumb_<%= i %>" src="<%= model_url( model ) %>/model_files/<%= @file.filename %>" />
<% end %>
<% end %>
</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" />
<CircularLayout pos="0 0 0" radius="4.5" scale="1 1 1" >
<% @models.each_with_index do |model, i| %>
<object id="o_<%= i %>">
<link url="<%= model_url( model ) %>" image_id="thumb_<%= i %>" pos="0 0 0.001" draw_glow="true" scale="3 3 0.1" thumb_id="thumb_<%= i %>" title="<%= model.name %>"/>
<text text="<%= model.name %>" pos="0 2.3 0.3" font_scale="false" scale="0.15 0.15 0.2" />
</object>
<% end %>
</CircularLayout>
</room>
</fireboxroom>
<% end %>
<% end %>

View file

@ -1,46 +0,0 @@
<%= render "janusroom" %>
<% content_for :items do %>
<% if @models.empty? %>
<div class="alert alert-info">
<%= icon "person-arms-up", "" %>
<%= signed_in? ? t(".no_results_html") : t(".no_results_signed_out_html", link: new_user_session_path) %>
<% if @search %>
<%= t ".search.or_other_sites" %>
<%= render "search_links" %>
<% end %>
</div>
<% else %>
<div class="skip-link-container">
<div class="clearfix">
<%= skip_link "sidebar", t(".skip_models") %>
<%= render "order_buttons" %>
<% if pagination_settings["models"] %>
<%= paginate @models %>
<% end %>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render_component_collection Components::ModelCard, :model, @models %>
</div>
<% if pagination_settings["models"] %>
<%= paginate @models %>
<% end %>
<% if @search %>
<div class="alert alert-info">
<%= icon "search", "" %>
<%= t ".search.other_sites" %>
<%= render "search_links" %>
</div>
<% end %>
</div>
<% end %>
<% end %>
<% content_for :actions do %>
<%= link_to t(".bulk_edit"), edit_models_path((@additional_filters || {}).merge(@filter&.to_params)), class: "btn btn-secondary" if policy(:model).edit? %>
<% end %>
<% content_for :sidebar do %>
<%= render "filters_card" %>
<%= render "tags_card" %>
<% end %>

View file

@ -1,67 +0,0 @@
<table class="table">
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">menu</a>
</td>
<td>
This will generate a navigator-menu <b>into</b> your main 3D file.<br>
The links can be edited <%= link_to "here", edit_model_path(@model) %>
</td>
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">your@mastodon.online</a>
</td>
<td>
Simply enter your mastodon address as tag, and XRForge will generate a 3D model, based on your last post on mastodon.<br>
</td>
</tr>
</table>
<Hr/>
<b>JanusXR tags:</b>
<table class="table">
<tr>
<td style="vertical-align:top">
<a class="badge rounded-pill bg-secondary tag">room1</a><br>
<a class="badge rounded-pill bg-secondary tag">room2</a><br>
<a class="badge rounded-pill bg-secondary tag">room3</a><br>
<a class="badge rounded-pill bg-secondary tag">room4</a><br>
<a class="badge rounded-pill bg-secondary tag">room5</a><br>
<a class="badge rounded-pill bg-secondary tag">room6</a><br>
<a class="badge rounded-pill bg-secondary tag">room1_pedestal</a><br>
<a class="badge rounded-pill bg-secondary tag">room2_pedestal</a><br>
<a class="badge rounded-pill bg-secondary tag">room2_narrow</a><br>
</td>
<td>
This wraps a room around the 3D asset:<br><Br>
<img src="/assets/jml_templates.png" style="width:100%; max-width:800px"/>
</td>
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">singleuser</a>
</td>
<td>
This disables multiuser mode (the <a alt="start a meeting at this location" href="#" class="btn btn-secondary btn-sm"><i class="bi bi-telephone"></i></a> button).<br>
Set this tag for experiences (meditation, personal productivity space) which are better off without multiple users.
</td>
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">showavatar</a>
</td>
<td>
This shows avatars of users participating in the space.<Br>
Enable this if you want metaverse-ish kind of experiences in which user-locations are important.
</td>
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">nodescription</a>
</td>
<td>
Don't render the item in the 3D scene.
</td>
</tr>
</table>

View file

@ -1,64 +0,0 @@
<h1><%= t ".title" %></h1>
<p>Click on the image below, to open the 'howto' in a new browser-tab:</p>
<center>
<a href="/assets/howto.png" target="_blank">
<img src="/assets/howto.png" style="width:200px;margin-top:20px;margin-bottom:40px"/>
</a>
</center>
<!--
<p><%= t ".description" %></p>
-->
<%= form_with url: models_path, id: "upload-form" do |form| %>
<% if SiteSettings.show_libraries %>
<div class="row mb-3 input-group">
<%= form.label :library, Library.model_name.human, class: "col-sm-2 col-form-label" %>
<div class='col-sm-10 ps-0'>
<%= form.select :library,
policy_scope(Library).all.map { |lib| [library_select_title(lib), lib.to_param] },
{include_blank: false, selected: Library.default.to_param},
{class: "form-control form-select"} %>
<span class="form-text"><%= t ".library.help" %></span>
</div>
</div>
<% end %>
<div class="row mb-3 input-group" data-turbo-permanent>
<%= form.label :files, t(".files.label"), class: "col-sm-2 col-form-label" %>
<div class='col-sm-10 ps-0'>
<%= content_tag :div, nil, data: {
controller: "upload",
action: "turbo:morph@window->upload#reconnect",
max_file_size: SiteSettings.max_file_upload_size,
allowed_file_types: input_accept_string,
upload_endpoint: upload_path
} %>
</div>
</div>
<%= render "bulk_fields", form: form, default_tags: SiteSettings.model_tags_auto_tag_new %>
<div class="row mb-3">
<%= form.label translate(".permission_preset"), for: :permission_preset, class: "col-sm-2 col-form-label" %>
<div class="col-sm-10">
<%= form.select :permission_preset, [
[translate("settings.multiuser.default_viewer_role.options.private"), "private"],
[translate("settings.multiuser.default_viewer_role.options.member"), "member"],
[translate("settings.multiuser.default_viewer_role.options.public"), "public"]
],
{selected: SiteSettings.default_viewer_role, include_blank: true},
{class: "form-control col-auto form-select"} %>
</div>
</div>
<div class="row mb-3 input-group">
<div class='offset-sm-2 col-sm-10 ps-0'>
<!-- <%= submit_tag translate(".submit"), class: "btn btn-primary" %> -->
<%= submit_tag "Create!", class: "btn btn-primary" %>
</div>
</div>
<% end %>

View file

@ -1,407 +1,217 @@
<%
def clean_description(str)
truncate(
sanitize(
str.gsub(/^(<!--|<fireboxroom).*/im,"") # strip metadata
.gsub(/<\/?[^>]+>/, '') # strip html tags
.gsub(/&(#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/, "") # strip html entities
),
length:80
)
.strip()
end
%>
<% content_for :head do %>
<%= tag.meta property: "og:type", content: "website" %>
<%= tag.meta property: "og:image", content: model_model_file_url(@model, @model.preview_file, format: @model.preview_file.extension) if @model.preview_file&.is_image? && !@model.sensitive %>
<%= tag.meta name: "description", content: clean_description(@model.notes) if @model.notes.present? %>
<%= tag.meta name: "fediverse:creator", content: @model.creator.federails_actor.at_address if @model.creator %>
<%= tag.link rel: "alternate", type: Mime[:oembed], href: model_url(@model, format: :oembed), title: @model.name %>
<% end %>
<%
filePath = "/mnt/"+@model.library.name+"/"+@model.path+"/"
janusURLCall = "/view/index.html?profile=default#janus.url="+model_url(@model)+"/"
janusURLPreview = "/view/index.html?profile=preview#janus.url="+model_url(@model)+"/"
#ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html"
%>
<div itemscope itemtype="https://schema.org/3DModel">
<%= turbo_stream_from @model %>
<h1>
<span itemprop="name">
<a contenteditable="plaintext-only"
data-controller="editable"
data-action="focus->editable#onFocus blur->editable#onBlur"
data-editable-field="model[name]"
data-editable-path="<%= model_path(@model) %>"><%= @model.name %></a>
</span>
<%= link_to icon("search", t(".search")),
"https://yeggi.com/q/#{ERB::Util.url_encode(@model.name)}/",
class: "btn btn-outline", target: "manyfold_search", rel: "noreferrer" %>
</h1>
<div class="row row-cols-md-2 mt-2">
<div class="col-md-9" id="item_list">
<% if @locked_files > 0 %>
<div class="alert alert-info"><%= t(".preview", count: @locked_files) %></div>
<% end %>
<% if File.exist?( filePath+".xrforge/janusxr.html" ) %>
<% viewer_width = 360 %>
<% if alert || notice %>
<div class="shimcontainer" style="height: <%=viewer_width%>px">
<article style="height: <%=viewer_width%>px">
<div class="shimmer"></div>
<img src="/assets/roundel-1d688b1e.svg" />
<br><br>
🚀 (Re)generating the experience..<br><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>
<% end %>
<div style="height:15px"></div>
<% else %>
<%= render "image_carousel", images: @images %>
<% end %>
<%= card(:secondary) do %>
<p class="card-text" itemprop="description"><%= markdownify @model.notes.gsub(/^(<!--|<fireboxroom).*/im,"") %></p>
<% end if @model.notes.present? %>
<% if @num_files > 0 %>
<h2><a name="files"><%= t(".files") %></a></h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: (@groups.delete(nil)) + @images %>
</div>
<% @groups.each_pair do |group, files| %>
<h3><a name="<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: files %>
</div>
<% end %>
<% end %>
</div>
<div class="col-md-3" id="sidebar">
<%= card :secondary, "XR networks" do %>
<table class="table table-borderless table-sm">
<% if File.exist?( filePath+".xrforge/janusxr.html" ) %>
<tr>
<td>
<img src="/assets/janusxr.svg" style="width: 16px; transform: translate(0px,-2px);">
</td>
<td>
<%= link_to "JanusXR", janusURLCall, target:"_blank" %>
<a href="" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_janusxr"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_janusxr" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
Click to view this page in a JanusXR (JML) browser like JanusWeb.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open XR content-weblayer since 2013, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
</small>
</div>
</div>
</td>
</tr>
<%
# lets generate a JanusXR spatial layer for the current scene
jmlfile = @model.library.path+"/"+@model.path+"/.xrforge/scene.jml"
jml = ""
if File.exist?(jmlfile)
jml = File.read(jmlfile)
.gsub(/src=(["'])(?!(?:http|\/|\.\/))(.*?)/i, "src=\\1#{model_path(@model)}/model_files/\\2")
.gsub("-->","")
.gsub("--%3E","")
end
%>
<!-- JML (https://janusxr.org) spatial markup -->
<!--
<%== jml %>
-->
<% if File.exist?( filePath+".xrforge/scene.gltf" ) %>
<tr>
<td>⁂</td>
<td>
<%= link_to "XR Fragments", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.gltf" %>
<label for="toggle_gltf"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_gltf" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the glTF JSON URL with embedded hyperlinks for immersive navigation to other 3D scene-files, as per the <a href="https://xrfragment.org" target="_blank">XR Fragments</a> spec.<br>
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% if SiteSettings.federation_enabled? %>
<tr>
<td>⁂</td>
<td><% if @model.remote? %>
<%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %>
<% else %>
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
<% end %>
<label for="toggle_activitypub"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_activitypub" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the <a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">fediverse</a> activitypub address of this experience.<br>
Follow updates by copy/pasting it into ActivityPub <a href="https://codeberg.org/fediverse/delightful-fediverse-clients" target="_blank">clients</a>.
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% if File.exist?( filePath+".xrforge/scene.mml" ) %>
<tr>
<td>⁂</td>
<td>
<%= link_to "MML", "https://viewer.mml.io/main/v1/?url="+URI.encode_www_form_component(ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml"), target:"_blank" %>
<a href="<%= ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml" %>" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_mml"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_mml" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the MML address.<br>
Metaverse Markup Language (<a href="https://mml.io" target="_blank">MML</a>) is an open markup language used to define experiences.
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% end %>
</table>
<% end %>
<%= card :secondary, t(".model_details") do %>
<table class="table table-borderless table-sm">
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<tr>
<td>
<i class="bi bi-journal-check" role="img"></i>
</td>
<td>
<%= link_to "build log", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/log.txt", target: "_blank" %>
<label for="toggle_log"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_log" hidden>
<div class="hidden-tooltip" style="max-height:400px">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the build log of XR Forge.<br>
When you add files, they are processed, validated (for <a href="https://xrfragment.org" target="_blank">XR Fragment</a> compliance).<br>
Features can be toggled via tags:<br>
<br>
<%= render 'models/tags_info' %>
</small>
</div>
</div>
</td>
</tr>
<% if @model.collection %>
<tr>
<td><%= icon "collection", Collection.model_name.human(count: 100) %></td>
<td><%= link_to @model.collection.name, models_path({collection: @model.collection}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if SiteSettings.show_libraries || current_user&.is_administrator? %>
<tr>
<td><%= icon "boxes", Library.model_name.human %></td>
<td><%= link_to @model.library.name, models_path({library: @model.library}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if @model.license %>
<tr>
<td><%= icon "card-heading", t(".license") %></td>
<td>
<%= Spdx.licenses[@model.license]&.fetch("reference") ?
link_to(t_license(@model.license), Spdx.licenses[@model.license]["reference"], itemprop: "license") :
t_license(@model.license) %>
</td>
</tr>
<% end %>
<% if @model.sensitive %>
<tr>
<td><%= icon("explicit", Model.human_attribute_name(:sensitive)) %></td>
<td>
<%= Model.human_attribute_name(:sensitive) %>
</td>
</tr>
<% end %>
<tr>
<td><%= icon "folder", t(".path") %></td>
<td>
<%= content_tag(:code, class: "path") { safe_join @model.path.split("/"), safe_join([tag.wbr, "/"]) } %>
<% unless @model.contains_other_models? %>
<div><%= button_tag(t(".organize.button_text"), class: "btn btn-warning btn-sm", "data-bs-toggle": "modal", "data-bs-target": "#confirm-move") if @model.needs_organizing? && policy(@model).edit? %></div>
<% end %>
</td>
</tr>
<tr>
<td><%= icon "tag", t(".tags") %></td>
<td><%= render "tag_list", tags: @model.tags.order(taggings_count: :desc, name: :asc), filter: @filter %>
<label for="toggle_tags"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_tags" hidden>
<div class="hidden-tooltip" style="max-height:400px">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
<%= render 'models/tags_info' %>
</small>
</div>
</div>
</td>
</tr>
<% if SiteSettings.social_enabled? %>
<tr>
<td><%= icon "people", t(".followers") %></td>
<td><%= t("general.followers", count: @model.followers.count) %></td>
</tr>
<% end %>
<tr>
<td><%= render Components::AccessIndicator.new(object: @model, text: false) %></td>
<td><%= render Components::AccessIndicator.new(object: @model, text: true, icon: false) %></td>
</tr>
</table>
<%= render Components::FollowButton.new(follower: current_user, target: @model) unless @model.remote? %>
<%= render Components::GoButton.new(icon: "pencil", label: t("general.edit"), href: edit_model_path(@model), variant: "primary") if policy(@model).edit? %>
<%= render Components::DoButton.new(icon: "trash", label: t("general.delete"), href: model_path(@model), method: :delete, variant: "outline-danger", confirm: translate("models.destroy.confirm")) if policy(@model).destroy? %>
<% end %>
<div class="mb-3 w-100 text-center">
<%= render Components::DownloadButton.new(model: @model) %>
</div>
<%= card :secondary, "Packages" do %>
<table class="table table-borderless table-sm">
<tr>
<td>
<i class="bi bi-controller" role="img"></i>
</td>
<td>
<%= link_to "Godot project", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/godot.zip" %>
<label for="toggle_godot"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_godot" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is a Godot project which wraps your (3D file) experience.<br>
<a href="https://godot.org" target="_blank">Godot</a> is a Free and Opensource Game engine.<br>
The Godot project is basically its own XR Fragment browser (which you can extend).<br><br>
<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 %>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>
<%= render Components::ReportButton.new(object: @model, path: new_model_report_path(@model)) %>
<% end if SiteSettings.multiuser_enabled? %>
<% if !@model.parents.empty? && policy(@model).merge? %>
<%= card :danger, t(".merge.heading") do %>
<p>
<%= t(".merge.warning") %>
</p>
<strong><%= t(".merge.with") %>:</strong>
<% @model.parents.each do |parent| %>
<%= render Components::DoButton.new(icon: "union", label: parent.name, href: merge_models_path(target: parent.public_id, models: [@model.public_id]), method: :post, variant: "danger") %>
<% end %>
<% end %>
<% end %>
<%= render partial: "problem", collection: @model.problems.visible(problem_settings) %>
<%= card :secondary, t(".files_card.heading") do %>
<a href="#files">Top</a>
<ul>
<% @groups.each_pair do |group, files| %>
<li><a href="#<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></li>
<% end %>
</ul>
<%= link_to t(".files_card.bulk_edit"), bulk_edit_model_model_files_path(@model), class: "btn btn-secondary" if policy(@model).edit? %>
<%= link_to t(".rescan"), scan_model_path(@model), class: "btn btn-warning", method: :post if policy(@model).scan? %>
<% end %>
<%= render "links_card", links: @model.links %>
<div class="modal fade" id="confirm-move" data-bs-backdrop='static' tabindex="-1" aria-labelledby="confirmMoveLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="confirmMoveLabel"><%= t(".organize.button_text") %></div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
<%= t(".organize.confirm.summary_html", from: @model.path, to: @model.formatted_path) %>
</p>
<p>
<%= t(".organize.confirm.are_you_sure") %>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".organize.confirm.no") %></button>
<%= button_to t(".organize.confirm.yes"), model_path(@model, "model[organize]": true), method: :patch, class: "btn btn-warning" %>
</div>
</div>
</div>
</div>
<% if policy(@model).upload? %>
<%= card :warning, t(".upload_card.heading") do %>
<%= form_with url: model_model_files_path(@model), id: "upload-form", class: "d-grid" do |form| %>
<%= content_tag :div, nil, class: "mb-3", data: {
controller: "upload",
action: "turbo:morph@window->upload#reconnect",
max_file_size: SiteSettings.max_file_upload_size,
allowed_file_types: input_accept_string,
upload_endpoint: upload_path
} %>
<%= submit_tag translate(".submit"), class: "btn btn-primary d-block" %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% content_for :head do %>
<%= tag.meta property: "og:type", content: "website" %>
<%= tag.meta property: "og:image", content: model_model_file_url(@model, @model.preview_file, format: @model.preview_file.extension) if @model.preview_file&.is_image? && !@model.sensitive %>
<%= tag.meta name: "description", content: truncate(sanitize(@model.notes), length: 80) if @model.notes.present? %>
<%= tag.meta name: "fediverse:creator", content: @model.creator.federails_actor.at_address if @model.creator %>
<%= tag.link rel: "alternate", type: Mime[:oembed], href: model_url(@model, format: :oembed), title: @model.name %>
<% end %>
<div itemscope itemtype="https://schema.org/3DModel">
<%= turbo_stream_from @model %>
<h1>
<span itemprop="name">
<a contenteditable="plaintext-only"
data-controller="editable"
data-action="focus->editable#onFocus blur->editable#onBlur"
data-editable-field="model[name]"
data-editable-path="<%= model_path(@model) %>"><%= @model.name %></a>
</span>
<%= link_to icon("search", t(".search")),
"https://yeggi.com/q/#{ERB::Util.url_encode(@model.name)}/",
class: "btn btn-outline", target: "manyfold_search", rel: "noreferrer" %>
</h1>
<div class="row row-cols-md-2 mt-2">
<div class="col-md-9" id="item_list">
<% if @locked_files > 0 %>
<div class="alert alert-info"><%= t(".preview", count: @locked_files) %></div>
<% end %>
<%= render "image_carousel", images: @images %>
<%= card(:secondary) do %>
<p class="card-text" itemprop="description"><%= markdownify @model.notes %></p>
<% end if @model.notes.present? %>
<% if @num_files > 0 %>
<h2><a name="files"><%= t(".files") %></a></h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: @groups.delete(nil) %>
</div>
<% @groups.each_pair do |group, files| %>
<h3><a name="<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: files %>
</div>
<% end %>
<% end %>
</div>
<div class="col-md-3" id="sidebar">
<%= card :secondary, t(".model_details") do %>
<table class="table table-borderless table-sm">
<% if SiteSettings.federation_enabled? %>
<tr>
<td>⁂</td>
<td><% if @model.remote? %>
<small><%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %></small>
<% else %>
<small>
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
</small>
<% end %></td>
</tr>
<% end %>
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<tr>
<td>
<i class="bi bi-file-zip" role="img"></i>
</td>
<td><%= link_to "zip archive", "/"+@model.library.name+"/"+@model.path.gsub("#","%23")+".zip" %></td>
</tr>
<tr>
<td>
<i class="bi bi-controller" role="img"></i>
</td>
<td><%= link_to "Godot project", "/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/package_godot.zip" %></td>
</tr>
<% if @model.collection %>
<tr>
<td><%= icon "collection", Collection.model_name.human(count: 100) %></td>
<td><%= link_to @model.collection.name, models_path({collection: @model.collection}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if SiteSettings.show_libraries || current_user&.is_administrator? %>
<tr>
<td><%= icon "boxes", Library.model_name.human %></td>
<td><%= link_to @model.library.name, models_path({library: @model.library}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if @model.license %>
<tr>
<td><%= icon "card-heading", t(".license") %></td>
<td>
<%= Spdx.licenses[@model.license]&.fetch("reference") ?
link_to(t_license(@model.license), Spdx.licenses[@model.license]["reference"], itemprop: "license") :
t_license(@model.license) %>
</td>
</tr>
<% end %>
<% if @model.sensitive %>
<tr>
<td><%= icon("explicit", Model.human_attribute_name(:sensitive)) %></td>
<td>
<%= Model.human_attribute_name(:sensitive) %>
</td>
</tr>
<% end %>
<tr>
<td><%= icon "folder", t(".path") %></td>
<td>
<%= content_tag(:code, class: "path") { safe_join @model.path.split("/"), safe_join([tag.wbr, "/"]) } %>
<% unless @model.contains_other_models? %>
<div><%= button_tag(t(".organize.button_text"), class: "btn btn-warning btn-sm", "data-bs-toggle": "modal", "data-bs-target": "#confirm-move") if @model.needs_organizing? && policy(@model).edit? %></div>
<% end %>
</td>
</tr>
<tr>
<td><%= icon "tag", t(".tags") %></td>
<td><%= render "tag_list", tags: @model.tags.order(taggings_count: :desc, name: :asc), filter: @filter %></td>
</tr>
<% if SiteSettings.social_enabled? %>
<tr>
<td><%= icon "people", t(".followers") %></td>
<td><%= t("general.followers", count: @model.followers.count) %></td>
</tr>
<% end %>
<tr>
<td><%= render Components::AccessIndicator.new(object: @model, text: false) %></td>
<td><%= render Components::AccessIndicator.new(object: @model, text: true, icon: false) %></td>
</tr>
</table>
<%= render Components::FollowButton.new(follower: current_user, target: @model) unless @model.remote? %>
<%= render Components::GoButton.new(icon: "pencil", label: t("general.edit"), href: edit_model_path(@model), variant: "primary") if policy(@model).edit? %>
<%= render Components::DoButton.new(icon: "trash", label: t("general.delete"), href: model_path(@model), method: :delete, variant: "outline-danger", confirm: translate("models.destroy.confirm")) if policy(@model).destroy? %>
<% end %>
<div class="mb-3 w-100 text-center">
<%= render Components::DownloadButton.new(model: @model) %>
</div>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>
<%= render Components::ReportButton.new(object: @model, path: new_model_report_path(@model)) %>
<% end if SiteSettings.multiuser_enabled? %>
<% if !@model.parents.empty? && policy(@model).merge? %>
<%= card :danger, t(".merge.heading") do %>
<p>
<%= t(".merge.warning") %>
</p>
<strong><%= t(".merge.with") %>:</strong>
<% @model.parents.each do |parent| %>
<%= render Components::DoButton.new(icon: "union", label: parent.name, href: merge_models_path(target: parent.public_id, models: [@model.public_id]), method: :post, variant: "danger") %>
<% end %>
<% end %>
<% end %>
<%= render partial: "problem", collection: @model.problems.visible(problem_settings) %>
<%= card :secondary, t(".files_card.heading") do %>
<a href="#files">Top</a>
<ul>
<% @groups.each_pair do |group, files| %>
<li><a href="#<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></li>
<% end %>
</ul>
<%= link_to t(".files_card.bulk_edit"), bulk_edit_model_model_files_path(@model), class: "btn btn-secondary" if policy(@model).edit? %>
<%= link_to t(".rescan"), scan_model_path(@model), class: "btn btn-warning", method: :post if policy(@model).scan? %>
<% end %>
<%= render "links_card", links: @model.links %>
<div class="modal fade" id="confirm-move" data-bs-backdrop='static' tabindex="-1" aria-labelledby="confirmMoveLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="confirmMoveLabel"><%= t(".organize.button_text") %></div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
<%= t(".organize.confirm.summary_html", from: @model.path, to: @model.formatted_path) %>
</p>
<p>
<%= t(".organize.confirm.are_you_sure") %>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".organize.confirm.no") %></button>
<%= button_to t(".organize.confirm.yes"), model_path(@model, "model[organize]": true), method: :patch, class: "btn btn-warning" %>
</div>
</div>
</div>
</div>
<% if policy(@model).upload? %>
<%= card :warning, t(".upload_card.heading") do %>
<%= form_with url: model_model_files_path(@model), id: "upload-form", class: "d-grid" do |form| %>
<%= content_tag :div, nil, class: "mb-3", data: {
controller: "upload",
action: "turbo:morph@window->upload#reconnect",
max_file_size: SiteSettings.max_file_upload_size,
allowed_file_types: input_accept_string,
upload_endpoint: upload_path
} %>
<%= submit_tag translate(".submit"), class: "btn btn-primary d-block" %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>

View file

@ -1,25 +0,0 @@
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
echo "Preparing database..."
bundle exec rails db:prepare:with_data
echo "run boot 'hook'"
/manyfold/cli/manyfold.sh hook boot || true
echo "Setting database file ownership (SQLite3 only)..."
bundle exec rake db:chown
echo "Cleaning up old cache files..."
bundle exec rake tmp:cache:clear
echo "Setting temporary directory permissions..."
chown -R $PUID:$PGID tmp log
echo "Launching application..."
export RAILS_PORT=$PORT
export RAILS_LOG_TO_STDOUT=true
exec s6-setuidgid $PUID:$PGID "$@"

View file

@ -8,30 +8,3 @@ Rails.application.configure do
end
end
end
Rails.application.config.after_initialize do
Rails.application.configure do
config.content_security_policy do |policy|
# Default policy for all content
policy.default_src :self, :https, :http
# *** ADD THE DOMAIN(S) FOR YOUR IFRAME CONTENT HERE ***
allowed = (ENV['FEDERATE_SERVERS'] || '')
.gsub(/[" ]/,'')
.split(",")
.concat([
:self,
ENV['FEDERATE_DRIVE_HOST'],
ENV['SERVER_CORS'],
ENV['SERVER_JANUS']
])
.compact # remove potential nils
policy.frame_src *allowed
policy.script_src *allowed
policy.style_src *allowed
policy.connect_src *allowed
# ...
end
end
end

View file

@ -1,11 +0,0 @@
# always allow cors so remote XR viewers can load content
#Rails.application.config.middleware.insert_after Rack::Head, Rack::Cors do
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :options, :head]
end
File.open("/tmp/out","w") do |f|
f.write("Hello Ruby!")
end
end

View file

@ -1,109 +0,0 @@
# this calls the xrforge unix file-hooks in /root/hook.d/experience_updated (but in a safe way)
# why: the database-write are sometimes chatty/duplicated.
# therefore it ratelimits and prevents processing unchanged files.
require 'digest'
Rails.application.config.to_prepare do
Model # Zeitwerk autoload model
ModelFile
class ModelFile
# The macro is now run within the context of the existing Model class.
after_save :run_cli_hooks
def run_cli_hooks
file = "#{self.model.library.path || ENV['UPLOAD_PATH'] || "/mnt/experiences"}"
file = file.strip() + "/" + self.path_within_library().strip()
if File.exist?(file) && (ENV['BOOT_SCAN'].present? || file.match?("datapackage") )
cache_key = "ttl:file:cli_hook:#{self.id}#{Digest::MD5.file(file).hexdigest}"
ttl = 60.0 # dont trigger hook twice for the same file within 60 seconds
now = Time.current
# 1. Read the last run time from the shared cache
last_run_time_str = Rails.cache.read(cache_key)
last_run_time = last_run_time_str ? Time.parse(last_run_time_str) : nil
if last_run_time.nil? || (now - last_run_time) > ttl
# 2. Write the new execution time to the shared cache
# Use `write` to set the new time. Caching a string representation is often safer/easier.
Rails.cache.write(cache_key, now.to_s, expires_in: ttl + 3.second)
puts "[app/config/initializers/xrforge.rb] running hook #{file}\n"
Bundler.with_unbundled_env do
#`TS_SLOTS=5 ts /manyfold/cli/manyfold.sh hook experience_updated #{file} &`
`/manyfold/cli/manyfold.sh hook experience_updated #{file}`
end
Model.find(self.model.id).add_new_files_later(include_all_subfolders:false)
else
puts "[app/config/initializers/xrforge.rb] skipping hook\n"
end
end
end
end
end
#class XRForgeRoutes
# # Rack endpoints must implement a #call method that accepts the Rack environment hash (env).
# def call(env)
# # Wrap the environment in a Rails Request object for easier parameter access
# request = ActionDispatch::Request.new(env)
#
# # 1. Access the model ID from the route parameters
# # The route path parameters are stored in the routing information
# model_id = request.path_parameters[:id]
#
# begin
# # 2. Find the model
# # Note: We are outside the controller, so we access Model directly.
# model = Model.find(model_id)
#
# puts "-------------------------"
# Rails.logger.info("build process for Model ID: #{model.id}")
# url_helpers = Rails.application.routes.url_helpers
# redirect_path = url_helpers.model_path(model)
#
# # 302 Found response for a temporary redirect
# [
# 302,
# {
# 'Content-Type' => 'text/html',
# 'Location' => redirect_path
# },
# ['Build process started successfully.']
# ]
#
# rescue ActiveRecord::RecordNotFound
# # Handle model not found error
# [
# 404,
# { 'Content-Type' => 'text/plain' },
# ["Model with ID #{model_id} not found."]
# ]
# rescue => e
# # Handle other errors
# Rails.logger.error("Build Rack App Error: #{e.message}")
# [
# 500,
# { 'Content-Type' => 'text/plain' },
# ["Internal Server Error: Could not initiate build."]
# ]
# end
# end
#end
#
#Rails.configuration.to_prepare do
# Rails.application.routes.draw do
# resources :models, only: [] do
# member do
# # Point the route directly to an instance of the Rack application class.
# # The route is: POST /models/:id/build
# match 'build', to: XRForgeRoutes.new, via: :post
# end
# end
# end
#end

View file

@ -1,802 +0,0 @@
---
en:
activerecord:
attributes:
collection:
ai_indexable: Allow use for AI training
caption: Caption
collection: Parent Collection
indexable: Allow search indexing
models: Experiences
name: Name
notes: Description
creator:
ai_indexable: Allow use for AI training
caption: Tagline
indexable: Allow search indexing
name: Creator Name
notes: Description
slug: Handle
doorkeeper/application:
access_token: Access Token
confidential: Confidential
created_at: Created
name: Name
owner: Owner
redirect_uri: Redirect URI
scopes: Scopes
secret: Client Secret
uid: Client ID
federails/moderation/domain_block:
created_at: Created at
domain: Domain
federails/moderation/report:
content: Comment
created_at: Received at
federails_actor: Reported by
object: Object
library:
caption: Caption
create_path_if_not_on_disk: Auto-create folder
default: Default
icon: Icon
name: Name
notes: Notes
path: Path
s3_access_key_id: Access Key ID
s3_bucket: Bucket Name
s3_endpoint: Endpoint URL
s3_path_style: Use path-style URLs
s3_region: Region
s3_secret_access_key: Secret Access Key
storage_service: Storage Service
tag_regex: Required Tags
link:
url: Link
model:
ai_indexable: Allow use for AI training
caption: Caption
collection: Collection
collection_id: Collection
creator: Creator
creator_id: Creator
images: Images
indexable: Allow search indexing
library_id: Library
license: License
model_files: Files
name: Name
notes: Description
path: Path
preview_file: Preview File
sensitive: Sensitive Content
tags: Tags
model_file:
caption: Caption
digest: Digest
filename: Filename
model_id: Model
notes: Notes
presupported: Presupported
presupported_version: Presupported version
printed: Printed
size: File Size
unsupported_version: Unsupported version
y_up: Y Up
problem:
category: Category
ignored: Hidden
note: Note
problematic_type: Object Type
severity: Severity
user:
approved: Account pending
confirmation_sent_at: Confirmation sent at
confirmation_token: Confirmation token
confirmed_at: Confirmed at
created_at: Created at
current_password: Current password
current_sign_in_at: Current sign in at
current_sign_in_ip: Current sign in IP
email: Email
encrypted_password: Encrypted password
failed_attempts: Failed attempts
last_sign_in_at: Last sign in at
last_sign_in_ip: Last sign in IP
locked_at: Locked at
password: Password
password_confirmation: Confirm password
remember_created_at: Remember created at
remember_me: Remember me
reset_password_sent_at: Reset password sent at
reset_password_token: Reset password token
sign_in_count: Sign in count
unconfirmed_email: Unconfirmed email
unlock_token: Unlock token
updated_at: Updated at
username: Account name
errors:
models:
collection:
attributes:
collection:
private: must be public
creator:
private: must be public
doorkeeper/application:
attributes:
redirect_uri:
forbidden_uri: is forbidden by the server.
fragment_present: cannot contain a fragment.
invalid_uri: must be a valid URI.
relative_uri: must be an absolute URI.
secured_uri: must be an HTTPS/SSL URI.
unspecified_scheme: must specify a scheme.
scopes:
not_match_configured: doesn't match configured on the server.
library:
attributes:
path:
cannot_be_contained: cannot be inside another library
cannot_contain: cannot contain other libraries
non_writable: must be writable
not_found: could not be found on disk
unsafe: cannot be a privileged system path
model:
attributes:
creator:
private: must be public
library:
nested: can't be changed, model contains other models
license:
invalid_spdx: is not a valid license
path:
destination_exists: already exists
nested: can't be changed, model contains other models
model_file:
attributes:
filename:
cannot_change_type: is not the same file type
case_change_only: cannot be a case-only change
presupported_version:
already_presupported: cannot be set on a presupported file
not_supported: is not a presupported file
models:
acts_as_taggable_on/tag:
few: Tags
many: Tags
one: Tag
other: Tags
two: Tags
zero: Tags
collection:
few: Collections
many: Collections
one: Collection
other: Collections
two: Collections
zero: Collections
creator:
few: Creators
many: Creators
one: Creator
other: Creators
two: Creators
zero: Creators
federails/moderation/domain_block:
few: Domain Blocks
many: Domain Blocks
one: Domain Block
other: Domain Blocks
two: Domain Blocks
zero: Domain Blocks
federails/moderation/report:
few: Reports
many: Reports
one: Report
other: Reports
two: Reports
zero: Reports
library:
few: Libraries
many: Libraries
one: Library
other: Libraries
two: Libraries
zero: Libraries
link:
few: Links
many: Links
one: Link
other: Links
two: Links
zero: Links
model:
few: Experiences
many: Experiences
one: Experience
other: Experiences
two: Experiences
zero: Experiences
model_file:
few: Files
many: Files
one: File
other: Files
two: Files
zero: Files
problem:
few: Problems
many: Problems
one: Problem
other: Problems
two: Problems
zero: Problems
user:
few: Accounts
many: Accounts
one: Account
other: Accounts
two: Accounts
zero: Accounts
activity:
index:
description: Entries are discard after %{retention_period}.
message: Message
name: Name
time: When
title: Recent Activity
activity_helper:
status_icon:
completed: Complete
error: Errored
queued: Queued
working: Working
application:
caber_relation_fields:
delete: Delete
permissions:
edit: Can edit
own: Owner (can view, edit, delete, and share)
preview: 'Preview: specific previewable files only'
view: View only
subject:
placeholder: Email address, account name, or role
role:
member: Any logged-in local account
public: Everyone (without login)
you: "(you)"
caber_relations_form:
add: add another permission
permissions: Sharing
demo_mode: This instance is in demo mode. You cannot add or remove models, but you can do everything else.
filters_card:
missing_tags: Missing tags
remove_collection_filter: Remove collection filter
remove_creator_filter: Remove creator filter
remove_library_filter: Remove library filter
remove_missing_tag_filter: Remove missing tag filter
remove_search_filter: Remove search filter
remove_tag_filter: Remove tag filter
search: Search
title: Filters
unknown: Unknown
footer:
about: About this instance
api: Explore our API
by_html: Designed and built by <a href="https://floppy.org.uk" target="_blank" rel="noreferrer">James</a> with help from <a href="https://github.com/manyfold3d/manyfold/graphs/contributors" target="_blank" rel="noreferrer">our contributors</a>.
community: Join the community
instance_heading: Instance Details
issues: Report a problem
open_source_html: <a href="https://github.com/manyfold3d/manyfold" target="_blank" rel="noreferrer">Open Source</a> under the <a href="https://github.com/manyfold3d/manyfold/blob/main/LICENSE.md" target="_blank" rel="noreferrer" rel="license">MIT license</a>.
powered_by_html: Powered by <a href="https://forgejo.isvery.ninja/coderofsalvation/xrforge">XR Forge</a>, <a href="https://manifold.app" target="_blank">Manyfold</a>, <a href="https://xrfragment.org">XR Fragments</a> and <a href="https://nixos.org" target="_blank">NIX</a>
sponsor: Sponsor development
support: Support this instance
version: Version
link_fields:
url:
delete: Delete
placeholder: Any related web page
links_form:
add: add another link
navbar:
account: My Settings
activity: Activity
check_existing: Rescan all models
check_results: Rescan filtered models
home: Homepage
log_in: Sign in
log_out: Sign out
moderator_settings: Moderator Settings
navbar:
toggler:
label: Toggle navigation
scan: Scan
scan_changes: Scan for new files
scanning: Scanning
search: Search
settings: Site Settings
upload: Upload
order_buttons:
sort:
name: Sort by Name
time: Sort by Time
search_error: Error in search syntax. Please check and try again!
tag_list:
unrelated_tag_count:
one: "%{count} unrelated tag hidden"
other: "%{count} unrelated tags hidden"
tagline: Helping you keep track of your 3d print files
tags_card:
skip_tags: Skip tag list
title: xrforge
application_helper:
ai_indexable_select_options:
always_no: Always no
always_yes: Always yes
inherit: Inherit from parent object or default site setting; currently '%{inherited}'
indexable_select_options:
always_no: Always no
always_yes: Always yes
inherit: Inherit from parent object or default site setting; currently '%{inherited}'
'no': 'No'
'yes': 'Yes'
components:
altcha_widget:
help: privacy-friendly spam protection by ALTCHA
copy_button:
copy: Copy to Clipboard
display_user_quota:
request_increase: To request a quota increase, contact your site administrator.
download_button:
download:
missing: Request download
preparing: Preparing download, please wait
ready: Ready to download
file_type: "%{type} Files Only"
label: Download All
menu_header: Download Options
supported: Supported Files Only
unsupported: Unsupported Files Only
follow_button:
follow: Follow %{name}
pending: Requested
unfollow: Unfollow %{name}
link_list:
sync: Synchronize
modal:
close: Close
model_card:
delete_button:
label: Delete model %{name}
text: Delete
edit_button:
label: Edit model %{name}
text: Edit
no_preview: No preview available
open_button:
label: Open model %{name}
text: Open
search_help:
boolean: Use "or" to find models that match any of the terms.
federation: Search for any Fediverse username to follow it.
filename: You can search within filenames by explicitly specifying the field.
intro: 'Find what you need with our powerful search syntax:'
more_details_html: For more information, read the full documentation for <a href="https://github.com/wvanbergen/scoped_search/wiki/Query-language">scoped_search's query language</a>.
negation: To exclude terms, use "not", "!", or "-".
parentheses: Group terms with parentheses for more complex logic combinations.
path: Search within model folder paths by explicitly specifying it; use `~` for a partial match.
quotes: To look for multiple words in a single term, use quotes; only models with the exact text will be shown.
simple: By default, search will find models that match all terms.
specific_fields: You can look for terms in a few specific fields. Use "~" to match part of the field; "=" will try to match the whole thing. Model descriptions and library names are only searched if you explicitly specify the fields.
tag: Finds models with a specific tag
title: Search Syntax
unset: Use "set?" to query if a particular field is set, and add "not" to find the opposite.
without_tag: Use "!=" to find models without a certain tag
concerns:
linkable:
sync:
bad_request: 'Synchronization failed: missing link ID'
success: Synchronization requested successfully
doorkeeper:
applications:
buttons:
authorize: Authorize
cancel: Cancel
destroy: Destroy
edit: Edit
submit: Submit
confirmations:
destroy: Are you sure?
edit:
title: Edit application
form:
error: Whoops! Check your form for possible errors
help:
blank_redirect_uri: Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI.
confidential: Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.
redirect_uri: Use one line per URI
scopes: Separate scopes with spaces. Leave blank to use the default scopes.
index:
actions: Actions
callback_url: Callback URL
confidential: Confidential?
confidentiality:
'no': 'No'
'yes': 'Yes'
name: Name
new: New Application
title: Your applications
new:
title: New Application
show:
actions: Actions
application_id: UID
callback_urls: Callback urls
confidential: Confidential
not_defined: Not defined
scopes: Scopes
secret: Secret
secret_hashed: Secret hashed
title: 'Application: %{name}'
authorizations:
buttons:
authorize: Authorize
deny: Deny
error:
title: An error has occurred
form_post:
title: Submit this form
new:
able_to: This application will be able to
prompt: Authorize %{client_name} to use your account?
title: Authorization required
show:
title: Authorization code
authorized_applications:
buttons:
revoke: Revoke
confirmations:
revoke: Are you sure?
index:
application: Application
created_at: Created At
date_format: "%Y-%m-%d %H:%M:%S"
title: Your authorized applications
errors:
messages:
access_denied: The resource owner or authorization server denied the request.
admin_authenticator_not_configured: Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.
credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
forbidden_token:
missing_scope: Access to this resource requires scope "%{oauth_scopes}".
invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
invalid_code_challenge_method:
one: The code_challenge_method must be %{challenge_methods}.
other: The code_challenge_method must be one of %{challenge_methods}.
zero: The authorization server does not support PKCE as there are no accepted code_challenge_method values.
invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
invalid_redirect_uri: The requested redirect uri is malformed or doesn't match client redirect URI.
invalid_request:
invalid_code_challenge: Code challenge is required.
missing_param: 'Missing required parameter: %{value}.'
request_not_authorized: Request need to be authorized. Required parameter for authorizing request is missing or invalid.
unknown: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
invalid_scope: The requested scope is invalid, unknown, or malformed.
invalid_token:
expired: The access token expired
revoked: The access token was revoked
unknown: The access token is invalid
resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.
revoke:
unauthorized: You are not authorized to revoke this token
server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request.
temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
unauthorized_client: The client is not authorized to perform this request using this method.
unsupported_grant_type: The authorization grant type is not supported by the authorization server.
unsupported_response_mode: The authorization server does not support this response mode.
unsupported_response_type: The authorization server does not support this response type.
flash:
applications:
create:
notice: Application created.
destroy:
notice: Application deleted.
update:
notice: Application updated.
authorized_applications:
destroy:
notice: Application revoked.
layouts:
admin:
nav:
applications: Applications
home: Home
oauth2_provider: OAuth2 Provider
title: Doorkeeper
application:
title: OAuth authorization required
pre_authorization:
status: Pre-authorization
doorkeeper_applications:
create:
failure: An error occurred, and the application could not be created.
success: Application created successfully.
destroy:
success: Application deleted successfully.
edit:
title: Edit application
form:
confidential:
help: A confidential application can hold secrets securely (e.g. a web server backend, or machine-to-machine script).
redirect_uri:
help: Use "urn:ietf:wg:oauth:2.0:oob" if your application does not need a redirect URI (e.g. machine-to-machine apps).
scopes:
label: Scopes
submit: Save application
index:
description: OAuth applications allow you to access Manyfold resources from other services via our API.
new: New application
title: OAuth Applications
new:
title: New application
show:
destroy: Delete
edit: Edit
title: Application details
update:
failure: An error occurred, and the application could not be saved.
success: Application saved successfully.
errors:
messages:
already_confirmed: was already confirmed, please try signing in
confirmation_period_expired: needs to be confirmed within %{period}, please request a new one
expired: has expired, please request a new one
not_found: not found
not_locked: was not locked
not_saved:
one: '1 error prohibited this %{resource} from being saved:'
other: "%{count} errors prohibited this %{resource} from being saved:"
weak_password: not strong enough. Consider adding a number, symbols or more letters to make it stronger.
follows:
actor_table:
actions: Actions
address: Fediverse Address
name: Name
non_manyfold_account: This is not a Manyfold account; you can follow it, but probably nothing interesting will happen, at least for now.
follow_remote_actor:
followed: Followed %{actor} successfully
index:
followers: Followers
following: Following
title: Connections
new:
help: You can follow public creators, collections or models on another Manyfold server, in fact any public account in the Fediverse! Just enter the account name in the search box!
no_results: Sorry, couldn't find anything for "%{query}". Is it a valid ActivityPub account or URL?
results: Search Results
title: Follow the Fediverse
remote_follow:
help: You don't need an account on this server to follow %{name}; enter your own account name here, and we'll send you home to complete the process.
no_results_html: We couldn't find your home account; did you enter it correctly?
placeholder: Your Fediverse handle, e.g. @manyfold@3dp.chat
submit: Take me home
title: Follow %{name}
search_form:
placeholder: Enter a Fediverse account or URL, e.g. @admin@try.manyfold.app
submit: Search
unfollow_remote_actor:
unfollowed: Unfollowed %{actor}
general:
delete: Delete
download: Download
edit: Edit
expand: Expand
followers:
few: "%{count} Followers"
many: "%{count} Followers"
one: "%{count} Follower"
other: "%{count} Followers"
two: "%{count} Followers"
zero: "%{count} Followers"
menu: Menu
new: New
private: Private
public: Publicly visible
report: Report %{type}
save: Save
shared: Shared with local users
view: View
home:
activity:
created: added %{time} ago
updated: updated %{time} ago
browsing:
content: You can explore models by clicking the links in the menu bar; browse a complete list and filter by tag, or browse by collection or creator. Alternatively just type into the search box to find what you want!
manual_link: User guide
more_access: Currently you have read-only access to this instance; to get more permissions, such as uploading, contact your instance administrator.
title: Browsing
federation:
content_html: This Manyfold instance is part of the <a href="https://jointhefediverse.net">Fediverse</a>, a network of social media sites that all work together. That means that if you have an account here, you can follow content on other Manyfold instances, or people can follow your content from other platforms like Mastodon.
creator_handle_html: 'The fediverse handle of your creator profile is: <code>%{handle}</code>.'
following: If you know the handle of someone or something you want to follow, just enter it in the search box; otherwise, enter your personal handle above when you follow something on another instance.
handle_html: 'Your fediverse handle is: <code>%{handle}</code>'
title: Federation
index:
no_activities: There are no activities to display for now.
open_search_help: Search syntax
recent_activity: Recent Activity
search:
placeholder: What are you looking for?
submit: Search
publishing:
content: You can publish content publicly by giving "view" or "preview" permission to the "public" role on the item's edit page. Creators for public models will automatically be made public, but collections need to be expicitly published if you want them to be visible.
existing_creator:
button: Edit your creator profile
content: 'If you''re publishing your own work, you will probably want to customise your creator profile:'
new_creator:
button: Set up a new creator profile
content: 'If you''re publishing your own work, you will probably want to set up your own creator profile:'
title: Publishing
support:
content: Manyfold instances are run by people like you! If you find this instance useful, you can help keep it running by clicking below.
manyfold_html: To support development of the Manyfold software itself, you can do so at <a href="https://opencollective.com/manyfold">OpenCollective</a>.
support_link: Support this instance
title: Support
uploading:
how_to_upload: You can add models by clicking the upload button in the menu bar. To upload lots of files as a single model, compress them in a single archive file (e.g. ZIP or RAR).
permissions:
edit: You can grant additional permissions on the item's edit page.
member: By default, uploaded content will be visible to any local logged-in user.
private: By default, uploaded content will not be visible to any other users.
quota: You can upload up to %{quota} of content, and you can always view your current quota usage on your settings page.
title: Uploading
upload: Upload
welcome:
lead: This site is running Manyfold, software for managing and sharing 3D models; here's a quick guide...
title: Welcome to %{site_name}!
imports:
create:
success: Imported requested; the results should appear shortly.
new:
description: From some sites, Manyfold can download models for you with just a link!
heading: Import from a link
import: Import this link
import_type_html: "<code>%{url}</code> will be added as a new %{object_type}. The following data can be imported automatically:"
jobs:
activity:
collection_published:
comment: A new collection of 3D models, ["%{name}"](%{url}), was just published!
model_collected:
comment: '["%{model_name}"](%{model_url}) was just added to the ["%{collection_name}"](%{collection_url}) collection.'
model_published:
comment: A new 3D model, ["%{name}"](%{url}), was just published!
updated_model:
comment: The 3D model ["%{name}"](%{url}), was just updated!
analysis:
analyse_model_file:
detect_duplicates: Detecting duplicate files
detect_ineffiency: Detecting inefficient formats
file_statistics: Calculating file statistics
matching: Matching supported files
file_conversion:
exporting: Exporting new file
loading_mesh: Loading mesh
geometric_analysis:
direction_check: Checking surface orientation
loading_mesh: Loading mesh
manifold_check: Checking that mesh is manifold
scan:
detect_filesystem_changes:
building_filename_list: Building file list
building_folder_list: Building changed folder list
creating_models: Creating models
kaminari:
first_page:
label: Go to first page
last_page:
label: Go to last page
next_page:
label: Go to next page
page:
current_page: Current page
label: Go to page %{page}
paginator:
label: Page navigation
prev_page:
label: Go to previous page
layouts:
application:
alert:
danger: Danger
info: Info
skip_to_content: Skip to main content
card_list_page:
actions_heading: Actions
settings:
activeadmin: Advanced Administration
appearance: Appearance
downloads: Downloads
libraries: Libraries
moderation_settings_title: Moderation Settings
organization: Organization
performance: Performance Dashboard
pghero: PgHero
sidekiq: Sidekiq
site_settings_title: Site Settings
tools_heading: Advanced Tools
licenses:
0BSD: BSD Zero Clause License
CC-BY-40: Creative Commons Attribution
CC-BY-NC-40: Creative Commons Attribution NonCommercial
CC-BY-NC-ND-40: Creative Commons Attribution NonCommercial NoDerivatives
CC-BY-NC-SA-40: Creative Commons Attribution NonCommercial ShareAlike
CC-BY-ND-40: Creative Commons Attribution NoDerivatives
CC-BY-SA-40: Creative Commons Attribution ShareAlike
CC-PDDC: Creative Commons Public Domain Declaration
CC0-10: Creative Commons Zero
GPL-20-only: GNU General Public License v2.0
GPL-30-only: GNU General Public License v3.0
LGPL-20-only: GNU Lesser General Public License v2
LGPL-30-only: GNU Lesser General Public License v3
LicenseRef-Commercial: Commercial; private use only
MIT: MIT
moderator_mailer:
new_approval:
greeting: Hi!
message: Someone new has signed up for an account, and requires approval. Approve the account at %{link}
subject: New account needs approval
new_report:
greeting: Hi!
message: Someone has reported content which needs moderations. Review the report at %{link}
subject: New report received
renderer:
errors:
canvas: 'Could not find #webgl canvas!'
load: Load Error
webglrenderer: Could not create renderer!
load: Load
processing: Reticulating splines...
reports:
create:
success: Report submitted. Thank you!
new:
description: If this item violates any laws or server policies, you can report it to our moderators. Add a comment to let us know why!
submit: Send report
title: 'Report %{type}: "%{name}"'
scans:
create:
success: Scan started.
security:
running_as_root_html: Manyfold is running as root, which is a security risk. Run as a different system user by setting the <code>PUID</code> and <code>PGID</code> environment variables. See <a href='https://manyfold.app/sysadmin/configuration.html#required'>the configuration documentation</a> for details.
sites:
cgtrader: CGTrader
comicsgamesandthings: Comics, Games, and Things
cults3d: Cults3D
github: GitHub
makerworld: MakerWorld
manyfold: Manyfold
myminifactory: MyMiniFactory
printables: Printables
thangs: Thangs
theminiindex: The Mini Index
thingiverse: Thingiverse
yeggi: yeggi
user_mailer:
account_approved:
greeting: Hi!
message: Your account has been approved; you may now sign in at %{link}
subject: Account approved
test_email:
subject: Test email
test_email_message: Test email
users:
registrations:
create:
altcha_failed: ALTCHA verification failed
views:
pagination:
first: "« First"
last: Last »
next: Next
previous: " Prev"
truncate: "…"

View file

@ -1,105 +0,0 @@
---
en:
models:
bulk_edit:
description: 'Select models to change:'
form_subtitle: 'Select changes to make:'
merge: Merge selected models
needs_organizing: Needs organizing
remove_tags: Remove tags
select: Select model '%{name}'
select_all: Select all models
submit: Update Selected Experiences
title: Bulk Edit Experiences
update_all: Update All %{count} Experiences
bulk_fields:
add_tags: Add tags
bulk_update:
success: Experiences updated successfully.
configure_merge:
common_root:
description: The models will be combined into a single one in the shared root folder
title: New model in common root folder
description: Select one of the models to merge the others into, or create a new one.
heading: Merge models
new_model:
description: A new model will be created from the combined data, and automatically organised on disk.
title: New model
create:
success: File(s) uploaded successfully.
destroy:
confirm: This will delete associated files if they exist on disk. Are you sure you want to continue?
success: Model deleted!
file:
delete: Delete file
edit: Edit file
open_button:
label: View details for %{name}
text: Open
presupported: Presupported Version
set_as_preview: Set as preview
form:
notes:
help_html: You can use <a href="https://www.markdownguide.org/cheat-sheet/" target="markdown">Markdown</a>.
preview_file:
help: The file displayed as a model preview in library pages
tags: Tags
general:
edit: Edit Model
image_carousel:
next: Next
play_pause: Play or pause images
previous: Previous
select_slide: Choose image to display
slide_label: "%{name} (%{index} of %{count})"
list:
bulk_edit: Edit All Experiences
no_results_html: Sorry, we couldn't find anything to show you! Try changing your filters or search terms, or uploading some models.
no_results_signed_out_html: Sorry, we couldn't find anything to show you! There might be more to see if you <a href="%{link}">sign in</a>.
skip_models: Skip model list
merge:
success: Experiences merged successfully.
new:
description: Add new models by uploading files! If you upload a compressed archive, it will be extracted and become a single model containing all the files. If you upload individual files, they will each become a separate model.
files:
label: Select Files
free_space: "(%{available} free)"
library:
help: The library to upload to.
submit: Create models
title: Upload
problem:
merge_all: Merge all
scan:
success: Model scan started
show:
download_preparing: Download is being prepared, please wait.
download_requested: Download requested and will be ready soon, please wait.
files: Files
files_card:
bulk_edit: Edit all files
heading: Files
followers: Followers
license: License
merge:
heading: Merge
warning: Merging moves all files from this model to the target, and removes this model. File metadata is preserved, but any model metadata will be lost!
with: Merge with
model_details: Model Details
organize:
button_text: Organize files
confirm:
are_you_sure: Are you sure you want to do this?
'no': No, cancel
summary_html: The folder and files that make up this model will be moved from:<br> <code>%{from}</code><br> to<br> <code>%{to}</code>
'yes': Yes, move the files
path: Path
preview: This is just a preview of the complete model, which contains %{count} more files. Contact the model owner to get full access.
rescan: Rescan files
search: Search the Internet for models with this name
submit: Upload Files
tags: Tags
upload_card:
heading: Upload
update:
success: Model details saved.

View file

@ -1,3 +0,0 @@
if ENV['CORS_PROXY']
match '/cors/*target_url_segment', to: 'cors_proxy#proxy', via: :all, format: false
end

View file

@ -1,411 +0,0 @@
<!doctype html>
<html lang="en" _data-theme="light">
<head>
<meta charset="utf-8">
<title>XRForge - link AR/VR experiences together 🥽</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="description" content="XRForge - link AR/VR experiences together 🥽" />
<meta name="publisher" content="XRForge - link AR/VR experiences together" />
<meta name="author" content="XRForge"/>
<link rel="stylesheet" href="/assets/themes/slate-9cc3cc7d.css" nonce="1de89072246b25ca36376d8f1cf5e051" />
<link rel="stylesheet" href="/assets/xrforge.css" />
</head>
<body>
<nav class="navbar navbar-expand-md bg-primary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand ms-2" href="/" aria-label="Homepage">
<img alt="XRForge" height="40px" class="me-2" src="/assets/roundel-1d688b1e.svg">
<span class="d-md-none">XRForge</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbarTogglerDemo01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse row" id="navbar">
<ul class="navbar-nav col ps-4 ps-md-0 align-self-start">
<li class="nav-item">
<a class="nav-link active" href="/models"><span><i class="bi bi-box" role="img" title="Experiences"></i></span> <span class="d-md-none d-lg-inline">Experiences</span></a>
</li>
<li class="nav-item">
<a class="nav-link " href="/creators"><span><i class="bi bi-people" role="img" title="Creators"></i></span> <span class="d-md-none d-lg-inline">Creators</span></a>
</li>
<li class="nav-item">
<a class="nav-link " href="/collections"><span><i class="bi bi-collection" role="img" title="Collections"></i></span> <span class="d-md-none d-lg-inline">Collections</span></a>
</li>
</ul>
<ul class="navbar-nav col-auto pe-4 align-self-start justify-content-end">
<li class="nav-item">
</li>
<li class="nav-item">
</li>
<li id="nav-search" class="nav-item ms-1 me-3" data-turbo-permanent="">
<form action="/models" accept-charset="UTF-8" method="get">
<input class="form-control" placeholder="Search" aria_label="Search" aria_describedby="button-search" type="search" name="q" id="q">
</form> </li>
<li class="nav-item">
<a class="btn btn-secondary" nofollow="true" href="/users/sign_in"><span><i class="bi bi-box-arrow-in-right" role="img" title="Sign in"></i></span> <span>Sign in</span></a>
</li>
</ul>
</div>
</div>
</nav>
<header class="container">
<div style="padding:20px 10px;text-align:right">
<small>member of <a href="https://xrhf.isvery.ninja" target="_blank">XR Hypermedia Federation</a></small>
<div id="xrhf"></div>
</div>
<div id="logo"></div>
<h2>Turn files into AR/VR 🥽 experiences</h2>
<br>
<center>
<a href="https://codeberg.org/coderofsalvation/xrforge" class="btn btn-secondary">
<img src="assets/codeberg.svg" style="width:20px"/>
Check the sourcecode<br>
<small>irc.isvery.ninja port 443 via ObsidianIRC</small>
</a>
&nbsp;&nbsp;&nbsp;
<a href="https://isvery.ninja/chat/index.html" 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>
</a>
<br>
</center>
<div id="badges"></div>
<br>
<div id="hint"></div>
<div class="clear"></div>
</header>
<!-- ./ Header -->
<!-- Main -->
<main class="container">
<div class="telescopic">
Publish AR/VR experiences without
<u tabindex="0">obstacles 🈚
<span>like appstores, rent-seeking, blockchain-minting etc</span>
</u>
<br>
Use existing
<u tabindex="0">ecosystems
<span>like the 2D web, or file-collections</span>
</u> within <b>AR/VR-headsets</b> via open
<u tabindex="0">xrfragments 🔌
<span>: <a href="https://en.wikipedia.org/wiki/RSS" target="_blank">RSS</a>, HTML, <a href="https://janusxr.org/docs/build/introtojml/index.html" target="_blank">JML</a>, Fediverse's <a href="https://activitypub.rocks/" target="_blank">ActivityPub</a> and <a href="https://xrfragment.org">URI's using XR Fragments</a> e.g.</span>
</u>
<br>
</div>
<br>
<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>
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
<u tabindex="0">datastores
<span>
<br>
<br>
<b>Support for datastores:</b>
<br>
<table class="table table-dark table-striped">
<tr><td>
Azure Blob <br>
Azure Files <br>
B2 <br>
Box <br>
Cloudinary <br>
Doi <br>
Drime <br>
Drive <br>
Dropbox <br>
Fichier <br>
Filefabric <br>
Filelu <br>
Filen <br>
Filescom <br>
FTP <br>
Gofile <br>
</td>
<td>
Google Cloud Storage <br>
Google Photos <br>
HDFS <br>
Hidrive <br>
HTTP <br>
Iclouddrive <br>
Imagekit <br>
Internet Archive <br>
Internxt <br>
Jottacloud <br>
Koofr <br>
Linkbox <br>
Local <br>
Mailru <br>
Mega <br>
</td>
<td>
Memory <br>
Netstorage <br>
Onedrive <br>
Opendrive <br>
Oracle Object Storage <br>
Pcloud <br>
Pikpak <br>
Pixeldrain <br>
Premiumizeme <br>
Proton Drive <br>
Putio <br>
Qingstor <br>
Quatrix <br>
S3 <br>
Seafile <br>
</td>
<td>
SFTP <br>
Shade <br>
Sharefile <br>
Sia <br>
SMB <br>
Storj <br>
Sugarsync <br>
Swift <br>
Ulozto <br>
WebDAV <br>
Yandex <br>
Zoho <br>
</td>
</tr>
</table>
</span>
</u>, making it perfect for large organisations.
<div id="xrecosystem"></div>
The
<u tabindex="0">Fediverse
<span>, a fleet of ad-free <a href="https://jointhefediverse.net/learn" target="_blank">federated platforms</a>,</span>
</u> makes an excellent backoffice for virtual <b>XR hypermedia</b> experiences.<br>
<b>Federated</b> experiences <b>allow anybody</b> to extend
<u tabindex="0">existing
<span>open-source foundations and shared data</span>
</u>
foundations to accelerate XR
<u tabindex="0">innovation
<span>, by allowing them to "remix" and expand the world </span>
</u>.<br>
<b>Byebye</b> constantly reinventing the wheel / walled gardens.<br>
<b>Hello</b> post-scarcity technology.
<br><br>
<center>
<img src="assets/xrforge_term.svg" style="border-radius:7px; border-radius: 7px; width: 100%; max-width: 450px; margin-bottom: 40px;">
</center>
</div>
<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"/>
</a>
<br>
</div>
</center>
<div class="spectrum">
<div></div>
<div></div>
</div>
<!--
<div id="cards">
<h3 id="howto">How to use XR Forge:&nbsp;&nbsp;</h3>
<input type="radio" id="reset" name="color"/>
<label for="reset"><kbd>#all</kbd></label>
<input type="radio" id="xrfragment" name="color" />
<label for="xrfragment"><kbd>#protocol</kbd></label>
<input type="radio" id="openlearning" name="color"/>
<label for="openlearning"><kbd>#openlearning</kbd></label>
<input type="radio" id="browser" name="color"/>
<label for="browser"><kbd>#xrbrowser</kbd></label>
<input type="radio" id="xrdiscovery" name="color"/>
<label for="xrdiscovery"><kbd>#xrdiscovery</kbd></label>
<input type="radio" id="xrtranslator" name="color"/>
<label for="xrtranslator"><kbd>#xrtranslator</kbd></label>
<input type="radio" id="worldpreservation" name="color"/>
<label for="worldpreservation"><kbd>#worldpreservation</kbd></label>
<br/>
<br/>
<div class="tile xrdiscovery">
<div id="xrforge" class="thumb"></div>
<a class="description" href="https://xrforge.isvery.ninja" target="_blank">
<b>XR Forge</b> is a selfhostable federated platform for JanusWeb/File-based XR Experiences.
#cost-efficient #collaboration #Universities #E-learning #activitpub
</a>
</div>
<div class="tile worldpreservation">
<div id="nexus" class="thumb"></div>
<a class="description" href="https://github.com/TheNexusCity/TheNexus" target="_blank">
The Nexus is an open source, community-driven, CC0 city for open virtual worlds.<br>
#3Dmodels #CreativeCommons
</a>
</div>
<div class="tile worldpreservation">
<div id="lobby" class="thumb"></div>
<a class="description" href="https://github.com/madjin/webxr-lobby" target="_blank">
Opensource Lobby models as 'startingpage' for virtual worlds.<br>
A lobby is an entry-space with doors to other spaces.<br>
</a>
</div>
<div class="tile xrfragment">
<div id="xrfragment" class="thumb"></div>
<a class="description" href="https://xrfragment.org" target="_blank">
XR (URI) Fragments is a browser-xrfragment for
spatial anchors in URLs (to 3D files).<br>
In <b>gamer language</b>: spawnpoints via URLs.
</a>
</div>
<div class="tile xrbrowser">
<div id="januswebbrowser" class="thumb"></div>
<a class="description" href="https://coderofsalvation.github.io/janus-guide/" target="_blank">
<b>JanusWeb</b> browses the XR web inside the web.<br>
Its JML syntax allows regular webpages to project a 3D version.<br>
It also supports 3D files via XR URLs/Fragments.
</a>
</div>
<div class="tile openlearning">
<div id="biggu_s_gate" class="thumb"></div>
<a class="description" href="https://git.benetou.fr/utopiah/biggu_s_gate" target="_blank">
<b>Biggu's Gate</b> is self-hostable web-software to remix simple pedagogical XR games. Learners themselves can then, together with parents and teachers.
</a>
</div>
<div class="tile xrtranslator">
<div id="translators" class="thumb"></div>
<div id="janusweb" class="thumb hue-1" style="background-position: 50% 77px"></div>
<a class="description" href="https://coderofsalvation.github.io/janus-guide/#/wiki/translators" target="_blank">
JanusWeb's <b>XR translators</b> spatialize the web, embedding webstandards like (RSS/HTML) and popular fediverse platforms (peertube/mastodon e.g.).<br>
</a>
</div>
<div class="tile worldpreservation">
<div id="archive_org" class="thumb"></div>
<div id="janusweb" class="thumb hue-2" style="background-position: 50% 77px"></div>
<a class="description" href="https://github.com/jbaicoianu/janusweb" target="_blank">
The XRForge project is currently implementating ways to prevent link / virtualworld <b>rot</b> by integrating backups to <b>archive.org</b>.
</a>
</div>
<div class="tile xrfragment">
<div id="jml" class="thumb"></div>
<div id="janusweb" class="thumb hue-3" style="background-position: 50% 77px"></div>
<a class="description" href="https://janusxr.org/docs/build/introtojml/index.html" target="_blank">
JML is a portable markup-lanuage which allows describing 3D spaces (and link them with portals).
</a>
</div>
<div class="tile xrdiscovery">
<div id="searxr" class="thumb"></div>
<a class="description" href="https://searxr.me" target="_blank">
<b>SearXR</b> is a SearX-based meta-searchengine for <b>WebXR</b> experiences.
</a>
</div>
<div class="tile xrtranslator">
<div id="xrshthumb" class="thumb"></div>
<a class="description" href="https://xrsh.isvery.ninja" target="_blank">
<b>XRSH</b> is a Linux Shell for <b>WebXR</b>, which makes the Linux ecosystem of cli-tools available in <b>WebXR</b>.
</a>
</div>
<div class="tile xrbrowser">
<div id="aframeverse" class="thumb"></div>
<a class="description" href="https://github.com/coderofsalvation/aframe-verse" target="_blank">
AFRAME-verse was a XR Hypermedia network-experiment which traverses crossdomain AFRAME XR experiences seamlessly. The learnings became the fundament for the XR (URI) Fragments.<br>
</a>
</div>
<div class="tile xrbrowser">
<div id="elfa" class="thumb"></div>
<a class="description" href="https://fosstodon.org/@elfa" target="_blank">
<b>ELFA</b> is a suite of <b>E</b>ncrypted <b>L</b>ocal<b>F</b>irst<b> A</b>pplications, an integrated Suite of Apps, integrated with VR/XR.<br>
</a>
</div>
<div class="tile xrtranslator">
<div id="peertube" class="thumb"></div>
<a class="description" href="https://codeberg.org/coderofsalvation/janus-script-peertube" target="_blank">
<b>Peertube</b> is a free and open-source platform for sharing videos.<br>This translator makes it possible to view peertube in JanusWeb.<br>
</a>
</div>
<div class="tile openlearning">
<div id="janusdialog" class="thumb"></div>
<a class="description" href="https://codeberg.org/coderofsalvation/janus-script-dialog" target="_blank">
This janusweb AR/VR dialog-component makes it possible to make spatial elearnings more dynamic.
</a>
</div>
<div class="tile xrtranslator">
<div id="janusrss" class="thumb"></div>
<a class="description" href="https://codeberg.org/coderofsalvation/janus-script-rss" target="_blank">
This janusweb AR/VR component makes it possible to read RSS-feeds, an important web-buildingblock in Janusweb.
</a>
</div>
<div class="tile xrtranslator">
<div id="janusfetch" class="thumb"></div>
<a class="description" href="https://codeberg.org/coderofsalvation/janus-script-fetch" target="_blank">
This janusweb AR/VR component makes it possible to import partial content from the web in realtime.
</a>
</div>
<div class="clear"></div>
</div>
<br>
<div class="spectrum">
<div></div>
<div></div>
</div>
<div class="footer">
<small>Jump to:</small>
<a href="#howto"><kbd>#howto</kbd></a>
<a href="#timeline"><kbd>#timeline</kbd></a>
<a href="#faq"><kbd>#getfunding</kbd></a>
<a href="#faq"><kbd>#faq</kbd></a>
</div>
-->
</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

@ -1,994 +0,0 @@
:root {
--pico-line-height: 2;
--pico-primary-background: #77F !important;
}
.container{
}
main{
margin-bottom:120px;
}
small{
color: var(--pico-contrast-underline);
}
.hue-1{ filter: hue-rotate(45deg); }
.hue-2{ filter: hue-rotate(90deg); }
.hue-3{ filter: hue-rotate(135deg); }
.hue-4{ filter: hue-rotate(180deg); }
a,
a:active{
color: #95F;
}
a:visited{
color: #888;
}
header > .spectrum,
div#logo{
width:290px;
}
/* images (hidden for html2text browsers like lynx) */
div#logo{
background: url(../img/xrh-full.svg);
height:112px;
display:inline-block;
background-size:cover;
}
div#lvk{
background: url(../img/lvk.jpg);
height:100px;
width:100px;
background-size:cover;
display:inline-block;
border-radius:50%;
float:left;
margin-right:25px;
}
div#jin{
background: url(../img/madjin.png);
height:100px;
width:100px;
background-size:cover;
display:inline-block;
border-radius:50%;
float:left;
margin-right:25px;
}
div#fabien{
background: url(../img/fabien.jpg);
height:100px;
width:100px;
background-size:cover;
display:inline-block;
border-radius:50%;
float:left;
margin-right:25px;
}
button,
.btn{
box-shadow: 0px 0px 15px #77F7;
}
.btn{
-webkit-appearance: button;
--pico-background-color: var(--pico-primary-background);
--pico-border-color: var(--pico-primary-border);
--pico-color: var(--pico-primary-inverse);
--pico-box-shadow: var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));
padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
border: var(--pico-border-width) solid var(--pico-border-color);
border-radius: var(--pico-border-radius);
outline: 0;
background-color: var(--pico-background-color);
box-shadow: var(--pico-box-shadow);
color: var(--pico-color);
font-weight: var(--pico-font-weight);
font-size: 1rem;
line-height: var(--pico-line-height);
text-align: center;
text-decoration: none;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
transition: background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition);
}
div.banner,
div#banner {
overflow:hidden;
height: 40vh;
max-height:200px;
width: 100%;
margin:30px 0px;
border-radius:7px;
}
div#banner{
background: url(../img/banner.jpg);
background-size: 10%;
background-position: center;
-webkit-animation: scrolly 20s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
animation: scrolly 20s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
div#hint{
float:right;
background: url(../img/blinkhint.svg);
height: 129px;
background-size: cover;
display:inline-block;
width: 200px;
}
div#xrecosystem{
background: #000 url(../img/xrecosystem.png);
background-repeat: no-repeat;
height: 200px;
background-size: cover;
width: 100%;
border-radius:7px;
background-size: contain;
background-position: center;
margin:30px 0px;
}
div#translators{
background: url(../img/translators.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: contain;
width: 100%;
border-radius:7px;
margin:15px 0px;
position:absolute;
z-index:100;
}
div#archive_org{
background: url(../img/archive.org.jpg);
height: 153px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
margin:15px 0px;
position:absolute;
z-index:100;
}
div#xrfragment{
background: url(../img/xrfragments.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#badges{
background: url(../img/badges.png);
height: 90px;
background-repeat:no-repeat;
background-size: contain;
background-position:bottom;
width: 100%;
max-width:535px;
display:inline-block;
}
div#peertube{
background: url(../img/peertube-app.webp);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
background-position:center;
width: 100%;
}
div#jml{
background: url(../img/jml.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: contain;
width: 100%;
border-radius:7px;
margin:15px 0px;
position:absolute;
z-index:100;
}
div#janusdialog{
background: url(../img/janus-script-dialog.webp);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
background-position:center;
width: 100%;
}
div#janusrss{
background: url(../img/janus-script-rss.png);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
background-position:center;
width: 100%;
}
div#janusfetch{
background: url(../img/janus-script-fetch.png);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
background-position:center;
width: 100%;
}
div#xrshthumb{
background: url(../img/xrsh-thumb.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
background-position:center;
width: 100%;
}
div#janusweb{
background: url(../img/janusweb.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#janus1{
background: url(../img/janus1.jpg);
height: 100px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
border-radius:7px;
}
div#xrflogo{
background: url(../img/xrf.svg);
height: 46px;
background-repeat:no-repeat;
background-size: cover;
width: 120px;
border-radius:7px;
}
div#hanlogo{
background: url(../img/han.jpg);
height: 46px;
background-repeat:no-repeat;
background-size: cover;
width: 88px;
border-radius:7px;
}
div#searxrlogo{
background: url(../img/searxr.jpg);
height: 23px;
background-repeat:no-repeat;
background-size: cover;
width: 100px;
border-radius:7px;
}
div#aframeverse{
height: 400px;
background: url(../img/aframe-verse.gif);
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#elfa{
height: 400px;
background: url(../img/elfa.jpg);
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#xrsh{
background: url(../img/xrsh.webp);
height: 200px;
background-repeat:no-repeat;
background-size: cover;
width: 250px;
border-radius:7px;
}
div#janusxrlogo{
background: url(../img/janusxr-logo.png);
height: 71px;
filter: brightness(0.66);
background-repeat:no-repeat;
background-size: cover;
width: 200px;
border-radius:7px;
}
div#xrforge{
background: url(../img/xrforge-thumb.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#nexus{
background: url(../img/nexus.jpg);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#lobby{
background: url(../img/lobby.png);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#biggu_s_gate{
background: url(../img/biggu.webp);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#searxr{
background: url(../img/searxr.gif);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#januswebbrowser{
background: url(../img/janusoasis.webp);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#euvalues{
background: url(../img/EUvalues.webp);
height: 400px;
background-size: cover;
width: 100%;
border-radius:7px;
margin:15px 0px;
background-position:center;
}
.spectrum{
margin-top:10px;
box-shadow: 0px 0px 5px #F0F;
margin-bottom:50px;
}
.spectrum > div,
.ruler{
margin:3px 0px;
clear:both;
background:linear-gradient(90deg, #F07, #77F, #F07);
height:2px;
}
/* sticky footer */
.footer {
position: fixed;
bottom: 0;
/* Optional: Add background, padding, etc. */
background: linear-gradient( 0deg, var(--pico-background-color) 66%, #0000);
padding: 1rem;
width:100%;
z-index: 1000; /* Ensures it stays above other content */
}
.clear{
clear:both;
}
/* telescopic text */
u {
cursor: pointer;
display: inline;
text-decoration:none;
white-space:nowrap;
font-weight:bold;
padding:0;
}
u > u,
u > span{
display: none;
}
u:focus-within > u,
u:focus-within > span {
display: inline;
}
u span{
font-weight:normal;
white-space: normal;
background:none;
}
/* underline settings */
.telescopic u:nth-child(odd)::before,
.telescopic u:nth-child(even)::before{
border-bottom:4px solid #F0F;
content:' ';
display:inline-block;
margin-right:5px;
width:23px;
height:32px;
position: absolute;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.telescopic u:nth-child(odd)::before{
border-bottom:4px solid #77F;
}
.vertical u:focus-within > u::after,
.vertical u:focus-within > span::after {
content:' ';
display:block;
}
.show > u,
.show > span{
display:inline;
}
.telescopic u.show::before,
.telescopic u.show::after{
display:none;
}
/* bg mode */
.bg > u,
.bg u:focus-within > u {
background:#EEE;
border-radius:4px;
padding:4px;
}
.bg u.show {
background:#FFF;
}
@keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.3; }
to { opacity: 1.0; }
}
@-webkit-keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.3; }
to { opacity: 1.0; }
}
/* cards filter */
input[type="radio"] {
display:none;
}
label {
padding-bottom:10px;
cursor:pointer;
}
input[type="radio"][id="openlearning"]:checked ~ .protocol,
input[type="radio"][id="openlearning"]:checked ~ .xrdiscovery,
input[type="radio"][id="openlearning"]:checked ~ .xrbrowser,
input[type="radio"][id="openlearning"]:checked ~ .xrtranslator,
input[type="radio"][id="openlearning"]:checked ~ .worldpreservation,
input[type="radio"][id="protocol"]:checked ~ .openlearning,
input[type="radio"][id="protocol"]:checked ~ .xrdiscovery,
input[type="radio"][id="protocol"]:checked ~ .xrbrowser,
input[type="radio"][id="protocol"]:checked ~ .xrtranslator,
input[type="radio"][id="protocol"]:checked ~ .worldpreservation,
input[type="radio"][id="worldpreservation"]:checked ~ .openlearning,
input[type="radio"][id="worldpreservation"]:checked ~ .xrdiscovery,
input[type="radio"][id="worldpreservation"]:checked ~ .xrbrowser,
input[type="radio"][id="worldpreservation"]:checked ~ .xrtranslator,
input[type="radio"][id="worldpreservation"]:checked ~ .protocol,
input[type="radio"][id="xrdiscovery"]:checked ~ .openlearning,
input[type="radio"][id="xrdiscovery"]:checked ~ .protocol,
input[type="radio"][id="xrdiscovery"]:checked ~ .xrbrowser,
input[type="radio"][id="xrdiscovery"]:checked ~ .xrtranslator,
input[type="radio"][id="xrdiscovery"]:checked ~ .worldpreservation,
input[type="radio"][id="xrtranslator"]:checked ~ .openlearning,
input[type="radio"][id="xrtranslator"]:checked ~ .protocol,
input[type="radio"][id="xrtranslator"]:checked ~ .xrbrowser,
input[type="radio"][id="xrtranslator"]:checked ~ .xrdiscovery,
input[type="radio"][id="xrtranslator"]:checked ~ .worldpreservation,
input[type="radio"][id="browser"]:checked ~ .protocol,
input[type="radio"][id="browser"]:checked ~ .xrdiscovery,
input[type="radio"][id="browser"]:checked ~ .xrtranslator,
input[type="radio"][id="browser"]:checked ~ .worldpreservation,
input[type="radio"][id="browser"]:checked ~ .openlearning {
width:0;
height:0;
padding:0;
margin:0;
opacity:0;
}
.tile {
width:30%;
border: 1px solid #FFF2;
height:550px;
float:left;
overflow:hidden;
transition:all 0.3s;
margin:0 1% 1% 0;
background:#7773;
border-radius:7px;
position:relative;
}
@media screen and (max-width: 660px) {
.tile{
width:100%;
margin-bottom:30px;
}
}
.tile .description{
color:#888;
font-size:16px;
display:block;
text-decoration:none;
cursor:pointer;
padding:15px;
}
.tile .description:hover{
text-decoration:underline;
}
/* pulsating circle */
.pulsating-circle {
position: absolute;
left: 4px;
top: 20px;
transform: translateX(-50%) translateY(-50%);
width: 30px;
height: 30px;
z-index:-100;
opacity:0.3;
}
.pulsating-circle:before {
content: "";
position: relative;
display: block;
width: 300%;
height: 300%;
box-sizing: border-box;
margin-left: -100%;
margin-top: -100%;
border-radius: 45px;
background-color: #A4F;
-webkit-animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
.pulsating-circle:after {
content: "";
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
background-color: white;
border-radius: 15px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
-webkit-animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
}
@-webkit-keyframes pulse-ring {
0% {
transform: scale(0.33);
}
80%, 100% {
opacity: 0;
}
}
@keyframes pulse-ring {
0% {
transform: scale(0.33);
}
80%, 100% {
opacity: 0;
}
}
@-webkit-keyframes pulse-dot {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.8);
}
}
@keyframes pulse-dot {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.8);
}
}
@-webkit-keyframes scrolly {
0% {
transform: scale(1);
}
50% {
transform: scale(10);
}
100% {
transform: scale(1);
}
}
@keyframes scrolly {
0% {
transform: scale(1);
}
50% {
transform: scale(10);
}
100% {
transform: scale(1);
}
}
@media (max-width: 600px) {
@-webkit-keyframes scrolly {
0% {
transform: scale(1);
}
50% {
transform: scale(27);
}
100% {
transform: scale(1);
}
}
@keyframes scrolly {
0% {
transform: scale(1);
}
50% {
transform: scale(27);
}
100% {
transform: scale(1);
}
}
}
/* ================ The Timeline ================ */
.timeline {
font-size:15px;
position: relative;
width: 660px;
margin: 0 auto;
margin-top: 20px;
padding: 1em 0;
list-style-type: none;
}
.timeline:before {
position: absolute;
left: 50%;
top: 0;
content: ' ';
display: block;
width: 6px;
height: 100%;
margin-left: -3px;
background: rgb(80,80,80);
background: -moz-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(30,87,153,1)), color-stop(100%,rgba(125,185,232,1)));
background: -webkit-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
background: -o-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
background: -ms-linear-gradient(top, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
background: linear-gradient(to bottom, rgba(80,80,80,0) 0%, rgb(80,80,80) 8%, rgb(80,80,80) 92%, rgba(80,80,80,0) 100%);
z-index: 5;
}
.timeline li {
padding: 0 0;
list-style: none;
text-decoration:none;
}
.timeline *,
.timeline *::after,
.timeline *::before{
box-sizing:content-box;
}
.timeline li:after {
content: "";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.direction-l {
position: relative;
width: 300px;
float: left;
text-align: right;
}
.direction-r {
position: relative;
width: 300px;
float: right;
}
.flag-wrapper {
position: relative;
display: inline-block;
text-align: center;
}
.flag {
color:#666;
position: relative;
display: inline;
background: rgb(248,248,248);
padding: 6px 10px;
border-radius: 5px;
font-weight: 600;
text-align: left;
}
.direction-l .flag {
-webkit-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
-moz-box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
box-shadow: -1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
}
.direction-r .flag {
-webkit-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
-moz-box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
box-shadow: 1px 1px 1px rgba(0,0,0,0.15), 0 0 1px rgba(0,0,0,0.15);
}
.direction-l .flag:before,
.direction-r .flag:before {
position: absolute;
top: 50%;
right: -40px;
content: ' ';
display: block;
width: 12px;
height: 12px;
margin-top: -10px;
background: #fff;
border-radius: 10px;
border: 4px solid #77F;
z-index: 10;
}
.direction-r .flag:before {
left: -40px;
}
.direction-l .flag:after {
content: "";
position: absolute;
left: 100%;
top: 50%;
height: 0;
width: 0;
margin-top: -8px;
border: solid transparent;
border-left-color: rgb(248,248,248);
border-width: 8px;
pointer-events: none;
}
.direction-r .flag:after {
content: "";
position: absolute;
right: 100%;
top: 50%;
height: 0;
width: 0;
margin-top: -8px;
border: solid transparent;
border-right-color: rgb(248,248,248);
border-width: 8px;
pointer-events: none;
}
.time-wrapper {
display: inline;
line-height: 1em;
font-size: 0.96666em;
color: #07F;
vertical-align: middle;
}
.direction-l .time-wrapper {
float: left;
}
.direction-r .time-wrapper {
float: right;
}
.time {
display: inline-block;
padding: 4px 6px;
margin: 5px 15px;
background: rgb(248,248,248);
}
.desc {
margin: 1em 0.75em 0 0;
color:#888;
font-size: 1em;
font-style: italic;
line-height: 1.5em;
}
.direction-r .desc {
margin: 1em 0 0 0.75em;
}
/* ================ Timeline Media Queries ================ */
@media screen and (max-width: 660px) {
.timeline {
width: 100%;
padding: 4em 0 1em 0;
}
.timeline ul li {
padding: 2em 0;
}
.direction-l,
.direction-r {
float: none;
width: 100%;
text-align: center;
}
.flag-wrapper {
text-align: center;
}
.flag {
background: rgb(255,255,255);
z-index: 15;
}
.direction-l .flag:before,
.direction-r .flag:before {
position: absolute;
top: -30px;
left: 50%;
content: ' ';
display: block;
width: 12px;
height: 12px;
margin-left: -9px;
background: #fff;
border-radius: 10px;
border: 4px solid rgb(255,80,80);
z-index: 10;
}
.direction-l .flag:after,
.direction-r .flag:after {
content: "";
position: absolute;
left: 50%;
top: -8px;
height: 0;
width: 0;
margin-left: -8px;
border: solid transparent;
border-bottom-color: rgb(255,255,255);
border-width: 8px;
pointer-events: none;
}
.time-wrapper {
display: block;
position: relative;
margin: 4px 0 0 0;
z-index: 14;
}
.direction-l .time-wrapper {
float: none;
}
.direction-r .time-wrapper {
float: none;
}
.desc {
position: relative;
margin: 1em 0 0 0;
padding: 1em;
background: var(--pico-background-color);
-webkit-box-shadow: 0 0 1px rgba(0,0,0,0.20);
-moz-box-shadow: 0 0 1px rgba(0,0,0,0.20);
box-shadow: 0 0 5px #8887;
border-radius: 7px;
z-index: 15;
}
.direction-l .desc,
.direction-r .desc {
position: relative;
margin: 1em 1em 0 1em;
padding: 1em;
z-index: 15;
}
}
@media screen and (min-width: 400px ?? max-width: 660px) {
.direction-l .desc,
.direction-r .desc {
margin: 1em 4em 0 4em;
}
}
@media screen and (max-width: 660px) {
.timeline li{
margin: 57px 0px !important;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

View file

@ -1,164 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 4.2333332 4.2333335"
version="1.1"
id="svg1468"
sodipodi:docname="codeberg-logo_icon_white.svg"
inkscape:version="1.2-alpha1 (b6a15bb, 2022-02-23)"
inkscape:export-filename="/home/robert/Documents/Codeberg/Logo-Kit/svg/Codeberg-favicon_64px.png"
inkscape:export-xdpi="384"
inkscape:export-ydpi="384"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title16">Codeberg logo</title>
<defs
id="defs1462">
<linearGradient
xlink:href="#linearGradient6924"
id="linearGradient6918"
x1="42519.285"
y1="-7078.7891"
x2="42575.336"
y2="-6966.9307"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient6924">
<stop
style="stop-color:#2185d0;stop-opacity:0"
offset="0"
id="stop6920" />
<stop
id="stop6926"
offset="0.49517274"
style="stop-color:#2185d0;stop-opacity:0.48923996" />
<stop
style="stop-color:#2185d0;stop-opacity:0.63279623"
offset="1"
id="stop6922" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient6924-6"
id="linearGradient6918-3"
x1="42519.285"
y1="-7078.7891"
x2="42575.336"
y2="-6966.9307"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient6924-6">
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="0"
id="stop6920-7" />
<stop
id="stop6926-5"
offset="0.49517274"
style="stop-color:#ffffff;stop-opacity:0.30000001;" />
<stop
style="stop-color:#ffffff;stop-opacity:0.30000001;"
offset="1"
id="stop6922-3" />
</linearGradient>
</defs>
<sodipodi:namedview
showborder="false"
id="base"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="2.8505242"
inkscape:cy="18.274291"
inkscape:document-units="px"
inkscape:current-layer="svg1468"
inkscape:document-rotation="0"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px"
inkscape:snap-global="false"
inkscape:snap-page="true"
showguides="false"
inkscape:window-width="1531"
inkscape:window-height="873"
inkscape:window-x="69"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid2067" />
</sodipodi:namedview>
<metadata
id="metadata1465">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Codeberg logo</dc:title>
<cc:license
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
<dc:creator>
<cc:Agent>
<dc:title>Robert Martinez</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>Codeberg and the Codeberg Logo are trademarks of Codeberg e.V.</dc:title>
</cc:Agent>
</dc:rights>
<dc:date>2020-04-09</dc:date>
<dc:publisher>
<cc:Agent>
<dc:title>Codeberg e.V.</dc:title>
</cc:Agent>
</dc:publisher>
<dc:source>codeberg.org</dc:source>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="g370484"
inkscape:label="logo"
transform="matrix(0.06551432,0,0,0.06551432,-2.232417,-1.431776)">
<path
id="path6733-5"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#linearGradient6918-3);fill-opacity:1;stroke:none;stroke-width:3.67846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke markers fill;stop-color:#000000;stop-opacity:1"
d="m 42519.285,-7078.7891 a 0.76086879,0.56791688 0 0 0 -0.738,0.6739 l 33.586,125.8886 a 87.182358,87.182358 0 0 0 39.381,-33.7636 l -71.565,-92.5196 a 0.76086879,0.56791688 0 0 0 -0.664,-0.2793 z"
transform="matrix(0.37058478,0,0,0.37058478,-15690.065,2662.0533)"
inkscape:label="berg" />
<path
id="path360787"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke-width:17.0055;paint-order:markers fill stroke;stop-color:#000000"
d="m 11249.461,-1883.6961 c -12.74,0 -23.067,10.3275 -23.067,23.0671 0,4.3335 1.22,8.5795 3.522,12.2514 l 19.232,-24.8636 c 0.138,-0.1796 0.486,-0.1796 0.624,0 l 19.233,24.8646 c 2.302,-3.6721 3.523,-7.9185 3.523,-12.2524 0,-12.7396 -10.327,-23.0671 -23.067,-23.0671 z"
sodipodi:nodetypes="sccccccs"
inkscape:label="sky"
transform="matrix(1.4006354,0,0,1.4006354,-15690.065,2662.0533)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="500.44669"
height="526.20935"
id="svg46"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs46" />
<path
d="m 355.95692,38.225006 c 2.73479,2.461154 5.39209,4.994684 8.01758,7.571777 1.34542,1.316746 2.71389,2.609828 4.08398,3.900879 6.87186,6.601735 12.61753,13.935978 18.35938,21.511719 1.13437,-0.639375 2.26875,-1.27875 3.4375,-1.9375 8.27553,-4.036494 14.6524,-3.37541 23.29687,-0.410156 20.96255,8.650811 34.56671,29.342441 43.21143,49.559575 11.18602,27.52311 17.36113,56.91586 12.51514,86.51855 -2.69159,19.09202 5.52731,31.81769 15.36645,47.34644 17.668,27.88651 17.668,27.88651 14.77417,41.84497 -2.16162,7.24282 -7.64806,11.92453 -13.60156,16.26562 -8.45868,6.85964 -13.04155,18.66184 -14.44947,29.16578 -0.37215,3.75471 -0.68688,7.51068 -0.98803,11.27172 -1.87048,20.90882 -5.32469,40.51095 -21.5625,55.375 -2.85414,2.34651 -5.75482,4.23758 -9,6 -0.82371,0.45118 -1.64742,0.90235 -2.4961,1.36719 -16.13456,7.73544 -35.26629,7.046 -52.77587,7.16699 -10.30918,-0.0912 -10.30918,-0.0912 -19.72803,3.46582 -3.4632,5.12978 -5.11284,11.14152 -6.69922,17.05469 -0.48693,1.75394 -0.974,3.50785 -1.46118,5.26172 -0.24896,0.90766 -0.49792,1.81532 -0.75443,2.75049 -7.2809,26.46075 -16.04317,51.78931 -27.08517,76.9331 -5.72933,-0.6399 -10.95971,-2.06834 -16.44922,-3.80078 -0.89387,-0.27764 -1.78774,-0.55529 -2.7087,-0.84135 -2.84477,-0.88514 -5.68714,-1.77777 -8.52958,-2.67037 -1.88983,-0.58835 -3.7798,-1.17624 -5.66992,-1.76367 -5.21932,-1.62524 -10.43287,-3.26807 -15.64258,-4.92383 -1.01372,-0.32142 -1.01372,-0.32142 -2.04791,-0.64935 -1.70079,-0.5419 -3.39885,-1.09234 -5.09662,-1.64361 -0.88664,-0.28771 -1.77327,-0.57541 -2.68677,-0.87183 -2.1687,-0.83521 -2.1687,-0.83521 -5.1687,-2.83521 -4.26651,-0.38661 -8.54211,-0.32916 -12.82398,-0.31567 -2.10282,0.003 -4.20423,-0.0203 -6.30688,-0.0457 -1.34049,-0.002 -2.68099,-0.003 -4.02149,-0.002 -1.82519,-0.004 -1.82519,-0.004 -3.68725,-0.009 -3.56773,0.4203 -4.39448,1.22301 -7.1604,3.37231 -2.98157,1.18491 -6.04867,2.07743 -9.1211,2.9961 -0.89574,0.27482 -1.79148,0.54963 -2.71437,0.83278 -2.86509,0.87713 -5.73343,1.74305 -8.60203,2.60862 -1.89541,0.57804 -3.7906,1.15681 -5.68555,1.73633 -3.64787,1.11522 -7.29838,2.22117 -10.95068,3.32178 -3.01197,0.91131 -6.01048,1.8496 -8.99707,2.84082 -1.14324,0.37496 -2.28648,0.74991 -3.46436,1.13623 -0.95399,0.32364 -1.90797,0.64727 -2.89087,0.98071 -2.99203,0.63542 -4.71313,0.64942 -7.57397,-0.45337 -1.38697,-2.02075 -1.38697,-2.02075 -2.47266,-4.73828 -0.41854,-1.02907 -0.83708,-2.05815 -1.26831,-3.1184 -0.43611,-1.14042 -0.87221,-2.28084 -1.32153,-3.45582 -0.46898,-1.19262 -0.93796,-2.38524 -1.42115,-3.61401 -8.70125,-22.36667 -16.18549,-45.23058 -21.93115,-68.53369 -2.03413,-10.5326 -2.03413,-10.5326 -8.26758,-18.66968 -6.69644,-2.74275 -14.2617,-2.30183 -21.38613,-2.35119 -22.502322,-0.18974 -46.337252,-0.43669 -63.931492,-16.51893 -13.83191,-15.39279 -17.60146,-34.36322 -19,-54.3125 -1.22377,-17.31546 -5.66451,-30.87225 -19,-42.6875 -1.0789999,-0.83894 -2.1618899,-1.67292 -3.2499999,-2.5 -4.15117,-3.77378 -6.29127995,-7.45743 -7.13280995,-13.03906 -0.75997,-17.5805 10.99539985,-33.43582 20.07542985,-47.59253 9.56114,-14.99879 12.65147,-25.61969 9.80738,-43.24341 -5.4019,-36.3138 6.589,-77.94207 27.51562,-107.953124 9.29206,-12.511354 22.11961,-24.30302 38.10938,-26.671875 7.020142,-0.401789 13.117592,2.099822 18.875002,6 0.41572,-0.877852 0.41572,-0.877852 0.83984,-1.773438 3.97707,-7.632757 10.32837,-13.996733 16.16016,-20.226562 1.21816,-1.316777 1.21816,-1.316777 2.46094,-2.660157 11.76049,-12.272588 25.4555,-21.61809 40.53906,-29.339843 0.92546,-0.477114 0.92546,-0.477114 1.86963,-0.963867 56.77867,-28.87966 131.24829,-21.222612 179.66943,20.979552 z"
fill="#4cb96f"
id="path1" />
<path
d="m 104.48036,74.209386 c 14.01919,9.544982 17.28909,27.479924 20.9375,43.000004 0.34781,1.46349 0.34781,1.46349 0.70264,2.95654 3.9666,16.83356 7.13574,33.77169 10.17238,50.78966 4.94765,27.65632 10.55273,55.18357 18.12498,82.2538 0.21592,0.77408 0.43184,1.54816 0.6543,2.3457 5.69367,20.18367 12.22766,40.2264 22.3457,58.6543 0.69292,1.29807 1.38562,2.59625 2.07813,3.89453 4.85041,8.96022 10.35159,17.39712 16.05078,25.83594 2.51273,3.72383 4.97364,7.47022 7.37109,11.26953 0.58781,0.92813 1.17563,1.85625 1.78125,2.8125 4.16437,7.72302 6.41065,15.04331 7.34375,23.75 0.11916,1.03649 0.23832,2.07297 0.36108,3.14087 1.31022,12.42718 1.79484,24.92019 2.28003,37.40234 0.32998,8.23421 0.78685,16.41561 1.69092,24.60913 0.0781,0.7089 0.15615,1.4178 0.23659,2.14818 1.638,13.89438 5.50414,26.91584 10.16429,40.07887 0.3408,0.97437 0.68159,1.94874 1.03272,2.95264 0.30969,0.86472 0.61939,1.72944 0.93847,2.62036 0.70466,2.61015 0.719,4.79568 0.6709,7.48511 -6.7243,2.12356 -13.45057,4.24083 -20.17822,6.35376 -2.28757,0.71926 -4.57468,1.43995 -6.86133,2.16211 -3.29033,1.03886 -6.58224,2.07253 -9.87451,3.10522 -1.52846,0.48442 -1.52846,0.48442 -3.0878,0.97863 -4.64994,1.45342 -9.14539,2.79279 -13.99814,3.40028 -4.25493,-8.82759 -7.84647,-17.67925 -11.0625,-26.9375 -0.40799,-1.15951 -0.81598,-2.31902 -1.23633,-3.51367 -0.82345,-2.34028 -1.64329,-4.68184 -2.45947,-7.02466 -0.83606,-2.3729 -1.68896,-4.73992 -2.55762,-7.10107 -2.70984,-7.38288 -4.90529,-14.70373 -6.68408,-22.3606 -1.68682,-6.94843 -3.38042,-13.87552 -5.5625,-20.6875 -0.35191,-1.10602 -0.70383,-2.21203 -1.06641,-3.35156 -2.24045,-4.9405 -4.92423,-8.26055 -9.80859,-10.83594 -6.07494,-2.02498 -11.81683,-2.33875 -18.15234,-2.39844 -1.68947,-0.0281 -3.37892,-0.0568 -5.06836,-0.0859 -2.62573,-0.0409 -5.25129,-0.0778 -7.8772,-0.10449 C 84.939569,413.59976 66.85999,412.17514 52.382702,398.4047 38.718144,383.13734 36.06592,361.22023 35.364635,341.59611 34.551434,325.69568 26.593093,312.67134 15.042858,302.14689 c -1.235459,-1.09677 -2.472338,-2.19194 -3.710937,-3.28516 -3.7754319,-3.43635 -3.7754319,-3.43635 -4.1874999,-7.05078 -0.586941,-17.20186 10.8112229,-31.757 19.6821279,-45.75171 6.320153,-10.07205 10.787181,-18.70157 10.778809,-30.72485 6.45e-4,-0.94948 0.0013,-1.89895 0.002,-2.8772 -0.04662,-6.22488 -0.64097,-12.10354 -1.732422,-18.23608 -4.852679,-31.97792 8.967014,-72.55721 27.833252,-98.270269 9.00489,-11.875077 24.526541,-27.04615 40.772172,-21.741455 z"
fill="#eff7f9"
id="path2" />
<path
d="m 406.41786,73.209386 c 19.25538,6.768868 32.14526,23.622413 40.86719,41.308594 12.36273,26.41954 19.99939,56.17564 15.63281,85.25391 -3.53699,23.64241 4.2922,39.12412 17.125,58.5 5.48203,8.27792 12.41526,19.13006 12.625,29.25 0.0387,1.10795 0.0387,1.10795 0.0781,2.23828 -0.88208,6.58409 -4.79511,10.09514 -9.57813,14.13672 -13.9383,12.30953 -18.45791,26.64607 -19.75,44.6875 -1.35283,18.73921 -4.43964,38.99647 -18.8125,52.5 -12.2884,9.85902 -27.59803,12.18836 -42.92969,12.33594 -1.66016,0.0281 -3.32032,0.0567 -4.98047,0.0859 -2.57568,0.0412 -5.15117,0.0781 -7.72705,0.1045 -15.51745,-0.24013 -15.51745,-0.24013 -29.42529,5.72363 -4.7446,6.41917 -6.8127,13.74762 -8.5625,21.4375 -1.9491,8.124 -4.15709,16.07942 -6.77002,24.01611 -0.93383,2.85328 -1.8442,5.71376 -2.75732,8.57373 -4.93702,15.29249 -10.60339,30.12249 -17.03516,44.84769 -6.07975,-0.65079 -11.63799,-2.2844 -17.45703,-4.125 -1.47626,-0.46044 -1.47626,-0.46044 -2.98235,-0.93018 -2.06909,-0.64641 -4.13738,-1.29538 -6.20491,-1.94677 -3.17349,-0.99971 -6.34931,-1.99174 -9.52563,-2.98243 -2.01251,-0.62976 -4.02489,-1.25995 -6.03711,-1.89062 -0.95204,-0.29793 -1.90408,-0.59587 -2.88497,-0.90283 -1.31728,-0.41548 -1.31728,-0.41548 -2.66117,-0.83936 -1.1608,-0.36521 -1.1608,-0.36521 -2.34505,-0.73779 -1.90178,-0.64502 -1.90178,-0.64502 -3.90178,-1.64502 0.58313,-7.25383 2.64659,-13.78262 4.875,-20.6875 4.45365,-13.85851 7.51171,-27.09272 8.6875,-41.625 0.12559,-1.45997 0.25124,-2.91993 0.37695,-4.37988 0.69327,-8.30279 1.20649,-16.60591 1.58789,-24.92871 0.0411,-0.88719 0.0821,-1.77437 0.12441,-2.68843 0.19608,-4.31989 0.37451,-8.63956 0.52623,-12.96123 0.77791,-20.25966 6.05514,-34.69661 17.57202,-51.41675 15.399,-22.76918 26.3875,-45.27462 35.25,-71.3125 0.28617,-0.83789 0.57234,-1.67578 0.86719,-2.53906 11.82929,-35.01939 18.09788,-71.56471 24.78988,-107.8216 13.6546,-73.951398 13.6546,-73.951398 29.21402,-87.237001 3.86755,-2.547613 7.67898,-2.136857 12.12891,-1.402343 z"
fill="#eff7f9"
id="path3" />
</svg>

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

View file

@ -1,495 +0,0 @@
#sidebar td > label > i {
opacity:0.4;
}
#toggle:checked + .detail-tooltip {
display: block;
}
.hidden-tooltip {
display: none;
margin-top:7px;
max-height: 230px;
overflow-y:scroll;
background: var(--bs-body-bg);
padding: 12px;
border-radius: 6px;
}
/* Show the tooltip when the corresponding checkbox is checked */
input[type="checkbox"]:checked + .hidden-tooltip {
display: block;
}
#model_notes {
min-height: 50vh;
font-family: monospace;
font-size: 13px;
color: #CCF;
background: #000;
}
.col-form-label{
width:111px;
}
/* shimmer loader */
.shimcontainer {
width: 100%;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
background: #000;
border-radius: 10px;
}
article {
background: #000;
width: 100%;
position: relative;
box-sizing: border-box;
border-radius: 10px;
overflow: hidden;
text-align:center;
color:#AAA;
}
.shimmer {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
background: linear-gradient(
100deg,
rgba(255, 255, 255, 0) 20%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0) 80%
);
animation: shimmer 2s infinite linear;
}
@keyframes shimmer {
from {
transform: translateX(-200%);
}
to {
transform: translateX(200%);
}
}
.shimcontainer img {
height: 175px;
margin-top: 37px;
display:inline-block;
}
/*
* styling for pages in /public/*
*/
header,
main,
.footer{
font-size:1.2em;
line-height:2em;
}
small{
font-size:0.7em;
}
.footer a {
text-decoration:none !important;
}
header{
margin-bottom:50px;
background: url(/assets/backdrop.jpg);
background-repeat:no-repeat;
background-size: cover;
background-position:center;
border-radius: 10px;
text-align:center;
}
main{
margin-bottom:120px;
}
.hue-1{ filter: hue-rotate(45deg); }
.hue-2{ filter: hue-rotate(90deg); }
.hue-3{ filter: hue-rotate(135deg); }
.hue-4{ filter: hue-rotate(180deg); }
header > .spectrum,
div#logo{
width:290px;
}
div#logo{
background: url(/assets/roundel-1d688b1e.svg);
height:333px;
display:inline-block;
background-size:cover;
display: inline-block;
background-size: contain;
background-repeat: no-repeat;
margin-top: 20px;
margin-left: 20px;
width: 100%;
background-position: center;
}
div#xrhf{
background: url(/assets/xrh-full.svg);
height: 33px;
width:78px;
background-repeat:no-repeat;
background-size: contain;
background-position:top;
display:inline-block;
}
div#hint{
float:right;
background: url(/assets/blinkhint.svg);
height: 129px;
background-size: cover;
display:inline-block;
width: 200px;
}
div#badges{
background: url(/assets/badges.png);
height: 90px;
background-repeat:no-repeat;
background-size: contain;
background-position:bottom;
width: 100%;
max-width:677px;
display:inline-block;
}
div#xrecosystem{
background: #000 url(/assets/xrecosystem.png);
background-repeat: no-repeat;
height: 200px;
background-size: cover;
width: 100%;
border-radius:7px;
background-size: contain;
background-position: center;
margin:30px 0px;
}
div#xrflogo{
background: url(/assets/xrf.svg);
height: 46px;
background-repeat:no-repeat;
background-size: cover;
width: 120px;
border-radius:7px;
}
div#janusxrlogo{
background: url(/assets/janusxr-logo.png);
height: 71px;
filter: brightness(0.66);
background-repeat:no-repeat;
background-size: cover;
width: 200px;
border-radius:7px;
}
div#januswebbrowser{
background: url(/assets/janusoasis.webp);
height: 400px;
background-repeat:no-repeat;
background-size: cover;
width: 100%;
}
div#euvalues{
background: url(/assets/EUvalues.webp);
height: 400px;
background-size: cover;
width: 100%;
border-radius:7px;
margin:15px 0px;
background-position:center;
}
.spectrum{
margin-top:10px;
box-shadow: 0px 0px 5px #F0F;
margin-bottom:50px;
}
.spectrum > div,
.ruler{
margin:3px 0px;
clear:both;
background:linear-gradient(90deg, #F07, #77F, #F07);
height:2px;
}
/* sticky footer */
.footer {
position: fixed;
bottom: 0;
/* Optional: Add background, padding, etc. */
background: linear-gradient( 0deg, var(--bs-body-bg) 66%, #0000);
padding: 1rem;
width:100%;
z-index: 1000; /* Ensures it stays above other content */
}
.clear{
clear:both;
}
/* telescopic text */
u {
cursor: pointer;
display: inline;
text-decoration:none;
white-space:nowrap;
font-weight:bold;
padding:0;
}
u > u,
u > span{
display: none;
}
u:focus-within > u,
u:focus-within > span {
display: inline;
}
u span{
font-weight:normal;
white-space: normal;
background:none;
}
/* underline settings */
.telescopic u:nth-child(odd)::before,
.telescopic u:nth-child(even)::before{
border-bottom:4px solid #F0F;
content:' ';
display:inline-block;
margin-right:5px;
width:23px;
height:32px;
position: absolute;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.telescopic u:nth-child(odd)::before{
border-bottom:4px solid #77F;
}
.vertical u:focus-within > u::after,
.vertical u:focus-within > span::after {
content:' ';
display:block;
}
.show > u,
.show > span{
display:inline;
}
.telescopic u.show::before,
.telescopic u.show::after{
display:none;
}
/* bg mode */
.bg > u,
.bg u:focus-within > u {
background:#EEE;
border-radius:4px;
padding:4px;
}
.bg u.show {
background:#FFF;
}
@keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.3; }
to { opacity: 1.0; }
}
@-webkit-keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.3; }
to { opacity: 1.0; }
}
/* cards filter */
input[type="radio"] {
display:none;
}
label {
padding-bottom:10px;
cursor:pointer;
}
input[type="radio"][id="openlearning"]:checked ~ .protocol,
input[type="radio"][id="openlearning"]:checked ~ .xrdiscovery,
input[type="radio"][id="openlearning"]:checked ~ .xrbrowser,
input[type="radio"][id="openlearning"]:checked ~ .xrtranslator,
input[type="radio"][id="openlearning"]:checked ~ .worldpreservation,
input[type="radio"][id="protocol"]:checked ~ .openlearning,
input[type="radio"][id="protocol"]:checked ~ .xrdiscovery,
input[type="radio"][id="protocol"]:checked ~ .xrbrowser,
input[type="radio"][id="protocol"]:checked ~ .xrtranslator,
input[type="radio"][id="protocol"]:checked ~ .worldpreservation,
input[type="radio"][id="worldpreservation"]:checked ~ .openlearning,
input[type="radio"][id="worldpreservation"]:checked ~ .xrdiscovery,
input[type="radio"][id="worldpreservation"]:checked ~ .xrbrowser,
input[type="radio"][id="worldpreservation"]:checked ~ .xrtranslator,
input[type="radio"][id="worldpreservation"]:checked ~ .protocol,
input[type="radio"][id="xrdiscovery"]:checked ~ .openlearning,
input[type="radio"][id="xrdiscovery"]:checked ~ .protocol,
input[type="radio"][id="xrdiscovery"]:checked ~ .xrbrowser,
input[type="radio"][id="xrdiscovery"]:checked ~ .xrtranslator,
input[type="radio"][id="xrdiscovery"]:checked ~ .worldpreservation,
input[type="radio"][id="xrtranslator"]:checked ~ .openlearning,
input[type="radio"][id="xrtranslator"]:checked ~ .protocol,
input[type="radio"][id="xrtranslator"]:checked ~ .xrbrowser,
input[type="radio"][id="xrtranslator"]:checked ~ .xrdiscovery,
input[type="radio"][id="xrtranslator"]:checked ~ .worldpreservation,
input[type="radio"][id="browser"]:checked ~ .protocol,
input[type="radio"][id="browser"]:checked ~ .xrdiscovery,
input[type="radio"][id="browser"]:checked ~ .xrtranslator,
input[type="radio"][id="browser"]:checked ~ .worldpreservation,
input[type="radio"][id="browser"]:checked ~ .openlearning {
width:0;
height:0;
padding:0;
margin:0;
opacity:0;
}
.tile {
width:30%;
border: 1px solid #FFF2;
height:550px;
float:left;
overflow:hidden;
transition:all 0.3s;
margin:0 1% 1% 0;
background:#7773;
border-radius:7px;
position:relative;
}
@media screen and (max-width: 660px) {
.tile{
width:100%;
margin-bottom:30px;
}
}
.tile .description{
color:#888;
font-size:16px;
display:block;
text-decoration:none;
cursor:pointer;
padding:15px;
}
.tile .description:hover{
text-decoration:underline;
}
/* pulsating circle */
.pulsating-circle {
position: absolute;
left: 4px;
top: 20px;
transform: translateX(-50%) translateY(-50%);
width: 30px;
height: 30px;
z-index:-100;
opacity:0.3;
}
.pulsating-circle:before {
content: "";
position: relative;
display: block;
width: 300%;
height: 300%;
box-sizing: border-box;
margin-left: -100%;
margin-top: -100%;
border-radius: 45px;
background-color: #A4F;
-webkit-animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite;
}
.pulsating-circle:after {
content: "";
position: absolute;
left: 0;
top: 0;
display: block;
width: 100%;
height: 100%;
background-color: white;
border-radius: 15px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
-webkit-animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -0.4s infinite;
}
@-webkit-keyframes pulse-ring {
0% {
transform: scale(0.33);
}
80%, 100% {
opacity: 0;
}
}
@keyframes pulse-ring {
0% {
transform: scale(0.33);
}
80%, 100% {
opacity: 0;
}
}
@-webkit-keyframes pulse-dot {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.8);
}
}
@keyframes pulse-dot {
0% {
transform: scale(0.8);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.8);
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,235 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="113.57502mm"
height="43.909458mm"
version="1.1"
viewBox="0 0 113.57502 43.90946"
xml:space="preserve"
id="svg29"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><linearGradient
id="linearGradient19"><stop
style="stop-color:#53daff;stop-opacity:1;"
offset="0"
id="stop18" /><stop
style="stop-color:#52f7ff;stop-opacity:1;"
offset="1"
id="stop19" /></linearGradient><linearGradient
id="linearGradient17"><stop
style="stop-color:#c753ff;stop-opacity:1;"
offset="0"
id="stop16" /><stop
style="stop-color:#e653ff;stop-opacity:1;"
offset="1"
id="stop17" /></linearGradient><linearGradient
id="linearGradient15"><stop
style="stop-color:#298cff;stop-opacity:1;"
offset="0"
id="stop14" /><stop
style="stop-color:#ff79f7;stop-opacity:1;"
offset="1"
id="stop15" /></linearGradient><linearGradient
id="linearGradient12"><stop
style="stop-color:#29bbff;stop-opacity:1;"
offset="0"
id="stop12" /><stop
style="stop-color:#0cd9ff;stop-opacity:0;"
offset="1"
id="stop13" /></linearGradient><linearGradient
id="linearGradient9"><stop
style="stop-color:#ff3cf9;stop-opacity:1;"
offset="0"
id="stop9" /><stop
style="stop-color:#00f8f8;stop-opacity:1;"
offset="1"
id="stop8" /></linearGradient><linearGradient
id="linearGradient3"><stop
style="stop-color:#ff3cf9;stop-opacity:1;"
offset="0"
id="stop7" /><stop
style="stop-color:#00f8f8;stop-opacity:1;"
offset="1"
id="stop6" /></linearGradient><linearGradient
id="linearGradient1"><stop
style="stop-color:#003ad1;stop-opacity:1;"
offset="0"
id="stop1" /><stop
style="stop-color:#eb04bf;stop-opacity:1;"
offset="0.5"
id="stop5" /><stop
style="stop-color:#22a2af;stop-opacity:1;"
offset="1"
id="stop2" /></linearGradient><rect
x="429.14212"
y="58.686104"
width="1093.0287"
height="187.06195"
id="rect29" /><linearGradient
id="linearGradient8637"
x1="154.78"
x2="273.13"
y1="24.048"
y2="24.048"
gradientUnits="userSpaceOnUse"><stop
stop-color="#276fff"
offset="0"
id="stop3" /><stop
stop-color="#ff16bc"
offset="1"
id="stop4" /></linearGradient><linearGradient
xlink:href="#linearGradient1"
id="linearGradient2"
x1="425.34225"
y1="127.16445"
x2="1181.6075"
y2="127.16445"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#linearGradient3"
id="linearGradient7"
x1="121.41179"
y1="86.926689"
x2="171.91747"
y2="86.926689"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0076142,0,0,1.0454544,-0.92445533,-4.0153177)" /><linearGradient
xlink:href="#linearGradient9"
id="linearGradient8"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0076142,0,0,0.27272726,-0.98880088,58.794164)"
x1="121.41179"
y1="86.926689"
x2="171.91747"
y2="86.926689" /><linearGradient
xlink:href="#linearGradient9"
id="linearGradient10"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0025381,0,0,0.09090907,-0.21968892,69.915444)"
x1="121.41179"
y1="86.926689"
x2="171.91747"
y2="86.926689" /><linearGradient
xlink:href="#linearGradient9"
id="linearGradient11"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0076142,0,0,0.13636362,-0.79494564,67.591196)"
x1="121.41179"
y1="86.926689"
x2="171.91747"
y2="86.926689" /><linearGradient
xlink:href="#linearGradient12"
id="linearGradient13"
x1="151.02299"
y1="83.016991"
x2="159.7397"
y2="83.016991"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.132353,0,0,1,-21.142036,4.5394395e-7)" /><linearGradient
xlink:href="#linearGradient15"
id="linearGradient14"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.132353,0,0,1.0493827,315.49058,-4.1582868)"
x1="151.02299"
y1="83.016991"
x2="159.7397"
y2="83.016991" /><linearGradient
xlink:href="#linearGradient17"
id="linearGradient16"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1.132353,0,0,1.0493827,302.62006,-4.8206892)"
x1="151.02299"
y1="83.016991"
x2="159.7397"
y2="83.016991" /><linearGradient
xlink:href="#linearGradient19"
id="linearGradient18"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.132353,0,0,1.0493827,-7.1348208,-4.3306826)"
x1="151.02299"
y1="83.016991"
x2="159.7397"
y2="83.016991" /></defs><g
transform="translate(-58.856519,-44.496792)"
id="g29"><g
transform="matrix(2.7826,0,0,3.2096,58.857,52.964672)"
filter="url(#filter12743)"
id="g28"
style="display:inline" /><g
transform="matrix(2.7825999,0,0,3.2096001,-73.783976,-5.2700571)"
id="g27"
style="display:inline"><g
transform="scale(0.26458)"
fill="#f7f7f7"
style="font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;display:inline"
aria-label="SEARXR"
id="g26"><path
d="m 158.52,0.12891 c -1.1293,0.010438 -2.3334,0.038083 -3.5879,0.09375 -0.30457,-0.134 -0.0547,13.736 -0.0547,13.736 2.819,0.06348 6.7944,-0.31507 8.3398,1.041 3.9748,3.094 3.1915,2.418 5.7773,5.0039 2.5859,2.5859 5.236,5.1719 7.9512,7.7578 l -21.293,18.782 c -1.2934,1.293 -2.4553,1.5706 8.4254,1.3029 9.5932,-0.08935 8.1924,0.72076 10.737,-1.1372 7.8396,-6.1216 9.6129,-7.4704 12.328,-10.185 3.0599,2.4348 5.7548,4.0928 10.435,7.2182 0,0 4.5675,2.327 9.0355,3.4666 5.1614,0.86971 19.065,0.8565 19.629,0.65998 h 4.7207 c 0,0 0.0977,-8.958 0.16992,-16.066 0.12771,-10.91 2.172,-13.092 4.7578,-15.807 2.7151,-2.7151 6.0119,-2.0723 9.8906,-2.0723 l 59.02898,0.136307 V 0.28984634 L 245.78162,0.15354 c -7.62788,-0.0176139 -14.158,2.7143 -19.588,8.1445 -5.3009,5.3009 -7.9512,11.767 -7.9512,19.395 v 9.0977 c -2.0502,0.03977 -4.1264,0.0029 -5.9297,-0.16016 -1.858,-0.1679 -4.6841,-1.3207 -8.3076,-3.0888 0,0 -4.8858,-3.0652 -7.4717,-5.7803 l 15.003,-15.778 c 1.2599,-1.325 3.9009,-4.6189 8.5281,-9.5835 1.6357,-1.7549 0.84973,-2.3113 -4.041,-2.1953 -2.4125,0.00862 -6.6915,0.017578 -8.6309,0.017578 -1.8101,0 -3.4255,0.71063 -4.8477,2.1328 l -15.709,15.709 c -2.7151,-2.5859 -5.3654,-5.1719 -7.9512,-7.7578 l -7.9512,-7.9512 c -1.4222,-1.4222 -3.0395,-2.1328 -4.8496,-2.1328 -1.4545,0 -4.1766,-0.12506 -7.5644,-0.09375 z"
fill-opacity="0.99608"
stroke="url(#linearGradient8637)"
stroke-width="3.7795"
style="font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;display:inline;fill:#ffffff;fill-opacity:1;stroke:url(#linearGradient8637)"
id="path25"
transform="translate(27.273,60.371)" /></g></g><path
style="fill:url(#linearGradient13);stroke-width:0.988572;paint-order:markers stroke fill"
d="m 149.86931,77.825413 9.87039,10.383149 -3.19336,-0.128188 z"
id="path12" /><path
style="fill:url(#linearGradient14);stroke-width:1.01269;paint-order:markers stroke fill"
d="m 144.47923,77.510356 -9.87039,10.895897 3.19336,-0.134519 z"
id="path12-2" /><text
xml:space="preserve"
transform="matrix(0.05313799,0,0,0.05313799,104.086,62.583943)"
id="text29"
style="font-weight:600;font-size:133.333px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Semi-Bold';text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect29);display:none;opacity:1;fill:#8e8e8e;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:9.48474;stroke-linejoin:miter;stroke-dasharray:none;paint-order:stroke markers fill"><tspan
x="429.14258"
y="181.23104"
id="tspan3"><tspan
style="font-weight:900;-inkscape-font-specification:'Montserrat Heavy';fill:none"
id="tspan1">federation</tspan></tspan></text><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.64444px;font-family:'MxPlus ToshibaSat 9x14';-inkscape-font-specification:'MxPlus ToshibaSat 9x14';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:none;fill-opacity:1;stroke:#54438e;stroke-width:0.504;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
x="178.69518"
y="61.019711"
id="text5"><tspan
id="tspan5"
style="stroke-width:0.504"
x="178.69518"
y="61.019711" /></text><path
style="fill:#818181;fill-opacity:1;stroke:none;stroke-width:0.342798;stroke-dasharray:none;stroke-opacity:1"
d="m 163.50345,74.673891 c -0.65149,-0.129167 -0.94963,-0.267244 -1.31816,-0.610486 -0.40637,-0.378499 -0.54495,-0.90499 -0.38393,-1.458847 0.15793,-0.543256 0.68179,-0.964899 1.46326,-1.1777 0.45885,-0.124953 1.57628,-0.126818 2.08376,-0.0034 0.67889,0.164988 1.19199,0.509495 1.43622,0.96428 0.16204,0.301766 0.16214,0.971773 1.8e-4,1.273869 -0.23913,0.44603 -0.72716,0.783984 -1.38342,0.957939 -0.43754,0.115976 -1.44213,0.144791 -1.89791,0.05443 z m 1.69855,-0.696146 c 0.56669,-0.147603 0.8752,-0.485371 0.87553,-0.958577 3.3e-4,-0.434413 -0.30155,-0.780338 -0.81725,-0.936456 -0.28645,-0.08672 -1.58178,-0.08672 -1.86824,0 -0.50997,0.154393 -0.81745,0.502622 -0.81745,0.925823 0,0.498108 0.33339,0.850197 0.94043,0.993131 0.34056,0.08021 1.34175,0.06601 1.68698,-0.02393 z m -43.44423,-0.979697 v -1.698489 h 2.38847 2.38853 v 0.371543 0.371547 h -1.99043 -1.99041 v 0.318466 0.318465 h 1.85771 1.85774 v 0.371546 0.371546 h -1.85774 -1.85771 v 0.636932 0.636937 h -0.39809 -0.39807 z m 5.41392,0 v -1.698489 h 2.36198 2.36195 v 0.345006 0.345003 h -1.96388 -1.96389 v 0.318468 0.31847 h 1.88427 1.88425 v 0.345003 0.345007 h -1.88425 -1.88427 v 0.345007 0.345004 h 1.96389 1.96388 v 0.345006 0.345008 h -2.36195 -2.36198 z m 5.57318,0.001 v -1.705682 l 1.89754,0.01633 1.89753,0.01633 0.29758,0.139551 c 0.29556,0.138587 0.47097,0.318988 0.66427,0.683154 0.08,0.15105 0.0986,0.30482 0.0991,0.822705 4.9e-4,0.589358 -0.009,0.659336 -0.14673,0.936831 -0.083,0.168944 -0.23149,0.364129 -0.34011,0.44699 -0.39196,0.298954 -0.46854,0.309075 -2.49823,0.330119 l -1.87101,0.01941 z m 3.69294,0.85372 c 0.28847,-0.131017 0.40968,-0.385741 0.40968,-0.861002 0,-0.450736 -0.12671,-0.713807 -0.41715,-0.866169 -0.19179,-0.100616 -0.31843,-0.111104 -1.54909,-0.128149 l -1.34023,-0.01856 v 0.987237 0.987235 l 1.34023,-0.0012 c 1.18944,-9.99e-4 1.36455,-0.01228 1.55656,-0.09941 z m 1.93329,-0.854812 v -1.698447 h 2.36198 2.36196 v 0.345008 0.345005 h -1.99041 -1.99043 v 0.318468 0.318466 h 1.91081 1.91081 v 0.345004 0.345007 h -1.91081 -1.91081 v 0.345006 0.345006 h 1.99043 1.99041 v 0.345006 0.345006 h -2.36196 -2.36198 z m 5.52011,-0.0028 v -1.70142 l 2.05677,0.01613 c 2.28294,0.0179 2.22103,0.0086 2.55022,0.383519 0.2585,0.294417 0.34012,0.72711 0.21927,1.16236 -0.0725,0.260873 -0.37675,0.597138 -0.61006,0.674144 -0.10019,0.03309 -0.18229,0.07272 -0.18229,0.08807 0,0.02271 0.40288,0.526781 0.78779,0.985602 0.0733,0.08729 0.0504,0.09288 -0.37874,0.09288 h -0.45666 l -0.40878,-0.530775 -0.40876,-0.53078 h -1.1863 -1.1863 v 0.53078 0.530775 h -0.39808 -0.39808 z m 3.96904,-0.160216 c 0.29816,-0.103934 0.32303,-0.599872 0.0366,-0.730363 -0.0938,-0.04266 -0.61445,-0.06195 -1.6727,-0.06195 h -1.53679 v 0.424624 0.424624 h 1.50479 c 0.93414,0 1.56673,-0.02159 1.66809,-0.05693 z m 1.65719,0.163084 v -1.698492 h 2.28236 2.28236 v 1.698492 1.698489 h -0.37153 -0.37156 v -0.557324 -0.557316 h -1.53927 -1.53924 v 0.557316 0.557317 h -0.37156 -0.37156 z m 3.81043,-0.570588 -0.0154,-0.464431 h -1.51271 -1.51271 l -0.0154,0.464431 -0.0154,0.46443 h 1.54338 1.54339 z m 3.39046,2.233692 c -0.0194,-0.01948 -0.0353,-0.616587 -0.0353,-1.326946 V 72.04258 h -0.98196 -0.98191 v -0.371546 -0.371546 h 2.41502 2.41504 v 0.371546 0.371546 h -0.98194 -0.98194 v 1.326944 1.326945 h -0.41577 c -0.22867,0 -0.43171,-0.01593 -0.45118,-0.0354 z m 3.52083,-1.660872 v -1.700724 h 0.4254 0.42536 l -0.0139,1.685221 -0.014,1.685223 -0.41136,0.0155 -0.41134,0.0155 z M 167.617,72.998 v -1.698492 h 0.47708 0.47707 l 1.36735,1.214685 1.3674,1.214683 0.0143,-1.214683 0.0143,-1.214685 h 0.42366 0.42362 V 72.998 74.69649 h -0.46704 -0.46707 l -1.37738,-1.223174 -1.37743,-1.223174 -0.0143,1.223174 -0.0143,1.223174 h -0.42363 -0.42366 z"
id="path5" /><rect
style="fill:url(#linearGradient7);stroke-width:0.953489;paint-order:markers stroke fill"
id="rect1"
width="50.89024"
height="2.9483016"
x="121.41179"
y="85.388451" /><rect
style="fill:url(#linearGradient8);stroke-width:0.486998;paint-order:markers stroke fill"
id="rect1-3"
width="50.89024"
height="0.76912206"
x="121.34745"
y="82.116882" /><rect
style="fill:url(#linearGradient10);stroke-width:0.28046;paint-order:markers stroke fill"
id="rect1-3-1"
width="50.633865"
height="0.25637397"
x="121.50026"
y="77.689682" /><rect
style="fill:url(#linearGradient11);stroke-width:0.34436;paint-order:markers stroke fill"
id="rect1-3-1-3"
width="50.89024"
height="0.384561"
x="121.5413"
y="79.252556" /><path
style="fill:url(#linearGradient16);stroke-width:1.01269;paint-order:markers stroke fill"
d="m 136.35163,77.87345 -14.7415,8.075783 0.75781,1.660099 z"
id="path12-2-5" /><path
style="fill:url(#linearGradient18);stroke-width:1.01269;paint-order:markers stroke fill"
d="m 158.49271,77.850708 13.45963,8.075783 0.1395,1.916473 z"
id="path12-2-5-9" /></g><path
style="fill:#808080;fill-opacity:1;stroke-width:0.5;stroke-dasharray:none"
d="m 63.006633,21.639473 v -1.621936 h 0.372844 0.372845 l 0.01393,0.661252 0.01393,0.661249 h 1.522126 1.522123 l 0.01393,-0.661249 0.01393,-0.661252 h 0.372843 0.372846 v 1.621936 1.621936 h -0.374299 -0.374293 v -0.623821 -0.623821 h -1.547077 -1.547075 v 0.623821 0.623821 h -0.374302 -0.374293 z m 7.036705,0.954941 V 21.92742 l -1.010589,-0.913735 c -0.555826,-0.502554 -1.031926,-0.932278 -1.058003,-0.954942 -0.02607,-0.02267 0.213139,-0.04121 0.531585,-0.04121 h 0.578997 l 0.516436,0.483696 c 0.284038,0.266032 0.599431,0.56135 0.700872,0.65626 l 0.18444,0.172561 0.694306,-0.656258 0.694311,-0.656259 h 0.556238 0.55624 l -1.046068,0.960684 -1.046072,0.960687 -0.002,0.661249 -0.002,0.661252 h -0.424378 -0.424199 z m 3.293778,-0.954941 v -1.621936 h 1.79415 c 1.116205,0 1.890852,0.02073 2.050069,0.05488 0.489799,0.105044 0.794821,0.514897 0.794821,1.067993 0,0.492998 -0.202384,0.820418 -0.62231,1.006785 -0.172896,0.07672 -0.399864,0.09067 -1.733544,0.106525 l -1.534601,0.01824 v 0.494726 0.494727 h -0.374292 -0.374293 z m 3.882235,-0.190133 c 0.202334,-0.183249 0.15767,-0.598766 -0.07383,-0.68678 -0.06704,-0.02548 -0.782923,-0.04634 -1.590858,-0.04634 h -1.46897 v 0.426269 0.42627 l 1.516961,-0.01456 c 1.381681,-0.01325 1.525856,-0.02261 1.616689,-0.104875 z m 1.407769,0.190133 v -1.621936 h 2.245759 2.245756 v 0.324387 0.324389 H 81.222219 79.3258 v 0.324386 0.324388 h 1.821559 1.821561 v 0.324386 0.324387 H 81.147359 79.3258 v 0.324388 0.324387 h 1.896419 1.896416 v 0.324388 0.324386 H 80.872879 78.62712 Z m 5.290008,-0.0031 v -1.625055 l 1.962816,0.01561 1.962819,0.01561 0.241635,0.149719 c 0.364119,0.225608 0.485737,0.487551 0.465144,1.00183 -0.02027,0.506376 -0.15667,0.744052 -0.535249,0.932705 l -0.251333,0.125245 0.371495,0.457546 c 0.204323,0.251647 0.384612,0.478769 0.400645,0.504709 0.01744,0.02821 -0.141616,0.04716 -0.395672,0.04716 h -0.424819 l -0.388805,-0.499126 -0.388805,-0.499057 h -1.135644 -1.135641 v 0.499057 0.499056 h -0.374295 -0.374291 z m 3.848457,-0.1383 c 0.09282,-0.0446 0.193905,-0.243837 0.193905,-0.382137 0,-0.05207 -0.05418,-0.157691 -0.120396,-0.234704 L 87.718719,20.741215 H 86.204695 84.69067 l -0.01493,0.344388 c -0.0083,0.189413 -0.0028,0.376191 0.01214,0.41506 0.02276,0.0593 0.265854,0.06789 1.5121,0.05341 0.816738,-0.0095 1.521239,-0.03469 1.565557,-0.05599 z m 1.541359,0.141444 v -1.621979 l 0.486582,0.0017 0.48658,0.0017 0.916984,1.245982 c 0.504344,0.685289 0.931039,1.244853 0.948211,1.243474 0.01717,-0.0014 0.443594,-0.562818 0.9476,-1.247643 l 0.91638,-1.245111 h 0.468505 0.468504 v 1.621926 1.621936 h -0.4242 -0.424196 v -0.948274 c 0,-0.521514 -0.01684,-0.947327 -0.03743,-0.94625 -0.02058,0.001 -0.351833,0.426889 -0.736109,0.94625 l -0.698679,0.944293 -0.48096,0.0019 -0.480959,0.0019 -0.716778,-0.957465 -0.716778,-0.957462 -0.01354,0.957462 -0.01353,0.957465 h -0.448077 -0.448072 z m 6.837085,0 v -1.621979 h 2.245756 2.245755 v 0.323786 0.323787 l -1.883939,0.01308 -1.88394,0.01308 v 0.299436 0.299433 l 1.809083,0.0131 1.809076,0.01309 v 0.323765 0.323765 h -1.821553 -1.82156 v 0.324386 0.324388 h 1.896417 1.896416 v 0.324389 0.324386 h -2.245755 -2.245756 z m 5.290001,-0.0032 v -1.625251 l 1.834,0.01579 c 1.8108,0.01559 1.83739,0.01729 2.09822,0.135154 0.31998,0.144582 0.57516,0.429253 0.69339,0.77348 0.10376,0.301938 0.11662,1.043036 0.0239,1.363287 -0.0732,0.252205 -0.32612,0.596392 -0.54864,0.746371 -0.28101,0.189359 -0.57873,0.216413 -2.38153,0.216413 h -1.71927 z m 3.63346,0.826754 c 0.23346,-0.151816 0.33409,-0.392935 0.33321,-0.798492 -9.9e-4,-0.484294 -0.10418,-0.705975 -0.39388,-0.846191 -0.1955,-0.09463 -0.30351,-0.102014 -1.49254,-0.102014 h -1.28176 v 0.92619 0.926188 l 1.35989,-0.01541 c 1.10521,-0.01253 1.3815,-0.02944 1.47508,-0.09026 z m 1.85615,-0.823445 v -1.621938 h 0.4242 0.42419 v 1.621935 1.621936 h -0.42419 -0.4242 z m 1.99621,0 v -1.621938 h 1.99623 2.34129 v 1.621935 1.621936 h -0.51973 -0.17466 v -0.524157 -0.524014 h -1.47222 -1.47223 v 0.524014 0.524009 h -0.34933 -0.34935 z m 3.64313,-0.524008 v -0.449212 h -1.47222 -1.47223 v 0.449153 0.449151 h 1.47223 1.47222 z"
id="path2" /></svg>

Before

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show more