This commit is contained in:
Leon van Kammen 2025-11-10 15:05:01 +01:00
parent d725c9c73a
commit 0ace214070
7 changed files with 534 additions and 367 deletions

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

View file

@ -19,77 +19,89 @@ begin
# Read and parse the JSON file
data = JSON.parse( File.read( "datapackage.json" ) )
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
if data['keywords'].empty? || data['keywords'].include?('janusxr')
XRForge.log("✅ starting build janusXR XR scene", logfile)
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
# Extract the desired field (assuming the field is named 'model_file')
thumb_file = data['image']
XRForge.log("✅ starting build janusXR XR scene", logfile)
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
# 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
# Log a message for the file that was not found, but don't stop
XRForge.log("⚠️ 3D file '#{filename}' not detected", logfile)
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 = <<~JML
<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>
JML
html = <<~HTML
<!DOCTYPE html>
<html>
<head>
<title>janusxr room</title>
</head>
<body>
<script src="https://web.janusvr.com/janusweb.js"></script>
<janus-viewer>
#{jml}
</janus-viewer>
</body>
</html>
HTML
File.write('.xrforge/janusxr.html', html)
File.write('.xrforge/scene.jml', jml)
XRForge.log("✅ generated scene.jml", logfile)
XRForge.log("✅ generated janusxr.html", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('janusxr')
data['keywords'].push('janusxr')
File.write(file_path, JSON.pretty_generate(data) )
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"

View file

@ -0,0 +1,87 @@
#!/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" ) )
if data['keywords'].empty? || data['keywords'].include?('mml')
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
XRForge.log("✅ starting build mml XR scene", logfile)
# 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- / MML-compatible experience", logfile)
end
# Get the value of the environment variable FEDERATE_DRIVE_HOST
federate_drive_host = ENV['FEDERATE_DRIVE_HOST']
# https://viewer.mml.io/main/v1/?url=https%3A%2F%2Ffoo.org%2Fbar.mml
mml = <<~MML
<m-model src="#{federate_drive_host}/#{model_file.gsub("#","%23")}" anim-loop="true" anim-enabled="true"></m-model>
MML
File.write('.xrforge/scene.mml', mml)
XRForge.log("✅ generated scene.mml", logfile)
XRForge.log(" ", logfile)
# tag it!
if ! data['keywords'].include?('mml')
data['keywords'].push('mml')
File.write(file_path, JSON.pretty_generate(data) )
end
end
rescue Errno::ENOENT
puts "File #{filename} not found"
rescue JSON::ParserError
puts "Error parsing JSON from #{filename}"
rescue => e
puts "An error occurred: #{e.message}"
end

View file

@ -0,0 +1,32 @@
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>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">janusxr</a>
</td>
<td>
This generates an JanusXR address.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open Immersive Web-layer since 2015, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
</td>
</tr>
<tr>
<td>
<a class="badge rounded-pill bg-secondary tag">mml</a>
</td>
<td>
This generates an MML address.<br>
Metaverse Markup Language (<a href="https://mml.io" target="_blank">MML</a>) is an open markup language used to define experiences.
</td>
</tr>
</table>

View file

@ -1,298 +1,338 @@
<% content_for :head do %>
<%= tag.meta property: "og:type", content: "website" %>
<%= tag.meta property: "og:image", content: model_model_file_url(@model, @model.preview_file, format: @model.preview_file.extension) if @model.preview_file&.is_image? && !@model.sensitive %>
<%= tag.meta name: "description", content: truncate(sanitize(@model.notes), length: 80) if @model.notes.present? %>
<%= tag.meta name: "fediverse:creator", content: @model.creator.federails_actor.at_address if @model.creator %>
<%= tag.link rel: "alternate", type: Mime[:oembed], href: model_url(@model, format: :oembed), title: @model.name %>
<% end %>
<div itemscope itemtype="https://schema.org/3DModel">
<%= turbo_stream_from @model %>
<h1>
<span itemprop="name">
<a contenteditable="plaintext-only"
data-controller="editable"
data-action="focus->editable#onFocus blur->editable#onBlur"
data-editable-field="model[name]"
data-editable-path="<%= model_path(@model) %>"><%= @model.name %></a>
</span>
<%= link_to icon("search", t(".search")),
"https://yeggi.com/q/#{ERB::Util.url_encode(@model.name)}/",
class: "btn btn-outline", target: "manyfold_search", rel: "noreferrer" %>
</h1>
<div class="row row-cols-md-2 mt-2">
<div class="col-md-9" id="item_list">
<% if @locked_files > 0 %>
<div class="alert alert-info"><%= t(".preview", count: @locked_files) %></div>
<% end %>
<%= render "image_carousel", images: @images %>
<%= card(:secondary) do %>
<p class="card-text" itemprop="description"><%= markdownify @model.notes %></p>
<% end if @model.notes.present? %>
<% if @num_files > 0 %>
<h2><a name="files"><%= t(".files") %></a></h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: @groups.delete(nil) %>
</div>
<% @groups.each_pair do |group, files| %>
<h3><a name="<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: files %>
</div>
<% end %>
<% end %>
</div>
<div class="col-md-3" id="sidebar">
<%= card :secondary, t(".model_details") do %>
<table class="table table-borderless table-sm">
<% if SiteSettings.federation_enabled? %>
<tr>
<td>⁂</td>
<td><% if @model.remote? %>
<small><%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %></small>
<% else %>
<small>
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
</small>
<% end %>
<label for="toggle_activitypub"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_activitypub" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the <a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">fediverse</a> activitypub address of this experience.<br>
Follow updates by copy/pasting it into ActivityPub <a href="https://codeberg.org/fediverse/delightful-fediverse-clients" target="_blank">clients</a>.
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<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>
<% content_for :head do %>
<%= tag.meta property: "og:type", content: "website" %>
<%= tag.meta property: "og:image", content: model_model_file_url(@model, @model.preview_file, format: @model.preview_file.extension) if @model.preview_file&.is_image? && !@model.sensitive %>
<%= tag.meta name: "description", content: truncate(sanitize(@model.notes), length: 80) if @model.notes.present? %>
<%= tag.meta name: "fediverse:creator", content: @model.creator.federails_actor.at_address if @model.creator %>
<%= tag.link rel: "alternate", type: Mime[:oembed], href: model_url(@model, format: :oembed), title: @model.name %>
<% end %>
<div itemscope itemtype="https://schema.org/3DModel">
<%= turbo_stream_from @model %>
<h1>
<span itemprop="name">
<a contenteditable="plaintext-only"
data-controller="editable"
data-action="focus->editable#onFocus blur->editable#onBlur"
data-editable-field="model[name]"
data-editable-path="<%= model_path(@model) %>"><%= @model.name %></a>
</span>
<%= link_to icon("search", t(".search")),
"https://yeggi.com/q/#{ERB::Util.url_encode(@model.name)}/",
class: "btn btn-outline", target: "manyfold_search", rel: "noreferrer" %>
</h1>
<div class="row row-cols-md-2 mt-2">
<div class="col-md-9" id="item_list">
<% if @locked_files > 0 %>
<div class="alert alert-info"><%= t(".preview", count: @locked_files) %></div>
<% end %>
<%= render "image_carousel", images: @images %>
<%= card(:secondary) do %>
<p class="card-text" itemprop="description"><%= markdownify @model.notes %></p>
<% end if @model.notes.present? %>
<% if @num_files > 0 %>
<h2><a name="files"><%= t(".files") %></a></h2>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: @groups.delete(nil) %>
</div>
<% @groups.each_pair do |group, files| %>
<h3><a name="<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mb-4">
<%= render partial: "file", collection: files %>
</div>
<% end %>
<% end %>
</div>
<div class="col-md-3" id="sidebar">
<%= card :secondary, t(".model_details") do %>
<table class="table table-borderless table-sm">
<% if SiteSettings.federation_enabled? %>
<tr>
<td>⁂</td>
<td><% if @model.remote? %>
<small><%= link_to @model.federails_actor.at_address, @model.federails_actor.profile_url, target: "new" %></small>
<% else %>
<small>
<%= @model.federails_actor.short_at_address %>
<%= render Components::CopyButton.new(text: @model.federails_actor.at_address) %>
</small>
<% end %>
<label for="toggle_activitypub"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_activitypub" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the <a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank">fediverse</a> activitypub address of this experience.<br>
Follow updates by copy/pasting it into ActivityPub <a href="https://codeberg.org/fediverse/delightful-fediverse-clients" target="_blank">clients</a>.
</small>
</div>
</div>
</td>
</tr>
<% end %>
<% if @model.collection %>
<tr>
<td><%= icon "collection", Collection.model_name.human(count: 100) %></td>
<td><%= link_to @model.collection.name, models_path({collection: @model.collection}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if SiteSettings.show_libraries || current_user&.is_administrator? %>
<tr>
<td><%= icon "boxes", Library.model_name.human %></td>
<td><%= link_to @model.library.name, models_path({library: @model.library}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if @model.license %>
<tr>
<td><%= icon "card-heading", t(".license") %></td>
<td>
<%= Spdx.licenses[@model.license]&.fetch("reference") ?
link_to(t_license(@model.license), Spdx.licenses[@model.license]["reference"], itemprop: "license") :
t_license(@model.license) %>
</td>
</tr>
<% end %>
<% if @model.sensitive %>
<tr>
<td><%= icon("explicit", Model.human_attribute_name(:sensitive)) %></td>
<td>
<%= Model.human_attribute_name(:sensitive) %>
</td>
</tr>
<% end %>
<tr>
<td><%= icon "folder", t(".path") %></td>
<td>
<%= content_tag(:code, class: "path") { safe_join @model.path.split("/"), safe_join([tag.wbr, "/"]) } %>
<% unless @model.contains_other_models? %>
<div><%= button_tag(t(".organize.button_text"), class: "btn btn-warning btn-sm", "data-bs-toggle": "modal", "data-bs-target": "#confirm-move") if @model.needs_organizing? && policy(@model).edit? %></div>
<% end %>
</td>
</tr>
<tr>
<td><%= icon "tag", t(".tags") %></td>
<td><%= render "tag_list", tags: @model.tags.order(taggings_count: :desc, name: :asc), filter: @filter %></td>
</tr>
<% if SiteSettings.social_enabled? %>
<tr>
<td><%= icon "people", t(".followers") %></td>
<td><%= t("general.followers", count: @model.followers.count) %></td>
</tr>
<% end %>
<tr>
<td><%= render Components::AccessIndicator.new(object: @model, text: false) %></td>
<td><%= render Components::AccessIndicator.new(object: @model, text: true, icon: false) %></td>
</tr>
</table>
<%= render Components::FollowButton.new(follower: current_user, target: @model) unless @model.remote? %>
<%= render Components::GoButton.new(icon: "pencil", label: t("general.edit"), href: edit_model_path(@model), variant: "primary") if policy(@model).edit? %>
<%= render Components::DoButton.new(icon: "trash", label: t("general.delete"), href: model_path(@model), method: :delete, variant: "outline-danger", confirm: translate("models.destroy.confirm")) if policy(@model).destroy? %>
<% end %>
<div class="mb-3 w-100 text-center">
<%= render Components::DownloadButton.new(model: @model) %>
</div>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>
<%= render Components::ReportButton.new(object: @model, path: new_model_report_path(@model)) %>
<% end if SiteSettings.multiuser_enabled? %>
<% if !@model.parents.empty? && policy(@model).merge? %>
<%= card :danger, t(".merge.heading") do %>
<p>
<%= t(".merge.warning") %>
</p>
<strong><%= t(".merge.with") %>:</strong>
<% @model.parents.each do |parent| %>
<%= render Components::DoButton.new(icon: "union", label: parent.name, href: merge_models_path(target: parent.public_id, models: [@model.public_id]), method: :post, variant: "danger") %>
<% end %>
<% end %>
<% end %>
<%= render partial: "problem", collection: @model.problems.visible(problem_settings) %>
<%= card :secondary, t(".files_card.heading") do %>
<a href="#files">Top</a>
<ul>
<% @groups.each_pair do |group, files| %>
<li><a href="#<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></li>
<% end %>
</ul>
<%= link_to t(".files_card.bulk_edit"), bulk_edit_model_model_files_path(@model), class: "btn btn-secondary" if policy(@model).edit? %>
<%= link_to t(".rescan"), scan_model_path(@model), class: "btn btn-warning", method: :post if policy(@model).scan? %>
<% end %>
<%= render "links_card", links: @model.links %>
<div class="modal fade" id="confirm-move" data-bs-backdrop='static' tabindex="-1" aria-labelledby="confirmMoveLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="confirmMoveLabel"><%= t(".organize.button_text") %></div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
<%= t(".organize.confirm.summary_html", from: @model.path, to: @model.formatted_path) %>
</p>
<p>
<%= t(".organize.confirm.are_you_sure") %>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".organize.confirm.no") %></button>
<%= button_to t(".organize.confirm.yes"), model_path(@model, "model[organize]": true), method: :patch, class: "btn btn-warning" %>
</div>
</div>
</div>
</div>
<% if policy(@model).upload? %>
<%= card :warning, t(".upload_card.heading") do %>
<%= form_with url: model_model_files_path(@model), id: "upload-form", class: "d-grid" do |form| %>
<%= content_tag :div, nil, class: "mb-3", data: {
controller: "upload",
action: "turbo:morph@window->upload#reconnect",
max_file_size: SiteSettings.max_file_upload_size,
allowed_file_types: input_accept_string,
upload_endpoint: upload_path
} %>
<%= submit_tag translate(".submit"), class: "btn btn-primary d-block" %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% if @model.creator %>
<tr>
<td><%= icon "person", Creator.model_name.human %></td>
<td><%= link_to @model.creator.name, @model.creator, itemprop: "author" %></td>
</tr>
<% end %>
<% if ENV['FEDERATE_DRIVE_HOST'].present? %>
<% if @model.tags.where(name: "janusxr" ).any? %>
<tr>
<td>
<i class="bi bi-eye-fill" role="img"></i>
</td>
<td>
<%
fediURL = ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/janusxr.html"
%>
<%= link_to "JanusXR address", fediURL, target:"_blank" %>
<a href="" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_janusxr"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_janusxr" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the JanusXR address.<br>
<a href="https://janusxr.org/" target="_blank">JanusXR</a> is an Open Immersive Web-layer since 2015, which allows existing webpages to become spatial.<br>
It is Free and Opensource, and allows you to meet others in this experience (avatars, chat and voice etc).<br>
</small>
</div>
</div>
</td>
</tr>
<%
# lets generate a JanusXR spatial layer for the current scene
jmlfile = @model.library.path+"/"+@model.path+"/.xrforge/scene.jml"
jml = ""
if File.exist?(jmlfile)
jml = File.read(jmlfile)
end
%>
<!-- JML (https://janusxr.org) spatial markup
<%== jml %>
-->
<% end %>
<% if @model.tags.where(name: "mml" ).any? %>
<tr>
<td>
<i class="bi bi-eye-fill" role="img"></i>
</td>
<td>
<%= link_to "MML address", "https://viewer.mml.io/main/v1/?url="+URI.encode_www_form_component(ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml"), target:"_blank" %>
<a href="<%= ENV['FEDERATE_DRIVE_HOST']+"/"+@model.library.name+"/"+@model.path.gsub("#","%23")+"/.xrforge/scene.mml" %>" target="_blank"><i class="bi bi-link-45deg"></i></a>
<label for="toggle_mml"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_mml" hidden>
<div class="hidden-tooltip">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
This is the MML address.<br>
Metaverse Markup Language (<a href="https://mml.io" target="_blank">MML</a>) is an open markup language used to define experiences.
</small>
</div>
</div>
</td>
</tr>
<% end %>
<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>
<% render 'models/tags_info' %>
</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 %>
<% if @model.collection %>
<tr>
<td><%= icon "collection", Collection.model_name.human(count: 100) %></td>
<td><%= link_to @model.collection.name, models_path({collection: @model.collection}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if SiteSettings.show_libraries || current_user&.is_administrator? %>
<tr>
<td><%= icon "boxes", Library.model_name.human %></td>
<td><%= link_to @model.library.name, models_path({library: @model.library}.merge(@filter&.to_params)) %></td>
</tr>
<% end %>
<% if @model.license %>
<tr>
<td><%= icon "card-heading", t(".license") %></td>
<td>
<%= Spdx.licenses[@model.license]&.fetch("reference") ?
link_to(t_license(@model.license), Spdx.licenses[@model.license]["reference"], itemprop: "license") :
t_license(@model.license) %>
</td>
</tr>
<% end %>
<% if @model.sensitive %>
<tr>
<td><%= icon("explicit", Model.human_attribute_name(:sensitive)) %></td>
<td>
<%= Model.human_attribute_name(:sensitive) %>
</td>
</tr>
<% end %>
<tr>
<td><%= icon "folder", t(".path") %></td>
<td>
<%= content_tag(:code, class: "path") { safe_join @model.path.split("/"), safe_join([tag.wbr, "/"]) } %>
<% unless @model.contains_other_models? %>
<div><%= button_tag(t(".organize.button_text"), class: "btn btn-warning btn-sm", "data-bs-toggle": "modal", "data-bs-target": "#confirm-move") if @model.needs_organizing? && policy(@model).edit? %></div>
<% end %>
</td>
</tr>
<tr>
<td><%= icon "tag", t(".tags") %></td>
<td><%= render "tag_list", tags: @model.tags.order(taggings_count: :desc, name: :asc), filter: @filter %>
<label for="toggle_tags"><i class="bi bi-info-circle"></i></label>
<div class="toggle-box">
<input type="checkbox" id="toggle_tags" hidden>
<div class="hidden-tooltip" style="max-height:400px">
<i class="bi bi-arrow-90deg-up"></i>&nbsp;
<small>
<% render 'models/tags_info' %>
</small>
</div>
</div>
</td>
</tr>
<% if SiteSettings.social_enabled? %>
<tr>
<td><%= icon "people", t(".followers") %></td>
<td><%= t("general.followers", count: @model.followers.count) %></td>
</tr>
<% end %>
<tr>
<td><%= render Components::AccessIndicator.new(object: @model, text: false) %></td>
<td><%= render Components::AccessIndicator.new(object: @model, text: true, icon: false) %></td>
</tr>
</table>
<%= render Components::FollowButton.new(follower: current_user, target: @model) unless @model.remote? %>
<%= render Components::GoButton.new(icon: "pencil", label: t("general.edit"), href: edit_model_path(@model), variant: "primary") if policy(@model).edit? %>
<%= render Components::DoButton.new(icon: "trash", label: t("general.delete"), href: model_path(@model), method: :delete, variant: "outline-danger", confirm: translate("models.destroy.confirm")) if policy(@model).destroy? %>
<% end %>
<div class="mb-3 w-100 text-center">
<%= render Components::DownloadButton.new(model: @model) %>
</div>
<%= card :secondary, t("layouts.card_list_page.actions_heading") do %>
<%= render Components::ReportButton.new(object: @model, path: new_model_report_path(@model)) %>
<% end if SiteSettings.multiuser_enabled? %>
<% if !@model.parents.empty? && policy(@model).merge? %>
<%= card :danger, t(".merge.heading") do %>
<p>
<%= t(".merge.warning") %>
</p>
<strong><%= t(".merge.with") %>:</strong>
<% @model.parents.each do |parent| %>
<%= render Components::DoButton.new(icon: "union", label: parent.name, href: merge_models_path(target: parent.public_id, models: [@model.public_id]), method: :post, variant: "danger") %>
<% end %>
<% end %>
<% end %>
<%= render partial: "problem", collection: @model.problems.visible(problem_settings) %>
<%= card :secondary, t(".files_card.heading") do %>
<a href="#files">Top</a>
<ul>
<% @groups.each_pair do |group, files| %>
<li><a href="#<%= group.parameterize %>"><%= group.strip.careful_titleize %>*</a></li>
<% end %>
</ul>
<%= link_to t(".files_card.bulk_edit"), bulk_edit_model_model_files_path(@model), class: "btn btn-secondary" if policy(@model).edit? %>
<%= link_to t(".rescan"), scan_model_path(@model), class: "btn btn-warning", method: :post if policy(@model).scan? %>
<% end %>
<%= render "links_card", links: @model.links %>
<div class="modal fade" id="confirm-move" data-bs-backdrop='static' tabindex="-1" aria-labelledby="confirmMoveLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="confirmMoveLabel"><%= t(".organize.button_text") %></div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
<%= t(".organize.confirm.summary_html", from: @model.path, to: @model.formatted_path) %>
</p>
<p>
<%= t(".organize.confirm.are_you_sure") %>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".organize.confirm.no") %></button>
<%= button_to t(".organize.confirm.yes"), model_path(@model, "model[organize]": true), method: :patch, class: "btn btn-warning" %>
</div>
</div>
</div>
</div>
<% if policy(@model).upload? %>
<%= card :warning, t(".upload_card.heading") do %>
<%= form_with url: model_model_files_path(@model), id: "upload-form", class: "d-grid" do |form| %>
<%= content_tag :div, nil, class: "mb-3", data: {
controller: "upload",
action: "turbo:morph@window->upload#reconnect",
max_file_size: SiteSettings.max_file_upload_size,
allowed_file_types: input_accept_string,
upload_endpoint: upload_path
} %>
<%= submit_tag translate(".submit"), class: "btn btn-primary d-block" %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>

View file

@ -76,6 +76,7 @@ rec
pkgs.pkgsStatic.inotify-tools # inotifywait e.g.
pkgs.pkgsStatic.zip # inotifywait e.g.
pkgs.pkgsStatic.ts # job management
pkgs.janus-gateway # webrtc server
myAssimp # cli 3D editing/conversion
./..
];