html: migrate to typescript

This commit is contained in:
Shuanglei Tao
2019-05-15 23:52:22 +08:00
parent ba7779547d
commit cd1241c0f3
10 changed files with 614 additions and 453 deletions

View File

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

225
html/js/app.ts Normal file
View File

@@ -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 = <ITtydTerminal>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(() => (<any>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 = <number><any>setTimeout(() => (<any>term).fit(), 250);
});
window.addEventListener('beforeunload', unloadCallback);
term.open(terminalContainer);
(<any>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) {
(<any>term).showOverlay('Connection Closed', null);
}
}
window.removeEventListener('beforeunload', unloadCallback);
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && autoReconnect > 0) {
term.reconnectTimeout = <number><any>setTimeout(openWs, autoReconnect * 1000);
}
};
};
if (document.readyState === 'complete' || document.readyState !== 'loading') {
openWs();
} else {
document.addEventListener('DOMContentLoaded', openWs);
}

View File

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

68
html/js/overlay.ts Normal file
View File

@@ -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 = <IOverlayAddonTerminal> 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 = <number><any>setTimeout(() => {
addonTerminal.__overlayNode.style.opacity = '0';
addonTerminal.__overlayTimeout = <number><any>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 {
(<any>terminalConstructor.prototype).showOverlay = function (msg: string, timeout?: number): void {
return showOverlay(this, msg, timeout);
};
}

191
html/js/zmodem.ts Normal file
View File

@@ -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 = <HTMLInputElement>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 = <HTMLProgressElement>document.getElementById('progress-bar');
this.bytesReceived = document.getElementById('bytes-received');
this.bytesFile = document.getElementById('bytes-file');
this.percentReceived = document.getElementById('percent-received');
this.skip = <HTMLLinkElement>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 () {
(<HTMLLinkElement>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 () {
(<HTMLInputElement>this).disabled = true;
let files:FileList = (<HTMLInputElement>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<any> {
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<any> {
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;
}
}

View File

@@ -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",

11
html/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"lib": [ "dom", "es5"],
"jsx": "react",
"allowJs": true
}
}

View File

@@ -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',
};

View File

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

2
src/index.html vendored

File diff suppressed because one or more lines are too long