diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..49c154d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +# editorconfig-tools is unable to ignore longs strings or urls +max_line_length = null diff --git a/app/app.html b/app/app.html new file mode 100644 index 0000000..232eaaa --- /dev/null +++ b/app/app.html @@ -0,0 +1,24 @@ +
+ +
+ +
+ + + +
+ +

{{$route.current.title}}

+
+ +
+ +
diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..925719e --- /dev/null +++ b/app/app.js @@ -0,0 +1,62 @@ +app.controller('appController', AppController); + +function AppController($scope, $rootScope, connectionService, $timeout, $route, $location) { + $scope.$route = $route; + + $scope.pages = $.map($route.routes, (route) => { + if (route.nav) { + return [route]; + } + }); + + $scope.openLeftMenu = function () { + $mdSidenav('left').toggle(); + }; + + $scope.isConnected = function () { + return connectionService.isConnected(); + } + + $rootScope.nav = function (url) { + return url.replace(':address', connectionService.address); + } + + $rootScope.$on('$stateChangeStart', function (next, current) { + console.log(next); + }); + + connectionService.onOpen = function () { + $scope.connected = true; + $scope.$broadcast('onConnected'); + $scope.$digest(); + $scope.address = connectionService.address; + } + + connectionService.onClose = function (ev) { + $scope.$broadcast('onDisconnected', ev); + $scope.$digest(); + } + + connectionService.onError = function (ev) { + $scope.$broadcast('onConnectionError', ev); + $scope.$digest(); + } + + connectionService.onMessage = function (data) { + $scope.$apply(function () { + $scope.$broadcast('onMessage', data); + }); + } + + $scope.disconnect = function () { + if (confirm('Do you really want to disconnect?')) { + connectionService.disconnect(); + $scope.connected = false; + + $location.path('/home'); + } + } + + // hide loading spinner + $('#pageloader').hide(); +} diff --git a/app/chat/chat.html b/app/chat/chat.html new file mode 100644 index 0000000..be0627f --- /dev/null +++ b/app/chat/chat.html @@ -0,0 +1,22 @@ +
+ +
+
+ {{(line.Time * 1000) | date:'shortTime'}} + + {{line.Username}}: + + {{line.Message}} +
+
+ +
+
+ + + + +
+
+ +
diff --git a/app/chat/chat.js b/app/chat/chat.js new file mode 100644 index 0000000..4cb60e8 --- /dev/null +++ b/app/chat/chat.js @@ -0,0 +1,83 @@ +app.controller('chatController', ChatController); + +function ChatController($scope, connectionService, $timeout) { + $scope.output = []; + + $scope.submitCommand = function () { + if (!$scope.command) { + return; + } + + connectionService.command('say ' + $scope.command, 1); + $scope.command = ''; + } + + $scope.$on('onMessage', function (event, data) { + if (data.Type !== 'Chat') return; + + $scope.onMessage(JSON.parse(data.Message)); + }); + + $scope.onMessage = function (data) { + data.Message = stripHtml(data.Message); + $scope.output.push(data); + + if ($scope.isOnBottom()) { + $scope.scrollToBottom(); + } + } + + $scope.scrollToBottom = function () { + var element = $('#ChatController .Output'); + + $timeout(function () { + element.scrollTop(element.prop('scrollHeight')); + }, 50); + } + + $scope.isOnBottom = function () { + // get jquery element + var element = $('#ChatController .Output'); + + // height of the element + var height = element.height(); + + // scroll position from top position + var scrollTop = element.scrollTop(); + + // full height of the element + var scrollHeight = element.prop('scrollHeight'); + + if ((scrollTop + height) > (scrollHeight - 10)) { + return true; + } + + return false; + } + + // + // Calls console.tail - which returns the last 256 entries from the console. + // This is then added to the console + // + $scope.getHistory = function () { + connectionService.request('chat.tail 512', $scope, function (data) { + var messages = JSON.parse(data.Message); + + messages.forEach(function (message) { + $scope.onMessage(message); + }); + + $scope.scrollToBottom(); + }); + } + + connectionService.installService($scope, $scope.getHistory) +} + +function stripHtml(html) { + if (!html) return ''; + + var tmp = document.createElement('div'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ''; +} diff --git a/app/connection/connect.html b/app/connection/connect.html new file mode 100644 index 0000000..bf8222a --- /dev/null +++ b/app/connection/connect.html @@ -0,0 +1,54 @@ +
+ +
+ +

Connect

+ +
+ {{lastErrorMessage}} +
+ +

Enter the address (including rcon port) and the rcon password below to connect.

+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+
+ +
+ +
+

Saved Connections

+ +
+ +
+
+ +
+ +
diff --git a/app/connection/connection.js b/app/connection/connection.js new file mode 100644 index 0000000..8775a93 --- /dev/null +++ b/app/connection/connection.js @@ -0,0 +1,129 @@ +app.controller('connectionController', ConnectionController); + +function ConnectionController($scope, connectionService, $routeParams, $timeout, $location) { + $scope.address = ''; + $scope.password = ''; + $scope.saveConnection = true; + + $scope.connectionsLimit = 5; + + function _loadFromLocalStorage() { + var connections = []; + + // load and parse from local storage + if (localStorage && localStorage.previousConnections) { + connections = angular.fromJson(localStorage.previousConnections); + } + + // double check + if (!connections) { + connections = []; + } + + return connections; + } + + function _addWithoutDuplicates(connections, connection) { + // prepare new array + var filteredConnections = []; + + for (var i in connections) { + if (connections[i].address !== connection.address && connections[i].password !== connection.password) { + // add old connection info to our new array + filteredConnections.push(connections[i]); + } + } + + // add new connection + filteredConnections.push(connection); + + return filteredConnections; + } + + $scope.toggleConnectionsLimit = function () { + // toggle limit between undefined and 5 + // undefined sets limit to max + if ($scope.connectionsLimit === undefined) { + $scope.connectionsLimit = 5; + } else { + $scope.connectionsLimit = undefined; + } + } + + $scope.connect = function () { + $scope.address = $scope.address.trim(); + $scope.password = $scope.password.trim(); + + $scope.lastErrorMessage = null; + connectionService.connect($scope.address, $scope.password); + + $location.path('/' + $scope.address + '/info'); + } + + $scope.connectTo = function (c) { + $scope.saveConnection = false; + $scope.lastErrorMessage = null; + connectionService.connect(c.address, c.password); + } + + $scope.$on('onDisconnected', function (x, ev) { + $scope.lastErrorMessage = 'Connection was closed - Error ' + ev.code; + $scope.$digest(); + + $location.path('/home'); + }); + + $scope.$on('onConnected', function (x, ev) { + if ($scope.saveConnection) { + // new connection to add + var connection = { + address: $scope.address, + password: $scope.password, + date: new Date() + }; + + // remove old entries and add our new connection info + var connections = _addWithoutDuplicates(_loadFromLocalStorage(), connection); + + // push to scope and save data + $scope.previousConnects = connections; + localStorage.previousConnections = angular.toJson($scope.previousConnects); + } + }); + + $scope.previousConnects = _loadFromLocalStorage(); + + // + // If a server address is passed in.. try to connect if we have a saved entry + // + $timeout(function () { + $scope.address = $routeParams.address; + + // + // If a password was passed as a search param, use that + // + var pw = $location.search().password; + if (pw) { + $scope.password = pw; + $location.search('password', null); + } + + if ($scope.address != null) { + // If we have a password (passed as a search param) then connect using that + if ($scope.password != '') { + $scope.connect(); + return; + } + + var foundAddress = $scope.previousConnects.find((item) => { + return item.address === $scope.address + }) + if (foundAddress != null) { + $scope.connectTo(foundAddress); + } + + } + + }, 20); + +} diff --git a/app/console/console.html b/app/console/console.html new file mode 100644 index 0000000..ef2ee68 --- /dev/null +++ b/app/console/console.html @@ -0,0 +1,16 @@ +
+ +
+
{{line.Message}}
+
+ +
+
+ + + + +
+
+ +
diff --git a/app/console/console.js b/app/console/console.js new file mode 100644 index 0000000..51e96a6 --- /dev/null +++ b/app/console/console.js @@ -0,0 +1,141 @@ +app.controller('consoleController', ConsoleController); + +function ConsoleController($scope, connectionService, $timeout) { + $scope.output = []; + $scope.commandHistory = []; + $scope.commandHistoryIndex = 0; + + $scope.keyUp = function (event) { + switch (event.keyCode) { + + // Arrow Key Up + case 38: + + // rotate through commandHistory + $scope.commandHistoryIndex--; + if ($scope.commandHistoryIndex < 0) { + $scope.commandHistoryIndex = $scope.commandHistory.length; + } + + // set command from history + if ($scope.commandHistory[$scope.commandHistoryIndex]) { + $scope.command = $scope.commandHistory[$scope.commandHistoryIndex]; + } + + break; + + // Arrow Key Down + case 40: + + // rotate through commandHistory + $scope.commandHistoryIndex++; + if ($scope.commandHistoryIndex >= $scope.commandHistory.length) { + $scope.commandHistoryIndex = 0; + } + + // set command from history + if ($scope.commandHistory[$scope.commandHistoryIndex]) { + $scope.command = $scope.commandHistory[$scope.commandHistoryIndex]; + } + + break; + + default: + // reset command history index + $scope.commandHistoryIndex = $scope.commandHistory.length; + break; + } + } + + $scope.submitCommand = function () { + $scope.onMessage({ + Message: $scope.command, + Type: 'Command' + }); + + $scope.commandHistory.push($scope.command); + + connectionService.command($scope.command, 1); + $scope.command = ''; + $scope.commandHistoryIndex = 0; + } + + $scope.$on('onMessage', function (event, data) { + $scope.onMessage(data); + }); + + $scope.onMessage = function (data) { + + if (data.Message.startsWith('[rcon] ')) { + return; + } + + switch (data.Type) { + case 'Generic': + case 'Log': + case 'Error': + case 'Warning': + $scope.addOutput(data); + break; + + default: + console.log(data); + return; + } + } + + $scope.scrollToBottom = function () { + var element = $('#ConsoleController .Output'); + + $timeout(function () { + element.scrollTop(element.prop('scrollHeight')); + }, 50); + } + + $scope.isOnBottom = function () { + // get jquery element + var element = $('#ConsoleController .Output'); + + // height of the element + var height = element.height(); + + // scroll position from top position + var scrollTop = element.scrollTop(); + + // full height of the element + var scrollHeight = element.prop('scrollHeight'); + + if ((scrollTop + height) > (scrollHeight - 10)) { + return true; + } + + return false; + } + + // + // Calls console.tail - which returns the last 128 entries from the console. + // This is then added to the console + // + $scope.getHistory = function () { + connectionService.request('console.tail 128', $scope, function (data) { + var messages = JSON.parse(data.Message); + + messages.forEach(function (data) { + $scope.onMessage(data); + }); + + $scope.scrollToBottom(); + }); + } + + $scope.addOutput = function (data) { + data.Class = data.Type; + $scope.output.push(data); + + if ($scope.isOnBottom()) { + $scope.scrollToBottom(); + } + } + + connectionService.installService($scope, $scope.getHistory) +} diff --git a/app/core/connectionService.js b/app/core/connectionService.js new file mode 100644 index 0000000..05c42c8 --- /dev/null +++ b/app/core/connectionService.js @@ -0,0 +1,128 @@ +function ConnectionService() { + + var ConnectionStatus = { + 'CONNECTING': 0, + 'OPEN': 1, + 'CLOSING': 2, + 'CLOSED': 3 + }; + + var service = { + socket: null, + address: null, + callbacks: {} + }; + + var lastIndex = 1001; + + service.connect = function (addr, pass) { + this.socket = new WebSocket('ws://' + addr + '/' + pass); + this.address = addr; + + this.socket.onmessage = function (e) { + var data = JSON.parse(e.data); + + // + // This is a targetted message, it has an identifier + // So feed it back to the right callback. + // + if (data.Identifier > 1000) { + var callback = service.callbacks[data.Identifier]; + if (callback) { + callback.scope.$apply(function () { + callback.fn(data); + }); + } + service.callbacks[data.Identifier] = null; + + return; + } + + // + // Generic console message, let OnMessage catch it + // + if (service.onMessage != null) { + service.onMessage(data); + } + }; + + this.socket.onopen = this.onOpen; + this.socket.onclose = this.onClose; + this.socket.onerror = this.onError; + } + + service.disconnect = function () { + if (this.socket) { + this.socket.close(); + this.socket = null; + } + + this.callbacks = {}; + } + + service.command = function (msg, identifier) { + if (!this.isConnected()) + return; + + if (identifier === null) + identifier = -1; + + var packet = { + Identifier: identifier, + Message: msg, + Name: 'WebRcon' + }; + + this.socket.send(JSON.stringify(packet)); + }; + + // + // Make a request, call this function when it returns + // + service.request = function (msg, scope, callback) { + lastIndex++; + this.callbacks[lastIndex] = { + scope: scope, + fn: callback + }; + service.command(msg, lastIndex); + } + + // + // Returns true if websocket is connected + // + service.isConnected = function () { + if (!this.socket) + return false; + + return this.socket.readyState === ConnectionStatus.OPEN; + } + + // + // Helper for installing connectivity logic + // + // Basically if not connected, call this function when we are + // And if we are - then call it right now. + // + service.installService = function (scope, callback) { + if (this.isConnected()) { + callback(); + } else { + scope.$on('onConnected', () => { + callback(); + }); + } + } + + service.getPlayers = function (callback, scope) { + this.request('playerlist', scope, (response) => { + var players = JSON.parse(response.Message); + + if (typeof callback === 'function') { + callback.call(scope, players); + } + }); + } + + return service; +} diff --git a/html/header.html b/app/core/header.html similarity index 60% rename from html/header.html rename to app/core/header.html index d6825a1..28a01de 100644 --- a/html/header.html +++ b/app/core/header.html @@ -1,4 +1,4 @@ -