(function() { 'use strict'; var app = angular.module('DockerPlay', ['ngMaterial']); // Automatically redirects user to a new session when bypassing captcha. // Controller keeps code/logic separate from the HTML // app.controller("BypassController", ['$scope', '$log', '$http', '$location', '$timeout', function($scope, $log, $http, $location, $timeout) { // setTimeout(function() { // document.getElementById("welcomeFormBypass").submit(); // }, 500); // }]); app.controller('PlayController', ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$window', 'TerminalService', 'KeyboardShortcutService', 'InstanceService', function($scope, $log, $http, $location, $timeout, $mdDialog, $window, TerminalService, KeyboardShortcutService, InstanceService) { $scope.sessionId = window.location.pathname.replace('/p/', ''); $scope.instances = []; $scope.idx = {}; $scope.selectedInstance = null; $scope.isAlive = true; $scope.ttl = '--:--:--'; $scope.connected = true; $scope.isInstanceBeingCreated = false; $scope.newInstanceBtnText = '+ 创建工作台'; $scope.deleteInstanceBtnText = '删除工作台'; $scope.isInstanceBeingDeleted = false; var selectedKeyboardShortcuts = KeyboardShortcutService.getCurrentShortcuts(); angular.element($window).bind('resize', function() { if ($scope.selectedInstance) { $scope.resize($scope.selectedInstance.term.proposeGeometry()); } }); $scope.$on("settings:shortcutsSelected", function(e, preset) { selectedKeyboardShortcuts = preset; }); $scope.showAlert = function(title, content, parent) { $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector(parent || '#popupContainer'))) .clickOutsideToClose(true) .title(title) .textContent(content) .ok('收到!') ); } $scope.resize = function(geometry) { $scope.socket.emit('viewport resize', geometry.cols, geometry.rows); } KeyboardShortcutService.setResizeFunc($scope.resize); $scope.closeSession = function() { $scope.socket.emit('session close'); } $scope.upsertInstance = function(info) { var i = info; if (!$scope.idx[i.name]) { $scope.instances.push(i); i.buffer = ''; $scope.idx[i.name] = i; } else { $scope.idx[i.name].ip = i.ip; $scope.idx[i.name].hostname = i.hostname; } return $scope.idx[i.name]; } $scope.newInstance = function() { updateNewInstanceBtnState(true); $http({ method: 'POST', url: '/sessions/' + $scope.sessionId + '/instances', data : { ImageName : InstanceService.getDesiredImage() } }).then(function(response) { var i = $scope.upsertInstance(response.data); $scope.showInstance(i); }, function(response) { if (response.status == 409) { $scope.showAlert('工作台到达上限', '工作台最大个数是 5,已达上限') } }).finally(function() { updateNewInstanceBtnState(false); }); } $scope.getSession = function(sessionId) { $http({ method: 'GET', url: '/sessions/' + $scope.sessionId, }).then(function(response) { if (response.data.created_at) { $scope.expiresAt = moment(response.data.expires_at); setInterval(function() { $scope.ttl = moment.utc($scope.expiresAt.diff(moment())).format('HH:mm:ss'); $scope.$apply(); }, 1000); } var socket = io({ path: '/sessions/' + sessionId + '/ws' }); socket.on('terminal out', function(name, data) { var instance = $scope.idx[name]; if (!instance) { // instance is new and was created from another client, we should add it $scope.upsertInstance({ name: name }); instance = $scope.idx[name]; } if (!instance.term) { instance.buffer += data; } else { instance.term.write(data); } }); socket.on('session end', function() { $scope.showAlert('实验室关门啦!', '你的实验室关门了,所有工作台都已删除。', '#sessionEnd') $scope.isAlive = false; }); socket.on('viewport', function(rows, cols) { }); socket.on('new instance', function(name, ip, hostname) { $scope.upsertInstance({ name: name, ip: ip, hostname: hostname }); $scope.$apply(function() { if ($scope.instances.length == 1) { $scope.showInstance($scope.instances[0]); } }); }); socket.on('delete instance', function(name) { $scope.removeInstance(name); $scope.$apply(); }); socket.on('viewport resize', function(cols, rows) { // viewport has changed, we need to resize all terminals $scope.instances.forEach(function(instance) { instance.term.resize(cols, rows); }); }); socket.on('connect_error', function() { $scope.connected = false; }); socket.on('connect', function() { $scope.connected = true; }); socket.on('instance stats', function(name, mem, cpu, isManager, ports) { $scope.idx[name].mem = mem; $scope.idx[name].cpu = cpu; $scope.idx[name].isManager = isManager; $scope.idx[name].ports = ports; $scope.$apply(); }); $scope.socket = socket; var i = response.data; for (var k in i.instances) { var instance = i.instances[k]; $scope.instances.push(instance); $scope.idx[instance.name] = instance; } // If instance is passed in URL, select it let inst = $scope.idx[$location.hash()]; if (inst) $scope.showInstance(inst); }, function(response) { if (response.status == 404) { document.write('该实验室没找到'); return } }); } $scope.getProxyUrl = function(instance, port) { var url = window.location.protocol + '//pwd' + instance.ip.replace(/\./g, '_') + '-' + port + '.' + window.location.host; return url; } $scope.showInstance = function(instance) { $scope.selectedInstance = instance; $location.hash(instance.name); if (!instance.creatingTerminal) { if (!instance.term) { $timeout(function() { createTerminal(instance); TerminalService.setFontSize(TerminalService.getFontSize()); instance.term.focus(); }, 0, false); return } } $timeout(function() { instance.term.focus(); }, 0, false); } $scope.removeInstance = function(name) { if ($scope.idx[name]) { delete $scope.idx[name]; $scope.instances = $scope.instances.filter(function(i) { return i.name != name; }); if ($scope.instances.length) { $scope.showInstance($scope.instances[0]); } } } $scope.deleteInstance = function(instance) { updateDeleteInstanceBtnState(true); $http({ method: 'DELETE', url: '/sessions/' + $scope.sessionId + '/instances/' + instance.name, }).then(function(response) { $scope.removeInstance(instance.name); }, function(response) { console.log('error', response); }).finally(function() { updateDeleteInstanceBtnState(false); }); } $scope.getSession($scope.sessionId); function createTerminal(instance, cb) { if (instance.term) { return instance.term; } var terminalContainer = document.getElementById('terminal-' + instance.name); var term = new Terminal({ cursorBlink: false }); term.attachCustomKeydownHandler(function(e) { // Ctrl + Alt + C if (e.ctrlKey && e.altKey && (e.keyCode == 67)) { document.execCommand('copy'); return false; } }); term.attachCustomKeydownHandler(function(e) { if (selectedKeyboardShortcuts == null) return; var presets = selectedKeyboardShortcuts.presets .filter(function(preset) { return preset.keyCode == e.keyCode }) .filter(function(preset) { return (preset.metaKey == undefined && !e.metaKey) || preset.metaKey == e.metaKey }) .filter(function(preset) { return (preset.ctrlKey == undefined && !e.ctrlKey) || preset.ctrlKey == e.ctrlKey }) .filter(function(preset) { return (preset.altKey == undefined && !e.altKey) || preset.altKey == e.altKey }) .forEach(function(preset) { preset.action({ terminal : term })}); }); term.open(terminalContainer); // Set geometry during the next tick, to avoid race conditions. setTimeout(function() { $scope.resize(term.proposeGeometry()); }, 4); term.on('data', function(d) { $scope.socket.emit('terminal in', instance.name, d); }); instance.term = term; if (instance.buffer) { term.write(instance.buffer); instance.buffer = ''; } if (cb) { cb(); } } function updateNewInstanceBtnState(isInstanceBeingCreated) { if (isInstanceBeingCreated === true) { $scope.newInstanceBtnText = '+ 创建中...'; $scope.isInstanceBeingCreated = true; } else { $scope.newInstanceBtnText = '+ 创建工作台'; $scope.isInstanceBeingCreated = false; } } function updateDeleteInstanceBtnState(isInstanceBeingDeleted) { if (isInstanceBeingDeleted === true) { $scope.deleteInstanceBtnText = '删除中...'; $scope.isInstanceBeingDeleted = true; } else { $scope.deleteInstanceBtnText = '删除工作台'; $scope.isInstanceBeingDeleted = false; } } }]) .config(['$mdIconProvider', '$locationProvider', function($mdIconProvider, $locationProvider) { $locationProvider.html5Mode({enabled: true, requireBase: false}); $mdIconProvider.defaultIconSet('../assets/social-icons.svg', 24); }]) .component('settingsIcon', { template : "settings", controller : function($mdDialog) { var $ctrl = this; $ctrl.onClick = function() { $mdDialog.show({ controller : function() {}, template : "", parent: angular.element(document.body), clickOutsideToClose : true }) } } }) .component("settingsDialog", { templateUrl : "settings-modal.html", controller : function($mdDialog, KeyboardShortcutService, $rootScope, InstanceService, TerminalService) { var $ctrl = this; $ctrl.$onInit = function() { $ctrl.keyboardShortcutPresets = KeyboardShortcutService.getAvailablePresets(); $ctrl.selectedShortcutPreset = KeyboardShortcutService.getCurrentShortcuts(); $ctrl.instanceImages = InstanceService.getAvailableImages(); $ctrl.selectedInstanceImage = InstanceService.getDesiredImage(); $ctrl.terminalFontSizes = TerminalService.getFontSizes(); }; $ctrl.currentShortcutConfig = function(value) { if (value !== undefined) { value = JSON.parse(value); KeyboardShortcutService.setCurrentShortcuts(value); $ctrl.selectedShortcutPreset = angular.copy(KeyboardShortcutService.getCurrentShortcuts()); $rootScope.$broadcast('settings:shortcutsSelected', $ctrl.selectedShortcutPreset); } return JSON.stringify(KeyboardShortcutService.getCurrentShortcuts()); }; $ctrl.currentDesiredInstanceImage = function(value) { if (value !== undefined) { InstanceService.setDesiredImage(value); } return InstanceService.getDesiredImage(value); }; $ctrl.currentTerminalFontSize = function(value) { if (value !== undefined) { // set font size TerminalService.setFontSize(value); return; } return TerminalService.getFontSize(); } $ctrl.close = function() { $mdDialog.cancel(); } } }) .service("InstanceService", function($http) { var instanceImages = []; _prepopulateAvailableImages(); return { getAvailableImages : getAvailableImages, setDesiredImage : setDesiredImage, getDesiredImage : getDesiredImage, }; function getAvailableImages() { return instanceImages; } function getDesiredImage() { var image = localStorage.getItem("settings.desiredImage"); if (image == null) return instanceImages[0]; return image; } function setDesiredImage(image) { if (image === null) localStorage.removeItem("settings.desiredImage"); else localStorage.setItem("settings.desiredImage", image); } function _prepopulateAvailableImages() { return $http .get("/instances/images") .then(function(response) { instanceImages = response.data; }); } }) .run(function(InstanceService) { /* forcing pre-populating for now */ }) .service("KeyboardShortcutService", ['TerminalService', function(TerminalService) { var resizeFunc; return { getAvailablePresets : getAvailablePresets, getCurrentShortcuts : getCurrentShortcuts, setCurrentShortcuts : setCurrentShortcuts, setResizeFunc : setResizeFunc }; function setResizeFunc(f) { resizeFunc = f; } function getAvailablePresets() { return [ { name : "None", presets : [ { description : "全屏终端", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullscreen(context.terminal, resizeFunc); }} ] }, { name : "Mac OSX", presets : [ { description : "清空终端", command : "Cmd+K", metaKey : true, keyCode : 75, action : function(context) { context.terminal.clear(); }}, { description : "全屏终端", command : "Alt+enter", altKey : true, keyCode : 13, action : function(context) { TerminalService.toggleFullscreen(context.terminal, resizeFunc); }} ] } ] } function getCurrentShortcuts() { var shortcuts = localStorage.getItem("shortcut-preset-name"); if (shortcuts == null) { shortcuts = getDefaultShortcutPrefixName(); if (shortcuts == null) return null; } var preset = getAvailablePresets() .filter(function(preset) { return preset.name == shortcuts; }); if (preset.length == 0) console.error("Unable to find preset with name '" + shortcuts + "'"); return preset[0]; return (shortcuts == null) ? null : JSON.parse(shortcuts); } function setCurrentShortcuts(config) { localStorage.setItem("shortcut-preset-name", config.name); } function getDefaultShortcutPrefixName() { if (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0) return "Mac OSX"; return "None"; } }]) .service('TerminalService', ['$window', function($window) { var fullscreen; var fontSize = getFontSize(); return { getFontSizes : getFontSizes, setFontSize : setFontSize, getFontSize : getFontSize, increaseFontSize : increaseFontSize, decreaseFontSize : decreaseFontSize, toggleFullscreen : toggleFullscreen }; function getFontSizes() { var terminalFontSizes = []; for (var i=3; i<40; i++) { terminalFontSizes.push(i+'px'); } return terminalFontSizes; }; function getFontSize() { if (!fontSize) { return $('.terminal').css('font-size'); } return fontSize; } function setFontSize(value) { fontSize = value; var size = parseInt(value); $('.terminal').css('font-size', value).css('line-height', (size + 2)+'px'); //.css('line-height', value).css('height', value); angular.element($window).trigger('resize'); } function increaseFontSize() { var sizes = getFontSizes(); var size = getFontSize(); var i = sizes.indexOf(size); if (i == -1) { return; } if (i+1 > sizes.length) { return; } setFontSize(sizes[i+1]); } function decreaseFontSize() { var sizes = getFontSizes(); var size = getFontSize(); var i = sizes.indexOf(size); if (i == -1) { return; } if (i-1 < 0) { return; } setFontSize(sizes[i-1]); } function toggleFullscreen(terminal, resize) { if(fullscreen) { terminal.toggleFullscreen(); resize(fullscreen); fullscreen = null; } else { fullscreen = terminal.proposeGeometry(); terminal.toggleFullscreen(); angular.element($window).trigger('resize'); } } }]); })();