wip: remotestorage

This commit is contained in:
Leon van Kammen 2025-05-15 18:48:49 +02:00
parent ac2989df40
commit 5dd7f6a764
9 changed files with 512 additions and 72 deletions

293
dist/xrfragment.plugin.remotestorage.js vendored Normal file
View file

@ -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) => (`
<style type="text/css">
body #files .rs-button-big{
background: #FFF;
box-shadow: none;
border: 1px solid #CCC;
padding: 10px 0px 49px 10px;
}
#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
}
</style>
<div id="remoteFilesTab">
<div id="rswidget"></div>
<br>
<div id="buttons" style="display:none">
<select id="listing" alt="your files"></select>
<i class="gg-close-o" id="delete" alt="delete" onclick="$remotestorage.remove()"></i>
<a href="https://inspektor.5apps.com/?path=%2Fwebxr%2F" target="_blank" style="margin-left:15px">manage files</a>
<br>
<button onclick="$remotestorage.savePrivate()"><i class="gg-software-download"></i> save experience</button>
<br>
<button onclick="$remotestorage.savePublic()" ><i class="gg-globe-alt"></i> publish online</button>
<br>
<div id="links">
<br>
<div><i class="gg-link"></i>&nbsp;&nbsp;&nbsp;3D file</div>
<input type="text" value='' id="file">
<br>
<div><i class="gg-link"></i>&nbsp;&nbsp;&nbsp;3D file in webviewer</div>
<input type="text" value='' id="webviewer">
</div>
</div>
</div>
`),
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({})

View file

@ -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;

View file

@ -434,6 +434,7 @@ chatComponent.css = `
margin-right:15px; margin-right:15px;
width:100%; width:100%;
max-width:40%; max-width:40%;
margin-bottom:7px;
} }
.envelope, .envelope,

View file

@ -20,7 +20,7 @@ filesComponent = (el) => new Proxy({
<div class="msg ui"> <div class="msg ui">
<div> <div>
<div id="files"> <div id="files">
<i class="gg-close-o" id="close" onclick="$files.remove()"></i> <a onclick="$files.toggle(false)"><i class="gg-close-o" id="close"></i></a>
<br> <br>
<div class="tab-frame"> <div class="tab-frame">
${data.tabs.map( (t) => ${data.tabs.map( (t) =>
@ -29,8 +29,8 @@ filesComponent = (el) => new Proxy({
` `
).join('') ).join('')
} }
<br><br><br> <br><br>
<div class="tab"> <div class="tab" style="margin-top:15px;">
<div id="localFilesTab"> <div id="localFilesTab">
<button id="localOpen" onclick="$files.fileLoaders()" ><i class="gg-software-upload"></i> open experience</button> <button id="localOpen" onclick="$files.fileLoaders()" ><i class="gg-software-upload"></i> open experience</button>
<br> <br>

View file

@ -364,8 +364,9 @@ document.head.innerHTML += `
opacity:0.5 opacity:0.5
} }
body.menu .js-snackbar__wrapper { body.menu .js-snackbar__wrapper,
top: 64px; body.topbar .js-snackbar__wrapper {
transform: translateY(40px);
} }
.transcript{ .transcript{

View file

@ -43,7 +43,8 @@ window.frontend = (opts) => new Proxy({
.setupCapture() .setupCapture()
.setupUserHints() .setupUserHints()
.setupNetworkListeners() .setupNetworkListeners()
.hidetopbarWhenMenuCollapse() .setupTopbar()
.hideTopbarWhenMenuCollapse()
.hideUIWhenNavigating() .hideUIWhenNavigating()
window.notify = this.notify window.notify = this.notify
@ -68,6 +69,16 @@ window.frontend = (opts) => new Proxy({
return this 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(){ setupIframeUrlHandler(){
// allow iframe to open url // allow iframe to open url
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
@ -175,9 +186,9 @@ window.frontend = (opts) => new Proxy({
return this return this
}, },
hidetopbarWhenMenuCollapse(){ hideTopbarWhenMenuCollapse(){
// hide topbar when menu collapse button is pressed // 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 return this
}, },
@ -250,9 +261,9 @@ window.frontend = (opts) => new Proxy({
window.frontend.emit("notify",opts) 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}}) ) document.dispatchEvent( new CustomEvent('frontend.export',{detail:{ scene: model.scene,ext}}) )
xrf.emit('export', {scene: model.scene, ext}) xrf.emit('export', {scene: model.scene, ext})
@ -266,15 +277,17 @@ window.frontend = (opts) => new Proxy({
return false; return false;
} }
if( !cb ) cb = download
// setup exporters // setup exporters
let defaultExporter = THREE.GLTFExporter let defaultExporter = THREE.GLTFExporter
if( !xrf.loaders['gltf'].exporter ) xrf.loaders['gltf'].exporter = defaultExporter if( !xrf.loaders['gltf'].exporter ) xrf.loaders['gltf'].exporter = defaultExporter
if( !xrf.loaders['glb'].exporter ) xrf.loaders['glb'].exporter = defaultExporter if( !xrf.loaders['glb'].exporter ) xrf.loaders['glb'].exporter = defaultExporter
const exporter = new xrf.loaders[ext].exporter() const exporter = new xrf.loaders[ext].exporter()
debugger
exporter.parse( exporter.parse(
model.scene, model.scene,
function ( glb ) { download(glb, `${file}`) }, // ready function ( glb ) { cb(glb, `${file}`) }, // ready
function ( error ) { console.error(error) }, // error function ( error ) { console.error(error) }, // error
{ {
binary:true, binary:true,
@ -293,9 +306,15 @@ window.frontend = (opts) => new Proxy({
const Loader = xrf.loaders[fileExt] const Loader = xrf.loaders[fileExt]
loader = new Loader().setPath( dir ) loader = new Loader().setPath( dir )
notify('exporting scene<br><br>please wait..') notify('exporting scene<br><br>please wait..')
loader.load(url, (model) => {
exportScene(model,fileExt,file) fetch(url, { method: 'HEAD' })
}, console.error ) .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){ updateHashPosition(randomize){

View file

@ -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)
}
}
}
}};

View file

@ -1,7 +0,0 @@
const WebXRF = { name: 'webxr', builder: function(privateClient, publicClient) {
return {
exports: {
addScene: function() {}
}
}
}};

View file

@ -14,30 +14,65 @@ remoteStorageComponent = (el) => new Proxy({
#files .rs-button{ #files .rs-button{
background:#CCC; 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
}
</style> </style>
<div id="remoteFilesTab"> <div id="remoteFilesTab">
<div id="rswidget"></div> <div id="rswidget"></div>
<br> <br>
<div id="buttons" style="display:none"> <div id="buttons" style="display:none">
<button id="remoteOpen" ><i class="gg-software-upload"></i> open experience</button> <select id="listing" alt="your files"></select>
<i class="gg-close-o" id="delete" alt="delete" onclick="$remotestorage.remove()"></i>
<a href="https://inspektor.5apps.com/?path=%2Fwebxr%2F" target="_blank" style="margin-left:15px">manage files</a>
<br> <br>
<button id="remoteSave"><i class="gg-software-download"></i> save private experience</button> <button onclick="$remotestorage.savePrivate()"><i class="gg-software-download"></i> save experience</button>
<br> <br>
<button id="remoteSave"><i class="gg-globe-alt"></i> save public experience</button> <button onclick="$remotestorage.savePublic()" ><i class="gg-globe-alt"></i> publish online</button>
<br>
<div id="links">
<br>
<div><i class="gg-link"></i>&nbsp;&nbsp;&nbsp;3D file</div>
<input type="text" value='' id="file">
<br>
<div><i class="gg-link"></i>&nbsp;&nbsp;&nbsp;3D file in webviewer</div>
<input type="text" value='' id="webviewer">
</div>
</div> </div>
</div> </div>
`), `),
connected: false, connected: false,
$listing: false,
$links: false,
init(opts){ init(opts){
// create HTML element // 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.innerHTML = this.html(this)
el.className = "tab" el.className = "tab"
document.querySelector("#files .tab-frame").appendChild(el); 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 // setup input listeners
(['click']).map( (e) => el.addEventListener(e, (ev) => typeof this[e] == 'function' && this[e](ev.target.id,ev) ) ) (['click']).map( (e) => el.addEventListener(e, (ev) => typeof this[e] == 'function' && this[e](ev.target.id,ev) ) )
@ -68,21 +103,27 @@ remoteStorageComponent = (el) => new Proxy({
dropbox: "4jc8nx1lbarp472" dropbox: "4jc8nx1lbarp472"
} }
const modules = [] const modules = []
if( typeof WebXRF != undefined ){ if( typeof WebXR != undefined ){
modules.push(WebXRF) // defined in remotestorage-module-webXRF.js modules.push(WebXR) // defined in remotestorage-module-webXRF.js
} }
window.remoteStorage = new RemoteStorage({logging: true, modules }) window.remoteStorage = new RemoteStorage({logging: true, modules })
if( Object.keys(apis).length ) remoteStorage.setApiKeys(apis) if( Object.keys(apis).length ) remoteStorage.setApiKeys(apis)
remoteStorage.on('connected', (e) => { this.connected = true }) remoteStorage.on('not-connected', (e) => { this.connected = false })
//remoteStorage.on('network-offline', (e) => this.el.sceneEl.emit('remoteStorage.network-offline',e) ) remoteStorage.on('ready', (e) => { })
//remoteStorage.on('network-online', (e) => this.el.sceneEl.emit('remoteStorage.network-online',e) ) remoteStorage.on('connected', (e) => {
//remoteStorage.on('error', (e) => this.el.sceneEl.emit('remoteStorage.error',e) ) this.connected = true
remoteStorage.on('ready', (e) => { } ) // 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 this.updateFiles()
remoteStorage.caching.enable( `/webxr/` ) // local-first, remotestorage-second })
remoteStorage.caching.enable( `/public/webxr/` ) // local-first, remotestorage-second
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 // create widget
let opts = {} 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){ click(id,e){
//switch(id){ //switch(id){
// case "more": return this.toggle(); break; // case "more": return this.toggle(); break;