Compare commits

..

No commits in common. "master" and "fix/reproduce" have entirely different histories.

44 changed files with 133 additions and 34572 deletions

3
.gitignore vendored
View file

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

0
.gitmodules vendored
View file

View file

@ -4,9 +4,6 @@
![](https://i.imgur.com/0NA7MMg.png)
* View [index.html](https://coderofsalvation.codeberg.page/xrforge) for the official docs
* Click [here](manyfold/README.md) for backend-installation instructions.
Powered by:
* [NIX](https://nixos.org) for reproducibility and reliability

3836
index.html

File diff suppressed because one or more lines are too long

View file

@ -1,47 +1,14 @@
# Manyfold container
The XRForge-serverimage is a pre-configured [Manyfold](https://manyfold.app) container (reproducably via [nix](https://nixos.org) dockertools).
It also contains some extra's, to better fit an XR audience & enable community libraries.
The XRForge-serverimage is a pre-configured Manyfold container (reproducably via [nix](https://nixos.org) dockertools).
It also contains some extra's, to better fit an XR audience.
> To run the container, see the docker-cmd below [sysadmin](https://manyfold.app/sysadmin/) documentation of the [manyfold](https://github.com/manyfold3d/manyfold) project.
> To run the container, see the [sysadmin](https://manyfold.app/sysadmin/) documentation of the [manyfold](https://github.com/manyfold3d/manyfold) project.
```
$ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest
```
To persist data:
```
$ mkdir mnt config
$ docker run -t xrforge docker.io/coderofsalvation/xrforge:latest -v ./mnt:/mnt -v ./config:/config
```
# Build & Run the container-image
> **NOTE**: [nix](https://nixos.org) is used to promote reproducability-over-repeatability
# Build the container-image
```bash
$ git clone --recurse-submodules --depth 1 https://codeberg.org/coderofsalvation/xrforge.git
$ cd xrforge
$ docker load < $(nix-build nix/docker.nix)
$ manyfold/cli/manyfold run
[xrforge] podman detected..starting OCI container
+ /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
...
```
> NOTE: if you don't want the default XR Fragments asset-library, omit `--recurse-submodules` in the git cmd
To preserve your uploaded models, add the `experiences` as a docker volume-flag, for example:
```
$ mkdir experiences
$ manyfold/cli/manyfold run -v ./experiences:/mnt/experiences
```
# Extra environment-variables
@ -49,31 +16,18 @@ $ manyfold/cli/manyfold run -v ./experiences:/mnt/experiences
| environment variable | default | info |
|-----------------------|--------------|------------------------|
| `APPNAME` | `manyfold` | manyfold instance name |
| `HOMEPAGE` | `/models` | show '/models' URL as homepage (use `/` for manyfold default) |
| `THEME` | `default` | bootstrap theme |
| `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_DEFAULTDB` | `` | disable the default db (activates manyfold installer) |
| `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_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 |
| `UPLOAD_PATH` | `/mnt/models`| specify default library where user-files are uploaded (regular dir or mounted rclone path) |
| `FEDERATE_DRIVE_PATH` | `/mnt/models`| serve path over HTTP (so other instances can add it as a remote). Specify `0` to disable |
| `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.) |
> 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.
# Default database / admin login
@ -88,75 +42,93 @@ $ manyfold/cli/manyfold run -v ./experiences:/mnt/experiences
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.).
# Federated libraries
# Federated drives (inbound)
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:
```
┌────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ server instance A │ │ server instance B │
│ │ │ │
│ ┌──────────────────┐ │ rclone │ ┌─────────────────┐ │
│ │ library │ │ │ │ library │ │
│ │ ┼───┼──────────────┼─┤ │ │
│ │ │ │ http-drive │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────────────┘ │ │ └─────────────────┘ │
│ │ │ │
└────────────────────────┘ └────────────────────────┘
READ / WRITE READ-ONLY
```
It does this by automatically mapping [rclone](https://rclone.org) network-drives as manyfold libraries.
> Thanks to [rclone](https://rclone.org) network-drives automatically show up as manyfold libraries.
![](https://i.imgur.com/4VMF3CQ.png)
To enable rclone to mount **readonly** network drives (=remotes), the container must be run with FUSE-device support.
To enable rclone to mount network drives, the container must be run with FUSE-device support.
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` 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!
* add `-v ./manyfold/root/.config:/root/.config --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse` to the docker cmd
* add network drives by running `docker exec -it rclone config` in a running container
* profit!
**Default behaviour**: your drives will/should get automagically mounted **readonly** and added as a library automagically (by [manyfold.sh](cli/manyfold.sh) `rclone_automount`-cmd) during container boot.
Your drives will get automagically mounted and added to the database automagically (by [manyfold.sh](cli/manyfold.sh) `rclone_automount`-cmd) during container boot.
* 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)
> NOTE: by default all rclone remotes automagically show up as separate manyfold libraries, however use `RCLONE_REMOTE` this to specify a [combined](https://rclone.org/combine/) or [union](https://rclone.org/union/) rclone remote.
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)
TIP: use **alphanumeric** names for rclone remotes (manyfold libraries choke on dot- or other special-characters)
# Git libraries
XRForge also automatically maps git-repositories as libraries.
Repositories are detected via the `/mnt` directory, which can be fed as a **volume** via the docker cmd:
By default environment-flag `FEDERATE_DRIVE_PATH` will share path `/mnt/models` as an open web directory.
This means it can be added as remote by other instances.
See the environment-flags for more options.
<details>
<summary>**Example connect to other XRForge instance**</summary>
<br>
```
$ cd xrforge
xrforge $ mkdir mnt && cd mnt
xrforge/mnt $ git clone https://codeberg.org/my/repo
xrforge/mnt $ ls repo
$ rclone config
Current remotes:
Name Type
==== ====
e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> n
john/#1023/foo.glb
john/#1023/bar.glb
mary/#103/flop.glb
Enter name for new remote.
name> xrforge_instanceC
xrforge $ docker run xrforge ... -v ./mnt/repo:/mnt/repo ...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
...
22 / HTTP
...
Storage> 22
Option url.
URL of HTTP host to connect to.
E.g. "https://example.com", or "https://user:pass@example.com" to use a username and password.
Enter a value.
url> http://url-to-another-xrforge-instance.com
Option no_escape.
Do not escape URL metacharacters in path names.
Enter a boolean value (true or false). Press Enter for the default (false).
no_escape>
Edit advanced config?
y) Yes
n) No (default)
y/n> n
Configuration complete.
Options:
- type: http
- url: http://localhost:8791
Keep this "test" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> y
```
> **NOTE**: repositories need to respect XRForge's `{creator}/{modelId}` modelpath. This automatically creates creators/models in the database. This does not mean creators can automatically log in (no passwords are set), which should be fine for most archive-like purposes.
</details>
# Unixy event hooks
Until WebEvents/WebSub [gets supported on a REST-level in manyfold](https://github.com/orgs/manyfold3d/projects/4/views/1?filterQuery=Pub&pane=issue&itemId=108834509&issue=manyfold3d%7Cmanyfold%7C4097), things like boot-phase, scheduler and file-changes can be reacted up via the `/root/hook.d` directory:
Until WebEvents [will get implemented on a REST-level in manyfold](https://github.com/orgs/manyfold3d/projects/4/views/1?filterQuery=Pub&pane=issue&itemId=108834509&issue=manyfold3d%7Cmanyfold%7C4097) Things like boot-phase, scheduler and file-changes can be reacted up via the `/root/hook.d` directory:
```
$ ls /root/hook.d
@ -171,17 +143,8 @@ You can put scripts in there, which are fired when needed.
> Example: [manyfold/root/hook.d/daily/delete_big_files.sh] is triggered daily to cleanup files which exceed a certain age/size.
Currently inotify events (`inotify_MODIFY` e.g.) are triggered for local file-changes (`/mnt/experiences` e.g.).
Currently inotify events (`inotify_MODIFY` e.g.) are triggered for local file-changes (`/mnt/models` e.g.).
In theory, federated drives can still be reacted upon, but by integrating with XRForge's ActivityPub (**Follow** feature e.g.)
> Perhaps in the future this will also work for rclone remotes, by writing a `hourly`-script which scans them and fires `inotify_MODIFY` accordingly.
# Customization
See the [manyfold](https://github.com/manyfold3d/manyfold) repository.
For a quick dev-environment run:
```
$ mkdir /dev
$ manyfold/cli/manyfold.sh run -e DEV=1
```

View file

@ -1,10 +1,7 @@
#!/bin/sh
oci=$(which podman || which docker)
test -n "$APPNAME" || export APPNAME=xrforge
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 "$APPNAME" || APPNAME=xrforge
test -n "$UPLOAD_PATH" || UPLOAD_PATH=/mnt/models
db=/config/manyfold.sqlite3
# utility funcs
@ -28,13 +25,10 @@ run(){
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:8791 \
-e SUDO_RUN_UNSAFELY=enabled \
-e MULTIUSER=enabled \
-e FEDERATION=enabled \
-e THEME=$THEME \
-e HOMEPAGE=$HOMEPAGE \
-e GODOT_VERSION=4.4.1-stable \
-e THEME=vapor \
-e FEDERATE_DRIVE_CACHE=5s \
--cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \
xrforge
@ -71,7 +65,7 @@ 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 "$@" || true; } 2>&1 | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1' | logger
done
@ -83,16 +77,8 @@ 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
# trigger hooks when files change in /mnt/models
inotifywait -r -m /mnt/models | awk '$2 ~ /(CREATE|MODIFY|MOVED_TO|DELETE)/ { system("'$0' hook inotify_"$2" "$1""$3) }' &
}
@ -115,7 +101,7 @@ db(){
add_lib_to_db(){
name=$(basename $1)
sqlite3 $db "INSERT INTO libraries SELECT NULL, '$1', DATE('NOW'), DATE('NOW'), '', '', '$name', NULL, '', 'filesystem', '', '', '', '', '', '$name', 1 WHERE NOT EXISTS (SELECT 1 FROM libraries WHERE path = '$1');"
debug sqlite3 $db "INSERT INTO libraries SELECT NULL, '$1', DATE('NOW'), DATE('NOW'), '', '', '$name', NULL, '', 'filesystem', '', '', '', '', '', '$name', 1 WHERE NOT EXISTS (SELECT 1 FROM libraries WHERE path = '$1');"
}
set_upload_path(){
@ -123,17 +109,10 @@ set_upload_path(){
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
add_lib_to_db $UPLOAD_PATH
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
sqlite3 $db "UPDATE settings set value = $id where var = 'default_library';"
debug sqlite3 $db "UPDATE settings set value = $id where var = 'default_library';"
}
mount_dir(){
find /mnt -type d -mindepth 1 -maxdepth 1 | while read dir; do
echocolor "[$APPNAME]" "mounting $dir as library"
add_lib_to_db "$dir"
done
}
mount_rclone(){
rclone_mount(){
libraries(){
rclone listremotes | while read remote; do
@ -167,61 +146,21 @@ set_modelpath(){
debug sqlite3 /config/manyfold.sqlite3 "UPDATE settings SET value = replace('--- \"{creator}/{modelId}\"\n','\n',char(10)) WHERE var == 'model_path_template';"
}
set_homepage(){
test "$HOMEPAGE" = "/" && return 0 # nothing to do
echocolor "[$APPNAME]" "enforcing homepage path"
debug sed -i 's|root to:.*|root to: redirect("'$HOMEPAGE'")|g' /usr/src/app/config/routes.rb
}
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://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
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
sed -i 's|Models|Experiences|g' /usr/src/app/config/locales/*.yml
sed -i 's|Model|Experience|g' /usr/src/app/config/locales/*.yml
}
start_syslog(){
touch /var/log/messages
syslogd -n & # start syslogd
echocolor started syslog | logger
tail -f /var/log/messages &
}
scan_libraries(){
sleep 10 # wait for manyfold/redis to boot first
cd /usr/src/app
echocolor "scanning libraries"
bin/manyfold libraries scan
}
rails_query(){
cd /usr/src/app
echo "$*" | bin/rails console
}
force_public(){
test -n "$NO_PUBLIC_ONLY" && return 0 # nothing to do
echocolor "forcing public-only models"
infinite 60 rails_query 'Model.find_each { |it| it.grant_permission_to("view", nil) }' &
}
get_xrfragment_assets(){
test -n "$NO_ASSETS" && return 0 # nothing to do here
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
}
add_lib_to_db /mnt/asset
add_lib_to_db /mnt/templates
}
# The new entrypoint of the docker
boot(){
echocolor "[$APPNAME]" "booting..."
@ -229,25 +168,13 @@ boot(){
test -z "$NO_DEFAULTDB" && db default
start_syslog
rename_app
set_homepage
set_theme
set_modelpath
mount_rclone
rclone_mount
set_upload_path
force_public
start_hook_daemon
get_xrfragment_assets
mount_dir
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
}
exec "$@" # exec prevents error 's6-overlay-suexec: fatal: can only run as pid 1'
}

View file

@ -136,22 +136,17 @@ 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 "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);
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(1,'default_library',replace('--- 2\n','\n',char(10)),'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');
@ -159,7 +154,7 @@ INSERT INTO settings VALUES(5,'about',replace('--- ''''\n','\n',char(10)),'2025-
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(9,'model_path_template',replace('--- "{tags}/{tags}/{modelName}{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');
@ -193,10 +188,7 @@ CREATE TABLE IF NOT EXISTS "federails_activities" ("id" integer PRIMARY KEY AUTO
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")
@ -223,7 +215,6 @@ FOREIGN KEY ("resource_owner_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")
@ -243,7 +234,6 @@ 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")
@ -251,23 +241,20 @@ FOREIGN KEY ("collection_id")
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);
DELETE FROM sqlite_sequence;
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('federails_actors',1);
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('creators',0);
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('libraries',5);
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");
@ -358,5 +345,4 @@ 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

@ -0,0 +1,10 @@
[scene]
type = ftp
host = ftp.scene.org
user = anonymous
pass = JMAFLA0Vk3ELCzRwhxANJZtlKai7hDW_Vw
[modland]
type = http
url = https://modland.com

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

@ -1,6 +0,0 @@
#!/bin/sh
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 /root/godot.zip
cp index.override.html index.html # make index.html autoload a manyfold zip-file (passed via HTTP query)

View file

@ -1,6 +1,5 @@
#!/bin/sh
test -z "$FEDERATE_DRIVE_HOST" && FEDERATE_DRIVE_HOST=http://localhost:3215
test -z "$FEDERATE_DRIVE_PATH" && FEDERATE_DRIVE_PATH=/mnt
test -z "$FEDERATE_DRIVE_PATH" && FEDERATE_DRIVE_PATH=/mnt/models
test -z "$FEDERATE_DRIVE_PORT" && FEDERATE_DRIVE_PORT=3215
test -z "$FEDERATE_DRIVE_CACHE" && FEDERATE_DRIVE_CACHE=1m0s
@ -10,12 +9,7 @@ test -n "$FEDERATE_DRIVE_USER" && test -m "$FEDERATE_DRIVE_PW" && {
AUTH="--user $FEDERATE_DRIVE_USER --pass $FEDERATE_DRIVE_PW"
}
test -n "$FEDERATE_DRIVE_CERT" && test -m "$FEDERATE_DRIVE_KEY" && {
SSL="--cert $FEDERATE_DRIVE_CERT --key $FEDERATE_DRIVE_KEY"
}
set -x
rclone serve http \
--exclude .xrforge --poll-interval $FEDERATE_DRIVE_CACHE \
--addr 0.0.0.0:$FEDERATE_DRIVE_PORT ${AUTH} ${SSL} $FEDERATE_DRIVE_PATH &> /var/log/rclone.log &
--poll-interval $FEDERATE_DRIVE_CACHE \
--addr 0.0.0.0:$FEDERATE_DRIVE_PORT ${AUTH} $FEDERATE_DRIVE_PATH &> /var/log/rclone.log &

View file

@ -1,13 +0,0 @@
#!/bin/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,5 +0,0 @@
#!/bin/sh
cd "$(dirname $1)"
echo "[v] scan (new) files of model"
id="$(basename "$dir" | sed 's/\#//g')"
#echo "Model.find(id).add_new_files_later()" | /usr/src/app/bin/rails console

View file

@ -1,5 +0,0 @@
#!/bin/sh
dir="$(dirname $1)"
cd "$dir"
echo "[package_experience.sh] zipping experience.zip"
zip -D ".xrforge/experience.zip * | tee -a .xrforge/log.txt

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 ~/template_godot.zip
cp ~/template_godot.zip package_godot.zip
zip .xrforge/godot.zip *.glb *.usdz *.obj

View file

@ -1,100 +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( 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 build janusXR XR scene", logfile)
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
model_file = nil # Initialize model_file to nil
# Loop over the list of extensions
XRForge::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", 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
# Check if a model file was found after the loop
if model_file
XRForge.log("✅ Final model file: '#{model_file}'", logfile)
else
XRForge.log("❌ No suitable 3D file found for XR Fragments- / JanusXR-compatible experience", logfile)
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
# Define the HTML content using a multi-line string (heredoc)
# Ruby's heredoc allows for variable interpolation (using #{})
jml = <<~HTML
<!DOCTYPE html>
<html>
<head>
<title>janusxr room</title>
</head>
<body>
<script src="https://web.janusvr.com/janusweb.js"></script>
<janus-viewer>
<FireBoxRoom>
<Assets>
<assetobject id="experience" src="#{federate_drive_host}/#{model_file.gsub("#","%23")}"/>
</Assets>
<Room>
<object pos="0 0 0" collision_id="experience" id="experience" />
</Room>
</FireBoxRoom>
</janus-viewer>
</body>
</html>
HTML
# Write the content to the specified file
# File.write is the concise equivalent of 'echo "$jml" > filename'
File.write('.xrforge/janusxr.html', jml)
XRForge.log("✅ written janusxr.html", 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,69 +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.chdir( File.dirname(filename) )
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
XRForge.log("✅ starting XR fragments check", logfile)
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile)
# Get the base name of the thumbnail file without its extension
base_name = File.basename(thumb_file, File.extname(thumb_file))
model_file = nil # Initialize model_file to nil
# Loop over the list of extensions
XRForge::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", logfile)
model_file = 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
# Check if a model file was found after the loop
if model_file
XRForge.log("✅ Final model file: '#{model_file}'", logfile)
else
XRForge.log("❌ No suitable 3D file found for XR Fragments-compatible experience", logfile)
end
# Construct the output filename
output_file = "#{File.basename(model_file, File.extname(model_file))}.blend"
# Execute the system call
puts("assimp", model_file, output_file)
system("assimp", model_file, output_file)
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

@ -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 @@
inotify_MODIFY

Binary file not shown.

View file

@ -1,13 +0,0 @@
module XRForge
MODEL_EXT = ['.glb', '.gltf', '.blend', '.usdz', '.obj', '.dae']
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
end

View file

@ -1,5 +0,0 @@
#!/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,25 +0,0 @@
# frozen_string_literal: true
class Components::DropdownItem < Components::Base
include Phlex::Rails::Helpers::LinkTo
def initialize(icon:, label:, path:, method: nil, aria_label: nil, confirm: nil, target: nil)
@icon = icon
@label = label
@path = path
@method = method
@aria_label = aria_label
@confirm = confirm
@target = target
end
def view_template
li do
link_to @path, method: @method, class: "dropdown-item", aria: {label: @aria_label}, data: {confirm: @confirm}, target: @target do
Icon(icon: @icon, label: @label)
whitespace
span { @label }
end
end
end
end

View file

@ -1,131 +0,0 @@
# frozen_string_literal: true
class Components::ModelCard < Components::Base
include Phlex::Rails::Helpers::ImageTag
include Phlex::Rails::Helpers::Sanitize
include Phlex::Rails::Helpers::LinkTo
register_output_helper :status_badges
register_output_helper :server_indicator
register_value_helper :policy
def initialize(model:)
@model = model
end
def view_template
div class: "col mb-4" do
div class: "card preview-card" do
div(class: "card-header position-absolute w-100 top-0 z-3 bg-body-secondary text-secondary-emphasis opacity-75") { server_indicator @model } if @model.remote?
PreviewFrame(object: @model)
div(class: "card-body") { info_row }
actions
end
end
end
private
def title
div class: "card-title" do
a "data-editable-field": "model[name]", "data-editable-path": model_path(@model), contenteditable: "plaintext-only", "data-controller": "editable", "data-action": "focus->editable#onFocus blur->editable#onBlur" do
@model.name
end
if @model.sensitive
whitespace
Icon(icon: "explicit", label: Model.human_attribute_name(:sensitive))
end
whitespace
AccessIndicator(object: @model)
end
end
def open_button
if @model.remote?
link_to @model.federails_actor.profile_url, {class: "btn btn-primary btn-sm", "aria-label": translate("components.model_card.open_button.label", name: @model.name)} do
span { "" }
whitespace
span { t("components.model_card.open_button.text") }
end
else
link_to t("components.model_card.open_button.text"), @model, {class: "btn btn-primary btn-sm", "aria-label": translate("components.model_card.open_button.label", name: @model.name)}
end
end
def credits
ul class: "list-unstyled" do
if @model.remote?
if (creator = @model.federails_actor.extensions["attributedTo"])
li { creator target: creator["url"], name: creator["name"] }
end
if (collection = @model.federails_actor.extensions["context"])
li { collection target: collection["url"], name: collection["name"] }
end
else
li { creator target: @model.creator, name: @model.creator.name } if @model.creator
li { collection target: @model.collection, name: @model.collection.name } if @model.collection
end
end
end
def creator(target:, name:)
Icon icon: "person", label: Creator.model_name.human
whitespace
link_to name, target, "aria-label": [Creator.model_name.human, name].join(": ")
end
def collection(target:, name:)
Icon icon: "collection", label: Collection.model_name.human
whitespace
link_to name, target, "aria-label": [Collection.model_name.human, name].join(": ")
end
def caption
if @model.caption
span class: "card-subtitle text-muted" do
sanitize @model.caption
end
end
end
def info_row
div class: "row" do
div class: "col" do
title
caption
end
div class: "col-auto" do
small do
credits
end
end
end
end
def actions
div class: "card-footer" do
div class: "row" do
div class: "col" do
#open_button
#whitespace
status_badges @model
end
div class: "col col-auto" do
i class: "bi bi-telephone"
whitespace
link_to "meeting", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html", {
target:"_blank",
}
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: "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?
DropdownItem(icon: "flag", label: t("general.report", type: ""), path: new_model_report_path(@model)) if SiteSettings.multiuser_enabled?
end
end
end
end
end
end

View file

@ -1,86 +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
a href: "/view?#{model_model_file_path(@file.model, @file, format: @file.extension)}", target:"_blank" do
if @file
local
elsif @object.remote?
remote
else
empty
end
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,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,5 +1,6 @@
<div class="col mb-4">
<div class="card preview-card <%= (file === @model.preview_file) ? "border-primary" : "" %>">
<h2>FOO</h2>
<% if file.is_image? %>
<%= content_tag :div, nil, class: "card-img-top card-img-top-background", style: "background-image: url(#{model_model_file_path(@model, file, format: file.extension)})" %>
<%= image_tag model_model_file_path(@model, file, format: file.extension), class: "card-img-top image-preview", alt: file.name %>

View file

@ -59,19 +59,7 @@
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
</small>
<% 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>
<% end %></td>
</tr>
<% end %>
<% if @model.creator %>
@ -80,87 +68,12 @@
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<% if ENV['FEDERATE_DRIVE_HOST'].present? %>
<tr>
<td>
<i class="bi bi-people" role="img"></i>
</td>
<td>
<%= link_to "JanusXR Metaverse", ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html" %>
<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>
This is the JanusXR address.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an established Metaverse since 2015.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).
</small>
</div>
</div>
</td>
</tr>
<tr>
<td>
<i class="bi bi-file-zip" role="img"></i>
</td>
<td><%= link_to "zip archive", "/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/experience.zip" %></td>
</tr>
<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>
But also features can be toggled via tags:<br>
<br>
<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>
</table>
</small>
</div>
</div>
</td>
</tr>
<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>
<% end %>
</td>
<td><%= link_to @model.path+".zip", @model.path+".zip" %></td>
</tr>
<% if @model.collection %>
<tr>
<td><%= icon "collection", Collection.model_name.human(count: 100) %></td>

View file

@ -1,10 +0,0 @@
Rails.application.configure do
config.content_security_policy_nonce_generator = lambda do |request|
# Always exclude /godot/* from CSP
if request.path.start_with?("/godot/")
nil
elsif !(Rails.env.development? && ENV.fetch("SCOUT_DEV_TRACE", false) == "true")
request.session.id.to_s
end
end
end

View file

@ -1,7 +0,0 @@
# always allow cors so remote XR viewers can load content
Rails.application.config.middleware.insert_after Rack::Head, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :options, :head]
end
end

View file

@ -1,44 +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 'pp'
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}/#{self.path_within_library()}"
if File.exist?(file)
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 + 1.minute)
puts "[app/config/initializers/xrforge.rb] runnin hook\n"
command = "TS_SLOTS=5 ts /manyfold/cli/manyfold.sh hook experience_updated #{file} &"
system(command)
else
puts "[app/config/initializers/xrforge.rb] skipping hook\n"
end
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,327 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="128.27411mm"
height="145.65482mm"
viewBox="0 0 128.27411 145.65482"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:export-filename="../../../../../Downloads/xrforge.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="logo.svg"
inkscape:dataloss="true"
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"><sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.56323837"
inkscape:cx="226.36952"
inkscape:cy="409.24058"
inkscape:window-width="1920"
inkscape:window-height="1030"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs2"><linearGradient
inkscape:collect="always"
id="linearGradient12286"><stop
style="stop-color:#ea0bfe;stop-opacity:0.11569338;"
offset="0"
id="stop12282" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop12284" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient12159"><stop
style="stop-color:#fe83ff;stop-opacity:0.35045233;"
offset="0"
id="stop12155" /><stop
style="stop-color:#3c9cff;stop-opacity:0.32712477;"
offset="1"
id="stop12157" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient12153"><stop
style="stop-color:#fe83ff;stop-opacity:0.29342434;"
offset="0"
id="stop12149" /><stop
style="stop-color:#3c9cff;stop-opacity:0.31577286;"
offset="1"
id="stop12151" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient12139"><stop
style="stop-color:#ea0bfe;stop-opacity:0.50826901;"
offset="0"
id="stop12135" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop12137" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient12102"><stop
style="stop-color:#fe83ff;stop-opacity:1;"
offset="0"
id="stop12098" /><stop
style="stop-color:#3c9cff;stop-opacity:0.81848603;"
offset="1"
id="stop12100" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient7688"><stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop7684" /><stop
style="stop-color:#ff13f3;stop-opacity:0;"
offset="1"
id="stop7686" /></linearGradient><linearGradient
id="linearGradient6742"><stop
style="stop-color:#276fff;stop-opacity:1"
offset="0"
id="stop6738" /><stop
style="stop-color:#ff16bc;stop-opacity:1"
offset="1"
id="stop6740" /></linearGradient><linearGradient
xlink:href="#linearGradient6742"
id="linearGradient8637"
x1="154.78049"
y1="24.048252"
x2="273.12695"
y2="24.048252"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7688"
id="linearGradient7692"
x1="115.42191"
y1="-2.709012"
x2="117.16759"
y2="131.87457"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12102"
id="linearGradient12104"
x1="54.029213"
y1="71.733955"
x2="176.85757"
y2="71.733955"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12159"
id="linearGradient12108"
gradientUnits="userSpaceOnUse"
x1="54.029213"
y1="71.733955"
x2="176.85757"
y2="71.733955"
gradientTransform="translate(0,4.7625002)" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12153"
id="linearGradient12112"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0,19.579173)"
x1="54.029213"
y1="71.733955"
x2="176.85757"
y2="71.733955" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12286"
id="linearGradient12141"
x1="137.33427"
y1="88.766113"
x2="177.37935"
y2="88.766113"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2850723,0,0,1.2367478,-50.791853,-16.999519)" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12139"
id="linearGradient12239"
gradientUnits="userSpaceOnUse"
x1="137.33427"
y1="88.766113"
x2="177.37935"
y2="88.766113"
gradientTransform="matrix(-1.2669282,0,0,1.2603766,278.3952,-19.18513)" /><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter12743"
x="-0.079006463"
y="-0.2479955"
width="1.1580434"
height="1.4959902"><feFlood
flood-opacity="1"
flood-color="rgb(204,26,255)"
result="flood"
id="feFlood12733" /><feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite12735" /><feGaussianBlur
in="composite1"
stdDeviation="1.1"
result="blur"
id="feGaussianBlur12737" /><feOffset
dx="0"
dy="0"
result="offset"
id="feOffset12739" /><feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite12741" /></filter><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter13745"
x="-0.13448349"
y="-0.73597109"
width="1.268967"
height="2.4719422"><feFlood
flood-opacity="1"
flood-color="rgb(26,135,255)"
result="flood"
id="feFlood13735" /><feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite13737" /><feGaussianBlur
in="composite1"
stdDeviation="4.48865"
result="blur"
id="feGaussianBlur13739" /><feOffset
dx="0"
dy="0"
result="offset"
id="feOffset13741" /><feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite13743" /></filter><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12286"
id="linearGradient14475"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2850723,0,0,1.2367478,-50.791853,-16.999519)"
x1="137.33427"
y1="88.766113"
x2="177.37935"
y2="88.766113" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-51.358538,-4.8451999)"><path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:2.3;stroke-dasharray:none"
id="path1638-3"
inkscape:flatsided="true"
sodipodi:sides="6"
sodipodi:cx="138.75616"
sodipodi:cy="263.41873"
sodipodi:r1="70.000412"
sodipodi:r2="60.622131"
sodipodi:arg1="-2.6179939"
sodipodi:arg2="-2.0943951"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 78.134029,228.41853 60.622131,-35.00021 60.62214,35.0002 0,70.00042 -60.62213,35.0002 -60.62214,-35.0002 z"
inkscape:transform-center-x="-4.0923148e-06"
inkscape:transform-center-y="-2.6621219e-06"
transform="matrix(1.0382846,0,0,1.0210168,-28.572793,-191.28234)" /><g
id="g12126"
inkscape:label="lines"><path
style="fill:none;fill-opacity:1;stroke:url(#linearGradient12104);stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
d="M 54.029427,71.745371 176.85736,71.722537"
id="path12042"
inkscape:label="line" /><path
style="fill:none;fill-opacity:1;stroke:url(#linearGradient12108);stroke-width:0.6;stroke-dasharray:none;stroke-opacity:1"
d="M 54.029427,76.507874 176.85736,76.48504"
id="path12106"
inkscape:label="line" /><path
style="fill:none;fill-opacity:1;stroke:url(#linearGradient12112);stroke-width:0.8;stroke-dasharray:none;stroke-opacity:1"
d="M 54.029427,91.32455 176.85736,91.301716"
id="path12110"
inkscape:label="line" /><path
style="fill:url(#linearGradient12239);fill-opacity:1;stroke:none;stroke-width:2.416;stroke-dasharray:none;stroke-opacity:1"
d="M 104.40252,71.315371 53.668551,108.90527 v 5.16648 z"
id="path12133-5" /><path
style="fill:url(#linearGradient14475);fill-opacity:1;stroke:none;stroke-width:2.41042;stroke-dasharray:none;stroke-opacity:1"
d="m 125.69256,71.804339 51.46081,36.885251 v 5.06963 z"
id="path12133" /><path
style="fill:url(#linearGradient12141);fill-opacity:1;stroke:none;stroke-width:2.41042;stroke-dasharray:none;stroke-opacity:1"
d="m 115.37951,70.94784 5.14781,74.3852 c 0,0 -4.42958,5.94658 -10.22727,-0.9757 z"
id="path12280"
sodipodi:nodetypes="cccc" /></g><path
style="fill:url(#linearGradient7692);fill-opacity:1;stroke:none;stroke-width:2.3;stroke-dasharray:none;stroke-opacity:1"
d="M 53.906377,71.659657 H 177.09207 l 0.24519,40.701533 -61.93861,35.18517 -61.54407,-35.3442 z"
id="path7625"
sodipodi:nodetypes="cccccc" /><g
id="g4494"
transform="matrix(2.7825702,0,0,3.2095953,58.857189,44.497537)"
style="filter:url(#filter12743)"><g
transform="translate(-47.668322,-15.505759)"
id="g113"><g
transform="scale(0.26458)"
style="font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal"
aria-label="SEARXR"
id="g111"><path
id="path105"
style="font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;opacity:1;fill:#f7f7f7;fill-opacity:0.996078;stroke:url(#linearGradient8637);stroke-width:3.77953;stroke-miterlimit:4;stroke-dasharray:none"
d="m 158.51953,0.12890625 c -1.12928,0.0104375 -2.33343,0.0380832 -3.58789,0.09375 -0.30457,-0.13399831 -0.0547,13.73632775 -0.0547,13.73632775 2.81897,0.06348 6.79437,-0.315067 8.33985,1.041016 3.97475,3.093961 3.19147,2.418039 5.77734,5.003906 2.58587,2.585868 5.23601,5.171945 7.95117,7.757813 l -21.2931,18.782442 c -1.29339,1.292984 -2.45529,1.570562 8.42537,1.302881 9.59316,-0.08935 8.19237,0.720755 10.73745,-1.137203 7.83955,-6.121602 9.61287,-7.470425 12.32804,-10.185491 3.0599,2.434819 5.75483,4.092804 10.43494,7.218213 0,0 4.56752,2.32698 9.03546,3.466611 5.1614,0.869711 19.0651,0.856505 19.62872,0.659979 h 4.7207 c 0,0 0.0977,-8.957996 0.16992,-16.066407 0.12771,-10.909788 2.17195,-13.091574 4.75781,-15.80664 2.71507,-2.715066 6.01188,-2.072266 9.89063,-2.072266 h 21.3457 V 0.15429688 h -21.3457 c -7.6279,0 -14.15766,2.71429962 -19.58789,8.14453122 -5.30093,5.3009329 -7.95117,11.7666269 -7.95117,19.3945309 v 9.097657 c -2.05016,0.03977 -4.12643,0.0029 -5.92969,-0.160157 -1.85798,-0.167898 -4.68413,-1.320736 -8.3076,-3.088808 0,0 -4.88583,-3.065166 -7.4717,-5.780332 l 15.00318,-15.777875 c 1.25994,-1.325002 3.90091,-4.6189385 8.52807,-9.5834534 1.63566,-1.75491056 0.84973,-2.31134101 -4.04101,-2.19531248 -2.41247,0.00862 -6.69149,0.0175781 -8.63086,0.0175781 -1.81008,0 -3.42548,0.71063042 -4.84766,2.13281258 L 186.83594,18.064453 c -2.71507,-2.585867 -5.36541,-5.171945 -7.95117,-7.757812 l -7.95118,-7.9511722 c -1.42218,-1.42217213 -3.03953,-2.13281255 -4.84961,-2.13281255 -1.45453,0 -4.17661,-0.12506257 -7.56445,-0.09375 z"
transform="matrix(1.0000126,0,0,1.0000126,27.273414,60.371154)" /><path
d="m 267.38547,88.252296 c 0,-7.665647 2.6505,-14.161997 7.9515,-19.48895 5.4303,-5.456886 11.96,-8.185379 19.588,-8.185379 h 37.34533 v 13.837416 h -37.34533 c -3.8788,0 -7.1758,1.364246 -9.8909,4.092639 -2.5859,2.728493 -3.8788,5.976618 -3.8788,9.744475 v 19.634563 h -13.77 z"
style="font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;fill:#f7f7f7;fill-opacity:0.997356;stroke:#54438e;stroke-width:3.78882;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path103" /><rect
style="fill:#f7f7f7;fill-opacity:0.996078;stroke:none;stroke-width:3.77958"
id="rect1105"
width="29.440615"
height="9.8188734"
x="269.93423"
y="62.516171" /></g></g><path
d="m 40.243755,6.8720423 -9.0863,-0.04493 c 0,0 -1.8872,0.04494 -1.8423,1.5727 0.04493,1.5277999 1.8772,1.4827999 1.8772,1.4827999 h 9.0514 z"
stroke="#000000"
stroke-width="0.26458px"
id="path117"
style="fill:#fefefe;fill-opacity:1;stroke:#54438e;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:19.7556px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Bold';text-align:center;text-anchor:middle;fill:#ffffff;stroke:#ffffff;stroke-width:0.5;stroke-dasharray:none"
x="69.809654"
y="107.18471"
id="text1004"><tspan
sodipodi:role="line"
id="tspan1002"
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:19.7556px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Thin';stroke-width:0.5;stroke-dasharray:none"
x="69.809654"
y="107.18471">[</tspan></text><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:19.7556px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Bold';text-align:center;text-anchor:middle;fill:#ffffff;stroke:#ffffff;stroke-width:0.5;stroke-dasharray:none"
x="160.00148"
y="107.33738"
id="text1004-3"><tspan
sodipodi:role="line"
id="tspan1002-6"
style="font-style:normal;font-variant:normal;font-weight:100;font-stretch:normal;font-size:19.7556px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Thin';stroke-width:0.5;stroke-dasharray:none"
x="160.00148"
y="107.33738">]</tspan></text><text
xml:space="preserve"
style="font-weight:100;font-size:16.2278px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Thin';text-align:center;letter-spacing:0.529167px;writing-mode:tb-rl;text-orientation:upright;text-anchor:middle;fill:#020202;fill-opacity:1;stroke:#000000;stroke-width:2.3;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter13745)"
x="85.100029"
y="91.992111"
id="text3268"><tspan
sodipodi:role="line"
id="tspan3266"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:16.2278px;font-family:Montserrat;-inkscape-font-specification:'Montserrat Bold';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.3;stroke-opacity:1"
x="85.100029"
y="91.992111">XR Forge</tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,23 +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;
}

View file

@ -1,785 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="author" content="Godot Engine">
<meta name="description" content="Use the Godot Engine editor directly in your web browser, without having to install anything.">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Godot">
<meta name="apple-mobile-web-app-title" content="Godot">
<meta name="theme-color" content="#202531">
<meta name="msapplication-navbutton-color" content="#202531">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="msapplication-starturl" content="/latest">
<meta property="og:site_name" content="Godot Engine Web Editor">
<meta property="og:url" name="twitter:url" content="https://editor.godotengine.org/releases/latest/">
<meta property="og:title" name="twitter:title" content="Free and open source 2D and 3D game engine">
<meta property="og:description" name="twitter:description" content="Use the Godot Engine editor directly in your web browser, without having to install anything.">
<meta property="og:image" name="twitter:image" content="https://godotengine.org/themes/godotengine/assets/og_image.png">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<link id="-gd-engine-icon" rel="icon" type="image/png" href="favicon.png">
<link rel="apple-touch-icon" type="image/png" href="favicon.png">
<link rel="manifest" href="manifest.json">
<title>Godot Engine Web Editor (4.4.1.stable.official)</title>
<style>
*:focus {
/* More visible outline for better keyboard navigation. */
outline: 0.125rem solid hsl(220, 100%, 62.5%);
/* Make the outline always appear above other elements. */
/* Otherwise, one of its sides can be hidden by tabs in the Download and More layouts. */
position: relative;
}
body {
touch-action: none;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
margin: 0;
border: 0 none;
padding: 0;
text-align: center;
background-color: #333b4f;
overflow: hidden;
}
a {
color: hsl(205, 100%, 75%);
text-decoration-color: hsla(205, 100%, 75%, 0.3);
text-decoration-thickness: 0.125rem;
}
a:hover {
filter: brightness(117.5%);
}
a:active {
filter: brightness(82.5%);
}
.welcome-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: hsla(0, 0%, 0%, 0.5);
text-align: left;
}
.welcome-modal-title {
text-align: center;
}
.welcome-modal-content {
background-color: #333b4f;
box-shadow: 0 0.25rem 0.25rem hsla(0, 0%, 0%, 0.5);
line-height: 1.5;
max-width: 38rem;
margin: 4rem auto 0 auto;
color: white;
border-radius: 0.5rem;
padding: 1rem 1rem 2rem 1rem;
}
#tabs-buttons {
/* Match the default background color of the editor window for a seamless appearance. */
background-color: #202531;
}
#tab-game {
/* Use a pure black background to better distinguish the running project */
/* from the editor window, and to use a more neutral background color (no tint). */
background-color: black;
/* Make the background span the entire page height. */
min-height: 100vh;
}
#canvas, #gameCanvas {
display: block;
margin: 0;
color: white;
}
/* Don't show distracting focus outlines for the main tabs' contents. */
#tab-editor canvas:focus,
#tab-game canvas:focus,
#canvas:focus,
#gameCanvas:focus {
outline: none;
}
.godot {
color: #e0e0e0;
background-color: #3b3943;
background-image: linear-gradient(to bottom, #403e48, #35333c);
border: 1px solid #45434e;
box-shadow: 0 0 1px 1px #2f2d35;
}
.btn {
appearance: none;
color: #e0e0e0;
background-color: #262c3b;
border: 1px solid #202531;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
}
.btn:not(:disabled):hover {
color: #e0e1e5;
border-color: #666c7b;
}
.btn:active {
border-color: #699ce8;
color: #699ce8;
}
.btn:disabled {
color: #aaa;
border-color: #242937;
}
.btn.tab-btn {
padding: 0.3rem 1rem;
}
.btn.close-btn {
padding: 0.3rem 1rem;
margin-left: -0.75rem;
font-weight: 700;
}
/* Status display */
#status {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
/* don't consume click events - make children visible explicitly */
visibility: hidden;
}
#status-progress {
width: 366px;
height: 7px;
background-color: #38363A;
border: 1px solid #444246;
padding: 1px;
box-shadow: 0 0 2px 1px #1B1C22;
border-radius: 2px;
visibility: visible;
}
@media only screen and (orientation:portrait) {
#status-progress {
width: 61.8%;
}
}
#status-progress-inner {
height: 100%;
width: 0;
box-sizing: border-box;
transition: width 0.5s linear;
background-color: #202020;
border: 1px solid #222223;
box-shadow: 0 0 1px 1px #27282E;
border-radius: 3px;
}
#status-indeterminate {
visibility: visible;
position: relative;
}
#status-indeterminate > div {
width: 4.5px;
height: 0;
border-style: solid;
border-width: 9px 3px 0 3px;
border-color: #2b2b2b transparent transparent transparent;
transform-origin: center 21px;
position: absolute;
}
#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
#status-notice {
margin: 0 100px;
line-height: 1.3;
visibility: visible;
padding: 4px 6px;
}
</style>
</head>
<body>
<div
id="welcome-modal"
class="welcome-modal"
role="dialog"
aria-labelledby="welcome-modal-title"
aria-describedby="welcome-modal-description"
onclick="if (event.target === this) closeWelcomeModal(false)"
>
<div class="welcome-modal-content">
<h2 id="welcome-modal-title" class="welcome-modal-title">Important - Please read before continuing</h2>
<div id="welcome-modal-description">
<p>
The Godot Web Editor has some limitations compared to the native version.
Its main focus is education and experimentation;
<strong>it is not recommended for production</strong>.
</p>
<p>
Refer to the
<a
href="https://docs.godotengine.org/en/latest/tutorials/editor/using_the_web_editor.html"
target="_blank"
rel="noopener"
>Web editor documentation</a> for usage instructions and limitations.
</p>
</div>
<div id="welcome-modal-missing-description" style="display: none">
<p>
<strong>The following features required by the Godot Web Editor are missing:</strong>
</p>
<ul id="welcome-modal-missing-list">
</ul>
<p>
If you are self-hosting the web editor,
refer to
<a
href="https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_web.html"
target="_blank"
rel="noopener"
>Exporting for the Web</a> for more information.
</p>
</div>
<div style="text-align: center">
<button id="welcome-modal-dismiss" class="btn" type="button" onclick="closeWelcomeModal(true)" style="margin-top: 1rem">
OK, don't show again
</button>
</div>
</div>
</div>
<div id="tabs-buttons">
<button id="btn-tab-loader" class="btn tab-btn" onclick="showTab('loader')">Loader</button>
<button id="btn-tab-editor" class="btn tab-btn" disabled="disabled" onclick="showTab('editor')">Editor</button>
<button id="btn-close-editor" class="btn close-btn" disabled="disabled" onclick="closeEditor()">×</button>
<button id="btn-tab-game" class="btn tab-btn" disabled="disabled" onclick="showTab('game')">Game</button>
<button id="btn-close-game" class="btn close-btn" disabled="disabled" onclick="closeGame()">×</button>
<button id="btn-tab-update" class="btn tab-btn" style="display: none;">Update</button>
</div>
<div id="tabs">
<div id="tab-loader">
<div style="color: #e0e0e0;" id="persistence">
<br >
<img src="logo.svg" alt="Godot Engine logo" width="1024" height="414" style="width: auto; height: auto; max-width: min(85%, 50vh); max-height: 250px">
<br >
4.4.1.stable.official
<br >
<a href="releases/">Need an old version?</a>
<br >
<br >
<br >
<label for="videoMode" style="margin-right: 1rem">Video driver:</label>
<select id="videoMode">
<option value="" selected="selected">Auto</option>
<option value="opengl3">WebGL 2</option>
</select>
<br >
<br >
<label for="zip-file" style="margin-right: 1rem">Preload project ZIP:</label>
<input id="zip-file" type="file" name="files" style="margin-bottom: 1rem">
<br >
<a href="demo.zip">(Try this for example)</a>
<br >
<br >
<button id="startButton" class="btn" style="margin-bottom: 4rem; font-weight: 700">Start Godot editor</button>
<br >
<button class="btn" onclick="clearPersistence()" style="margin-bottom: 1.5rem">Clear persistent data</button>
<br >
<a href="https://docs.godotengine.org/en/latest/tutorials/editor/using_the_web_editor.html">Web editor documentation</a>
</div>
</div>
<div id="tab-editor" style="display: none;">
<canvas id="editor-canvas" tabindex="1">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
</div>
<div id="tab-game" style="display: none;">
<canvas id="game-canvas" tabindex="2">
HTML5 canvas appears to be unsupported in the current browser.<br >
Please try updating or use a different browser.
</canvas>
</div>
<div id="tab-status" style="display: none;">
<div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();">
<div id="status-progress-inner"></div>
</div>
<div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="status-notice" class="godot" style="display: none;"></div>
</div>
</div>
<script>
window.addEventListener('load', () => {
function notifyUpdate(sw) {
const btn = document.getElementById('btn-tab-update');
btn.onclick = function () {
if (!window.confirm('Are you sure you want to update?\nClicking "OK" will reload all active instances!')) {
return;
}
sw.postMessage('update');
btn.innerHTML = 'Updating...';
btn.disabled = true;
};
btn.style.display = '';
}
if ('serviceWorker' in navigator) {
try {
navigator.serviceWorker.register('service.worker.js').then(function (reg) {
if (reg.waiting) {
notifyUpdate(reg.waiting);
}
reg.addEventListener('updatefound', function () {
const update = reg.installing;
update.addEventListener('statechange', function () {
if (update.state === 'installed') {
// It's a new install, claim and perform aggressive caching.
if (!reg.active) {
update.postMessage('claim');
} else {
notifyUpdate(update);
}
}
});
});
});
} catch (e) {
console.error('Error while registering service worker:', e);
}
}
const missing = Engine.getMissingFeatures({
threads: true,
});
if (missing.length) {
// Display error dialog as threading support is required for the editor.
document.getElementById('startButton').disabled = 'disabled';
document.getElementById('welcome-modal-description').style.display = 'none';
document.getElementById('welcome-modal-missing-description').style.display = 'block';
document.getElementById('welcome-modal-dismiss').style.display = 'none';
const list = document.getElementById('welcome-modal-missing-list');
for (let i = 0; i < missing.length; i++) {
const node = document.createElement('li');
node.innerText = missing[i];
list.appendChild(node);
}
}
if (missing.length || localStorage.getItem('welcomeModalDismissed') !== 'true') {
document.getElementById('welcome-modal').style.display = 'block';
document.getElementById('welcome-modal-dismiss').focus();
}
});
function closeWelcomeModal(dontShowAgain) { // eslint-disable-line no-unused-vars
document.getElementById('welcome-modal').style.display = 'none';
if (dontShowAgain) {
localStorage.setItem('welcomeModalDismissed', 'true');
}
}
</script>
<script src="godot.editor.js"></script>
<script>
let editor = null;
let game = null;
let setStatusMode;
let setStatusNotice;
let video_driver = '';
function clearPersistence() { // eslint-disable-line no-unused-vars
function deleteDB(path) {
return new Promise(function (resolve, reject) {
const req = indexedDB.deleteDatabase(path);
req.onsuccess = function () {
resolve();
};
req.onerror = function (err) {
reject(err);
};
req.onblocked = function (err) {
reject(err);
};
});
}
if (!window.confirm('Are you sure you want to delete all the locally stored files?\nClicking "OK" will permanently remove your projects and editor settings!')) {
return;
}
Promise.all([
deleteDB('/home/web_user'),
]).then(function (results) {
alert('Done.');
}).catch(function (err) {
alert('Error deleting local files. Please retry after reloading the page.');
});
}
function selectVideoMode() {
const select = document.getElementById('videoMode');
video_driver = select.selectedOptions[0].value;
}
const tabs = [
document.getElementById('tab-loader'),
document.getElementById('tab-editor'),
document.getElementById('tab-game'),
];
function showTab(name) {
tabs.forEach(function (elem) {
if (elem.id === `tab-${name}`) {
elem.style.display = 'block';
if (name === 'editor' || name === 'game') {
const canvas = document.getElementById(`${name}-canvas`);
canvas.focus();
}
} else {
elem.style.display = 'none';
}
});
}
function setButtonEnabled(id, enabled) {
if (enabled) {
document.getElementById(id).disabled = '';
} else {
document.getElementById(id).disabled = 'disabled';
}
}
function setLoaderEnabled(enabled) {
setButtonEnabled('btn-tab-loader', enabled);
setButtonEnabled('btn-tab-editor', !enabled);
setButtonEnabled('btn-close-editor', !enabled);
}
function setGameTabEnabled(enabled) {
setButtonEnabled('btn-tab-game', enabled);
setButtonEnabled('btn-close-game', enabled);
}
function closeGame() {
if (game) {
game.requestQuit();
}
}
function closeEditor() { // eslint-disable-line no-unused-vars
closeGame();
if (editor) {
editor.requestQuit();
}
}
function startEditor(zip) {
const INDETERMINATE_STATUS_STEP_MS = 100;
const persistentPaths = ['/home/web_user'];
let editorCanvas = document.getElementById('editor-canvas');
let gameCanvas = document.getElementById('game-canvas');
const statusProgress = document.getElementById('status-progress');
const statusProgressInner = document.getElementById('status-progress-inner');
const statusIndeterminate = document.getElementById('status-indeterminate');
const statusNotice = document.getElementById('status-notice');
const headerDiv = document.getElementById('tabs-buttons');
let initializing = true;
let statusMode = 'hidden';
showTab('status');
let animationCallbacks = [];
function animate(time) {
animationCallbacks.forEach((callback) => callback(time));
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
let lastScale = 0;
let lastWidth = 0;
let lastHeight = 0;
function adjustCanvasDimensions() {
const scale = window.devicePixelRatio || 1;
const headerHeight = headerDiv.offsetHeight + 1;
const width = window.innerWidth;
const height = window.innerHeight - headerHeight;
if (lastScale !== scale || lastWidth !== width || lastHeight !== height) {
editorCanvas.width = width * scale;
editorCanvas.height = height * scale;
editorCanvas.style.width = `${width}px`;
editorCanvas.style.height = `${height}px`;
lastScale = scale;
lastWidth = width;
lastHeight = height;
}
}
animationCallbacks.push(adjustCanvasDimensions);
adjustCanvasDimensions();
function replaceCanvas(from) {
const out = document.createElement('canvas');
out.id = from.id;
out.tabIndex = from.tabIndex;
from.parentNode.replaceChild(out, from);
lastScale = 0;
return out;
}
function animateStatusIndeterminate(ms) {
const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8);
if (statusIndeterminate.children[i].style.borderTopColor === '') {
Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => {
child.style.borderTopColor = '';
});
statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
}
}
setStatusMode = function (mode) {
if (statusMode === mode || !initializing) {
return;
}
[statusProgress, statusIndeterminate, statusNotice].forEach((elem) => {
elem.style.display = 'none';
});
animationCallbacks = animationCallbacks.filter(function (value) {
return (value !== animateStatusIndeterminate);
});
switch (mode) {
case 'progress':
statusProgress.style.display = 'block';
break;
case 'indeterminate':
statusIndeterminate.style.display = 'block';
animationCallbacks.push(animateStatusIndeterminate);
break;
case 'notice':
statusNotice.style.display = 'block';
break;
case 'hidden':
break;
default:
throw new Error('Invalid status mode');
}
statusMode = mode;
};
setStatusNotice = function (text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
const lines = text.split('\n');
lines.forEach((line) => {
statusNotice.appendChild(document.createTextNode(line));
statusNotice.appendChild(document.createElement('br'));
});
};
const gameConfig = {
'persistentPaths': persistentPaths,
'unloadAfterInit': false,
'canvas': gameCanvas,
'canvasResizePolicy': 1,
'onExit': function () {
gameCanvas = replaceCanvas(gameCanvas);
setGameTabEnabled(false);
showTab('editor');
game = null;
},
};
let OnEditorExit = function () {
showTab('loader');
setLoaderEnabled(true);
};
function Execute(args) {
const is_editor = args.filter(function (v) {
return v === '--editor' || v === '-e';
}).length !== 0;
const is_project_manager = args.filter(function (v) {
return v === '--project-manager';
}).length !== 0;
const is_game = !is_editor && !is_project_manager;
if (video_driver) {
args.push('--rendering-driver', video_driver);
}
if (is_game) {
if (game) {
console.error('A game is already running. Close it first');
return;
}
setGameTabEnabled(true);
game = new Engine(gameConfig);
showTab('game');
game.init().then(function () {
requestAnimationFrame(function () {
game.start({ 'args': args, 'canvas': gameCanvas }).then(function () {
gameCanvas.focus();
});
});
});
} else { // New editor instances will be run in the same canvas. We want to wait for it to exit.
OnEditorExit = function (code) {
setLoaderEnabled(true);
setTimeout(function () {
editor.init().then(function () {
setLoaderEnabled(false);
OnEditorExit = function () {
showTab('loader');
setLoaderEnabled(true);
};
editor.start({ 'args': args, 'persistentDrops': is_project_manager, 'canvas': editorCanvas });
});
}, 0);
OnEditorExit = null;
};
}
}
const editorConfig = {
'unloadAfterInit': false,
'onProgress': function progressFunction(current, total) {
if (total > 0) {
statusProgressInner.style.width = `${(current / total) * 100}%`;
setStatusMode('progress');
if (current === total) {
// wait for progress bar animation
setTimeout(() => {
setStatusMode('indeterminate');
}, 100);
}
} else {
setStatusMode('indeterminate');
}
},
'canvas': editorCanvas,
'canvasResizePolicy': 0,
'onExit': function () {
editorCanvas = replaceCanvas(editorCanvas);
if (OnEditorExit) {
OnEditorExit();
}
},
'onExecute': Execute,
'persistentPaths': persistentPaths,
};
editor = new Engine(editorConfig);
function displayFailureNotice(err) {
console.error(err);
if (err instanceof Error) {
setStatusNotice(err.message);
} else if (typeof err === 'string') {
setStatusNotice(err);
} else {
setStatusNotice('An unknown error occurred.');
}
setStatusMode('notice');
initializing = false;
}
if (!Engine.isWebGLAvailable()) {
displayFailureNotice('WebGL not available');
} else {
setStatusMode('indeterminate');
editor.init('godot.editor').then(function () {
if (zip) {
editor.copyToFS('/tmp/preload.zip', zip);
}
try {
// Avoid user creating project in the persistent root folder.
editor.copyToFS('/home/web_user/keep', new Uint8Array());
} catch (e) {
// File exists
}
selectVideoMode();
showTab('editor');
setLoaderEnabled(false);
const args = ['--project-manager', '--single-window'];
if (video_driver) {
args.push('--rendering-driver', video_driver);
}
editor.start({ 'args': args, 'persistentDrops': true }).then(function () {
setStatusMode('hidden');
initializing = false;
});
}).catch(displayFailureNotice);
}
}
function preloadZip(target) {
return new Promise(function (resolve, reject) {
if (target.files.length > 0) {
target.files[0].arrayBuffer().then(function (data) {
resolve(data);
});
} else {
resolve();
}
});
}
document.getElementById('startButton').onclick = function () {
preloadZip(document.getElementById('zip-file')).then(function (zip) {
startEditor(zip);
});
};
document.addEventListener('DOMContentLoaded', function(){
setTimeout( () => {
if( document.location.search ){
const urlParams = new URLSearchParams(window.location.search);
const url = urlParams.get('url');
if( url ){
fetch( url.replace('#','%23') )
.then( (res) => res.arrayBuffer() )
.then( (a) => {
console.dir(a)
return a
})
.then( (ab) => startEditor(ab) )
}
}
},500 )
})
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -22,36 +22,14 @@ let
# generate the reproducable blob below via:
# $ nix-shell -p nix-prefetch-docker --run 'nix-prefetch-docker ghcr.io/manyfold3d/manyfold-solo 0.120.0'
#manyfoldImage = pkgs.dockerTools.pullImage {
# imageName = "ghcr.io/manyfold3d/manyfold-solo";
# imageDigest = "sha256:6250e562a05bf9476ddcfdc897a7b03cbf2090c727d9fe051afde486579b54a6";
# sha256 = "sha256-V5y1N0l4JQjVDQbboGYX15MQaImXtP/HpNwPjDtxeJQ=";
# finalImageName = "ghcr.io/manyfold3d/manyfold-solo";
# finalImageTag = "0.121.0";
#};
manyfoldImage = pkgs.dockerTools.pullImage {
imageName = "ghcr.io/manyfold3d/manyfold-solo";
imageDigest = "sha256:465399a2d296034ef84dba18a13744b567694c652387bd17fe97d51c672d1fa9";
hash = "sha256-j7YSUGRFUDh6FJ1CrIQEzGU/0B8uPO8y1kd9lYLad4w=";
finalImageName = "ghcr.io/manyfold3d/manyfold-solo";
finalImageTag = "latest";
imageName = "ghcr.io/manyfold3d/manyfold-solo";
imageDigest = "sha256:84524b9cf8c8e6467ca4938e58ff65a2a5d8c507fd44e7056003b3e2dcffb266";
sha256 = "0sb4icq19vqsnhi01jbaqqr2k66bfma08hp0rm0y4hdnbqsscxvd";
finalImageName = "ghcr.io/manyfold3d/manyfold-solo";
finalImageTag = "0.120.0";
};
# generate the reproducable blob below via:
# $ nix-shell -p nix-prefetch-github --command 'nix-prefetch-github assimp assimp --rev e778c84cd62bc8b38d8e491ad3d2c27cb8ed37d5'
assimpSrc = pkgs.fetchFromGitHub {
"owner" = "assimp";
"repo" = "assimp";
"rev" = "e778c84cd62bc8b38d8e491ad3d2c27cb8ed37d5";
"hash" = "sha256-ja5pFwpnzLT2MDIR8ISwC6+eA5UXyqRZW2CMCCrF1Q0=";
};
myAssimp = pkgs.pkgsStatic.assimp.overrideAttrs (oldAttrs: {
inherit assimpSrc; # Set the source to the fetched commit
src = assimpSrc;
});
in
rec
{
@ -66,17 +44,15 @@ rec
# add nix pkgs + local files
contents = pkgs.buildEnv {
name = "image-root";
pathsToLink = ["/manyfold" "/bin" ];
pathsToLink = ["/manyfold" "/bin"];
paths = [
pkgs.pkgsStatic.rsync
pkgs.pkgsStatic.sqlite
pkgs.pkgsStatic.rclone
pkgs.pkgsStatic.fuse3
pkgs.pkgsStatic.acl # getfacl e.g.
pkgs.pkgsStatic.acl # getfacl e.g.
pkgs.pkgsStatic.inotify-tools # inotifywait e.g.
pkgs.pkgsStatic.zip # inotifywait e.g.
pkgs.pkgsStatic.ts # job management
myAssimp # cli 3D editing/conversion
pkgs.pkgsStatic.zip # inotifywait e.g.
./..
];
};

View file

@ -55,11 +55,6 @@
echo " $ # copy image to other server
echo " $ docker save xrforge | bzip2 | ssh user@host docker load"
echo ""
echo "Development:"
echo ""
echo "" $ cd xrforge-webxr && bun run build && cp dist/xrforge.html ../manyfold/usr/src/app/public/view/index.html
echo "" $ manyfold/cli/manyfold.sh run -e DEV=1 -v ./manyfold/usr/src/app/public/view:/usr/src/app/public/view
echo ""
'';
};