mirror of
https://github.com/tsl0922/ttyd.git
synced 2025-12-23 12:14:20 +01:00
html: migrate to typescript
This commit is contained in:
361
html/js/app.js
361
html/js/app.js
@@ -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
225
html/js/app.ts
Normal 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);
|
||||
}
|
||||
@@ -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
68
html/js/overlay.ts
Normal 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
191
html/js/zmodem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user