Files
ttyd/html/js/app.js
2018-01-17 18:11:28 +08:00

364 lines
12 KiB
JavaScript

// polyfills for ie11
require('core-js/fn/array');
require('core-js/fn/object');
require('core-js/fn/promise');
require('core-js/fn/typed');
require('fast-text-encoding');
var Zmodem = require('zmodem.js/src/zmodem_browser');
var Terminal = require('xterm').Terminal;
Terminal.applyAddon(require('xterm/lib/addons/fit'));
Terminal.applyAddon(require('xterm/lib/addons/winptyCompat'));
Terminal.applyAddon(require('./overlay'));
var 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;
var files = this.files;
var fileNames = '';
for (var 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) {
var size = xfer.get_details().size;
var offset = xfer.get_offset();
modal.progress.bytesReceived.textContent = bytesHuman(offset, 2);
modal.progress.bytesFile.textContent = bytesHuman(size, 2);
var 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;
var 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(function (res) {
showSendModal(function (files) {
Zmodem.Browser.send_files(
zsession,
files,
{
on_progress: function(obj, xfer) {
updateFileInfo(xfer.get_details());
updateProgress(xfer);
},
on_file_complete: function(obj) {
// console.log(obj);
}
}
).then(
zsession.close.bind(zsession),
console.error.bind(console)
).then(function () {
res();
});
});
});
}
function handleReceive(zsession) {
zsession.on('offer', function (xfer) {
showReceiveModal(xfer);
var fileBuffer = [];
xfer.on('input', function (payload) {
updateProgress(xfer);
fileBuffer.push(new Uint8Array(payload));
});
xfer.accept().then(function () {
Zmodem.Browser.save_to_disk(
fileBuffer,
xfer.get_details().name
);
}, console.error.bind(console));
});
var promise = new Promise(function (res) {
zsession.on('session_end', function () {
res();
});
});
zsession.start();
return promise;
}
var terminalContainer = document.getElementById('terminal-container'),
httpsEnabled = window.location.protocol === 'https:',
url = (httpsEnabled ? 'wss://' : 'ws://') + window.location.host + window.location.pathname + 'ws',
textDecoder = new TextDecoder(),
textEncoder = new TextEncoder(),
authToken = (typeof tty_auth_token !== 'undefined') ? tty_auth_token : null,
autoReconnect = -1,
reconnectTimer, term, title, wsError;
var openWs = function() {
var ws = new WebSocket(url, ['tty']);
var sendMessage = function (message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(textEncoder.encode(message));
}
};
var sendData = function (data) {
sendMessage('0' + data);
};
var unloadCallback = function (event) {
var message = 'Close terminal? this will also terminate the command.';
(event || window.event).returnValue = message;
return message;
};
var resetTerm = function() {
hideModal();
clearTimeout(reconnectTimer);
if (ws.readyState !== WebSocket.CLOSED) {
ws.close();
}
openWs();
};
var zsentry = new Zmodem.Sentry({
to_terminal: function _to_terminal(octets) {
var 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) {
var chunk = octets.splice(0, 4095);
var 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);
var zsession = detection.confirm();
var promise = zsession.type === 'send' ? handleSend(zsession) : handleReceive(zsession);
promise.catch(console.error.bind(console)).then(function () {
hideModal();
term.setOption('disableStdin', false);
});
}
});
ws.binaryType = 'arraybuffer';
ws.onopen = function(event) {
console.log('Websocket connection opened');
wsError = false;
sendMessage(JSON.stringify({AuthToken: authToken}));
if (typeof term !== 'undefined') {
term.destroy();
}
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'
}
});
term.on('resize', function(size) {
if (ws.readyState === WebSocket.OPEN) {
sendMessage('1' + JSON.stringify({columns: size.cols, rows: size.rows}));
}
setTimeout(function() {
term.showOverlay(size.cols + 'x' + size.rows);
}, 500);
});
term.on('title', function (data) {
if (data && data !== '') {
document.title = (data + ' | ' + title);
}
});
term.on('data', sendData);
while (terminalContainer.firstChild) {
terminalContainer.removeChild(terminalContainer.firstChild);
}
// https://stackoverflow.com/a/27923937/1727928
window.addEventListener('resize', function() {
clearTimeout(window.resizedFinished);
window.resizedFinished = setTimeout(function () {
term.fit();
}, 250);
});
window.addEventListener('beforeunload', unloadCallback);
term.open(terminalContainer, true);
term.winptyCompatInit();
term.fit();
term.focus();
};
ws.onmessage = function(event) {
var 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':
var preferences = JSON.parse(textDecoder.decode(data));
Object.keys(preferences).forEach(function(key) {
console.log('Setting ' + key + ': ' + preferences[key]);
term.setOption(key, preferences[key]);
});
break;
case '3':
autoReconnect = JSON.parse(textDecoder.decode(data));
console.log('Enabling reconnect: ' + autoReconnect + ' seconds');
break;
default:
console.log('Unknown command: ' + cmd);
break;
}
};
ws.onclose = function(event) {
console.log('Websocket connection closed with 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);
}