diff --git a/dist/xrfragment.plugin.remotestorage.js b/dist/xrfragment.plugin.remotestorage.js new file mode 100644 index 0000000..c70faef --- /dev/null +++ b/dist/xrfragment.plugin.remotestorage.js @@ -0,0 +1,293 @@ +/* + * v0.5.1 generated at Thu May 15 04:48:16 PM CEST 2025 + * https://xrfragment.org + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +(function(){ +// this demonstrates the remotestorage aframe component + +// reactive component for displaying the menu +remoteStorageComponent = (el) => new Proxy({ + + html: (data) => (` + + +
+
+
+ +
+ `), + + connected: false, + $listing: false, + $links: false, + + init(opts){ + // create HTML element + $files.tabs = $files.tabs.concat({id:"remoteFiles", name: "remote storage"}) + el.innerHTML = this.html(this) + el.className = "tab" + document.querySelector("#files .tab-frame").appendChild(el); + + // setup references + this.$listing = document.querySelector('#files .tab-frame select#listing'); + this.$links = document.querySelector('#files .tab-frame #links'); + + // setup input listeners + (['click']).map( (e) => el.addEventListener(e, (ev) => typeof this[e] == 'function' && this[e](ev.target.id,ev) ) ) + + // signal ready + setTimeout( () => { + document.dispatchEvent( new CustomEvent("remotestorage:ready", {detail: {$files:this,xrf}}) ) + },100) + + this.loadScript( () => this.initRemoteStorage() ) + + return this + }, + + loadScript(cb){ + let el = document.createElement("script") + el.setAttribute("defer","") + el.src = "https://unpkg.com/remotestoragejs@2.0.0-beta.7/release/remotestorage.js" + document.head.appendChild(el) + + el = document.createElement("script") + el.src = "https://unpkg.com/remotestorage-widget@1.6.0/build/widget.js" + el.addEventListener('load', () => setTimeout(cb,2000) ) + document.head.appendChild(el) + }, + + initRemoteStorage(){ + let apis = { + dropbox: "4jc8nx1lbarp472" + } + const modules = [] + if( typeof WebXR != undefined ){ + modules.push(WebXR) // defined in remotestorage-module-webXRF.js + } + window.remoteStorage = new RemoteStorage({logging: true, modules }) + if( Object.keys(apis).length ) remoteStorage.setApiKeys(apis) + + remoteStorage.on('not-connected', (e) => { this.connected = false }) + remoteStorage.on('ready', (e) => { }) + remoteStorage.on('connected', (e) => { + this.connected = true + // force open dialog and click remote-tab + frontend.$topbar.toggle(true) + $files.toggle(true) + document.querySelector("#files input#remoteFiles").click() + + this.updateFiles() + }) + + remoteStorage.access.claim( `webxr`, 'rw'); // our data dir + remoteStorage.caching.enable( `/webxr/` ) // local-first, remotestorage-second + remoteStorage.caching.enable( `/public/webxr/` ) // local-first, remotestorage-second + + // create widget + let opts = {} + opts.modalBackdrop = false + opts.leaveOpen = true + widget = new window.Widget(window.remoteStorage, opts) + widget.attach( "rswidget" ); + + }, + + savePrivate(){ + frontend.download( (data,filename) => { + filename = prompt('save-as filename', filename) + remoteStorage.webxr.add(data,{public:false,filename,mimetype: 'model/glb-binary'}) + .then( () => window.notify(`saved webxr/${filename} to remote storage`) ) + .catch( (e) => { + console.error(e) + window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'}) + }) + }) + }, + + savePublic(){ + frontend.download( (data,filename) => { + filename = prompt('save-as filename', filename) + opts = {public:true,filename,mimetype: 'model/glb-binary'} + remoteStorage.webxr.add(data,opts) + .then( (res) => { + window.notify(`saved webxr/${filename} to remote storage`) + const link = opts.client.storage.remote.href + opts.client.base + filename + const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `?${link}` + this.$links.querySelector("#file").value = link + this.$links.querySelector("#webviewer").value = linkWebView + this.$links.style.display = 'block' + }) + .catch( (e) => { + console.error(e) + window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'}) + }) + }) + }, + + openPrivate(file){ + if( !confirm(`teleport to ${file} on your remotestorage?`) ) return + remoteStorage.webxr.getFile(file) + .then( (res) => { + for( var i in xrf.loaders ){ + if( file.replace(/.*\./).match(i) ){ + xrf.navigator.URI.file = '' // bypass cached file (easy refresh same file for testing) + xrf.navigator.to(file,null, (new xrf.loaders[i]()), res.data) + return + } + } + throw 'unknown filetype: '+file + }) + .catch( (e) => { + console.error(e) + window.notify("could not load webxr/"+file) + }) + }, + + remove(){ + const currentFile = el.querySelector('select#listing').value + if( confirm("remove "+currentFile+" from remote storage?") ){ + remoteStorage.webxr.remove(currentFile) + .then( () => { + window.notify(`removed webxr/${filename} from remote storage`) + this.updateFiles() + }) + .catch( (e) => { + console.error(e) + window.notify(`could not webxr/${filename} from remote storage`,{status:'error'}) + }) + } + }, + + updateFiles(){ + remoteStorage.webxr.getListing() + .then( (listing) => { + + this.$listing.innerHTML = '' // empty + + const addOption = (value,text) => { + let opt = document.createElement("option") + opt.text = text + opt.value = value + this.$listing.appendChild(opt) + } + + addOption("","--- your experiences ---") + for( let file in listing ){ + if( file.match(/\.(glb|gltf|usd|obj|col|fbx)$/) ) addOption(file,file) + } + + // autoload selection + if( !this.updateFiles.autoload ){ // run once + this.$listing.addEventListener('change', () => { + if( this.$listing.options.selectedIndex > 0 ) this.openPrivate(this.$listing.value) + document.querySelector("#delete").style.display = this.$listing.options.selectedIndex == 0 ? "none" : "inline-block" + this.$links.style.display = 'none' + }) + this.updateFiles.autoload = true + } + }) + + }, + + click(id,e){ + //switch(id){ + // case "more": return this.toggle(); break; + //} + } +}, +{ + + get(me,k,v){ return me[k] }, + + set(me,k,v){ + me[k] = v + switch( k ){ + case 'connected': el.querySelector("#buttons").style.display = v ? 'block' : 'none'; break; + } + }, + +}) + +// reactify component! +document.addEventListener('$files:ready', (e) => { + window.$remotestorage = remoteStorageComponent( document.createElement('div') ).init(e.detail) +}) +const WebXR = { name: 'webxr', builder: function(privateClient, publicClient) { + return { + exports: { + add: function(data,opts) { + if( !data || !opts.filename || !opts.mimetype) throw 'webxr.add() needs filedata + filename + mimetype' + const client = opts.client = opts.public ? publicClient : privateClient; + return client.storeFile(opts.mimetype, opts.filename,data) + }, + + getListing: function(a,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getListing(a) + }, + + getFile: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getFile(file) + }, + + remove: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.remove(file) + } + + } + } +}}; +}).apply({}) diff --git a/src/3rd/js/aframe/build/three.module.js b/src/3rd/js/aframe/build/three.module.js deleted file mode 100644 index 177242c..0000000 --- a/src/3rd/js/aframe/build/three.module.js +++ /dev/null @@ -1,35 +0,0 @@ -import * as SUPER_THREE from 'super-three'; -import { DRACOLoader } from 'super-three/examples/jsm/loaders/DRACOLoader'; -import { GLTFLoader } from 'super-three/examples/jsm/loaders/GLTFLoader'; -import { KTX2Loader } from 'super-three/examples/jsm/loaders/KTX2Loader'; -import { OBB } from 'super-three/addons/math/OBB.js'; -import { OBJLoader } from 'super-three/examples/jsm/loaders/OBJLoader'; -import { FBXLoader } from 'super-three/examples/jsm/loaders/FBXLoader'; -import { USDZLoader } from 'super-three/examples/jsm/loaders/USDZLoader'; -import { ColladaLoader } from 'super-three/examples/jsm/loaders/ColladaLoader'; -import { MTLLoader } from 'super-three/examples/jsm/loaders/MTLLoader'; -import * as BufferGeometryUtils from 'super-three/examples/jsm/utils/BufferGeometryUtils'; -import { LightProbeGenerator } from 'super-three/examples/jsm/lights/LightProbeGenerator'; -import { TransformControls } from 'super-three/examples/jsm/controls/TransformControls.js'; -import { GLTFExporter } from 'super-three/examples/jsm/exporters/GLTFExporter.js'; - -var THREE = window.THREE = SUPER_THREE; - -// TODO: Eventually include these only if they are needed by a component. -require('../../vendor/DeviceOrientationControls'); // THREE.DeviceOrientationControls -THREE.DRACOLoader = DRACOLoader; -THREE.GLTFLoader = GLTFLoader; -THREE.KTX2Loader = KTX2Loader; -THREE.OBJLoader = OBJLoader; -THREE.MTLLoader = MTLLoader; -THREE.FBXLoader = FBXLoader; -THREE.USDZLoader = USDZLoader; -THREE.ColladaLoader = ColladaLoader; -THREE.OBB = OBB; -THREE.BufferGeometryUtils = BufferGeometryUtils; -THREE.LightProbeGenerator = LightProbeGenerator; -THREE.TransformControls = TransformControls; -THREE.GLTFExporter = GLTFExporter || console.error("GLTFExporter not found"); -//THREE.Text = Text - -export default THREE; diff --git a/src/3rd/js/plugin/frontend/$chat.js b/src/3rd/js/plugin/frontend/$chat.js index 5d6e2e6..83bcf32 100644 --- a/src/3rd/js/plugin/frontend/$chat.js +++ b/src/3rd/js/plugin/frontend/$chat.js @@ -434,6 +434,7 @@ chatComponent.css = ` margin-right:15px; width:100%; max-width:40%; + margin-bottom:7px; } .envelope, diff --git a/src/3rd/js/plugin/frontend/$files.js b/src/3rd/js/plugin/frontend/$files.js index 96aa734..6887f8a 100644 --- a/src/3rd/js/plugin/frontend/$files.js +++ b/src/3rd/js/plugin/frontend/$files.js @@ -20,7 +20,7 @@ filesComponent = (el) => new Proxy({
- +
${data.tabs.map( (t) => @@ -29,8 +29,8 @@ filesComponent = (el) => new Proxy({ ` ).join('') } -


-
+

+

diff --git a/src/3rd/js/plugin/frontend/css.js b/src/3rd/js/plugin/frontend/css.js index 5dd51b7..a44c976 100644 --- a/src/3rd/js/plugin/frontend/css.js +++ b/src/3rd/js/plugin/frontend/css.js @@ -364,8 +364,9 @@ document.head.innerHTML += ` opacity:0.5 } - body.menu .js-snackbar__wrapper { - top: 64px; + body.menu .js-snackbar__wrapper, + body.topbar .js-snackbar__wrapper { + transform: translateY(40px); } .transcript{ diff --git a/src/3rd/js/plugin/frontend/frontend.js b/src/3rd/js/plugin/frontend/frontend.js index 995d189..5faf924 100644 --- a/src/3rd/js/plugin/frontend/frontend.js +++ b/src/3rd/js/plugin/frontend/frontend.js @@ -43,7 +43,8 @@ window.frontend = (opts) => new Proxy({ .setupCapture() .setupUserHints() .setupNetworkListeners() - .hidetopbarWhenMenuCollapse() + .setupTopbar() + .hideTopbarWhenMenuCollapse() .hideUIWhenNavigating() window.notify = this.notify @@ -68,6 +69,16 @@ window.frontend = (opts) => new Proxy({ return this }, + setupTopbar(){ + // setup topbar handle + this.$topbar = this.el.querySelector("#topbar") + this.$topbar.toggle = (state) => { + this.$topbar.style.display = state ? 'block' : 'none' + document.body.classList[ state ? 'add' : 'remove' ](['topbar']) + } + return this + }, + setupIframeUrlHandler(){ // allow iframe to open url window.addEventListener('message', (event) => { @@ -175,9 +186,9 @@ window.frontend = (opts) => new Proxy({ return this }, - hidetopbarWhenMenuCollapse(){ + hideTopbarWhenMenuCollapse(){ // hide topbar when menu collapse button is pressed - document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none') + document.addEventListener('$menu:collapse', (e) => this.$topbar.toggle( e.detail === true ) ) return this }, @@ -250,9 +261,9 @@ window.frontend = (opts) => new Proxy({ window.frontend.emit("notify",opts) }, - download(){ + download(cb){ - function exportScene(model,ext,file){ + function exportScene(model,ext,file,cb){ document.dispatchEvent( new CustomEvent('frontend.export',{detail:{ scene: model.scene,ext}}) ) xrf.emit('export', {scene: model.scene, ext}) @@ -266,15 +277,17 @@ window.frontend = (opts) => new Proxy({ return false; } + if( !cb ) cb = download + // setup exporters let defaultExporter = THREE.GLTFExporter if( !xrf.loaders['gltf'].exporter ) xrf.loaders['gltf'].exporter = defaultExporter if( !xrf.loaders['glb'].exporter ) xrf.loaders['glb'].exporter = defaultExporter const exporter = new xrf.loaders[ext].exporter() - debugger + exporter.parse( model.scene, - function ( glb ) { download(glb, `${file}`) }, // ready + function ( glb ) { cb(glb, `${file}`) }, // ready function ( error ) { console.error(error) }, // error { binary:true, @@ -293,9 +306,15 @@ window.frontend = (opts) => new Proxy({ const Loader = xrf.loaders[fileExt] loader = new Loader().setPath( dir ) notify('exporting scene

please wait..') - loader.load(url, (model) => { - exportScene(model,fileExt,file) - }, console.error ) + + fetch(url, { method: 'HEAD' }) + .then( (res) => { // url exists + if( res.ok ){ + loader.load( url, (model) => exportScene(model,fileExt,file,cb), console.error ) + }else{ + exportScene(xrf.model,fileExt,file,cb) + } + }) }, updateHashPosition(randomize){ diff --git a/src/3rd/js/plugin/remotestorage/remotestorage-module-webXR.js b/src/3rd/js/plugin/remotestorage/remotestorage-module-webXR.js new file mode 100644 index 0000000..3728eda --- /dev/null +++ b/src/3rd/js/plugin/remotestorage/remotestorage-module-webXR.js @@ -0,0 +1,30 @@ +const WebXR = { name: 'webxr', builder: function(privateClient, publicClient) { + return { + exports: { + add: function(data,opts) { + if( !data || !opts.filename || !opts.mimetype) throw 'webxr.add() needs filedata + filename + mimetype' + const client = opts.client = opts.public ? publicClient : privateClient; + return client.storeFile(opts.mimetype, opts.filename,data) + }, + + getListing: function(a,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getListing(a) + }, + + getFile: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.getFile(file) + }, + + remove: function(file,opts){ + opts = opts || {} + const client = opts.public ? publicClient : privateClient; + return client.remove(file) + } + + } + } +}}; diff --git a/src/3rd/js/plugin/remotestorage/remotestorage-module-webXRF.js b/src/3rd/js/plugin/remotestorage/remotestorage-module-webXRF.js deleted file mode 100644 index 2b11695..0000000 --- a/src/3rd/js/plugin/remotestorage/remotestorage-module-webXRF.js +++ /dev/null @@ -1,7 +0,0 @@ -const WebXRF = { name: 'webxr', builder: function(privateClient, publicClient) { - return { - exports: { - addScene: function() {} - } - } -}}; diff --git a/src/3rd/js/plugin/remotestorage/remotestorage.js b/src/3rd/js/plugin/remotestorage/remotestorage.js index c277557..2a5315e 100644 --- a/src/3rd/js/plugin/remotestorage/remotestorage.js +++ b/src/3rd/js/plugin/remotestorage/remotestorage.js @@ -14,33 +14,68 @@ remoteStorageComponent = (el) => new Proxy({ #files .rs-button{ background:#CCC; } + #files input { + min-width:345px; + } + #files #buttons select, + #files #buttons button{ + width:255px; + max-width:unset; + } + #files #listing{ + margin-bottom:15px; + } + #files #delete{ + display: none; + transform: translate(10px, 5px); + } + #links{ + display:none + }

`), connected: false, + $listing: false, + $links: false, init(opts){ // create HTML element - $files.tabs = $files.tabs.concat({id:"remoteFiles", name: "online"}) + $files.tabs = $files.tabs.concat({id:"remoteFiles", name: "remote storage"}) el.innerHTML = this.html(this) el.className = "tab" document.querySelector("#files .tab-frame").appendChild(el); + // setup references + this.$listing = document.querySelector('#files .tab-frame select#listing'); + this.$links = document.querySelector('#files .tab-frame #links'); + // setup input listeners (['click']).map( (e) => el.addEventListener(e, (ev) => typeof this[e] == 'function' && this[e](ev.target.id,ev) ) ) - + // signal ready setTimeout( () => { document.dispatchEvent( new CustomEvent("remotestorage:ready", {detail: {$files:this,xrf}}) ) @@ -68,21 +103,27 @@ remoteStorageComponent = (el) => new Proxy({ dropbox: "4jc8nx1lbarp472" } const modules = [] - if( typeof WebXRF != undefined ){ - modules.push(WebXRF) // defined in remotestorage-module-webXRF.js + if( typeof WebXR != undefined ){ + modules.push(WebXR) // defined in remotestorage-module-webXRF.js } window.remoteStorage = new RemoteStorage({logging: true, modules }) if( Object.keys(apis).length ) remoteStorage.setApiKeys(apis) - remoteStorage.on('connected', (e) => { this.connected = true }) - //remoteStorage.on('network-offline', (e) => this.el.sceneEl.emit('remoteStorage.network-offline',e) ) - //remoteStorage.on('network-online', (e) => this.el.sceneEl.emit('remoteStorage.network-online',e) ) - //remoteStorage.on('error', (e) => this.el.sceneEl.emit('remoteStorage.error',e) ) - remoteStorage.on('ready', (e) => { } ) + remoteStorage.on('not-connected', (e) => { this.connected = false }) + remoteStorage.on('ready', (e) => { }) + remoteStorage.on('connected', (e) => { + this.connected = true + // force open dialog and click remote-tab + frontend.$topbar.toggle(true) + $files.toggle(true) + document.querySelector("#files input#remoteFiles").click() - remoteStorage.access.claim( `webxr`, 'rw'); // our data dir - remoteStorage.caching.enable( `/webxr/` ) // local-first, remotestorage-second - remoteStorage.caching.enable( `/public/webxr/` ) // local-first, remotestorage-second + this.updateFiles() + }) + + remoteStorage.access.claim( `webxr`, 'rw'); // our data dir + remoteStorage.caching.enable( `/webxr/` ) // local-first, remotestorage-second + remoteStorage.caching.enable( `/public/webxr/` ) // local-first, remotestorage-second // create widget let opts = {} @@ -93,6 +134,103 @@ remoteStorageComponent = (el) => new Proxy({ }, + savePrivate(){ + frontend.download( (data,filename) => { + filename = prompt('save-as filename', filename) + remoteStorage.webxr.add(data,{public:false,filename,mimetype: 'model/glb-binary'}) + .then( () => window.notify(`saved webxr/${filename} to remote storage`) ) + .catch( (e) => { + console.error(e) + window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'}) + }) + }) + }, + + savePublic(){ + frontend.download( (data,filename) => { + filename = prompt('save-as filename', filename) + opts = {public:true,filename,mimetype: 'model/glb-binary'} + remoteStorage.webxr.add(data,opts) + .then( (res) => { + window.notify(`saved webxr/${filename} to remote storage`) + const link = opts.client.storage.remote.href + opts.client.base + filename + const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `?${link}` + this.$links.querySelector("#file").value = link + this.$links.querySelector("#webviewer").value = linkWebView + this.$links.style.display = 'block' + }) + .catch( (e) => { + console.error(e) + window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'}) + }) + }) + }, + + openPrivate(file){ + if( !confirm(`teleport to ${file} on your remotestorage?`) ) return + remoteStorage.webxr.getFile(file) + .then( (res) => { + for( var i in xrf.loaders ){ + if( file.replace(/.*\./).match(i) ){ + xrf.navigator.URI.file = '' // bypass cached file (easy refresh same file for testing) + xrf.navigator.to(file,null, (new xrf.loaders[i]()), res.data) + return + } + } + throw 'unknown filetype: '+file + }) + .catch( (e) => { + console.error(e) + window.notify("could not load webxr/"+file) + }) + }, + + remove(){ + const currentFile = el.querySelector('select#listing').value + if( confirm("remove "+currentFile+" from remote storage?") ){ + remoteStorage.webxr.remove(currentFile) + .then( () => { + window.notify(`removed webxr/${filename} from remote storage`) + this.updateFiles() + }) + .catch( (e) => { + console.error(e) + window.notify(`could not webxr/${filename} from remote storage`,{status:'error'}) + }) + } + }, + + updateFiles(){ + remoteStorage.webxr.getListing() + .then( (listing) => { + + this.$listing.innerHTML = '' // empty + + const addOption = (value,text) => { + let opt = document.createElement("option") + opt.text = text + opt.value = value + this.$listing.appendChild(opt) + } + + addOption("","--- your experiences ---") + for( let file in listing ){ + if( file.match(/\.(glb|gltf|usd|obj|col|fbx)$/) ) addOption(file,file) + } + + // autoload selection + if( !this.updateFiles.autoload ){ // run once + this.$listing.addEventListener('change', () => { + if( this.$listing.options.selectedIndex > 0 ) this.openPrivate(this.$listing.value) + document.querySelector("#delete").style.display = this.$listing.options.selectedIndex == 0 ? "none" : "inline-block" + this.$links.style.display = 'none' + }) + this.updateFiles.autoload = true + } + }) + + }, + click(id,e){ //switch(id){ // case "more": return this.toggle(); break;