Compare commits

..

1 commit

Author SHA1 Message Date
1ebb8b32ab feat/remotestorage
All checks were successful
/ test (push) Successful in 6s
2025-04-25 16:18:40 +02:00
20 changed files with 1229 additions and 1231 deletions

View file

@ -4,8 +4,11 @@ AFRAME.registerComponent('cast', {
},
requires: {
dom: "com/dom.js",
window: "com/window.js",
dom: "./com/dom.js", // interpret .dom object
xd: "./com/xd.js", // allow switching between 2D/3D
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
},
dom: {
@ -20,7 +23,7 @@ AFRAME.registerComponent('cast', {
init: function () { },
etInstallables: function(){
getInstallables: function(){
const installed = document.querySelector('[launcher]').components.launcher.system.getLaunchables()
return this.data.comps.map( (c) => {
return installed[c] ? null : c
@ -34,54 +37,77 @@ AFRAME.registerComponent('cast', {
if( this.el.sceneEl.renderer.xr.isPresenting ){
this.el.sceneEl.exitVR() // *FIXME* we need a gui
}
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
// instance this component
this.el.setAttribute("dom","")
},
DOMready: function(){
this.setupCast();
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.uid}; faceuser: true`)
},
const el = document.querySelector('body');
const cropTarget = await CropTarget.fromElement(el);
const stream = await navigator.mediaDevices.getDisplayMedia();
const [track] = stream.getVideoTracks();
this.track = track
this.stream = stream
this.createWindow()
}
},
setupCast: async function(){
createWindow: async function(){
let s = await AFRAME.utils.require(this.requires)
const video = this.el.dom.querySelector('video')
// instance this component
const instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
instance.setAttribute("dom", "")
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
instance.setAttribute("grabbable","")
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
instance.track = this.track
instance.stream = this.stream
video.addEventListener( "loadedmetadata", () => {
const setupWindow = () => {
instance.dom.style.display = 'none'
let width = Math.round(window.innerWidth*0.4)
let factor = width / video.videoWidth
let height = Math.round( video.videoHeight * factor)
const video = instance.dom.querySelector('video')
video.addEventListener( "loadedmetadata", function () {
let width = Math.round(window.innerWidth*0.4)
let factor = width / this.videoWidth
let height = Math.round(this.videoHeight * factor)
new WinBox("Casting Tab",{
width,
height,
x:"center",
y:"center",
id: instance.uid, // important hint for html-mesh
root: document.querySelector("#overlay"),
mount: instance.dom,
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => {
const createVideoTexture = () => {
const texture = new THREE.VideoTexture( video );
texture.colorSpace = THREE.SRGBColorSpace;
const geometry = new THREE.PlaneGeometry( 16, 9 );
geometry.scale( 0.2, 0.2, 0.2 );
const material = new THREE.MeshBasicMaterial( { map: texture } );
const mesh = new THREE.Mesh( geometry, material );
mesh.lookAt( AFRAME.scenes[0].camera.position );
this.el.object3D.add(mesh)
}
// instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
}
});
instance.dom.style.display = '' // show
createVideoTexture.apply(this)
//this.el.setAttribute("window", `title: casting tab; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:${width}; height: ${height}`)
})
// hint grabbable's obb-collider to track the window-object
instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
instance.components['obb-collider'].update()
})
video.srcObject = instance.stream
video.play()
const el = document.querySelector('body');
const cropTarget = await CropTarget.fromElement(el);
const stream = await navigator.mediaDevices.getDisplayMedia();
const [track] = stream.getVideoTracks();
this.track = track
this.stream = stream
video.srcObject = this.stream
video.play()
this.createVideoTexture.apply(instance)
}
setTimeout( () => setupWindow(), 10 ) // give new components time to init
},
createVideoTexture: function(){
console.dir(this)
const texture = new THREE.VideoTexture( video );
texture.colorSpace = THREE.SRGBColorSpace;
const geometry = new THREE.PlaneGeometry( 16, 9 );
geometry.scale( 0.2, 0.2, 0.2 );
const material = new THREE.MeshBasicMaterial( { map: texture } );
const mesh = new THREE.Mesh( geometry, material );
mesh.lookAt( this.sceneEl.camera.position );
this.object3D.add(mesh)
},
manifest: { // HTML5 manifest to identify app to xrsh
@ -90,7 +116,6 @@ AFRAME.registerComponent('cast', {
"icons": [
{
"src": "https://css.gg/cast.svg",
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNMjAgNkg0VjhIMlY2QzIgNC44OTU0MyAyLjg5NTQzIDQgNCA0SDIwQzIxLjEwNDYgNCAyMiA0Ljg5NTQzIDIyIDZWMThDMjIgMTkuMTA0NiAyMS4xMDQ2IDIwIDIwIDIwSDE1VjE4SDIwVjZaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGgKICAgIGQ9Ik0yIDEzQzUuODY1OTkgMTMgOSAxNi4xMzQgOSAyMEg3QzcgMTcuMjM4NiA0Ljc2MTQyIDE1IDIgMTVWMTNaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTIgMTdDMy42NTY4NSAxNyA1IDE4LjM0MzEgNSAyMEgyVjE3WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBkPSJNMiA5QzguMDc1MTMgOSAxMyAxMy45MjQ5IDEzIDIwSDExQzExIDE1LjAyOTQgNi45NzA1NiAxMSAyIDExVjlaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+Cjwvc3ZnPg==",
"type": "image/svg+xml",
"sizes": "512x512"
}

View file

@ -50,9 +50,9 @@ if( !AFRAME.components.dom ){
this
.ensureOverlay()
.addCSS()
.createReactiveDOMElement()
.assignUniqueID()
.addCSS()
.scaleDOMvsXR()
.triggerKeyboardForInputs()

View file

@ -41,8 +41,7 @@ AFRAME.registerComponent('helloworld-window', {
-->
</div>`,
css: (me) => `.htmlform { padding:11px; }
`
css: (me) => `.htmlform { padding:11px; }`
},
@ -68,20 +67,15 @@ AFRAME.registerComponent('helloworld-window', {
myvalue: function(e){ this.el.dom.querySelector('#myvalue').innerText = this.data.myvalue },
launcher: async function(){
if( !this.el.getAttribute("dom") ){
let s = await AFRAME.utils.require(this.requires)
let s = await AFRAME.utils.require(this.requires)
// instance this component
this.el.setAttribute("dom", "")
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
}else{
// toggle visibility
this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
}
// instance this component
this.el.setAttribute("dom", "")
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
},
DOMready: function(){
this.el.setAttribute("window", `title: XRSH; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; class: no-min, no-max; width:250; height: 360`)
this.el.setAttribute("window", `title: XRSH; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:250; height: 360`)
// data2event demo
this.el.setAttribute("data2event","")
@ -89,21 +83,6 @@ AFRAME.registerComponent('helloworld-window', {
this.data.foo = `this.el ${this.el.uid}: `
setInterval( () => this.data.myvalue++, 500 )
// if you want your launch-icon to stick around after
// closing the window, then register it manually
window.launcher.register({
component: "helloworld-window",
...this.manifest,
icon: this.manifest.icons[0].src,
cb: () => {
const el = document.createElement("a-entity")
el.setAttribute("helloworld-window","")
el.setAttribute("pressable","")
el.setAttribute("position","0 1.6 0")
AFRAME.scenes[0].appendChild(el)
setTimeout( () => el.emit('launcher',null,false), 100 )
}
})
},
"window.oncreate": function(){

View file

@ -13,7 +13,6 @@
*
* | property | type | default | info |
* |-------------------|-----------|------------------------|------|
* | `title` | `string` | 'xrsh.iso' | window title |
* | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
* | `overlayfs` | `string` | '' | zip URL/file to autoextract on top of filesystem |
* | `width` | `number` | 800 ||
@ -73,7 +72,6 @@ if( typeof AFRAME != 'undefined '){
schema: {
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"},
title: { type:"string", "default":"xrsh.iso"},
width: { type: 'number',"default": 800 },
height: { type: 'number',"default": 600 },
depth: { type: 'number',"default": 0.03 },
@ -144,8 +142,6 @@ if( typeof AFRAME != 'undefined '){
.isoterminal{
padding: ${me.com.data.padding}px;
margin-top:-60px;
padding-bottom:60px;
width:100%;
height:99%;
resize: both;
@ -248,19 +244,18 @@ if( typeof AFRAME != 'undefined '){
-webkit-animation:none;
}
.winbox#${me.el.uid} .wb-header{
background: var(--xrsh-black) !important;
}
.wb-body:has(> .isoterminal){
background: var(--xrsh-black);
.wb-body:has(> .isoterminal){
background: #000C;
overflow:hidden;
}
.XR .isoterminal{
background: #000 !important;
.XR .wb-body:has(> .isoterminal){
background: transparent;
}
.XR .isoterminal{
background: #000;
}
.isoterminal *{
font-size: 14px;
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
@ -309,20 +304,15 @@ if( typeof AFRAME != 'undefined '){
remotekeyboard: "com/isoterminal/feat/remotekeyboard.js",
indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.js",
autorestore: "com/isoterminal/feat/autorestore.js",
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
httpfs: "com/isoterminal/feat/httpfs.js",
autorestore: "com/isoterminal/feat/autorestore.js",
}
if( document.location.hash.match(/#test/) || this.data.debug ){
features['tests'] = "tests/index.js"
}
if( this.data.emulator == 'fbterm' ){
features['fbtermjs'] = "com/isoterminal/term.js"
features['fbterm'] = "com/isoterminal/feat/term.js"
}
await AFRAME.utils.require(features)
// this one extends autorestore.js
await AFRAME.utils.require({remotestorage: "com/isoterminal/feat/remotestorage.js"})
this.el.setAttribute("selfcontainer","")
@ -347,7 +337,7 @@ if( typeof AFRAME != 'undefined '){
this.term.emit('term_init', {instance, aEntity:this})
//instance.winbox.resize(720,380)
let size = `width: ${this.data.width}; height: ${this.data.height}`
instance.setAttribute("window", `title: ${this.data.title}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-min, no-close, no-max, no-resize; grabbable: components.html.el.object3D.children.${this.el.children.length}`)
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize; grabbable: components.html.el.object3D.children.${this.el.children.length}`)
})
instance.addEventListener('window.oncreate', (e) => {
@ -375,6 +365,7 @@ if( typeof AFRAME != 'undefined '){
this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink')
this.term.emit('status',"running")
if( this.data.debug ) this.runTests()
})
this.term.addEventListener('status', function(e){
@ -394,23 +385,6 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.onmaximize', resize )
const focus = (e) => {
if( event.target == document.activeElement ){
// if we're already focused, the keyboard is already triggerend
// therefore we blur() the element for correctness
// which allows the WebXR keyboard dissappear if implemented at all
// [Meta Quest 2 does not do this]
event.target.blur()
}
// calculate distance between thumb and indexfinger to detect pinch
// which should prevent focus-event (annoying to have keyboard popping up during pinch)
if( e.detail?.withEl?.components['hand-tracking-controls'] ){
const hand = e.detail.withEl.components['hand-tracking-controls']
const thumb = hand.bones.find( (b) => b.name == 'thumb-tip' )
const diff = thumb.position.distanceTo(hand.indexTipPosition)
if( diff < 0.02) return // pinching! don't trigger keyboard (focus)
}
this.el.emit('focus',e.detail)
}
@ -429,6 +403,14 @@ if( typeof AFRAME != 'undefined '){
}
},
runTests: async function(){
await AFRAME.utils.require({
"test_util": "tests/util.js",
"test_isoterminal":"tests/ISOTerminal.js"
})
console.test.run()
},
setupPasteDrop: function(){
this.el.addEventListener('pasteFile', (e) => {
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
@ -452,10 +434,9 @@ if( typeof AFRAME != 'undefined '){
this.el.addEventListener('exec', (e) => this.term.exec( e.detail ) )
this.el.addEventListener('hook', (e) => this.term.hook( e.detail[0], e.detail[1] ) )
this.el.addEventListener('send', (e) => this.term.send( e.detail[0], e.detail[1] || 0 ) )
this.el.addEventListener('create_file', async (e) => await this.term.worker.create_file( e.detail[0], e.detail[1] ) )
this.el.addEventListener('create_file_from_url', async (e) => await this.term.worker.create_file_from_url( e.detail[0], e.detail[1] ) )
this.el.addEventListener('update_file', async (e) => await this.term.worker.update_file( e.detail[0], e.detail[1] ) )
this.el.addEventListener('append_file', async (e) => await this.term.worker.append_file( e.detail[0], e.detail[1] ) )
this.el.addEventListener('create_file', async (e) => await this.term.worker.create_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('update_file', async (e) => await this.term.worker.update_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('append_file', async (e) => await this.term.worker.append_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('read_file', async (e) => {
const buf = await this.term.worker.read_file( e.detail[0] )
const str = new TextDecoder().decode(buf)
@ -503,7 +484,6 @@ if( typeof AFRAME != 'undefined '){
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
/*
{
"name": "What is the latest news?",
"cli":{
@ -518,9 +498,8 @@ if( typeof AFRAME != 'undefined '){
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
*/
],
"description": "Runs an .iso file",
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
@ -530,7 +509,7 @@ if( typeof AFRAME != 'undefined '){
}
],
"help":`
XRSH application
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain

View file

@ -18,17 +18,14 @@ function ISOTerminal(instance,opts){
ISOTerminal.prototype.emit = function(event,data,sender){
data = data || false
const evObj = new CustomEvent(event, {detail: data} )
// forward event to worker/instance/AFRAME element or component-function
// this feels complex, but actually keeps event- and function-names more concise in codebase
let fire = () => {
this.preventFrameDrop( () => {
// forward event to worker/instance/AFRAME element or component-function
// this feels complex, but actually keeps event- and function-names more concise in codebase
this.dispatchEvent( evObj )
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) )
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
}
if( event.match(/^serial/) ){
this.preventFrameDrop( fire )
}else fire()
})
}
ISOTerminal.addEventListener = (event,cb) => {
@ -40,18 +37,20 @@ ISOTerminal.addEventListener = (event,cb) => {
ISOTerminal.prototype.exec = function(opts){
const shellscript = opts[0];
const cb = opts[1];
let cmd = `printf "\n\r"; { sh<<EOF\n ${shellscript}; \nEOF\n} &> /mnt/exec;\n`
console.log(cmd)
if( cb ){
window.cb = cb
cmd += `js 'document.querySelector("[isoterminal]").emit("read_file", ["exec", window.cb ])';\n`
this.send(`printf "\n\r"; ${shellscript} &> /mnt/exec; js '
document.querySelector("[isoterminal]").emit("read_file", ["exec", window.cb ])
';
\n`,1)
}else{
this.send(`printf "\n\r"; ${shellscript} \n`,1)
}
this.send( cmd, 1 )
}
ISOTerminal.prototype.hook = function(hookname,args){
let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`
this.exec([cmd])
this.exec(cmd)
}
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
@ -142,14 +141,8 @@ ISOTerminal.prototype.start = function(opts){
},
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
net_device:{
//type:"ne2k",
relay_url:"fetch"
//relay_url:"wss://relay.widgetry.org/"
//local_http: true,
//type:"virtio",
//relay_url:"fetch",
//cors_proxy: "https://corsproxy.io/"
relay_url:"fetch", // or websocket proxy "wss://relay.widgetry.org/",
type:"virtio"
},
//bzimage_initrd_from_filesystem: true,
//filesystem: {
@ -266,7 +259,6 @@ ISOTerminal.prototype.startVM = function(opts){
this.v86opts = opts
this.addEventListener('emulator-started', async (e) => {
if( this.boot.fromImage ) return this.emit('serial-output-string', "\r[!] downloading session...please wait\n\r[!] this could take a while depending on your connection..\n\r")
let line = ''
this.ready = false
@ -289,17 +281,12 @@ ISOTerminal.prototype.startVM = function(opts){
}
ISOTerminal.prototype.bootISO = function(){
const getImage = (str) => decodeURIComponent(
str.match(/\&img=/) ? str.replace(/.*img=/,'').replace(/\&.*/,'') : ''
)
let msglib = this.getLoaderMsg()
this.emit('status',msglib.loadmsg)
let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset
this.emit('serial-output-string', msg)
if( getImage(this.boot.hash) ){
this.boot.fromImage = true
}
this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency, img: getImage(this.boot.hash) })
this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency })
}

View file

@ -44,8 +44,7 @@ function PromiseWorker(file, onmessage){
this.resolvers = this.resolvers || {last:1,pending:{}}
msg.data.promiseId = this.resolvers.last++
// Send id and task to WebWorker
let dataTransferable = PromiseWorker.prototype.getTransferable(msg.data)
worker.postMessage(msg, dataTransferable )
worker.postMessage(msg, PromiseWorker.prototype.getTransferable(msg.data) )
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
},
@ -73,5 +72,6 @@ PromiseWorker.prototype.getTransferable = function(data){
for( var i in data ){
if( isTransferable(data[i]) ) objs.push(data[i])
}
if( objs.length ) debugger
return objs.length ? objs : undefined
}

View file

@ -28,20 +28,6 @@ emulator.fs9p.update_file = async function(file,data){
}
}
emulator.fs9p.create_file_from_url = async function(file,url){
const convert = ISOTerminal.prototype.convert
return fetch(url)
.then( (res) => res.arrayBuffer() )
.then( (buf) => {
let arr = new Uint8Array(buf)
return emulator.create_file(file, arr )
})
.catch( (e) => {
emulator.create_file(file, new Uint8Array() ) // empty file so at least other processes can check for error (v86 has no retcodes for fs9p)
console.error(e)
})
}
emulator.fs9p.append_file = async function(file,data){
const convert = ISOTerminal.prototype.convert

View file

@ -1,16 +1,8 @@
// this is restoring state to/from the v86 emulator
// however instead of passing the huge blob between webworker/browser
// we transfer it via localforage as a base64 string
if( typeof emulator != 'undefined' ){
// inside worker-thread
importScripts("localforage.js") // we don't instance it again here (just use its functions)
this.restore_state = async function(data){
// fastforward instance state
this.opts.muteUntilPrompt = false
this.ready = true
return new Promise( (resolve,reject) => {
localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err ){
@ -23,20 +15,15 @@ if( typeof emulator != 'undefined' ){
})
}
this.save_state = async function(){
return new Promise( async (resolve,reject ) => {
console.log("saving session")
let state = await emulator.save_state()
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
])
.then( () => {
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
console.log("state saved")
resolve()
})
.catch( reject )
console.log("saving session")
let state = await emulator.save_state()
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
}
@ -45,44 +32,42 @@ if( typeof emulator != 'undefined' ){
// inside browser-thread
ISOTerminal.addEventListener('emulator-started', function(e){
this.autorestore(e)
this.emit("autorestore-installed")
})
ISOTerminal.prototype.restore = async function(e){
ISOTerminal.prototype.autorestore = async function(e){
const onGetItem = (err,stateBase64) => {
const askConfirm = () => {
if( window.localStorage.getItem("restorestate") == "true" ) return true
try{
const scene = document.querySelector('a-scene');
if( scene.is('ar-mode') ) scene.exitAR()
if( scene.is('vr-mode') ) scene.exitVR()
}catch(e){}
return confirm( "Continue old session?" )
}
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
if( stateBase64 && !err && document.location.hash.length < 2 && askConfirm() ){
this.noboot = true // see feat/boot.js
try{
this.worker.restore_state()
.then( () => {
localforage.getItem("state", async (err,stateBase64) => {
const askConfirm = () => {
if( window.localStorage.getItem("restorestate") == "true" ) return true
try{
const scene = document.querySelector('a-scene');
if( scene.is('ar-mode') ) scene.exitAR()
if( scene.is('vr-mode') ) scene.exitVR()
}catch(e){}
return confirm('continue last session?')
}
if( stateBase64 && !err && document.location.hash.length < 2 && askConfirm() ){
this.noboot = true // see feat/boot.js
try{
await this.worker.restore_state()
// simulate / fastforward boot events
this.postBoot( () => {
// force redraw terminal issue
this.send("l")
setTimeout( () => this.send("l"), 200 )
//this.send("12")
this.emit("exec",["source /etc/profile.sh; hook wakeup\n"])
this.emit("restored")
this.send("l\n")
this.send("hook wakeup\n")
})
})
}catch(e){ console.error(e) }
}catch(e){ console.error(e) }
}
})
this.save = async () => {
await this.worker.save_state()
}
}
const doRestore = () => {
localforage.getItem("state", (err,stateBase64) => onGetItem(err,stateBase64) )
window.addEventListener("beforeunload", function (e) {
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
@ -90,17 +75,7 @@ if( typeof emulator != 'undefined' ){
return confirmationMessage; //Webkit, Safari, Chrome
});
}
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
])
.then( () => doRestore() )
})
}
ISOTerminal.prototype.autorestore = ISOTerminal.prototype.restore // alias to launch during boot
}

View file

@ -1,5 +1,5 @@
ISOTerminal.addEventListener('ready', function(e){
setTimeout( () => this.boot(), 50 ) // allow other features/plugins to settle first (autorestore.js e.g.)
setTimeout( () => this.boot(), 50 ) // because of autorestore.js
})
ISOTerminal.prototype.bootMenu = function(e){
@ -17,13 +17,13 @@ ISOTerminal.prototype.bootMenu = function(e){
}else{ // autoboot
if( this.term ){
this.term.handler( String(e.detail.bootMenu || e.detail.bootMenuURL).charAt(0) )
this.term.handler( e.detail.bootMenu || e.detail.bootMenuURL )
this.term.handler("\n")
}
}
}
ISOTerminal.addEventListener('bootMenu', function(e){this.bootMenu(e) })
ISOTerminal.addEventListener('bootMenu', function(e){ this.bootMenu(e) } )
ISOTerminal.prototype.boot = async function(e){
// set environment
@ -33,17 +33,10 @@ ISOTerminal.prototype.boot = async function(e){
'export BROWSER=1',
]
for ( let i in document.location ){
if( typeof document.location[i] == 'string' && !String(i).match(/(hash|search)/) ){
if( typeof document.location[i] == 'string' ){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
}
}
// we export the cached hash/query (because they might be gone due to remotestorage plugin)
if( this.boot.hash.charAt(2) == '&' ){ // strip bootoption
this.boot.hashExBoot = `#` + this.boot.hash.substr(3)
}
env.push( 'export HASH="'+decodeURIComponent( this.boot.hashExBoot || this.boot.hash ) +'"' )
env.push( 'export QUERY="'+decodeURIComponent( this.boot.query ) +'"' )
await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
if( this.serial_input == 0 ){
@ -54,12 +47,8 @@ ISOTerminal.prototype.boot = async function(e){
}
ISOTerminal.prototype.boot.fromImage = false
ISOTerminal.prototype.boot.menu = []
ISOTerminal.prototype.boot.hash = document.location.hash
ISOTerminal.prototype.boot.query = document.location.search
// here REPL's can be defined
ISOTerminal.prototype.boot.menu = []
// REPL: iso
if( typeof window.PromiseWorker != 'undefined' ){ // if xrsh v86 is able to run in in worker

View file

@ -19,39 +19,16 @@ if( typeof emulator != 'undefined' ){
}else{
// inside browser-thread
/*
* here we're going to execute javascript in the browser,
* both a terrible and great idea depending on a corporate vs personal computing angle.
*
* if a function-string gets evaluated, and it returns a promise..then we assume async.
*/
ISOTerminal.addEventListener('javascript-eval', async function(e){
const {script,PID} = e.detail
let res;
let error = false;
const output = (res,PID) => {
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){
this.worker.update_file(`run/${PID}.exit`, error ? "1" : "0")
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
}
}
try{
let f = new Function(`${script}`);
res = f();
if( res && typeof res.then == 'function' ){ // if we got a promise
res.then( (res) => output(res,PID) )
res.catch( (e) => output(e,PID) )
}else{ // normal sync function
output(res,PID)
}
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
}catch(err){
error = true
console.error(err)
console.dir(err)
res = "error: "+err.toString()
@ -64,7 +41,10 @@ if( typeof emulator != 'undefined' ){
res += script.split("\n")[lnr-1]
}else console.dir(script)
console.error(res)
output(res,PID)
}
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
}
})

View file

@ -1,8 +1,8 @@
ISOTerminal.prototype.redirectConsole = function(handler){
const log = console._log = console.log;
const dir = console._dir = console.dir;
const err = console._error = console.error;
const warn = console._warn = console.warn;
const log = console.log;
const dir = console.dir;
const err = console.error;
const warn = console.warn;
const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\r\n") : str
console.log = (...args)=>{
@ -37,13 +37,12 @@ ISOTerminal.prototype.enableConsole = function(opts){
let _str = typeof str == 'string' ? str : JSON.stringify(str)
let finalStr = "";
prefix = prefix ? prefix+' ' : ''
String(_str).trim().split("\n").map( (line) => {
_str.trim().split("\n").map( (line) => {
finalStr += `${opts.stdout ? '' : "\x1b[38;5;165m/dev/browser: \x1b[0m"}`+prefix+line+'\n'
})
if( opts.stdout ){
this.emit('serial-output-string', finalStr, "worker")
this.emit('serial-output-string', finalStr)
}else this.emit('append_file', ["/dev/browser/console",finalStr])
this.lastStr = finalStr
})
window.addEventListener('error', function(event) {

View file

@ -0,0 +1,36 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
}else{
// inside browser-thread
//
ISOTerminal.prototype.remoteStorageInit = async function(opts){
await AFRAME.utils.require({"remoteStorageWidget": "com/lib/aframe-remotestorage.min.js"})
let el = document.querySelector("[remotestorage]")
if( el ){
let value = el.getAttribute("remotestorage")
el.removeAttribute("remotestorage")
el.setAttribute("remotestorage", value) // (re)activate aframe component
this.remoteStorageMount()
}
}
ISOTerminal.prototype.remoteStorageGet = function(){
return document.querySelector('[remotestorage]')?.components.remotestorage.remoteStorage
}
ISOTerminal.prototype.remoteStorageMount = function(){
const rs = this.remoteStorageGet()
rs.addEventListener('connected', function(){
})
rs.addEventListener('not-connected', function(){
})
}
ISOTerminal.addEventListener('init', function(){
this.addEventListener('term_init', (opts) => this.remoteStorageInit(opts.detail) )
})
}

View file

@ -1,365 +0,0 @@
/* Remote storage feature
*
* NOTE: this feature extends the localstorage functions.
* this file can be excluded without crippling the
* core localstorage mechanism.
*/
// see https://remotestorage.io/rs.js/docs/data-modules/
ISOTerminal.prototype.remoteStorageModule = {
name: 'xrsh',
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)
}
}
}
}
};
// this is the HTML which we append to the remotestorage widget:
// https://remotestorage.io/rs.js/docs/getting-started/connect-widget.html
const _rs_widget_html = `
<div class="form" id="remote">
<select class="remote" id="states"></select>
<button id="save">save</button>
&nbsp;
<a class="btn remote" href="https://inspektor.5apps.com/?path=xrsh%2F" style="background:#888" target="_blank">filemanager</a>
<div id="result"></div>
</div>
<div class="form" id="local" style="position:relative">
<hr>
<div class="header">WebBrowser session:</div>
<div style="text-align:right">
<button id="saveLocal">save</button>
<button id="restoreLocal">restore</button>
</div>
</div>
<!-- disabled for now -->
<div class="form" id="local" style="position:relative; display:none">
<hr>
<div class="header">File session:</div>
<div style="text-align:right">
<button id="saveLocalFile">save file</button>
<button id="restoreLocalFile">import file</button>
</div>
</div>
<style type="text/css">
#remotestorage-widget{
position:fixed;
top:0;
right:0;
}
#remotestorage-widget h1,
#remotestorage-widget h2,
#remotestorage-widget h3,
#remotestorage-widget h4,
#remotestorage-widget h5{
color:#222;
}
#remotestorage-widget .rs-widget{
margin:25px;
}
#remotestorage-widget .rs-closed .form{
display:none !important;
}
#remotestorage-widget .form select#states{
padding: 5px;
margin-top: 10px;
margin-right: 10px;
border-radius: 5px;
}
#remotestorage-widget .form #result{
padding-top:10px;
}
#remotestorage-widget .form select,
#remotestorage-widget .form button,
#remotestorage-widget .form a{
height:35px;
margin-bottom:0;
}
#remotestorage-widget *{
font-size:15px;
}
#remotestorage-widget div.header{
position:absolute;
display:inline;
top:15px;
left:0;
font-weight:bold;
}
#remotestorage-widget .form .btn {
text-decoration: none;
color: white;
margin-top: 10px;
margin-bottom: 10px;
display: inline-block;
line-height: 15px;
}
#remotestorage-widget .form{
margin-top:10px;
}
#remotestorage-widget .btn:hover,
#remotestorage-widget button:hover {
background:#CCC;
}
</style>
`
const widgetForm = function(opts){
let el = document.querySelector("#remotestorage-widget select#states")
const $widget = document.querySelector("#remotestorage-widget .rs-widget")
const $result = document.querySelector("#remotestorage-widget #result")
if( !el ){
let div = document.createElement('div')
div.innerHTML = _rs_widget_html
$widget.appendChild(div)
$widget.querySelector("h3").innerText = "Click here to connect your storage"
el = $widget.querySelector("select#states")
el.addEventListener('change', () => {
if( el.options.selectedIndex == 0 ) return; // ignore default option
this.remoteStorage.widget.filename = el.value
opts.status = "loading..."
$widget.classList.add(["blink"])
this.remoteStorage.xrsh.getFile(el.value)
.then( (data,err) => {
this.restore({remotestorage:true, data:data.data})
})
})
this.setupStorageListeners($widget,opts)
}
this.updateStorageForm(el,$widget,$result,opts)
}
ISOTerminal.prototype.remoteStorageWidget = function(){
if( this.remoteStorageWidget.installed ) return // only do once
let apis = {
//dropbox: "ce876ce",
//googledrive: "c3983c3"
}
const modules = [ this.remoteStorageModule ]
const remoteStorage = this.remoteStorage = new RemoteStorage({logging: true, modules })
remoteStorage.setApiKeys(apis)
remoteStorage.on('not-connected', (e) => {
this.remoteStorage.connected = false
this.remoteStorage.widget.form({show:false})
})
remoteStorage.on('ready', (e) => { })
remoteStorage.on('connected', (e) => {
this.remoteStorage.connected = true
this.remoteStorage.widget.form({update:true,show:true})
})
remoteStorage.access.claim( `xrsh`, 'rw'); // our data dir
remoteStorage.caching.enable( `/xrsh/` ) // local-first, remotestorage-second
remoteStorage.caching.enable( `/public/xrsh/` ) // local-first, remotestorage-second
// create widget
let opts = {}
opts.modalBackdrop = false
opts.leaveOpen = false
const widget = remoteStorage.widget = new window.Widget(remoteStorage, opts)
widget.attach();
widget.form = widgetForm.bind(this)
this.remoteStorageWidget.installed = true
}
ISOTerminal.prototype.setupStorageListeners = function($widget,opts){
this.addEventListener("ready", () => {
opts.loading = false
opts.status = "session loaded"
})
// setup events
$widget.querySelector("button#save").addEventListener("click", () => {
this.save({remotestorage:true})
})
$widget.querySelector("button#saveLocal").addEventListener("click", async () => {
if( confirm("save current session?") ){
$widget.classList.add(['blink'])
await this.worker.save_state()
$widget.classList.remove(['blink'])
alert("succesfully saved session")
}
})
$widget.querySelector("button#restoreLocal").addEventListener("click", async () => {
this.restore({localstorage:true})
})
$widget.querySelector("button#saveLocalFile").addEventListener("click", () => {
alert("saveFile")
})
$widget.querySelector("button#restoreLocalFile").addEventListener("click", () => {
alert("saveFile")
})
$widget.addEventListener("click", () => this.remoteStorage.widget.open() )
this.addEventListener("restored", () => this.remoteStorage.widget.form({loading:false, status:"session restored"}) )
}
ISOTerminal.prototype.updateStorageForm = function(el,$widget,$result,opts){
if( typeof opts.show != 'undefined' ){
$widget.querySelector('.form#remote').style.display = opts.show ? 'block' : 'none'
}
if( typeof opts.status != 'undefined'){
$result.innerText = opts.status
}
if( opts.public ){
const link = this.remoteStorage.remote.href + '/public/xrsh/' + this.remoteStorage.widget.filename
const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `#1&img=${link}`
$result.innerHTML += `<br><a class="btn" href="${linkWebView}" target="_blank">public weblink</a>`
}
if( typeof opts.loading != 'undefined' ){
$widget.classList[ opts.loading ? 'add' : 'remove' ](["blink"])
}
if( opts.update ){
const createOption = (opt) => {
let option = document.createElement("option")
option.value = opt.value
option.innerText = opt.name || opt.value
el.appendChild(option)
}
el.innerHTML = ''
createOption({value:"-- snapshots --"})
this.remoteStorage.xrsh.getListing()
.then( (data,err) => {
for( let file in data ){
createOption({value:file})
}
})
}
}
const requireFiles = () => AFRAME.utils.require({remotestorageWidget:"assets/aframe-remotestorage.min.js"})
const getForage = () => localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
])
// we extend the autorestore feature at the init-event
ISOTerminal.addEventListener('init', function(e){
const decorateSave = () => {
let localSave = this.save
this.save = async (opts) => {
requireFiles()
.then( getForage )
.then( async () => {
if( opts.localstorage ){
this.save.localstorage(opts) // default behaviour
}
this.remoteStorageWidget()
if( opts.remotestorage ){
this.remoteStorage.widget.open() // force open dialog
let filename = String(this.remoteStorage.widget.filename || "snapshot").replace(/\.bin/,'')
filename = prompt("please name your snapshot:", filename )
filename = filename.replace(/\.bin*/,'') + '.bin'
// make it public or not (disabled for now as it does not work flawlessly)
const public = false // confirm('create a public link?')
this.remoteStorage.widget.filename = filename
this.remoteStorage.widget.form({loading:true,status: "saving.."})
await this.worker.save_state()
localforage.getItem("state", async (err,stateBase64) => {
this.remoteStorage.xrsh.add( stateBase64, {filename, mimetype: 'application/x-v86-base64', public})
.then( () => {
console.log("saved to remotestorage")
setTimeout(
() => this.remoteStorage.widget.form({update:true, loading:false, status:"saved succesfully", public})
,500 )
})
.catch( console.error )
})
}
})
}
this.save.localstorage = localSave
}
const decorateRestore = () => {
let localRestore = this.restore
this.restore = async (opts) => {
requireFiles()
.then( getForage )
.then( async () => {
document.querySelector("#remotestorage-widget .rs-widget").classList.add(['blink'])
if( opts.localstorage ){
this.restore.localstorage.apply(this,opts) // default behaviour
}
if( opts.remotestorage ){
localforage.setItem( "state", opts.data )
.then( () => this.restore.localstorage.call(this,opts) ) // now trigger default behaviour
}
})
}
this.restore.localstorage = localRestore
this.autorestore = this.restore // reroute automapping
}
// decorate save() and restore() of autorestore.js after it's declared (hence the setTimeout)
this.addEventListener('autorestore-installed', () => {
decorateSave()
decorateRestore()
})
})
// decorate autorestore
const autorestoreLocalStorage = ISOTerminal.prototype.autorestore;
ISOTerminal.prototype.autorestore = function(data){
requireFiles().then( async () => {
this.remoteStorageWidget()
})
}

View file

@ -25,6 +25,12 @@ ISOTerminal.prototype.TermInit = function(){
// patch Term-class
Term.prototype.move_textarea = function(){} /* *TODO* *FIXME* does not work in winbox */
Term.prototype.pasteHandler = function(original){
return function (ev){
original.apply(this,[ev])
}
}( Term.prototype.pasteHandler )
Term.prototype.keyDownHandler = function(original){
return function (e){
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -3,7 +3,7 @@ importScripts("ISOTerminal.js") // we don't instance it again here (just use it
this.runISO = async function(opts){
this.opts = opts
if( opts.debug ) console.dir(opts)
if( opts.debug ) console.dir(opts)
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url
if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url
@ -40,7 +40,6 @@ this.runISO = async function(opts){
emulator.add_listener("emulator-started", function(){
importScripts("feat/9pfs_utils.js")
this.postMessage({event:"emulator-started",data:false});
if( opts.img ) this.restoreImage(opts)
}.bind(this));
/*
@ -52,12 +51,11 @@ this.runISO = async function(opts){
arr[0] = String(arr[0]).replace(/^\/mnt/,'')
return arr
}
this.create_file = async function(){ return emulator.create_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.create_file_from_url = async function(){ return emulator.fs9p.create_file_from_url.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, stripMountDir(arguments[0]) ) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
this.create_file = async function(){ return emulator.create_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, stripMountDir(arguments[0]) ) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
// filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){
@ -68,12 +66,13 @@ this.runISO = async function(opts){
})
}
importScripts("feat/javascript.js")
importScripts("feat/index.html.js")
importScripts("feat/autorestore.js")
if( opts.overlayfs ) await this.addOverlayFS(opts)
}
/*
* forward events/functions so non-worker world can reach them
@ -114,21 +113,3 @@ this.addOverlayFS = async function(opts){
}
})
}
this.restoreImage = function(opts){
this.postMessage({event:"serial0-output-string",data:`\n\r[!] loading session image:\n\r[!] ${opts.img}\n\r[!] please wait...internet speed matters here..\n`})
fetch( opts.img )
.then( (res) => {
this.postMessage({event:"serial0-output-string",data:`\r[v] image downloaded..restoring..\n\r`})
return res
})
.then( (res) => res.arrayBuffer() )
.then( async (buf) => {
await this.emulator.restore_state(buf)
this.postMessage({event:"serial0-output-string",data:`[v] restored\n\r`})
})
.catch( console.error )
}

View file

@ -3,9 +3,6 @@
*
* displays app (icons) in 2D and 3D handmenu (enduser can launch desktop-like 'apps')
*
* They can be temporary processes (icon disappears after component is removed) or permanent icons
* when registered via .register({...})
*
* ```html
* <a-entity launcher>
* <a-entity launch="component: helloworld; foo: bar"><a-entity>
@ -102,7 +99,6 @@ AFRAME.registerComponent('launcher', {
flex-direction: row;
align-items: flex-start;
height: 50px;
width:764px;
overflow:hidden;
position: fixed;
bottom: 10px;
@ -311,8 +307,6 @@ AFRAME.registerSystem('launcher',{
register: function(launchable){
try{
let {name, description, cb} = launchable
const exist = this.registered.find( (r) => r.manifest.name == name )
if( exist ) return // already registered
this.registered.push({
manifest: {name, description, icons: launchable.icon ? [{src:launchable.icon}] : [] },
launcher: cb
@ -343,10 +337,7 @@ AFRAME.registerSystem('launcher',{
];
// collect manually registered launchables
this.registered.map( (launchable) => {
if( launchable.component ) seen[ launchable.component ] = true
this.launchables.push(launchable)
})
this.registered.map( (launchable) => this.launchables.push(launchable) )
// collect launchables in aframe dom elements
this.dom = els.filter( (el) => {
@ -355,11 +346,8 @@ AFRAME.registerSystem('launcher',{
for( let i in el.components ){
if( el.components[i].events && el.components[i].events[searchEvent] && !seen[i] ){
let com = hasEvent = seen[i] = el.components[i]
let alreadyAdded = this.launchables.find( (l) => l.manifest.name == com.manifest.name )
if( !alreadyAdded ){
com.launcher = () => com.el.emit('launcher',null,false) // important: no bubble
this.launchables.push({manifest: com.manifest, launcher: com.launcher})
}
com.launcher = () => com.el.emit('launcher',null,false) // important: no bubble
this.launchables.push(com)
}
}
}

416
com/lib/aframe-remotestorage.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -53,6 +53,16 @@ AFRAME.registerComponent('window', {
setupWindow: async function(){
await AFRAME.utils.require(this.dependencies)
if( !this.el.dom ) return console.error('window element requires dom-component as dependency')
const close = () => {
let e = {halt:false}
this.el.emit('window.onclose',e)
if( e.halt ) return true
this.data.dom.style.display = 'none';
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
this.data.dom.parentElement.remove()
return false
}
this.el.addEventListener('close', () => {
close()
this.el.winbox.close()
@ -81,12 +91,8 @@ AFRAME.registerComponent('window', {
this.el.components['obb-collider'].data.trackedObject3D = this.data.grabbable
this.el.components['obb-collider'].update()
},1000)
this.patchButtons(e)
},
onminimize: this.onminimize,
onrestore: this.onrestore,
onclose: this.onclose.bind(this),
onclose: close
});
this.data.dom.style.display = '' // show
@ -106,46 +112,7 @@ AFRAME.registerComponent('window', {
show: function(state){
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
},
onminimize: function(e){
if( AFRAME.scenes[0].renderer.xr.isPresenting ){
this.window.style.display = 'none'
}
},
onrestore: function(e){
if( AFRAME.scenes[0].renderer.xr.isPresenting ){
this.window.style.display = ''
}
},
onclose: function(){
let e = {halt:false}
this.el.emit('window.onclose',e)
if( e.halt ) return true
this.data.dom.style.display = 'none';
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
this.data.dom.parentElement.remove()
return false
},
// the buttons don't work in XR because HTMLMesh does not understand onclick on divs
patchButtons: function(e){
let wEl = e.mount;
let controls = [...wEl.closest(".winbox").querySelectorAll(".wb-control span")]
controls.map( (c) => {
if( c.className.match(/wb-(close|min)/) ){
let btn = document.createElement("button")
btn.className = `xr xr-${c.className}`
btn.innerText = c.className == "wb-close" ? "x" : "_"
btn.addEventListener("click", (e) => { } )// this will bubble up (to click ancestor in XR)
c.appendChild(btn)
}
})
}
})
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, margin ){