wip: feat/remotestorage
This commit is contained in:
		
							parent
							
								
									52f79a44c5
								
							
						
					
					
						commit
						96e11f3594
					
				
					 10 changed files with 147 additions and 47 deletions
				
			
		| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
             renderer="colorManagement: false; stencil: true; antialias:true; highRefreshRate:true; foveationLevel: 0.5; toneMapping: ACESFilmic; exposure: 3.0" 
 | 
			
		||||
             device-orientation-permission-ui xrf-gaze-always joystick
 | 
			
		||||
             light="defaultLightsEnabled: false">
 | 
			
		||||
      <a-entity id="player" movement-controls touch-controls="axis:y" wasd-controls="fly:false" look-controls="magicWindowTrackingEnabled:true">
 | 
			
		||||
      <a-entity id="player" movement-controls touch-controls="axis:y" wasd-controls="fly:true" look-controls="magicWindowTrackingEnabled:true">
 | 
			
		||||
        <a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
 | 
			
		||||
        <a-entity id="left-hand" hand-tracking-grab-controls="hand:left;modelColor:#cccccc" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor">
 | 
			
		||||
          <a-entity rotation="-35 0 0" position="0 0.1 0" id="navigator"> 
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								make
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								make
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -73,7 +73,7 @@ build(){
 | 
			
		|||
  aframe(){
 | 
			
		||||
    test -d src/3rd/js/aframe/build/aframe || git clone https://github.com/aframevr/aframe src/3rd/js/aframe/build/aframe --depth=1
 | 
			
		||||
    curdir=$(pwd)
 | 
			
		||||
    cd src/3rd/js/aframe/build && cp three.module.js aframe/src/lib/. # override to add extra loaders like fbx/collada e.g.
 | 
			
		||||
    cd src/3rd/js/aframe/build && cp three*.js aframe/src/lib/. # override to add extra loaders like fbx/collada e.g.
 | 
			
		||||
    #cd aframe && npm install && npm install troika-three-text && npm run dist
 | 
			
		||||
    cd aframe && npm install && npm run dist
 | 
			
		||||
    cd "$curdir"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										36
									
								
								src/3rd/js/aframe/build/three.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/3rd/js/aframe/build/three.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
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 = globalThis.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;
 | 
			
		||||
 | 
			
		||||
THREE.Cache.enabled = true;
 | 
			
		||||
 | 
			
		||||
export default THREE;
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ THREE.OBB = OBB;
 | 
			
		|||
THREE.BufferGeometryUtils = BufferGeometryUtils;
 | 
			
		||||
THREE.LightProbeGenerator = LightProbeGenerator;
 | 
			
		||||
THREE.TransformControls = TransformControls;
 | 
			
		||||
THREE.GLTFExporter      = GLTFExporter;
 | 
			
		||||
THREE.GLTFExporter      = GLTFExporter || console.error("GLTFExporter not found");
 | 
			
		||||
//THREE.Text = Text
 | 
			
		||||
 | 
			
		||||
export default THREE;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,19 +4,23 @@ filesComponent = (el) => new Proxy({
 | 
			
		|||
 | 
			
		||||
  html: (data) => `
 | 
			
		||||
      <style type="text/css">
 | 
			
		||||
        #messages .msg.ui, #messages .msg.ui #files div {
 | 
			
		||||
        #messages .msg.ui #files div {
 | 
			
		||||
          border:none;
 | 
			
		||||
          padding:0;
 | 
			
		||||
          border-radius:0;
 | 
			
		||||
          margin:0;
 | 
			
		||||
          box-shadow:none;
 | 
			
		||||
        }
 | 
			
		||||
        .msg.ui #files{
 | 
			
		||||
          min-width:415px;
 | 
			
		||||
        }
 | 
			
		||||
      </style>
 | 
			
		||||
 | 
			
		||||
      <div class="ui envelope">
 | 
			
		||||
        <div class="msg ui">
 | 
			
		||||
          <div>
 | 
			
		||||
            <div id="files">
 | 
			
		||||
              <i class="gg-close-o" id="close" onclick="$files.visible = false"></i>
 | 
			
		||||
              <i class="gg-close-o" id="close" onclick="$files.remove()"></i>
 | 
			
		||||
              <br>
 | 
			
		||||
              <div class="tab-frame">
 | 
			
		||||
                ${data.tabs.map( (t) => 
 | 
			
		||||
| 
						 | 
				
			
			@ -25,11 +29,12 @@ filesComponent = (el) => new Proxy({
 | 
			
		|||
                    `
 | 
			
		||||
                  ).join('')
 | 
			
		||||
                }
 | 
			
		||||
                <br><br>
 | 
			
		||||
                <br><br><br>
 | 
			
		||||
                <div class="tab">
 | 
			
		||||
                  <div id="localFilesTab">
 | 
			
		||||
                    <button id="uploadFile"  ><i class="gg-software-upload"></i> upload</button>
 | 
			
		||||
                    <button id="downloadfile"><i class="gg-software-download"></i> download</button>
 | 
			
		||||
                    <button id="localOpen" onclick="$files.fileLoaders()" ><i class="gg-software-upload"></i> open experience</button>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    <button id="localSave" onclick="frontend.download()"><i class="gg-software-download"></i> save current</button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +45,7 @@ filesComponent = (el) => new Proxy({
 | 
			
		|||
  `,
 | 
			
		||||
 | 
			
		||||
  tabs: [
 | 
			
		||||
    {name: "local files", id: "localFiles"}
 | 
			
		||||
    {name: "offline", id: "localFiles"}
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  show:    false,
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +55,10 @@ filesComponent = (el) => new Proxy({
 | 
			
		|||
    this.show = state !== undefined ? state : !this.show 
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  remove(){
 | 
			
		||||
    el.parent.remove(el)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init(opts){
 | 
			
		||||
    this.decorateFileButton()
 | 
			
		||||
    // create HTML element
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +81,7 @@ filesComponent = (el) => new Proxy({
 | 
			
		|||
    document.querySelector("#load").setAttribute("value","3D file")
 | 
			
		||||
    // decorate fileLoaders
 | 
			
		||||
    window.frontend.fileLoaders = ( (fileLoaders) => {
 | 
			
		||||
      this.fileLoader = fileLoaders
 | 
			
		||||
      this.fileLoaders = fileLoaders
 | 
			
		||||
      return () => this.toggle() 
 | 
			
		||||
    })( window.frontend.fileLoaders )
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -889,6 +889,38 @@ document.head.innerHTML += `
 | 
			
		|||
    bottom: 3px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .gg-globe-alt,
 | 
			
		||||
  .gg-globe-alt::after,
 | 
			
		||||
  .gg-globe-alt::before {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
    border: 2px solid;
 | 
			
		||||
  }
 | 
			
		||||
  .gg-globe-alt {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    transform: scale(var(--ggs, 1));
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    border-radius: 22px;
 | 
			
		||||
  }
 | 
			
		||||
  .gg-globe-alt::after,
 | 
			
		||||
  .gg-globe-alt::before {
 | 
			
		||||
    content: "";
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: 8px;
 | 
			
		||||
    border-radius: 100%;
 | 
			
		||||
    top: -2px;
 | 
			
		||||
    left: 3px;
 | 
			
		||||
  }
 | 
			
		||||
  .gg-globe-alt::after {
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 20px;
 | 
			
		||||
    border: 2px solid transparent;
 | 
			
		||||
    border-bottom: 2px solid;
 | 
			
		||||
    top: -11px;
 | 
			
		||||
    left: -5px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  </style>
 | 
			
		||||
`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -252,23 +252,26 @@ window.frontend = (opts) => new Proxy({
 | 
			
		|||
 | 
			
		||||
  download(){
 | 
			
		||||
 | 
			
		||||
    function download(dataurl, filename) {
 | 
			
		||||
      var a = document.createElement("a");
 | 
			
		||||
      a.href = URL.createObjectURL( new Blob([dataurl]) );
 | 
			
		||||
      a.setAttribute("download", filename);
 | 
			
		||||
      a.click();
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function exportScene(model,ext,file){
 | 
			
		||||
 | 
			
		||||
      document.dispatchEvent( new CustomEvent('frontend.export',{detail:{ scene: model.scene,ext}}) )
 | 
			
		||||
      xrf.emit('export', {scene: model.scene, ext})
 | 
			
		||||
      .then( () => {
 | 
			
		||||
 | 
			
		||||
        function download(dataurl, filename) {
 | 
			
		||||
          var a = document.createElement("a");
 | 
			
		||||
          a.href = URL.createObjectURL( new Blob([dataurl]) );
 | 
			
		||||
          a.setAttribute("download", filename);
 | 
			
		||||
          a.click();
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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]()
 | 
			
		||||
        const exporter = new xrf.loaders[ext].exporter()
 | 
			
		||||
          debugger
 | 
			
		||||
        exporter.parse(
 | 
			
		||||
          model.scene,
 | 
			
		||||
          function ( glb   ) { download(glb, `${file}`) },    // ready
 | 
			
		||||
| 
						 | 
				
			
			@ -287,13 +290,12 @@ window.frontend = (opts) => new Proxy({
 | 
			
		|||
    // load original scene and overwrite with updates
 | 
			
		||||
    let url = document.location.search.replace(/\?/,'')
 | 
			
		||||
    let {urlObj,dir,file,hash,fileExt} = xrf.navigator.origin = xrf.URI.parse(url)
 | 
			
		||||
    debugger
 | 
			
		||||
    const Loader = xrf.loaders[fileExt]
 | 
			
		||||
    loader = new Loader().setPath( dir )
 | 
			
		||||
    notify('exporting scene<br><br>please wait..')
 | 
			
		||||
    loader.load(url, (model) => {
 | 
			
		||||
      exportScene(model,fileExt,file)
 | 
			
		||||
    })
 | 
			
		||||
    }, console.error )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  updateHashPosition(randomize){
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
const WebXRF = { name: 'webxr', builder: function(privateClient, publicClient) {
 | 
			
		||||
  return {
 | 
			
		||||
    exports: {
 | 
			
		||||
      addScene: function() {}
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}};
 | 
			
		||||
| 
						 | 
				
			
			@ -3,33 +3,39 @@
 | 
			
		|||
// reactive component for displaying the menu 
 | 
			
		||||
remoteStorageComponent = (el) => new Proxy({
 | 
			
		||||
 | 
			
		||||
  html: `
 | 
			
		||||
  html: (data) => (`
 | 
			
		||||
    <style type="text/css">
 | 
			
		||||
      body div#remotestorage-widget .rs-button{
 | 
			
		||||
        background: #AAA; 
 | 
			
		||||
      body #files .rs-button-big{
 | 
			
		||||
        background: #FFF;
 | 
			
		||||
        box-shadow: none;
 | 
			
		||||
        border: 1px solid #CCC;
 | 
			
		||||
        padding: 10px 0px 49px 10px;
 | 
			
		||||
      }
 | 
			
		||||
      #files .rs-button{
 | 
			
		||||
        background:#CCC;
 | 
			
		||||
      }
 | 
			
		||||
    </style> 
 | 
			
		||||
 | 
			
		||||
    <div id="remoteFilesTab">
 | 
			
		||||
      <div id="rswidget"></div>
 | 
			
		||||
      <br><br>
 | 
			
		||||
      <button id="uploadFile"  ><i class="gg-software-upload"></i> upload</button>
 | 
			
		||||
      <button id="downloadfile"><i class="gg-software-download"></i> download</button>
 | 
			
		||||
      <br>
 | 
			
		||||
      <div id="buttons" style="display:none">
 | 
			
		||||
        <button id="remoteOpen"  ><i class="gg-software-upload"></i> open experience</button>
 | 
			
		||||
        <br>
 | 
			
		||||
        <button id="remoteSave"><i class="gg-software-download"></i> save private experience</button>
 | 
			
		||||
        <br>
 | 
			
		||||
        <button id="remoteSave"><i class="gg-globe-alt"></i> save public experience</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  `,
 | 
			
		||||
  `),
 | 
			
		||||
 | 
			
		||||
  show:    false,
 | 
			
		||||
 | 
			
		||||
  toggle(state){   
 | 
			
		||||
    this.show = state !== undefined ? state : !this.show 
 | 
			
		||||
  },
 | 
			
		||||
  connected: false,
 | 
			
		||||
 | 
			
		||||
  init(opts){
 | 
			
		||||
    // create HTML element
 | 
			
		||||
    $files.tabs = $files.tabs.concat({id:"remoteFiles", name: "remote files"})
 | 
			
		||||
    el.innerHTML = this.html
 | 
			
		||||
    $files.tabs = $files.tabs.concat({id:"remoteFiles", name: "online"})
 | 
			
		||||
    el.innerHTML = this.html(this)
 | 
			
		||||
    el.className = "tab"
 | 
			
		||||
    this.toggle(this.show) // trigger visibility
 | 
			
		||||
    document.querySelector("#files .tab-frame").appendChild(el);
 | 
			
		||||
 | 
			
		||||
    // setup input listeners
 | 
			
		||||
| 
						 | 
				
			
			@ -61,32 +67,36 @@ remoteStorageComponent = (el) => new Proxy({
 | 
			
		|||
    let apis = {
 | 
			
		||||
      dropbox: "4jc8nx1lbarp472"
 | 
			
		||||
    }
 | 
			
		||||
    window.remoteStorage = new RemoteStorage({logging: true })
 | 
			
		||||
    const modules = []
 | 
			
		||||
    if( typeof WebXRF != undefined ){
 | 
			
		||||
      modules.push(WebXRF) // 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) => { console.log("connected") } )
 | 
			
		||||
    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('ready',           (e) => { }  )
 | 
			
		||||
 | 
			
		||||
    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" );
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  click(id,e){
 | 
			
		||||
    switch(id){
 | 
			
		||||
      case "icon":
 | 
			
		||||
      case "more": return this.toggle(); break;
 | 
			
		||||
    }
 | 
			
		||||
    this.toggle(false)
 | 
			
		||||
    //switch(id){
 | 
			
		||||
    //  case "more": return this.toggle(); break;
 | 
			
		||||
    //}
 | 
			
		||||
  }
 | 
			
		||||
},
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +105,9 @@ remoteStorageComponent = (el) => new Proxy({
 | 
			
		|||
 | 
			
		||||
  set(me,k,v){ 
 | 
			
		||||
    me[k] = v    
 | 
			
		||||
    switch( k ){
 | 
			
		||||
        case 'connected': el.querySelector("#buttons").style.display = v ? 'block' : 'none'; break;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,6 @@ xrf.navigator.to = (url,flags,loader,data) => {
 | 
			
		|||
        evalFragment()
 | 
			
		||||
        return resolve(xrf.model)                         // eval non-positional fragments (no loader needed)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      xrf
 | 
			
		||||
      .emit('navigateLoading', {url,loader,data})
 | 
			
		||||
      .then( () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +108,7 @@ xrf.navigator.init = () => {
 | 
			
		|||
  xrf.navigator.URI = xrfragment.URI.parse(document.location.href)
 | 
			
		||||
 | 
			
		||||
  window.addEventListener('popstate', function (event){
 | 
			
		||||
    if( xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
 | 
			
		||||
    if( xrf.navigator.updateHash.active && document.location.hash.length > 1 ){ // ignore programmatic hash updates (causes infinite recursion)
 | 
			
		||||
      xrf.navigator.to( xrf.navigator.URI.last )
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +118,9 @@ xrf.navigator.init = () => {
 | 
			
		|||
  })
 | 
			
		||||
 | 
			
		||||
  // allow other libraries to trigger popstate event without triggering the navigate-fallbacks  during pageload
 | 
			
		||||
  setTimeout( xrf.navigator.setupNavigateFallbacks(), 1500 )
 | 
			
		||||
  setTimeout( () => {
 | 
			
		||||
    xrf.navigator.setupNavigateFallbacks()
 | 
			
		||||
  }, 2500 )
 | 
			
		||||
 | 
			
		||||
  // this allows selectionlines to be updated according to the camera (renderloop)
 | 
			
		||||
  xrf.focusLine = new xrf.THREE.Group()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue