From: Shuanglei Tao Date: Wed, 15 May 2019 15:52:22 +0000 (+0800) Subject: html: migrate to typescript X-Git-Url: http://git.prime8.dev/?a=commitdiff_plain;h=cd1241c0f3c80040adb235e93bda2d7ffce69d52;p=ttyd.git html: migrate to typescript --- diff --git a/html/js/app.js b/html/js/app.js deleted file mode 100644 index 45131f2..0000000 --- a/html/js/app.js +++ /dev/null @@ -1,361 +0,0 @@ -require('../sass/app.scss'); - -// polyfills for ie11 -require('core-js/fn/array'); -require('core-js/fn/object'); -require('core-js/fn/promise'); -require('core-js/fn/typed'); -require('core-js/fn/string/ends-with'); -require('fast-text-encoding'); - -let Zmodem = require('zmodem.js/src/zmodem_browser'); -let Terminal = require('xterm').Terminal; - -Terminal.applyAddon(require('xterm/lib/addons/fit/fit')); -Terminal.applyAddon(require('./overlay')); - -let modal = { - self: document.getElementById('modal'), - header: document.getElementById('header'), - status: { - self: document.getElementById('status'), - filesRemaining: document.getElementById('files-remaining'), - bytesRemaining: document.getElementById('bytes-remaining') - }, - choose: { - self: document.getElementById('choose'), - files: document.getElementById('files'), - filesNames: document.getElementById('file-names') - }, - progress: { - self: document.getElementById('progress'), - fileName: document.getElementById('file-name'), - progressBar: document.getElementById('progress-bar'), - bytesReceived: document.getElementById('bytes-received'), - bytesFile: document.getElementById('bytes-file'), - percentReceived: document.getElementById('percent-received'), - skip: document.getElementById('skip') - } -}; - -function updateFileInfo(fileInfo) { - modal.status.self.style.display = ''; - modal.choose.self.style.display = 'none'; - modal.progress.self.style.display = ''; - modal.status.filesRemaining.textContent = fileInfo.files_remaining; - modal.status.bytesRemaining.textContent = bytesHuman(fileInfo.bytes_remaining, 2); - modal.progress.fileName.textContent = fileInfo.name; -} - -function showReceiveModal(xfer) { - resetModal('Receiving files'); - updateFileInfo(xfer.get_details()); - modal.progress.skip.disabled = false; - modal.progress.skip.onclick = function () { - this.disabled = true; - xfer.skip(); - }; - modal.progress.skip.style.display = ''; - modal.self.classList.add('is-active'); -} - -function showSendModal(callback) { - resetModal('Sending files'); - modal.choose.self.style.display = ''; - modal.choose.files.disabled = false; - modal.choose.files.value = ''; - modal.choose.filesNames.textContent = ''; - modal.choose.files.onchange = function () { - this.disabled = true; - let files = this.files; - let fileNames = ''; - for (let i = 0; i < files.length; i++) { - if (i === 0) { - fileNames = files[i].name; - } else { - fileNames += ', ' + files[i].name; - } - } - modal.choose.filesNames.textContent = fileNames; - callback(files); - }; - modal.self.classList.add('is-active'); -} - -function hideModal() { - modal.self.classList.remove('is-active'); -} - -function resetModal(title) { - modal.header.textContent = title; - modal.status.self.style.display = 'none'; - modal.choose.self.style.display = 'none'; - modal.progress.self.style.display = 'none'; - modal.progress.bytesReceived.textContent = '-'; - modal.progress.percentReceived.textContent = '-%'; - modal.progress.progressBar.textContent = '0%'; - modal.progress.progressBar.value = 0; - modal.progress.skip.style.display = 'none'; -} - -function updateProgress(xfer) { - let size = xfer.get_details().size; - let offset = xfer.get_offset(); - modal.progress.bytesReceived.textContent = bytesHuman(offset, 2); - modal.progress.bytesFile.textContent = bytesHuman(size, 2); - - let percentReceived = (100 * offset / size).toFixed(2); - modal.progress.percentReceived.textContent = percentReceived + '%'; - - modal.progress.progressBar.textContent = percentReceived + '%'; - modal.progress.progressBar.setAttribute('value', percentReceived); -} - -function bytesHuman (bytes, precision) { - if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-'; - if (bytes === 0) return 0; - if (typeof precision === 'undefined') precision = 1; - let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'], - number = Math.floor(Math.log(bytes) / Math.log(1024)); - return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; -} - -function handleSend(zsession) { - return new Promise((res) => { - showSendModal((files) => { - Zmodem.Browser.send_files( - zsession, - files, - { - on_progress: (obj, xfer) => { - updateFileInfo(xfer.get_details()); - updateProgress(xfer); - }, - on_file_complete: (obj) => { - // console.log(obj); - } - } - ).then( - zsession.close.bind(zsession), - console.error.bind(console) - ).then(() => res()); - }); - }); -} - -function handleReceive(zsession) { - zsession.on('offer', (xfer) => { - showReceiveModal(xfer); - let fileBuffer = []; - xfer.on('input', (payload) => { - updateProgress(xfer); - fileBuffer.push(new Uint8Array(payload)); - }); - xfer.accept().then(() => { - Zmodem.Browser.save_to_disk( - fileBuffer, - xfer.get_details().name - ); - }, console.error.bind(console)); - }); - let promise = new Promise((res) => { - zsession.on('session_end', () => res()); - }); - zsession.start(); - return promise; -} - -let terminalContainer = document.getElementById('terminal-container'), - httpsEnabled = window.location.protocol === 'https:', - url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname - + (window.location.pathname.endsWith('/') ? '' : '/') + 'ws' + window.location.search, - textDecoder = new TextDecoder(), - textEncoder = new TextEncoder(), - authToken = (typeof tty_auth_token !== 'undefined') ? tty_auth_token : null, - autoReconnect = -1, - reconnectTimer, term, title, wsError; - -let openWs = function() { - let ws = new WebSocket(url, ['tty']); - let sendMessage = function (message) { - if (ws.readyState === WebSocket.OPEN) { - ws.send(textEncoder.encode(message)); - } - }; - let unloadCallback = function (event) { - let message = 'Close terminal? this will also terminate the command.'; - event.returnValue = message; - return message; - }; - let resetTerm = function() { - hideModal(); - clearTimeout(reconnectTimer); - if (ws.readyState !== WebSocket.CLOSED) { - ws.close(); - } - openWs(); - }; - - let zsentry = new Zmodem.Sentry({ - to_terminal: function _to_terminal(octets) { - let buffer = new Uint8Array(octets).buffer; - term.write(textDecoder.decode(buffer)); - }, - - sender: function _ws_sender_func(octets) { - // limit max packet size to 4096 - while (octets.length) { - let chunk = octets.splice(0, 4095); - let buffer = new Uint8Array(chunk.length + 1); - buffer[0]= '0'.charCodeAt(0); - buffer.set(chunk, 1); - ws.send(buffer); - } - }, - - on_retract: function _on_retract() { - // console.log('on_retract'); - }, - - on_detect: function _on_detect(detection) { - term.setOption('disableStdin', true); - let zsession = detection.confirm(); - let promise = zsession.type === 'send' ? handleSend(zsession) : handleReceive(zsession); - promise.catch(console.error.bind(console)).then(() => { - hideModal(); - term.setOption('disableStdin', false); - }); - } - }); - - ws.binaryType = 'arraybuffer'; - - ws.onopen = function() { - console.log('[ttyd] websocket opened'); - wsError = false; - sendMessage(JSON.stringify({AuthToken: authToken})); - - if (typeof term !== 'undefined') { - term.dispose(); - } - - // expose term handle for some programatic cases - // which need to get the content of the terminal - term = window.term = new Terminal({ - fontSize: 13, - fontFamily: '"Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace', - theme: { - foreground: '#d2d2d2', - background: '#2b2b2b', - cursor: '#adadad', - black: '#000000', - red: '#d81e00', - green: '#5ea702', - yellow: '#cfae00', - blue: '#427ab3', - magenta: '#89658e', - cyan: '#00a7aa', - white: '#dbded8', - brightBlack: '#686a66', - brightRed: '#f54235', - brightGreen: '#99e343', - brightYellow: '#fdeb61', - brightBlue: '#84b0d8', - brightMagenta: '#bc94b7', - brightCyan: '#37e6e8', - brightWhite: '#f1f1f0' - } - }); - - let addDomListener = function(element, type, handler) { - element.addEventListener(type, handler); - term._core.register({ dispose: () => element.removeEventListener(type, handler) }); - }; - - term.onResize((size) => { - if (ws.readyState === WebSocket.OPEN) { - sendMessage('1' + JSON.stringify({columns: size.cols, rows: size.rows})); - } - setTimeout(() => term.showOverlay(size.cols + 'x' + size.rows), 500); - }); - - term.onTitleChange((data) => { - if (data && data !== '') { - document.title = (data + ' | ' + title); - } - }); - - term.onData((data) => sendMessage('0' + data)); - - while (terminalContainer.firstChild) { - terminalContainer.removeChild(terminalContainer.firstChild); - } - - // https://stackoverflow.com/a/27923937/1727928 - window.addEventListener('resize', () => { - clearTimeout(window.resizedFinished); - window.resizedFinished = setTimeout(() => term.fit(), 250); - }); - window.addEventListener('beforeunload', unloadCallback); - - term.open(terminalContainer); - term.fit(); - term.focus(); - }; - - ws.onmessage = function(event) { - let rawData = new Uint8Array(event.data), - cmd = String.fromCharCode(rawData[0]), - data = rawData.slice(1).buffer; - switch(cmd) { - case '0': - try { - zsentry.consume(data); - } catch (e) { - console.error(e); - resetTerm(); - } - break; - case '1': - title = textDecoder.decode(data); - document.title = title; - break; - case '2': - let preferences = JSON.parse(textDecoder.decode(data)); - Object.keys(preferences).forEach((key) => { - console.log('[ttyd] xterm option: ' + key + '=' + preferences[key]); - term.setOption(key, preferences[key]); - }); - break; - case '3': - autoReconnect = JSON.parse(textDecoder.decode(data)); - console.log('[ttyd] reconnect: ' + autoReconnect + ' seconds'); - break; - default: - console.log('[ttyd] unknown command: ' + cmd); - break; - } - }; - - ws.onclose = function(event) { - console.log('[ttyd] websocket closed, code: ' + event.code); - if (term) { - term.off('data'); - term.off('resize'); - if (!wsError) { - term.showOverlay('Connection Closed', null); - } - } - window.removeEventListener('beforeunload', unloadCallback); - // 1000: CLOSE_NORMAL - if (event.code !== 1000 && autoReconnect > 0) { - reconnectTimer = setTimeout(openWs, autoReconnect * 1000); - } - }; -}; - -if (document.readyState === 'complete' || document.readyState !== 'loading') { - openWs(); -} else { - document.addEventListener('DOMContentLoaded', openWs); -} diff --git a/html/js/app.ts b/html/js/app.ts new file mode 100644 index 0000000..663bbdc --- /dev/null +++ b/html/js/app.ts @@ -0,0 +1,225 @@ +import '../sass/app.scss'; + +// polyfills for ie11 +import 'core-js/fn/array'; +import 'core-js/fn/object'; +import 'core-js/fn/promise'; +import 'core-js/fn/typed'; +import 'fast-text-encoding'; + +import { Terminal, ITerminalOptions, IDisposable } from 'xterm'; +import * as fit from 'xterm/lib/addons/fit/fit' +import * as overlay from './overlay' +import { Modal } from './zmodem' +import * as Zmodem from 'zmodem.js/src/zmodem_browser'; +import * as urljoin from 'url-join'; + +Terminal.applyAddon(fit); +Terminal.applyAddon(overlay); + +interface ITtydTerminal extends Terminal { + resizeDisposable: IDisposable; + dataDisposable: IDisposable; + reconnectTimeout: number; + showOverlay(msg: string, timeout?: number); +} + +export interface IWindowWithTerminal extends Window { + term: ITtydTerminal; + resizeTimeout?: number; + tty_auth_token?: string; +} +declare let window: IWindowWithTerminal; + +const modal = new Modal(); +const terminalContainer = document.getElementById('terminal-container'); +const protocol = window.location.protocol === 'https:' ? 'wss://': 'ws://'; +const url = urljoin(protocol, window.location.host, window.location.pathname, 'ws', window.location.search); +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); + +let authToken = (typeof window.tty_auth_token !== 'undefined') ? window.tty_auth_token : null; +let autoReconnect = -1; +let term: ITtydTerminal; +let title: string; +let wsError: boolean; + +let openWs = function() { + let ws = new WebSocket(url, ['tty']); + let sendMessage = function (message) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(textEncoder.encode(message)); + } + }; + let unloadCallback = function (event) { + let message = 'Close terminal? this will also terminate the command.'; + event.returnValue = message; + return message; + }; + let resetTerm = function() { + modal.hide(); + clearTimeout(term.reconnectTimeout); + if (ws.readyState !== WebSocket.CLOSED) { + ws.close(); + } + openWs(); + }; + + let zsentry = new Zmodem.Sentry({ + to_terminal: function _to_terminal(octets) { + let buffer = new Uint8Array(octets).buffer; + term.write(textDecoder.decode(buffer)); + }, + + sender: function _ws_sender_func(octets) { + // limit max packet size to 4096 + while (octets.length) { + let chunk = octets.splice(0, 4095); + let buffer = new Uint8Array(chunk.length + 1); + buffer[0]= '0'.charCodeAt(0); + buffer.set(chunk, 1); + ws.send(buffer); + } + }, + + on_retract: function _on_retract() { + // console.log('on_retract'); + }, + + on_detect: function _on_detect(detection) { + term.setOption('disableStdin', true); + let zsession = detection.confirm(); + let promise = zsession.type === 'send' ? modal.handleSend(zsession) : modal.handleReceive(zsession); + promise.catch(console.error.bind(console)).then(() => { + modal.hide(); + term.setOption('disableStdin', false); + }); + } + }); + + ws.binaryType = 'arraybuffer'; + + ws.onopen = function() { + console.log('[ttyd] websocket opened'); + wsError = false; + sendMessage(JSON.stringify({AuthToken: authToken})); + + if (typeof term !== 'undefined') { + term.dispose(); + } + + // expose term handle for some programatic cases + // which need to get the content of the terminal + term = window.term = new Terminal({ + fontSize: 13, + fontFamily: '"Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace', + theme: { + foreground: '#d2d2d2', + background: '#2b2b2b', + cursor: '#adadad', + black: '#000000', + red: '#d81e00', + green: '#5ea702', + yellow: '#cfae00', + blue: '#427ab3', + magenta: '#89658e', + cyan: '#00a7aa', + white: '#dbded8', + brightBlack: '#686a66', + brightRed: '#f54235', + brightGreen: '#99e343', + brightYellow: '#fdeb61', + brightBlue: '#84b0d8', + brightMagenta: '#bc94b7', + brightCyan: '#37e6e8', + brightWhite: '#f1f1f0' + } + } as ITerminalOptions); + + term.resizeDisposable = term.onResize((size: {cols: number, rows: number}) => { + if (ws.readyState === WebSocket.OPEN) { + sendMessage('1' + JSON.stringify({columns: size.cols, rows: size.rows})); + } + setTimeout(() => (term).showOverlay(size.cols + 'x' + size.rows), 500); + }); + + term.onTitleChange((data: string) => { + if (data && data !== '') { + document.title = (data + ' | ' + title); + } + }); + + term.dataDisposable = term.onData((data: string) => sendMessage('0' + data)); + + while (terminalContainer.firstChild) { + terminalContainer.removeChild(terminalContainer.firstChild); + } + + // https://stackoverflow.com/a/27923937/1727928 + window.addEventListener('resize', () => { + clearTimeout(window.resizeTimeout); + window.resizeTimeout = setTimeout(() => (term).fit(), 250); + }); + window.addEventListener('beforeunload', unloadCallback); + + term.open(terminalContainer); + (term).fit(); + term.focus(); + }; + + ws.onmessage = function(event: MessageEvent) { + let rawData = new Uint8Array(event.data), + cmd = String.fromCharCode(rawData[0]), + data = rawData.slice(1).buffer; + switch(cmd) { + case '0': + try { + zsentry.consume(data); + } catch (e) { + console.error(e); + resetTerm(); + } + break; + case '1': + title = textDecoder.decode(data); + document.title = title; + break; + case '2': + let preferences = JSON.parse(textDecoder.decode(data)); + Object.keys(preferences).forEach((key) => { + console.log('[ttyd] xterm option: ' + key + '=' + preferences[key]); + term.setOption(key, preferences[key]); + }); + break; + case '3': + autoReconnect = JSON.parse(textDecoder.decode(data)); + console.log('[ttyd] reconnect: ' + autoReconnect + ' seconds'); + break; + default: + console.log('[ttyd] unknown command: ' + cmd); + break; + } + }; + + ws.onclose = function(event: CloseEvent) { + console.log('[ttyd] websocket closed, code: ' + event.code); + if (term) { + term.resizeDisposable.dispose(); + term.dataDisposable.dispose(); + if (!wsError) { + (term).showOverlay('Connection Closed', null); + } + } + window.removeEventListener('beforeunload', unloadCallback); + // 1000: CLOSE_NORMAL + if (event.code !== 1000 && autoReconnect > 0) { + term.reconnectTimeout = setTimeout(openWs, autoReconnect * 1000); + } + }; +}; + +if (document.readyState === 'complete' || document.readyState !== 'loading') { + openWs(); +} else { + document.addEventListener('DOMContentLoaded', openWs); +} diff --git a/html/js/overlay.js b/html/js/overlay.js deleted file mode 100644 index 9f323c9..0000000 --- a/html/js/overlay.js +++ /dev/null @@ -1,66 +0,0 @@ -// ported from hterm.Terminal.prototype.showOverlay -// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); - -function showOverlay(term, msg, timeout) { - if (!term.overlayNode_) { - if (!term.element) - return; - term.overlayNode_ = document.createElement('div'); - term.overlayNode_.style.cssText = ( - 'border-radius: 15px;' + - 'font-size: xx-large;' + - 'opacity: 0.75;' + - 'padding: 0.2em 0.5em 0.2em 0.5em;' + - 'position: absolute;' + - '-webkit-user-select: none;' + - '-webkit-transition: opacity 180ms ease-in;' + - '-moz-user-select: none;' + - '-moz-transition: opacity 180ms ease-in;'); - - term.overlayNode_.addEventListener('mousedown', function(e) { - e.preventDefault(); - e.stopPropagation(); - }, true); - } - term.overlayNode_.style.color = "#101010"; - term.overlayNode_.style.backgroundColor = "#f0f0f0"; - - term.overlayNode_.textContent = msg; - term.overlayNode_.style.opacity = '0.75'; - - if (!term.overlayNode_.parentNode) - term.element.appendChild(term.overlayNode_); - - var divSize = term.element.getBoundingClientRect(); - var overlaySize = term.overlayNode_.getBoundingClientRect(); - - term.overlayNode_.style.top = - (divSize.height - overlaySize.height) / 2 + 'px'; - term.overlayNode_.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; - - if (term.overlayTimeout_) - clearTimeout(term.overlayTimeout_); - - if (timeout === null) - return; - - term.overlayTimeout_ = setTimeout(function() { - term.overlayNode_.style.opacity = '0'; - term.overlayTimeout_ = setTimeout(function() { - if (term.overlayNode_.parentNode) - term.overlayNode_.parentNode.removeChild(term.overlayNode_); - term.overlayTimeout_ = null; - term.overlayNode_.style.opacity = '0.75'; - }, 200); - }, timeout || 1500); -} -exports.showOverlay = showOverlay; - -function apply(terminalConstructor) { - terminalConstructor.prototype.showOverlay = function (msg, timeout) { - return showOverlay(this, msg, timeout); - }; -} -exports.apply = apply; diff --git a/html/js/overlay.ts b/html/js/overlay.ts new file mode 100644 index 0000000..9c39848 --- /dev/null +++ b/html/js/overlay.ts @@ -0,0 +1,68 @@ +// ported from hterm.Terminal.prototype.showOverlay +// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js +import { Terminal } from 'xterm'; + +interface IOverlayAddonTerminal extends Terminal { + __overlayNode?: HTMLElement + __overlayTimeout?: number +} + +export function showOverlay(term: Terminal, msg: string, timeout: number): void { + const addonTerminal = term; + if (!addonTerminal.__overlayNode) { + if (!term.element) + return; + addonTerminal.__overlayNode = document.createElement('div'); + addonTerminal.__overlayNode.style.cssText = ( + 'border-radius: 15px;' + + 'font-size: xx-large;' + + 'opacity: 0.75;' + + 'padding: 0.2em 0.5em 0.2em 0.5em;' + + 'position: absolute;' + + '-webkit-user-select: none;' + + '-webkit-transition: opacity 180ms ease-in;' + + '-moz-user-select: none;' + + '-moz-transition: opacity 180ms ease-in;'); + + addonTerminal.__overlayNode.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + }, true); + } + addonTerminal.__overlayNode.style.color = "#101010"; + addonTerminal.__overlayNode.style.backgroundColor = "#f0f0f0"; + + addonTerminal.__overlayNode.textContent = msg; + addonTerminal.__overlayNode.style.opacity = '0.75'; + + if (!addonTerminal.__overlayNode.parentNode) + term.element.appendChild(addonTerminal.__overlayNode); + + const divSize = term.element.getBoundingClientRect(); + const overlaySize = addonTerminal.__overlayNode.getBoundingClientRect(); + + addonTerminal.__overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px'; + addonTerminal.__overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px'; + + if (addonTerminal.__overlayTimeout) + clearTimeout(addonTerminal.__overlayTimeout); + + if (timeout === null) + return; + + addonTerminal.__overlayTimeout = setTimeout(() => { + addonTerminal.__overlayNode.style.opacity = '0'; + addonTerminal.__overlayTimeout = setTimeout(() => { + if (addonTerminal.__overlayNode.parentNode) + addonTerminal.__overlayNode.parentNode.removeChild(addonTerminal.__overlayNode); + addonTerminal.__overlayTimeout = null; + addonTerminal.__overlayNode.style.opacity = '0.75'; + }, 200); + }, timeout || 1500); +} + +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).showOverlay = function (msg: string, timeout?: number): void { + return showOverlay(this, msg, timeout); + }; +} diff --git a/html/js/zmodem.ts b/html/js/zmodem.ts new file mode 100644 index 0000000..b019915 --- /dev/null +++ b/html/js/zmodem.ts @@ -0,0 +1,191 @@ +import * as Promise from 'core-js/fn/promise'; +import * as Zmodem from 'zmodem.js/src/zmodem_browser'; + +class Status { + element: HTMLElement; + filesRemaining: HTMLElement; + bytesRemaining: HTMLElement; + + constructor() { + this.element = document.getElementById('status'); + this.filesRemaining = document.getElementById('files-remaining'); + this.bytesRemaining = document.getElementById('bytes-remaining'); + } +} + +class Choose { + element: HTMLElement; + files: HTMLInputElement; + filesNames: HTMLElement; + + constructor() { + this.element = document.getElementById('choose'); + this.files = document.getElementById('files'); + this.filesNames = document.getElementById('file-names'); + } +} + +class Progress { + element: HTMLElement; + fileName: HTMLElement; + progressBar: HTMLProgressElement; + bytesReceived: HTMLElement; + bytesFile: HTMLElement; + percentReceived: HTMLElement; + skip: HTMLLinkElement; + + constructor() { + this.element = document.getElementById('progress'); + this.fileName = document.getElementById('file-name'); + this.progressBar = document.getElementById('progress-bar'); + this.bytesReceived = document.getElementById('bytes-received'); + this.bytesFile = document.getElementById('bytes-file'); + this.percentReceived = document.getElementById('percent-received'); + this.skip = document.getElementById('skip'); + + } +} + +function bytesHuman (bytes: any, precision: number): string { + if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-'; + if (bytes === 0) return '0'; + if (typeof precision === 'undefined') precision = 1; + let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'], + number = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; +} + +export class Modal { + element: HTMLElement; + header: HTMLElement; + status: Status; + choose: Choose; + progress: Progress; + + constructor() { + this.element = document.getElementById('modal'); + this.header = document.getElementById('header'); + this.status = new Status(); + this.choose = new Choose(); + this.progress = new Progress(); + } + + public reset(title): void { + this.header.textContent = title; + this.status.element.style.display = 'none'; + this.choose.element.style.display = 'none'; + this.progress.element.style.display = 'none'; + this.progress.bytesReceived.textContent = '-'; + this.progress.percentReceived.textContent = '-%'; + this.progress.progressBar.textContent = '0%'; + this.progress.progressBar.value = 0; + this.progress.skip.style.display = 'none'; + } + + public hide(): void { + this.element.classList.remove('is-active'); + } + + public updateFileInfo(fileInfo): void { + this.status.element.style.display = ''; + this.choose.element.style.display = 'none'; + this.progress.element.style.display = ''; + this.status.filesRemaining.textContent = fileInfo.files_remaining; + this.status.bytesRemaining.textContent = bytesHuman(fileInfo.bytes_remaining, 2); + this.progress.fileName.textContent = fileInfo.name; + } + + public showReceive(xfer): void { + this.reset('Receiving files'); + this.updateFileInfo(xfer.get_details()); + this.progress.skip.disabled = false; + this.progress.skip.onclick = function () { + (this).disabled = true; + xfer.skip(); + }; + this.progress.skip.style.display = ''; + this.element.classList.add('is-active'); + } + + public showSend(callback): void { + this.reset('Sending files'); + this.choose.element.style.display = ''; + this.choose.files.disabled = false; + this.choose.files.value = ''; + this.choose.filesNames.textContent = ''; + let self:Modal = this; + this.choose.files.onchange = function () { + (this).disabled = true; + let files:FileList = (this).files; + let fileNames:string = ''; + for (let i = 0; i < files.length; i++) { + if (i === 0) { + fileNames = files[i].name; + } else { + fileNames += ', ' + files[i].name; + } + } + self.choose.filesNames.textContent = fileNames; + callback(files); + }; + this.element.classList.add('is-active'); + } + + public updateProgress(xfer): void { + let size = xfer.get_details().size; + let offset = xfer.get_offset(); + this.progress.bytesReceived.textContent = bytesHuman(offset, 2); + this.progress.bytesFile.textContent = bytesHuman(size, 2); + + let percentReceived = (100 * offset / size).toFixed(2); + this.progress.percentReceived.textContent = percentReceived + '%'; + + this.progress.progressBar.textContent = percentReceived + '%'; + this.progress.progressBar.setAttribute('value', percentReceived); + } + + public handleSend(zsession): Promise { + return new Promise((res) => { + this.showSend((files) => { + Zmodem.Browser.send_files( + zsession, + files, + { + on_progress: (obj, xfer) => { + this.updateFileInfo(xfer.get_details()); + this.updateProgress(xfer); + }, + on_file_complete: (obj) => { + // console.log(obj); + } + } + ).then( + zsession.close.bind(zsession), + console.error.bind(console) + ).then(() => res()); + }); + }); + } + + public handleReceive(zsession): Promise { + zsession.on('offer', (xfer) => { + this.showReceive(xfer); + let fileBuffer = []; + xfer.on('input', (payload) => { + this.updateProgress(xfer); + fileBuffer.push(new Uint8Array(payload)); + }); + xfer.accept().then(() => { + Zmodem.Browser.save_to_disk( + fileBuffer, + xfer.get_details().name + ); + }, console.error.bind(console)); + }); + let promise = new Promise((res) => { + zsession.on('session_end', () => res()); + }); + zsession.start(); + return promise; + } +} \ No newline at end of file diff --git a/html/package.json b/html/package.json index 7f3282f..939feae 100644 --- a/html/package.json +++ b/html/package.json @@ -16,6 +16,7 @@ "dependencies": { "core-js": "^2.5.3", "fast-text-encoding": "^1.0.0", + "url-join": "^4.0.0", "xterm": "^3.13.0", "zmodem.js": "^0.1.7" }, @@ -37,6 +38,8 @@ "optimize-css-assets-webpack-plugin": "^4.0.1", "sass-loader": "^6.0.6", "style-loader": "^0.19.1", + "ts-loader": "^6.0.0", + "typescript": "^3.4.5", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "^4.6.0", "webpack-cli": "^2.1.2", diff --git a/html/tsconfig.json b/html/tsconfig.json new file mode 100644 index 0000000..f2853b8 --- /dev/null +++ b/html/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "module": "commonjs", + "target": "es5", + "lib": [ "dom", "es5"], + "jsx": "react", + "allowJs": true + } +} \ No newline at end of file diff --git a/html/webpack.config.js b/html/webpack.config.js index d324eed..48cdfb6 100644 --- a/html/webpack.config.js +++ b/html/webpack.config.js @@ -3,7 +3,7 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const devMode = process.env.NODE_ENV !== 'production'; module.exports = { - entry: './js/app.js', + entry: './js/app.ts', output: { path: __dirname + '/dist', filename: devMode ? '[name].js' : '[name].[hash].js', @@ -12,7 +12,7 @@ module.exports = { rules: [ { test: /\.js$/, - exclude: /node_modules\/(?!zmodem.js\/)/, + include: __dirname + '/node_modules/zmodem.js', use: { loader: 'babel-loader', options: { @@ -20,6 +20,11 @@ module.exports = { } }, }, + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, { test: /\.s?[ac]ss$/, use: [ @@ -30,6 +35,9 @@ module.exports = { }, ] }, + resolve: { + extensions: [ '.tsx', '.ts', '.js' ] + }, plugins: [ new CopyWebpackPlugin([ { from: 'favicon.png', to: '.' } @@ -42,5 +50,5 @@ module.exports = { performance : { hints : false }, - devtool: devMode ? 'cheap-module-eval-source-map' : 'source-map', -} + devtool: 'inline-source-map', +}; diff --git a/html/yarn.lock b/html/yarn.lock index 4494eef..50d7524 100644 --- a/html/yarn.lock +++ b/html/yarn.lock @@ -464,7 +464,7 @@ babel-core@^6.26.0: babel-core@^6.26.3: version "6.26.3" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha1-suLwnjQtDwyI4vAuBneUEl51wgc= + integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== dependencies: babel-code-frame "^6.26.0" babel-generator "^6.26.0" @@ -633,9 +633,9 @@ babel-helpers@^6.24.1: babel-template "^6.24.1" babel-loader@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015" - integrity sha1-40Y5OL1ObVXRwXTFSF1AahiO0BU= + version "7.1.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" + integrity sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw== dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -853,7 +853,17 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015 babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: +babel-plugin-transform-es2015-modules-commonjs@^6.23.0: + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-commonjs@^6.24.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" integrity sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo= @@ -997,9 +1007,9 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-types "^6.24.1" babel-preset-env@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" - integrity sha1-oYtWTMm5r99KrleuPBsNmRiOb0g= + version "1.7.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" + integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg== dependencies: babel-plugin-check-es2015-constants "^6.22.0" babel-plugin-syntax-trailing-function-commas "^6.22.0" @@ -1028,7 +1038,7 @@ babel-preset-env@^1.6.1: babel-plugin-transform-es2015-unicode-regex "^6.22.0" babel-plugin-transform-exponentiation-operator "^6.22.0" babel-plugin-transform-regenerator "^6.22.0" - browserslist "^2.1.2" + browserslist "^3.2.6" invariant "^2.2.2" semver "^5.3.0" @@ -1311,6 +1321,13 @@ braces@^2.3.0, braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -1382,13 +1399,13 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" -browserslist@^2.1.2: - version "2.11.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" - integrity sha1-/jYWeu0bvN5IJ+v+cTR6LMcLmbI= +browserslist@^3.2.6: + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== dependencies: - caniuse-lite "^1.0.30000792" - electron-to-chromium "^1.3.30" + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" buffer-from@^1.0.0: version "1.0.0" @@ -1526,10 +1543,10 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000792.tgz#a7dac6dc9f5181b675fd69e5cb06fefb523157f8" integrity sha1-p9rG3J9RgbZ1/Wnlywb++1IxV/g= -caniuse-lite@^1.0.30000792: - version "1.0.30000792" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000792.tgz#d0cea981f8118f3961471afbb43c9a1e5bbf0332" - integrity sha1-0M6pgfgRjzlhRxr7tDyaHlu/AzI= +caniuse-lite@^1.0.30000844: + version "1.0.30000967" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000967.tgz#a5039577806fccee80a04aaafb2c0890b1ee2f73" + integrity sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ== capture-stack-trace@^1.0.0: version "1.0.0" @@ -1944,11 +1961,18 @@ content-type@^1.0.0: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha1-4TjMdeBAxyexlm/l5fjJruJW/js= -convert-source-map@^1.5.0, convert-source-map@^1.5.1: +convert-source-map@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" integrity sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU= +convert-source-map@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + cookies@~0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" @@ -2589,13 +2613,18 @@ electron-releases@^2.1.0: resolved "https://registry.yarnpkg.com/electron-releases/-/electron-releases-2.1.0.tgz#c5614bf811f176ce3c836e368a0625782341fd4e" integrity sha1-xWFL+BHxds48g242igYleCNB/U4= -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: +electron-to-chromium@^1.2.7: version "1.3.30" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.30.tgz#9666f532a64586651fc56a72513692e820d06a80" integrity sha1-lmb1MqZFhmUfxWpyUTaS6CDQaoA= dependencies: electron-releases "^2.1.0" +electron-to-chromium@^1.3.47: + version "1.3.134" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.134.tgz#550222bddac43c6bd6c445c3543a0fe8a615021d" + integrity sha512-C3uK2SrtWg/gSWaluLHWSHjyebVZCe4ZC0NVgTAoTq8tCR9FareRK5T7R7AS/nPZShtlEcjVMX1kQ8wi4nU68w== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -3028,6 +3057,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + find-cache-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" @@ -4427,6 +4463,11 @@ is-number@^4.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" integrity sha1-ACbjf1RU1z41bf5lZGmYZ8an8P8= +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -5440,6 +5481,14 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, mic snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -6409,6 +6458,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.5: + version "2.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" + integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7550,6 +7604,11 @@ semver@^5.0.3, semver@^5.1.0, semver@^5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs= +semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -8232,6 +8291,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -8276,6 +8342,17 @@ trim-right@^1.0.1: dependencies: glob "^6.0.4" +ts-loader@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.0.0.tgz#d489f49410725a12e696ad0b67c33937a7c49147" + integrity sha512-lszy+D41R0Te2+loZxADWS+E1+Z55A+i3dFfFie1AZHL++65JRKVDBPQgeWgRrlv5tbxdU3zOtXp8b7AFR6KEg== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -8311,6 +8388,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== + uglify-es@^3.3.4: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" diff --git a/src/index.html b/src/index.html index 9fe72d5..c0b98d8 100644 --- a/src/index.html +++ b/src/index.html @@ -1 +1 @@ -ttyd - Terminal
\ No newline at end of file +ttyd - Terminal
\ No newline at end of file