stable version: JML in description, multiple mastodon-tags supported
This commit is contained in:
parent
ec2d99f3de
commit
43ff366ca4
5 changed files with 251 additions and 145 deletions
|
|
@ -127,7 +127,8 @@ set_upload_path(){
|
||||||
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
|
test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH
|
||||||
add_lib_to_db $UPLOAD_PATH
|
add_lib_to_db $UPLOAD_PATH
|
||||||
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
|
id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';")
|
||||||
sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',replace('--- $id\n','\n',char(10);
|
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
|
||||||
|
sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',$id);
|
||||||
INSERT INTO sqlite_sequence VALUES('settings',6);"
|
INSERT INTO sqlite_sequence VALUES('settings',6);"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,8 +259,8 @@ init_database(){
|
||||||
set_global site_tagline "'$TAGLINE'"
|
set_global site_tagline "'$TAGLINE'"
|
||||||
set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))"
|
set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))"
|
||||||
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
|
set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))"
|
||||||
|
set_upload_path
|
||||||
#set_global about "$ABOUT"
|
#set_global about "$ABOUT"
|
||||||
set_upload_path &
|
|
||||||
get_xrfragment_assets
|
get_xrfragment_assets
|
||||||
mount_dir
|
mount_dir
|
||||||
BOOT_SCAN=1 scan_libraries &
|
BOOT_SCAN=1 scan_libraries &
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
require_relative './../../xrforge.rb'
|
||||||
|
|
||||||
|
# Check if a filename is provided
|
||||||
|
if ARGV.length != 1
|
||||||
|
puts "Usage: #{$0} <path/to/experience/datapackage.json>"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
filename = ARGV[0]
|
||||||
|
JMLHeuristic = /<fireboxroom>.*?<\/fireboxroom>/im
|
||||||
|
|
||||||
|
begin
|
||||||
|
|
||||||
|
# dont run for each file-update
|
||||||
|
if ! filename.end_with?("datapackage.json")
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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?('janusxr')
|
||||||
|
|
||||||
|
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']
|
||||||
|
|
||||||
|
autogenerate = true
|
||||||
|
|
||||||
|
if data['description'] && data['description'].match(JMLHeuristic)
|
||||||
|
if data['description'].match(/autogenerate=['"]false['"]/)
|
||||||
|
XRForge.log("✅ autogenerate='false' found in JML..keeping this JML")
|
||||||
|
autogenerate = false
|
||||||
|
jmlMatch = data['description'].match(JMLHeuristic)
|
||||||
|
jml = jmlMatch[0]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if autogenerate
|
||||||
|
jml = <<~JML
|
||||||
|
<FireBoxRoom>
|
||||||
|
<Assets>
|
||||||
|
<assetobject id="experience" src="#{federate_drive_host}/#{model_file.gsub("#","%23")}"/>
|
||||||
|
</Assets>
|
||||||
|
<Room autogenerate="true" #{ data['keywords'].include?('singleuser') ? "private='true'" : ""}>
|
||||||
|
<object pos="0 0 0" collision_id="experience" id="experience" />
|
||||||
|
</Room>
|
||||||
|
</FireBoxRoom>
|
||||||
|
<!-- archive.org hints -->
|
||||||
|
<a href="#{federate_drive_host}/#{model_file.gsub("#","%23")}"></a>
|
||||||
|
JML
|
||||||
|
|
||||||
|
data['description'] = data['description'] ? data['description'] : "your description here\n"
|
||||||
|
data['description'] = <<~DESCRIPTION#{data['description']}
|
||||||
|
<!-- Hi there! Below is autogenerated JanusXR Markup (JML). -->
|
||||||
|
<!-- If you want to tweak it, then first disable autogeneration -->
|
||||||
|
<!-- How? make sure to set autogenerate="false" (see room-tag below) -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- JML info: -->
|
||||||
|
<!-- https://coderofsalvation.github.io/janus-guide/#/examples/markup -->
|
||||||
|
<!-- https://janusxr.org/docs/build/introtojml/index.html -->
|
||||||
|
|
||||||
|
#{jml}
|
||||||
|
DESCRIPTION
|
||||||
|
File.write("datapackage.json", JSON.pretty_generate(data) )
|
||||||
|
end
|
||||||
|
|
||||||
|
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>
|
||||||
|
<!-- archive.org hints -->
|
||||||
|
<a href="#{federate_drive_host}/#{model_file.gsub("#","%23")}"></a>
|
||||||
|
</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("datapackage.json", JSON.pretty_generate(data) )
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
puts "File #{filename} not found"
|
||||||
|
rescue JSON::ParserError
|
||||||
|
puts "Error parsing JSON from #{filename}"
|
||||||
|
rescue => e
|
||||||
|
puts "An error occurred: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
@ -19,28 +19,11 @@ if ! filename.end_with?("datapackage.json")
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
def generate(aphandle,filename,dir,data,logfile)
|
||||||
# Change the directory
|
|
||||||
dir = File.dirname(filename)
|
|
||||||
Dir.chdir( dir )
|
|
||||||
# Read and parse the JSON file
|
|
||||||
data = JSON.parse( File.read( "datapackage.json" ) )
|
|
||||||
APHandle = false
|
|
||||||
|
|
||||||
data['keywords'].any? do |tag|
|
XRForge.log("✅ starting mastodon-post for #{aphandle}", logfile)
|
||||||
if tag.match?(/@.*@.*\./) # scan for activitypub handle (@foo@mastodon.online e.g.)
|
|
||||||
APHandle = tag
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ! APHandle # nothing to do
|
parts = aphandle.split("@")
|
||||||
exit 0
|
|
||||||
end
|
|
||||||
|
|
||||||
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
|
|
||||||
XRForge.log("✅ starting Mastodon post2image", logfile)
|
|
||||||
|
|
||||||
parts = APHandle.split("@")
|
|
||||||
server = parts[2].sub(/@/,"")
|
server = parts[2].sub(/@/,"")
|
||||||
rssUrl = "https://#{server}/@#{parts[1]}.rss"
|
rssUrl = "https://#{server}/@#{parts[1]}.rss"
|
||||||
XRForge.log("✅ checking #{rssUrl}", logfile)
|
XRForge.log("✅ checking #{rssUrl}", logfile)
|
||||||
|
|
@ -66,14 +49,29 @@ begin
|
||||||
puts description
|
puts description
|
||||||
|
|
||||||
# look for first image
|
# look for first image
|
||||||
MEDIA_REGEX = /<media:content url=['"]([^'"]+)['"]/
|
media_regex = /<media:content url=['"]([^'"]+)['"]/
|
||||||
img = feed.to_s.match(MEDIA_REGEX)
|
img = feed.to_s.match(media_regex)
|
||||||
if img and img[1] and img[1].match(/(png|jpg)/)
|
if img and img[1] and img[1].match(/(png|jpg)/)
|
||||||
imgurl = img[1]
|
imgurl = img[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
# generate the final .glb
|
# generate the final .glb
|
||||||
system("/root/templates/mastodon-post/mastodon-post.sh", description, feed.channel.title, feed.channel.link, APHandle, "#{dir}/mastodon-post.glb")
|
system("/root/templates/mastodon-post/mastodon-post.sh", description, feed.channel.title, feed.channel.link, aphandle, "#{dir}/#{aphandle}.glb")
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
# Change the directory
|
||||||
|
dir = File.dirname(filename)
|
||||||
|
Dir.chdir( dir )
|
||||||
|
# Read and parse the JSON file
|
||||||
|
data = JSON.parse( File.read( "datapackage.json" ) )
|
||||||
|
logfile = File.join( File.dirname(filename), ".xrforge/log.txt" )
|
||||||
|
|
||||||
|
data['keywords'].any? do |tag|
|
||||||
|
if tag.match?(/@.*@.*\./) # scan for activitypub handle (@foo@mastodon.online e.g.)
|
||||||
|
generate(tag,filename,dir,data,logfile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
XRForge.log(" ", logfile)
|
XRForge.log(" ", logfile)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,146 +14,96 @@ module XRForge
|
||||||
end
|
end
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 1. JSON (Ruby Hash) to XML Conversion
|
# 1. JSON (Ruby Hash) to XML String
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# Recursively builds REXML elements based on the input Hash/Array structure.
|
# Internal recursive helper for converting Hash/Array structure to REXML.
|
||||||
# It interprets keys starting with '@' as XML attributes.
|
# Handles attribute conversion (@key -> attribute), hash nesting, and arrays
|
||||||
# Arrays are processed as sequential child nodes.
|
# (which create multiple sibling elements of the same name).
|
||||||
#
|
#
|
||||||
# @param data [Hash, Array] The current portion of the data structure.
|
# @param data [Hash, Array, String] The data fragment.
|
||||||
# @param parent_rexml [REXML::Element] The parent REXML element to attach children/attributes to.
|
# @param p [REXML::Element] The parent REXML element.
|
||||||
def self.json2xml_recursive(data, parent_rexml)
|
def self.j2x_rec(data, p)
|
||||||
return if data.nil?
|
# Check if data is a Hash (for attributes and children)
|
||||||
|
if data.is_a?(Hash)
|
||||||
|
data.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_rec(v, p.add_element(k)) }
|
||||||
|
# Check if data is an Array (for repeated elements)
|
||||||
|
elsif data.is_a?(Array)
|
||||||
|
# The parent in this context (e.g., <obj>) is already created. For each array item,
|
||||||
|
# we need to create a new element with the same name as the current parent's *name*
|
||||||
|
# and attach it to the parent's *parent*.
|
||||||
|
# This requires looking up the parent's parent, which breaks terseness.
|
||||||
|
# We must use a simpler recursive structure for compactness.
|
||||||
|
|
||||||
# If the current value is an Array, we iterate over its items.
|
# We assume array structure requires us to create a new element named by the
|
||||||
# Each item is expected to define a new child element or a structure to recurse on.
|
# parent, and assign attributes/children recursively.
|
||||||
if data.is_a?(Array)
|
data.each { |v| j2x_rec(v, p) } # Recursive call to continue processing children
|
||||||
data.each do |item|
|
# Handle text content
|
||||||
# Recursively process each item in the array against the *current* parent.
|
else
|
||||||
json2xml_recursive(item, parent_rexml)
|
p.text = data.to_s
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# The data should be a Hash containing key-value pairs representing elements or attributes.
|
|
||||||
data.each do |key, value|
|
|
||||||
if key.start_with?('@')
|
|
||||||
# 1. Attribute: set on the parent REXML element (removing the leading '@')
|
|
||||||
parent_rexml.attributes[key[1..-1]] = value.to_s
|
|
||||||
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
||||||
# 2. Child Element: create a new element and recurse
|
|
||||||
new_element = parent_rexml.add_element(key)
|
|
||||||
json2xml_recursive(value, new_element)
|
|
||||||
else
|
|
||||||
# 3. Text Content: create a new element with primitive content (e.g., strings, numbers)
|
|
||||||
new_element = parent_rexml.add_element(key)
|
|
||||||
new_element.text = value.to_s
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Converts a Ruby Hash structure (JSON-like) into a compressed XML string.
|
# The entry point for converting Ruby Hash to XML string.
|
||||||
#
|
#
|
||||||
# @param data [Hash] The root hash structure.
|
# @param data [Hash] The root hash structure.
|
||||||
# @return [String] The resulting XML string.
|
# @return [String] The resulting compact XML string.
|
||||||
def self.json2xml(data)
|
def self.json2xml(data)
|
||||||
# Start with an empty REXML document
|
# Corrected recursive logic for arrays that are values (e.g., "obj" => [..])
|
||||||
doc = REXML::Document.new
|
j2x_map = lambda do |d, p_name, p|
|
||||||
|
if d.is_a?(Hash)
|
||||||
# Data must have a single root element (e.g., 'fireboxroom')
|
d.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_map.call(v, k, p.add_element(k)) }
|
||||||
data.each do |root_key, root_value|
|
elsif d.is_a?(Array)
|
||||||
root_element = REXML::Element.new(root_key)
|
# If the value is an array, we iterate, create a sibling element for each, and recurse
|
||||||
doc.add_element(root_element)
|
d.each { |v| j2x_map.call(v, p.name, p.parent.add_element(p.name)) }
|
||||||
json2xml_recursive(root_value, root_element)
|
p.remove # Remove the placeholder element created before hitting the array
|
||||||
# Since the input structure only defines one root key, we break after the first one.
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convert REXML document to XML string without the XML declaration
|
|
||||||
xml_output = ""
|
|
||||||
formatter = REXML::Formatters::Pretty.new(2) # indentlevel 2
|
|
||||||
|
|
||||||
# Write only the root element, ignoring the XML declaration
|
|
||||||
doc.root.write(xml_output, 0)
|
|
||||||
xml_output.strip
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# 2. XML to JSON (Ruby Hash) Conversion
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Recursively converts an REXML element into a Hash structure.
|
|
||||||
# Elements that appear multiple times become an Array in the Hash.
|
|
||||||
# Attributes are added with an '@' prefix.
|
|
||||||
#
|
|
||||||
# @param element [REXML::Element] The XML element to convert.
|
|
||||||
# @return [Hash, String] The resulting hash or a text string if the element has no children.
|
|
||||||
def self.xml2json_recursive(element)
|
|
||||||
hash = {}
|
|
||||||
|
|
||||||
# 1. Handle Attributes
|
|
||||||
element.attributes.each do |name, value|
|
|
||||||
hash["@#{name}"] = value
|
|
||||||
end
|
|
||||||
|
|
||||||
# 2. Handle Text Content
|
|
||||||
# If the element has text content but no children (e.g., <price>10.99</price>),
|
|
||||||
# return the text directly unless the hash already contains attributes.
|
|
||||||
if element.has_text?
|
|
||||||
text_content = element.get_text.value.strip
|
|
||||||
if !text_content.empty?
|
|
||||||
if element.elements.empty? && hash.empty?
|
|
||||||
# If it's pure text (no attributes, no children), return string value
|
|
||||||
return text_content
|
|
||||||
elsif element.elements.empty?
|
|
||||||
# If it has attributes but no children, the text is the '_content'
|
|
||||||
hash["#text"] = text_content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# 3. Handle Child Elements
|
|
||||||
element.elements.each do |child|
|
|
||||||
child_key = child.name
|
|
||||||
child_hash = xml2json_recursive(child)
|
|
||||||
|
|
||||||
if hash.key?(child_key)
|
|
||||||
# If key already exists, convert to array or append to array (standard convention)
|
|
||||||
if hash[child_key].is_a?(Array)
|
|
||||||
hash[child_key] << child_hash
|
|
||||||
else
|
|
||||||
hash[child_key] = [hash[child_key], child_hash]
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
# New key
|
p.text = d.to_s
|
||||||
hash[child_key] = child_hash
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Special handling to match the user's specific array format for the 'assets' node.
|
|
||||||
# This makes the output match the input test case, but is non-standard for general XML-JSON mappers.
|
|
||||||
if hash.key?("assets") && hash["assets"].is_a?(Hash) && hash["assets"].key?("asset")
|
|
||||||
asset_content = hash["assets"].delete("asset")
|
|
||||||
# Force the structure to be: [ {"asset" => asset_content} ]
|
|
||||||
hash["assets"] = [ {"asset" => asset_content} ]
|
|
||||||
end
|
|
||||||
|
|
||||||
hash
|
# Initialize Document and process the single root key
|
||||||
|
doc = REXML::Document.new
|
||||||
|
data.each { |k, v| j2x_map.call(v, k, doc.add_element(k)) }
|
||||||
|
|
||||||
|
# Output with 2-space indentation
|
||||||
|
o = ""; f = REXML::Formatters::Default.new; f.indentation = 2; f.write(doc.root, o); o.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
# Converts an XML string into a corresponding Ruby Hash (JSON-like) structure.
|
# ==============================================================================
|
||||||
|
# 2. XML String to JSON (Ruby Hash)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# Internal recursive helper for converting REXML Element to Hash structure.
|
||||||
|
# Handles attribute conversion (attribute -> @key), text, and array creation for siblings.
|
||||||
|
#
|
||||||
|
# @param e [REXML::Element] The XML element.
|
||||||
|
# @return [Hash, String] The resulting hash or simple text value.
|
||||||
|
def self.x2j_rec(e)
|
||||||
|
h = {}; e.attributes.each { |k, v| h["@#{k}"] = v } # Attributes first
|
||||||
|
|
||||||
|
e.elements.each do |c| # Iterate children
|
||||||
|
v = x2j_rec(c)
|
||||||
|
if h.key?(c.name)
|
||||||
|
h[c.name] = [h[c.name]] unless h[c.name].is_a?(Array)
|
||||||
|
h[c.name] << v
|
||||||
|
else
|
||||||
|
h[c.name] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for text content if no children were processed
|
||||||
|
t = e.get_text.to_s.strip; return t if t && !t.empty? && h.empty?; return h
|
||||||
|
end
|
||||||
|
|
||||||
|
# The entry point for converting XML string to Ruby Hash.
|
||||||
#
|
#
|
||||||
# @param xmlstr [String] The XML string.
|
# @param xmlstr [String] The XML string.
|
||||||
# @return [Hash] The resulting Ruby hash structure.
|
# @return [Hash] The resulting Ruby hash structure.
|
||||||
def self.xml2json(xmlstr)
|
def self.xml2json(xmlstr)
|
||||||
doc = REXML::Document.new(xmlstr)
|
doc = REXML::Document.new(xmlstr)
|
||||||
root_element = doc.root
|
root = doc.root
|
||||||
return {} unless root_element
|
{ root.name => x2j_rec(root) }
|
||||||
|
end
|
||||||
# The final output is a hash containing the single root element and its content.
|
|
||||||
{ root_element.name => xml2json_recursive(root_element) }
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,14 @@ input[type="checkbox"]:checked + .hidden-tooltip {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#model_notes {
|
||||||
|
min-height: 50vh;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #CCF;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-form-label{
|
||||||
|
width:111px;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue