"use strict"; var gSocket; var gCredentials; var gCurrentFile; var gFiles = {}; var gApp = {files: {}}; var gEditor; var gSplit; var gGraphs = {}; var gParentApp; var kErrorColor = "#dc322f"; var kStatusColor = "#fff"; 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; } var search = nodes.shift(); var head = document.head; var found = false; for (var i = 0; i < head.childNodes.length; i++) { if (head.childNodes[i].tagName == search.tagName) { var match = true; for (var 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 { var node = document.createElement(search.tagName); node.onreadystatechange = node.onload = function() { ensureLoaded(nodes, callback); }; for (var 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 toggleEdit() { if (editing()) { closeEditor(); } else { edit(); } } function edit() { if (editing()) { return; } if (gSplit) { gSplit.destroy(); gSplit = undefined; } gSplit = Split(['#editPane', '#viewPane'], {minSize: 0}); ensureLoaded([ {tagName: "script", attributes: {src: "/static/codemirror/codemirror.min.js"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/base16-dark.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/matchesonscrollbar.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/dialog.min.css"}}, {tagName: "link", attributes: {rel: "stylesheet", href: "/static/codemirror/codemirror.min.css"}}, {tagName: "script", attributes: {src: "/static/codemirror/trailingspace.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/dialog.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/search.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/searchcursor.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/jump-to-line.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/matchesonscrollbar.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/annotatescrollbar.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/javascript.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/css.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/xml.min.js"}}, {tagName: "script", attributes: {src: "/static/codemirror/htmlmixed.min.js"}}, ], function() { load(); }); } function trace() { var request = new XMLHttpRequest(); request.addEventListener("loadend", function() { if (request.status == 200) { /* The trace is loaded. */ var perfetto = window.open('/perfetto/'); var done = false; if (perfetto) { function message_handler(message) { if (message.data == 'PONG') { perfetto.postMessage({ perfetto: { buffer: request.response, title: 'Tilde Friends Trace', url: window.location.href, } }, '*'); done = true; } } window.addEventListener('message', message_handler); function ping_perfetto() { perfetto.postMessage('PING', window.location.origin); if (!done && !perfetto.closed) { setTimeout(ping_perfetto, 50); } else { window.removeEventListener('message', message_handler); } } setTimeout(ping_perfetto, 50); } else { alert("Unable to open perfetto."); } } else { alert("Failed to load trace: " + request.status + "."); } }); request.addEventListener("error", function() { alert("Error loading trace."); }); request.addEventListener("timeout", function() { alert("Timed out loading trace."); }); request.addEventListener("abort", function() { alert("Loading trace aborted."); }); request.responseType = 'arraybuffer'; request.open("GET", "/trace"); request.send(); } function stats() { document.getElementById("statsPane").style.display = 'flex'; } function guessMode(name) { return name.endsWith(".js") ? "javascript" : name.endsWith(".html") ? "htmlmixed" : null; } function loadFile(name, id) { return new Promise(function(resolve, reject) { var request = new XMLHttpRequest(); request.addEventListener("loadend", function() { if (request.status == 200) { gFiles[name].doc = new CodeMirror.Doc(request.responseText, guessMode(name)); if (!Object.values(gFiles).some(x => !x.doc)) { document.getElementById("editPane").style.display = 'flex'; openFile(Object.keys(gFiles).sort()[0]); resolve(); } } else { reject('Error loading source.'); } }); request.addEventListener("error", function() { alert("Error loading source."); closeEditor(); reject('Error loading source.'); }); request.addEventListener("timeout", function() { alert("Timed out loading source."); closeEditor(); reject('Timed out loading source.'); }); request.addEventListener("abort", function() { alert("Loading source aborted."); closeEditor(); reject('Loading source aborted.'); }); request.open("GET", "/" + id + "/view"); request.send(); }); } function load(path) { return new Promise(function(resolve, reject) { var request = new XMLHttpRequest(); request.addEventListener("loadend", function() { if (request.status == 200 || request.status == 404) { if (!gEditor) { gEditor = CodeMirror.fromTextArea(document.getElementById("editor"), { 'theme': 'base16-dark', 'lineNumbers': true, 'tabSize': 4, 'indentUnit': 4, 'indentWithTabs': true, 'showTrailingSpace': true, }); gEditor.on('changes', function() { updateFiles(); }); } gFiles = {}; var text; var isApp = false; var promises = []; if (request.status == 200) { text = request.responseText; try { var json = JSON.parse(text); 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.parse(text); } } catch { } } else { reject('Load failed.'); } if (!isApp) { document.getElementById("editPane").style.display = 'flex'; if (!text) { text = '// New script.\n'; } gCurrentFile = 'app.js'; gFiles[gCurrentFile] = { doc: new CodeMirror.Doc(text, guessMode(gCurrentFile)), }; openFile(gCurrentFile); } Promise.all(promises).then(resolve).catch(reject); } }); request.addEventListener("error", function() { alert("Error loading source."); closeEditor(); reject('Error loading source.'); }); request.addEventListener("timeout", function() { alert("Timed out loading source."); closeEditor(); reject('Timed out loading source.'); }); request.addEventListener("abort", function() { alert("Loading source aborted."); closeEditor(); reject('Loading source aborted.'); }); request.open("GET", (path || url()) + "view"); request.send(); }); } function closeStats() { document.getElementById("statsPane").style.display = 'none'; } function closeEditor() { 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; document.getElementById("push_to_parent").disabled = true; document.getElementById("pull_from_parent").disabled = true; if (gCurrentFile) { gFiles[gCurrentFile].doc = gEditor.getDoc(); } var appFinished = function(success) { document.getElementById("save").disabled = false; document.getElementById("push_to_parent").disabled = false; document.getElementById("pull_from_parent").disabled = false; Object.values(gFiles).forEach(function(file) { file.generation = file.doc.changeGeneration(); }); updateFiles(); } var save_path = save_to; if (!save_to) { var name = document.getElementById("name"); if (name && name.value) { save_path = name.value; } else { save_path = url(); } } var always = function() { var anyUnfinished = Object.values(gFiles).some(x => x.request); var anyUnsaved = Object.values(gFiles).some(x => !x.doc.isClean(x.generation) && !x.id); if (!anyUnfinished && !anyUnsaved) { var app = { type: "tildefriends-app", files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])), }; Object.values(gFiles).forEach(function(file) { delete file.id; }); gApp = JSON.parse(JSON.stringify(app)); var request = new XMLHttpRequest(); request.addEventListener("error", function() { alert("Error saving: " + request.responseText); appFinished(false); }); request.addEventListener("loadend", function() { if (request.status == 200) { if (save_path != window.location.pathname) { alert('Saved to ' + save_path + '.'); } else { reconnect(save_path); } appFinished(true); } else { alert("Unable to save: " + request.responseText); appFinished(false); } }); request.addEventListener("timeout", function() { alert("Timed out saving: " + request.responseText); appFinished(false); }); request.addEventListener("abort", function() { alert("Save aborted: " + request.responseText); appFinished(false); }); request.open("POST", save_path + 'save', true); request.setRequestHeader("Content-Type", "text/json"); request.send(JSON.stringify(app)); } else if (!anyUnfinished) { appFinished(false); } }; var anySkipped = false; Object.values(gFiles).forEach(function(file) { if (file.doc.isClean(file.generation)) { anySkipped = true; return; } delete file.id; file.request = new XMLHttpRequest(); file.request.addEventListener("error", function() { alert("Error saving: " + file.request.responseText); file.request = null; always(); }); file.request.addEventListener("loadend", function() { if (file.request.status == 200) { file.id = file.request.responseText; if (file.id.charAt(0) == '/') { file.id = file.id.substr(1); } } else { alert("Unable to save: " + file.request.responseText); } file.request = null; always(); }); file.request.addEventListener("timeout", function() { alert("Timed out saving: " + file.request.responseText); file.request = null; always(); }); file.request.addEventListener("abort", function() { alert("Save aborted: " + file.request.responseText); file.request = null; always(); }); file.request.open("POST", "/save", true); file.request.setRequestHeader("Content-Type", "text/plain"); file.request.send(file.doc.getValue()); }); if (anySkipped) { always(); } } function pullFromParent() { load(gParentApp ? gParentApp.path : null).then(x => save()); } function pushToParent() { save(gParentApp ? gParentApp.path : null); } function url() { var hash = window.location.href.indexOf('#'); var question = window.location.href.indexOf('?'); var 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 receive(message) { if (message && message.action == "session") { setStatusMessage("...Executing...", kStatusColor, true); gCredentials = message.credentials; gParentApp = message.parentApp; updateLogin(); var parent_enabled = message.parentApp; document.getElementById('push_to_parent').style.display = parent_enabled ? 'inline-block' : 'none'; document.getElementById('pull_from_parent').style.display = parent_enabled ? 'inline-block' : 'none'; } else if (message && message.action == "ready") { setStatusMessage(null); if (window.location.hash) { send({event: "hashChange", hash: window.location.hash}); } } else if (message && message.action == "setDocument") { var iframe = document.getElementById("document"); iframe.srcdoc = message.content; } else if (message && message.action == "postMessage") { var iframe = document.getElementById("document"); iframe.contentWindow.postMessage(message.message, "*"); } else if (message && message.action == "ping") { send({action: "pong"}); } else if (message && message.action == "error") { if (message.error) { if (typeof(message.error) == 'string') { setStatusMessage(message.error, '#f00', false); } else { setStatusMessage(message.error.message + '\n' + message.error.stack, '#f00', false); } } console.log('error', message); } else if (message && message.action == "stats") { var now = new Date().getTime(); for (var key of Object.keys(message.stats)) { if (!gGraphs[key]) { var graph = { chart: new SmoothieChart({ millisPerPixel: 100, minValue: 0, grid: { millisPerLine: 1000, verticalSections: 10, }, }), canvas: document.createElement('canvas'), timeseries: new TimeSeries(), }; gGraphs[key] = graph; graph.canvas.width = '320'; graph.canvas.width = '240'; var div = document.createElement('div'); div.innerText = key; document.getElementById('graphs').appendChild(div); document.getElementById('graphs').appendChild(graph.canvas); graph.chart.streamTo(graph.canvas, 1000); graph.chart.addTimeSeries(graph.timeseries, {lineWidth: 2}); } gGraphs[key].timeseries.append(now, message.stats[key]); } } } 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, keep) { var node = document.getElementById("status"); if (!keep) { while (node.firstChild) { node.removeChild(node.firstChild); } } if (message) { node.appendChild(document.createTextNode(message)); node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor)); } } function send(value) { try { gSocket.send(JSON.stringify(value)); } catch (error) { setStatusMessage("Send failed: " + error.toString(), kErrorColor); } } function updateLogin() { var login = document.getElementById("login"); while (login.firstChild) { login.removeChild(login.firstChild); } var a = document.createElement("a"); if (gCredentials && gCredentials.session) { a.appendChild(document.createTextNode("logout " + gCredentials.session.name)); a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash())); } else { a.appendChild(document.createTextNode("login")); a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash())); } login.appendChild(a); } var gOriginalInput; function dragHover(event) { event.stopPropagation(); event.preventDefault(); var 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) { var result = sourceData; var image = new Image(); image.crossOrigin = "anonymous"; image.referrerPolicy = "no-referrer"; image.onload = function() { if (image.width > maxWidth || image.height > maxHeight) { var downScale = Math.min(maxWidth / image.width, maxHeight / image.height); var canvas = document.createElement("canvas"); canvas.width = image.width * downScale; canvas.height = image.height * downScale; var 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); var done = false; if (!done) { var files = event.target.files || event.dataTransfer.files; for (var i = 0; i < files.length; i++) { var file = files[i]; if (file.type.substring(0, "image/".length) == "image/") { var reader = new FileReader(); reader.onloadend = fileDropRead; reader.readAsDataURL(file); done = true; } } } if (!done) { var html = event.dataTransfer.getData("text/html"); var match = /