import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js'; let gSocket; let gCurrentFile; let gFiles = {}; let gApp = {files: {}, emoji: '📦'}; let gEditor; let gSplit; let gGraphs = {}; let gTimeSeries = {}; let gOriginalInput; let kErrorColor = "#dc322f"; let kStatusColor = "#fff"; /* Functions that server-side app code can call through the app object. */ const k_api = { setDocument: {args: ['content'], func: api_setDocument}, postMessage: {args: ['message'], func: api_postMessage}, error: {args: ['error'], func: api_error}, localStorageSet: {args: ['key', 'value'], func: api_localStorageSet}, localStorageGet: {args: ['key'], func: api_localStorageGet}, requestPermission: {args: ['permission', 'id'], func: api_requestPermission}, print: {args: ['...'], func: api_print}, setHash: {args: ['hash'], func: api_setHash}, }; const k_global_style = css` a:link { color: #268bd2; } a:visited { color: #6c71c4; } a:hover { color: #859900; } a:active { color: #2aa198; } `; class TfNavigationElement extends LitElement { static get properties() { return { credentials: {type: Object}, permissions: {type: Object}, show_permissions: {type: Boolean}, status: {type: Object}, spark_lines: {type: Object}, }; } constructor() { super(); this.permissions = {}; this.show_permissions = false; this.status = {}; this.spark_lines = {}; } toggle_edit(event) { event.preventDefault(); if (editing()) { closeEditor(); } else { edit(); } } reset_permission(key) { send({action: "resetPermission", permission: key}); } get_spark_line(key, options) { if (!this.spark_lines[key]) { let spark_line = document.createElement('tf-sparkline'); spark_line.title = key; if (options) { if (options.max) { spark_line.max = options.max; } } this.spark_lines[key] = spark_line; this.requestUpdate(); } return this.spark_lines[key]; } render_login() { if (this?.credentials?.session?.name) { return html`logout ${this.credentials.session.name}`; } else { return html`login`; } } render_permissions() { if (this.show_permissions) { return html`
This app has the following permissions:
${Object.keys(this.permissions).map(key => html`
${key}: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
`)}
`; } } render() { let self = this; return html`
😎 TF apps edit self.show_permissions = !self.show_permissions}>🎛️ ${this.status.message} ${this.render_permissions()} ${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [x.dataset.emoji, x])} ${this.render_login()}
`; } } customElements.define('tf-navigation', TfNavigationElement); class TfFilesElement extends LitElement { static get properties() { return { current: {type: String}, files: {type: Object}, }; } constructor() { super(); this.files = {}; } file_click(file) { this.dispatchEvent(new CustomEvent('file_click', { detail: { file: file, }, bubbles: true, composed: true, })); } render_file(file) { let classes = ['file']; if (file == this.current) { classes.push('current'); } if (!this.files[file].clean) { classes.push('dirty'); } return html`
this.file_click(file)}>${file}
`; } render() { let self = this; return html`
${Object.keys(this.files).sort().map(x => self.render_file(x))}
`; } } customElements.define('tf-files', TfFilesElement); class TfSparkLineElement extends LitElement { static get properties() { return { lines: {type: Array}, min: {type: Number}, max: {type: Number}, }; } constructor() { super(); this.min = 0; this.max = 1.0; this.lines = []; } append(key, value) { let line = null; for (let it of this.lines) { if (it.name == key) { line = it; break; } } if (!line) { const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888']; line = { name: key, style: k_colors[this.lines.length % k_colors.length], values: [], }; this.lines.push(line); } line.values.push(value); if (line.values.length > 100) { line.values.shift(); } this.requestUpdate(); } render_line(line) { if (line?.values?.length >= 2) { let points = [].concat(...line.values.map((x, i) => [100.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (this.max - this.min)])); return svg` `; } } render() { return html` ${this.lines.map(x => this.render_line(x))} `; } } customElements.define('tf-sparkline', TfSparkLineElement); window.addEventListener("keydown", function(event) { if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) { if (editing()) { save(); event.preventDefault(); } } else if (event.keyCode == 66 && event.altKey) { if (editing()) { closeEditor(); event.preventDefault(); } } }); function ensureLoaded(nodes, callback) { if (!nodes.length) { callback(); return; } let search = nodes.shift(); let head = document.head; let found = false; for (let i = 0; i < head.childNodes.length; i++) { if (head.childNodes[i].tagName == search.tagName) { let match = true; for (let attribute in search.attributes) { if (head.childNodes[i].attributes[attribute].value != search.attributes[attribute]) { match = false; } } if (match) { found = true; break; } } } if (found) { ensureLoaded(nodes, callback); } else { let node = document.createElement(search.tagName); node.onreadystatechange = node.onload = function() { ensureLoaded(nodes, callback); }; for (let attribute in search.attributes) { node.setAttribute(attribute, search.attributes[attribute]); } head.insertBefore(node, head.firstChild); } } function editing() { return document.getElementById("editPane").style.display != 'none'; } function edit() { if (editing()) { return; } window.localStorage.setItem('editing', '1'); if (gSplit) { gSplit.destroy(); gSplit = undefined; } gSplit = Split(['#editPane', '#viewPane'], {minSize: 0}); ensureLoaded([ {tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/base16-dark.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}}, {tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/search.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/searchcursor.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/jump-to-line.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/matchesonscrollbar.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/annotatescrollbar.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/javascript.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/css.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/xml.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}}, {tagName: "script", attributes: {src: "/codemirror/lint.js"}}, {tagName: "script", attributes: {src: "/codemirror/jshint.js"}}, {tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}}, ], function() { load().catch(function(error) { alert(error); closeEditor(); }); }); } function hideFiles() { window.localStorage.setItem('files', '0'); document.getElementById('filesPane').classList.add('collapsed'); } function showFiles() { window.localStorage.setItem('files', '1'); document.getElementById('filesPane').classList.remove('collapsed'); } function trace() { window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`); } function stats() { window.localStorage.setItem('stats', '1'); document.getElementById("statsPane").style.display = 'flex'; } function closeStats() { window.localStorage.setItem('stats', '0'); document.getElementById("statsPane").style.display = 'none'; } function toggleStats() { if (document.getElementById("statsPane").style.display == 'none') { stats(); } else { closeStats(); } } function guessMode(name) { return name.endsWith(".js") ? "javascript" : name.endsWith(".html") ? "htmlmixed" : null; } function loadFile(name, id) { return fetch('/' + id + '/view').then(function(response) { if (!response.ok) { throw new Error('Request failed: ' + response.status + ' ' + response.statusText); } return response.text(); }).then(function(text) { gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name)); if (!Object.values(gFiles).some(x => !x.doc)) { document.getElementById("editPane").style.display = 'flex'; openFile(Object.keys(gFiles).sort()[0]); } }); } function load(path) { return fetch((path || url()) + 'view').then(function(response) { if (!response.ok) { if (response.status == 404) { return null; } else { throw new Error(response.status + ' ' + response.statusText); } } return response.json(); }).then(function(json) { if (!gEditor) { gEditor = CodeMirror.fromTextArea(document.getElementById("editor"), { 'theme': 'base16-dark', 'lineNumbers': true, 'tabSize': 4, 'indentUnit': 4, 'indentWithTabs': true, 'showTrailingSpace': true, 'gutters': ['CodeMirror-lint-markers'], 'mode': {'js': 'javascript'}[(path || url()).split('.').pop()], 'lint': { 'options': { 'esversion': 2021, }, }, }); gEditor.on('changes', function() { updateFiles(); }); } gFiles = {}; let isApp = false; let promises = []; if (json && json['type'] == 'tildefriends-app') { isApp = true; Object.keys(json['files']).forEach(function(name) { gFiles[name] = {}; promises.push(loadFile(name, json['files'][name])); }); if (Object.keys(json['files']).length == 0) { document.getElementById("editPane").style.display = 'flex'; } gApp = json; gApp.emoji = gApp.emoji || '📦'; document.getElementById('icon').value = gApp.emoji; } if (!isApp) { document.getElementById("editPane").style.display = 'flex'; let text = '// New script.\n'; gCurrentFile = 'app.js'; gFiles[gCurrentFile] = { doc: new CodeMirror.Doc(text, guessMode(gCurrentFile)), }; openFile(gCurrentFile); } return Promise.all(promises); }); } function closeEditor() { window.localStorage.setItem('editing', '0'); document.getElementById("editPane").style.display = 'none'; if (gSplit) { gSplit.destroy(); gSplit = undefined; } } function explodePath() { return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname); } function save(save_to) { document.getElementById("save").disabled = true; if (gCurrentFile) { gFiles[gCurrentFile].doc = gEditor.getDoc(); } let save_path = save_to; if (!save_path) { let name = document.getElementById("name"); if (name && name.value) { save_path = name.value; } else { save_path = url(); } } let promises = []; for (let name of Object.keys(gFiles)) { let file = gFiles[name]; if (file.doc.isClean(file.generation)) { continue; } delete file.id; promises.push(fetch('/save', { method: 'POST', headers: { 'Content-Type': 'text/plain', }, body: file.doc.getValue(), }).then(function(response) { if (!response.ok) { throw new Error('Saving "' + name + '": ' + response.status + ' ' + response.statusText); } return response.text(); }).then(function(text) { file.id = text; if (file.id.charAt(0) == '/') { file.id = file.id.substr(1); } })); } return Promise.all(promises).then(function() { let app = { type: "tildefriends-app", files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])), emoji: gApp.emoji || '📦', }; Object.values(gFiles).forEach(function(file) { delete file.id; }); gApp = JSON.parse(JSON.stringify(app)); return fetch(save_path + 'save', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(app), }).then(function(response) { if (!response.ok) { throw new Error(response.status + ' ' + response.statusText); } if (save_path != window.location.pathname) { alert('Saved to ' + save_path + '.'); } else { reconnect(save_path); } }); }).catch(function(error) { alert(error); }).finally(function() { document.getElementById("save").disabled = false; Object.values(gFiles).forEach(function(file) { file.generation = file.doc.changeGeneration(); }); updateFiles(); }); } function changeIcon() { let value = prompt('Enter a new app icon emoji:'); if (value !== undefined) { gApp.emoji = value || '📦'; document.getElementById('icon').value = gApp.emoji; } } function deleteApp() { let name = document.getElementById("name"); let path = name && name.value ? name.value : url(); if (confirm(`Are you sure you want to delete the app '${path}'?`)) { fetch(path + 'delete').then(function(response) { if (!response.ok) { throw new Error(response.status + ' ' + response.statusText); } alert('Deleted.'); }).catch(function(error) { alert(error); }); } } function url() { let hash = window.location.href.indexOf('#'); let question = window.location.href.indexOf('?'); let end = -1; if (hash != -1 && (hash < end || end == -1)) { end = hash; } if (question != -1 && (question < end || end == -1)) { end = question; } return end != -1 ? window.location.href.substring(0, end) : window.location.href; } function hash() { return window.location.hash != "#" ? window.location.hash : ""; } function api_setDocument(content) { let iframe = document.getElementById("document"); iframe.srcdoc = content; } function api_postMessage(message) { let iframe = document.getElementById("document"); iframe.contentWindow.postMessage(message, "*"); } function api_error(error) { if (error) { if (typeof(error) == 'string') { setStatusMessage('⚠️ ' + error, '#f00'); } else { setStatusMessage('⚠️ ' + error.message + '\n' + error.stack, '#f00'); } } console.log('error', error); } function api_localStorageSet(key, value) { window.localStorage.setItem('app:' + key, value); } function api_localStorageGet(key) { return window.localStorage.getItem('app:' + key); } function api_requestPermission(permission, id) { let outer = document.createElement('div'); outer.classList.add('permissions'); let container = document.createElement('div'); container.classList.add('permissions_contents'); let div = document.createElement('div'); div.appendChild(document.createTextNode('This app is requesting the following permission:')); let span = document.createElement('span'); span.style = 'font-weight: bold'; span.appendChild(document.createTextNode(permission)); div.appendChild(span); container.appendChild(div); div = document.createElement('div'); div.style = 'padding: 1em'; let check = document.createElement('input'); check.id = 'permissions_remember_check'; check.type = 'checkbox'; div.appendChild(check); let label = document.createElement('label'); label.htmlFor = check.id; label.appendChild(document.createTextNode('Remember this decision.')); div.appendChild(label); container.appendChild(div); const k_options = [ { text: '✅ Allow', grant: ['allow once', 'allow'], }, { text: '❌ Deny', grant: ['deny once', 'deny'], }, ]; return new Promise(function(resolve, reject) { div = document.createElement('div'); for (let option of k_options) { let button = document.createElement('button'); button.innerText = option.text; button.onclick = function() { resolve(option.grant[check.checked ? 1 : 0]); document.body.removeChild(outer); } div.appendChild(button); } container.appendChild(div); outer.appendChild(container); document.body.appendChild(outer); }); } function api_print() { console.log('app>', ...arguments); } function api_setHash(hash) { window.location.hash = hash; } function _receive_websocket_message(message) { if (message && message.action == "session") { setStatusMessage("🟢 Executing...", kStatusColor); document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials; } else if (message && message.action == 'permissions') { document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {}; } else if (message && message.action == "ready") { setStatusMessage(null); if (window.location.hash) { send({event: "hashChange", hash: window.location.hash}); } send({action: 'enableStats', enabled: true}); } else if (message && message.action == "ping") { send({action: "pong"}); } else if (message && message.action == "stats") { let now = new Date().getTime(); for (let key of Object.keys(message.stats)) { const k_groups = { rpc_in: {group: 'rpc', name: 'in'}, rpc_out: {group: 'rpc', name: 'out'}, cpu_percent: {group: 'cpu', name: 'main'}, thread_percent: {group: 'cpu', name: 'work'}, arena_percent: {group: 'memory', name: 'm'}, js_malloc_percent: {group: 'memory', name: 'js'}, memory_percent: {group: 'memory', name: 'tot'}, sqlite3_memory_percent: {group: 'memory', name: 'sql'}, tf_malloc_percent: {group: 'memory', name: 'tf'}, tls_malloc_percent: {group: 'memory', name: 'tls'}, uv_malloc_percent: {group: 'memory', name: 'uv'}, messages_stored: {group: 'store', name: 'messages'}, blobs_stored: {group: 'store', name: 'blobs'}, socket_count: {group: 'socket', name: 'total'}, socket_open_count: {group: 'socket', name: 'open'}, import_count: {group: 'functions', name: 'imports'}, export_count: {group: 'functions', name: 'exports'}, }; const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888']; let graph_key = k_groups[key]?.group || key; let graph = gGraphs[graph_key]; if (!graph) { graph = { chart: new SmoothieChart({ millisPerPixel: 100, minValue: 0, grid: { millisPerLine: 1000, verticalSections: 10, }, tooltip: true, }), canvas: document.createElement('canvas'), title: document.createElement('div'), series: [], }; gGraphs[graph_key] = graph; graph.canvas.width = 240; graph.canvas.height = 64; graph.title.innerText = graph_key; graph.title.style.flex = '0'; document.getElementById('graphs').appendChild(graph.title); document.getElementById('graphs').appendChild(graph.canvas); graph.chart.streamTo(graph.canvas, 1000); } let timeseries = gTimeSeries[key]; if (!timeseries) { let is_multi = key != graph_key || graph.series.length > 1; timeseries = new TimeSeries(); gTimeSeries[key] = timeseries; graph.chart.addTimeSeries(timeseries, {lineWidth: 2, strokeStyle: is_multi ? k_colors[graph.series.length] : '#fff'}); graph.series.push(k_groups[key]?.name || key); if (is_multi) { while (graph.title.firstChild) { graph.title.removeChild(graph.title.firstChild); } function makeColoredText(text, color) { let element = document.createElement('span'); element.style.color = color; element.innerText = text; return element; } graph.title.appendChild(makeColoredText(graph_key + ':', '#fff')); for (let series of graph.series) { graph.title.appendChild(makeColoredText(' ' + series, k_colors[graph.series.indexOf(series)])); } } } timeseries.append(now, message.stats[key]); if (graph_key == 'cpu' || graph_key == 'rpc' || graph_key == 'store') { let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 }); line.dataset.emoji = { 'cpu': '💻', 'rpc': '🔁', 'store': '💾', }[graph_key]; line.append(key, message.stats[key]); } } } else if (message && message.message === 'tfrpc' && message.method) { let api = k_api[message.method]; let id = message.id; let params = message.params; if (api) { Promise.resolve(api.func(...params)).then(function(result) { send({ message: 'tfrpc', id: id, result: result, }); }).catch(function(error) { send({ message: 'tfrpc', id: id, error: error, }); }); } } } function keyEvent(event) { send({ event: "key", type: event.type, which: event.which, keyCode: event.keyCode, charCode: event.charCode, character: String.fromCharCode(event.keyCode || event.which), altKey: event.altKey, }); } function setStatusMessage(message, color) { document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color}; } function send(value) { try { if (gSocket && gSocket.readyState == gSocket.OPEN) { gSocket.send(JSON.stringify(value)); } } catch (error) { setStatusMessage('🤷 Send failed: ' + error.toString(), kErrorColor); } } function dragHover(event) { event.stopPropagation(); event.preventDefault(); let input = document.getElementById("input"); if (event.type == "dragover") { if (!input.classList.contains("drop")) { input.classList.add("drop"); gOriginalInput = input.value; input.value = "drop file to upload"; } } else { input.classList.remove("drop"); input.value = gOriginalInput; } } function fixImage(sourceData, maxWidth, maxHeight, callback) { let result = sourceData; let image = new Image(); image.crossOrigin = "anonymous"; image.referrerPolicy = "no-referrer"; image.onload = function() { if (image.width > maxWidth || image.height > maxHeight) { let downScale = Math.min(maxWidth / image.width, maxHeight / image.height); let canvas = document.createElement("canvas"); canvas.width = image.width * downScale; canvas.height = image.height * downScale; let context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); image.width = canvas.width; image.height = canvas.height; context.drawImage(image, 0, 0, image.width, image.height); result = canvas.toDataURL(); } callback(result); }; image.src = sourceData; } function sendImage(image) { fixImage(image, 320, 240, function(result) { send({image: result}); }); } function fileDropRead(event) { sendImage(event.target.result); } function fileDrop(event) { dragHover(event); let done = false; if (!done) { let files = event.target.files || event.dataTransfer.files; for (let i = 0; i < files.length; i++) { let file = files[i]; if (file.type.substring(0, "image/".length) == "image/") { let reader = new FileReader(); reader.onloadend = fileDropRead; reader.readAsDataURL(file); done = true; } } } if (!done) { let html = event.dataTransfer.getData("text/html"); let match = / [].concat([key], value.args)), })); } gSocket.onmessage = function(event) { _receive_websocket_message(JSON.parse(event.data)); } gSocket.onclose = function(event) { const k_codes = { 1000: 'Normal closure', 1001: 'Going away', 1002: 'Protocol error', 1003: 'Unsupported data', 1005: 'No status received', 1006: 'Abnormal closure', 1007: 'Invalid frame payload data', 1008: 'Policy violation', 1009: 'Message too big', 1010: 'Missing extension', 1011: 'Internal error', 1012: 'Service restart', 1013: 'Try again later', 1014: 'Bad gateway', 1015: 'TLS handshake', }; setStatusMessage("🔴 Closed: " + (k_codes[event.code] || event.code), kErrorColor); } } } function openFile(name) { let newDoc = (name && gFiles[name]) ? gFiles[name].doc : new CodeMirror.Doc("", guessMode(name)); let oldDoc = gEditor.swapDoc(newDoc); if (gFiles[gCurrentFile]) { gFiles[gCurrentFile].doc = oldDoc; } gCurrentFile = name; updateFiles(); gEditor.focus(); } function updateFiles() { let files = document.getElementById("files_list"); files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, { clean: gFiles[file].doc.isClean(gFiles[file].generation), }])); files.current = gCurrentFile; gEditor.focus(); } function makeNewFile(name) { gFiles[name] = { doc: new CodeMirror.Doc("", guessMode(name)), generation: -1, }; openFile(name); } function newFile() { let name = prompt("Name of new file:", "file.js"); if (name && !gFiles[name]) { makeNewFile(name); } } function removeFile() { if (confirm("Remove " + gCurrentFile + "?")) { delete gFiles[gCurrentFile]; openFile(Object.keys(gFiles)[0]); } } window.addEventListener("load", function() { window.addEventListener("hashchange", hashChange); window.addEventListener("focus", focus); window.addEventListener("blur", blur); window.addEventListener("message", message, false); window.addEventListener("online", connectSocket); document.getElementById("name").value = window.location.pathname; document.getElementById('files_hide').addEventListener('click', () => hideFiles()); document.getElementById('files_show').addEventListener('click', () => showFiles()); document.getElementById('closeStats').addEventListener('click', () => closeStats()); document.getElementById('closeEditor').addEventListener('click', () => closeEditor()); document.getElementById('save').addEventListener('click', () => save()); document.getElementById('icon').addEventListener('click', () => changeIcon()); document.getElementById('delete').addEventListener('click', () => deleteApp()); document.getElementById('trace_button').addEventListener('click', function(event) { event.preventDefault(); trace(); }); document.getElementById('stats_button').addEventListener('click', function(event) { event.preventDefault(); toggleStats(); }); document.getElementById('new_file_button').addEventListener('click', () => newFile()); document.getElementById('remove_file_button').addEventListener('click', () => removeFile()); for (let tag of document.getElementsByTagName('a')) { if (tag.accessKey) { tag.classList.add('tooltip_parent'); let tooltip = document.createElement('div'); tooltip.classList.add('tooltip'); if (tag.dataset.tip) { let description = document.createElement('div'); description.innerText = tag.dataset.tip; tooltip.appendChild(description); } let parts = tag.accessKeyLabel ? tag.accessKeyLabel.split('+') : []; for (let i = 0; i < parts.length; i++) { let key = parts[i]; let kbd = document.createElement('kbd'); kbd.innerText = key; tooltip.appendChild(kbd); if (i < parts.length - 1) { tooltip.appendChild(document.createTextNode('+')); } } tag.appendChild(tooltip); } } enableDragDrop(); connectSocket(window.location.pathname); if (window.localStorage.getItem('editing') == '1') { edit(); } else { closeEditor(); } if (window.localStorage.getItem('files') == '1') { showFiles(); } else { hideFiles(); } if (window.localStorage.getItem('stats') == '1') { stats(); } else { closeStats(); } document.getElementById('files_list').addEventListener('file_click', event => openFile(event.detail.file)); });