├── test └── .gitkeep ├── models └── .gitkeep ├── public ├── app │ ├── .gitkeep │ ├── Notify.js │ ├── GenericFileModel.js │ ├── Panel.js │ ├── Common.js │ ├── Window.js │ ├── ExtensionManager.js │ ├── App.js │ └── ServerRunner.js ├── assets │ ├── .gitkeep │ └── app-icon.svg ├── styles │ ├── .gitkeep │ ├── common.css │ └── dashboard.css ├── views │ ├── .gitkeep │ ├── dashboard.panels.html │ ├── servers.panel.html │ ├── extensions.panel.html │ ├── dashboard.menu.html │ ├── extensions.window.html │ └── servers.window.html └── index.html ├── .bowerrc ├── midia ├── Tournamenter.ai ├── Tournamenter.png └── screenshot.png ├── .npmignore ├── helpers ├── isDev.js ├── emit.js ├── CheckAppUpdate.js ├── loader.js ├── bindProcessLogsToIPC.js └── CheckPackageUpdate.js ├── config ├── electron.js ├── env │ └── development.js ├── controllers.js ├── models.js ├── config.js ├── helpers.js ├── squirrel.js └── menu.js ├── .gitignore ├── appveyor.yml ├── .travis.yml ├── LICENSE ├── controllers ├── App.js ├── MainWindow.js ├── AutoUpdater.js ├── Settings.js ├── GenericFileModel.js ├── ServerRunner.js └── ExtensionManager.js ├── index.js ├── HISTORY.md ├── README.md ├── bower.json └── package.json /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "public/.components" 3 | } 4 | -------------------------------------------------------------------------------- /midia/Tournamenter.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TendaDigital/TournamenterApp/HEAD/midia/Tournamenter.ai -------------------------------------------------------------------------------- /midia/Tournamenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TendaDigital/TournamenterApp/HEAD/midia/Tournamenter.png -------------------------------------------------------------------------------- /midia/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TendaDigital/TournamenterApp/HEAD/midia/screenshot.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | test 3 | *.swp 4 | ._* 5 | .DS_Store 6 | .git 7 | .hg 8 | .lock-wscript 9 | .svn 10 | .wafpickle-* 11 | CVS 12 | npm-debug.log 13 | media 14 | midia -------------------------------------------------------------------------------- /helpers/isDev.js: -------------------------------------------------------------------------------- 1 | module.exports = function isDev() { 2 | return process.defaultApp || /[\\/]electron-prebuilt[\\/]/.test(process.execPath) || /[\\/]electron[\\/]/.test(process.execPath); 3 | } 4 | -------------------------------------------------------------------------------- /helpers/emit.js: -------------------------------------------------------------------------------- 1 | // 2 | // Emits an event to electron ipc 3 | // 4 | module.exports = function emit(channel, ...payload){ 5 | // Emits the event to the target main window 6 | var _window = app.controllers.MainWindow.getWindow(); 7 | _window && _window.webContents.send(channel, ...payload); 8 | } 9 | -------------------------------------------------------------------------------- /config/electron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('config.electron'); 3 | 4 | // Wait electron to be ready 5 | var start = function (app, next){ 6 | // Wait Electron initialization 7 | eApp.on('ready', function (a, b){ 8 | next && next() 9 | }) 10 | } 11 | 12 | module.exports = start 13 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | name: 'Tournamenter Manager', 5 | icon: path.join(__dirname, '../../', 'midia/Tournamenter_appicon.png'), 6 | settings_file: path.join(electron.app.getPath('userData'), '.settings.json'), 7 | fileModelsDir: 'FileModels', 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.DS_Store 4 | *.pyc 5 | npm-debug.log* 6 | 7 | output/ 8 | tests/ 9 | dist/ 10 | log/ 11 | tmp/* 12 | !tmp/.gitkeep 13 | .tmp/ 14 | .settings.json 15 | 16 | # Logs 17 | logs 18 | *.log 19 | 20 | # Dependency directory 21 | node_modules 22 | public/components 23 | public/_components 24 | public/.components 25 | 26 | *.DS_Store 27 | -------------------------------------------------------------------------------- /config/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('config.controllers'); 3 | 4 | var path = require('path'); 5 | 6 | function config(app, next){ 7 | var controllersDirectory = path.join(__dirname, '/../controllers/'); 8 | 9 | // Load All controllers 10 | app.controllers = {}; 11 | app.helpers.loader.load(controllersDirectory, app.controllers); 12 | 13 | // Debug loaded controllers 14 | console.log(TAG, 'Loaded Controllers:', _.keys(app.controllers).join(',')); 15 | 16 | next(); 17 | } 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /config/models.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('config.models'); 3 | 4 | var path = require('path'); 5 | 6 | // Load instantly, so that files can access models directly on root scope 7 | var modelsDirectory = path.join(__dirname, '/../models'; 8 | 9 | app.models = {}; 10 | 11 | // Load all helpers (including the loader itself lol) 12 | app.helpers.loader.load(modelsDirectory, app.models); 13 | 14 | console.log(TAG, 'Loaded Models:', _.keys(app.models).join(',')); 15 | 16 | module.exports = function config(app, next){ 17 | next(); 18 | }; 19 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | platform: 4 | - x64 5 | 6 | cache: 7 | - node_modules 8 | - public\.components 9 | - '%APPDATA%\npm-cache' 10 | - '%USERPROFILE%\.electron' 11 | 12 | init: 13 | - git config --global core.autocrlf input 14 | 15 | install: 16 | - ps: Install-Product node 7.4 x64 17 | - git reset --hard HEAD 18 | - npm install 19 | - npm prune 20 | 21 | build_script: 22 | - node --version 23 | - npm --version 24 | - npm run dist 25 | 26 | test: off 27 | 28 | branches: 29 | only: 30 | - deploy 31 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var path = require('path'); 7 | var extend = require('util')._extend; 8 | 9 | // Join configurations under app.config 10 | var ENV = process.env.NODE_ENV || 'development'; 11 | var enviroment = require('./env/'+ENV); 12 | 13 | var defaults = { 14 | root: path.normalize(path.join(__dirname, '/..')), 15 | env: ENV, 16 | }; 17 | 18 | app.config = extend(enviroment, defaults); 19 | 20 | /** 21 | * Expose 22 | */ 23 | module.exports = function (app, next){ 24 | next(); 25 | } 26 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('config.helpers'); 3 | 4 | var path = require('path'); 5 | 6 | // Load instantly, so that files can access helpers directly on root scope 7 | var helpersDirectory = path.join(__dirname, '/../helpers/'); 8 | 9 | app.helpers = {}; 10 | 11 | // Load only the loader by default 12 | var loader = require('../helpers/loader'); 13 | 14 | // Load all helpers (including the loader itself lol) 15 | loader.load(helpersDirectory, app.helpers); 16 | 17 | console.log(TAG, 'Installed Helpers:', _.keys(app.helpers).join(',')); 18 | 19 | module.exports = function config(app, next){ 20 | next(); 21 | } 22 | -------------------------------------------------------------------------------- /public/views/dashboard.panels.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /helpers/CheckAppUpdate.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | 3 | /* 4 | * Checks for update given the url. 5 | * Returns the Update object with '' 6 | */ 7 | module.exports = function CheckAppUpdate(url, next) { 8 | request({ 9 | url: url, 10 | json: true, 11 | }, function (error, response, body) { 12 | if (error) { 13 | return next && next('Failed to check for updates') 14 | } 15 | 16 | if (response.statusCode == 200) { 17 | // New update available. Parse version 18 | body.version = body.url.match(/version\/([\d\.]*)/g)[1] || body.name 19 | module.exports.newUpdate = body 20 | return next && next(null, body) 21 | } 22 | 23 | return next && next(null, null) 24 | }) 25 | } 26 | 27 | /* 28 | * Stores if there is an update 29 | */ 30 | module.exports.newUpdate = false -------------------------------------------------------------------------------- /helpers/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | // 6 | // Loads all files and returns in an object-form like: 7 | // { 8 | // somemodule: require('somemodule.js'), 9 | // ... 10 | // } 11 | // 12 | 13 | exports.load = function loadModules (dir, loaded){ 14 | // Default to creat a new object 15 | loaded = (typeof loaded) == 'object' ? loaded : {}; 16 | 17 | // Fix for lookup inside asar: Remove last '/' if exist. 18 | dir = dir.replace(/[\\/]$/, '') 19 | 20 | // Read all files from that path and load into modules 21 | fs.readdirSync(dir).forEach(function (file) { 22 | if (!file.endsWith('.js')) 23 | return; 24 | 25 | var mod = require(path.join(dir, file)); 26 | var name = path.basename(file, '.js'); 27 | 28 | loaded[name] = mod; 29 | }); 30 | 31 | return loaded; 32 | } 33 | -------------------------------------------------------------------------------- /helpers/bindProcessLogsToIPC.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline'); 2 | 3 | module.exports = function bindProcessLogsToIPC(proc, namespace, regexs = {}){ 4 | const emit = app.helpers.emit; 5 | 6 | // Push log messages to electron's IPC 7 | readline.createInterface({ 8 | input: proc.stdout, terminal: false 9 | }).on('line', function(line) { 10 | emit(`${namespace}:log`, 'debug', line); 11 | }); 12 | 13 | // Push log messages to electron's IPC 14 | readline.createInterface({ 15 | input: proc.stderr, terminal: false 16 | }).on('line', function(line) { 17 | if(!regexs.error || (regexs.error && regexs.error.test(line))) 18 | return emit(`${namespace}:log`, 'error', line); 19 | 20 | // Skip lines 21 | if(regexs.skip && regexs.skip.test(line)) 22 | return; 23 | 24 | // Emits a warning 25 | emit(`${namespace}:log`, 'warn', line); 26 | }); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /public/app/Notify.js: -------------------------------------------------------------------------------- 1 | angular.module('Notify', [ 2 | 'ngMaterial' 3 | ]) 4 | 5 | // Manage windows 6 | .service('NotifyService', function ($rootScope, $mdToast) { 7 | var service = this; 8 | 9 | service.notify = function notify(title, message, actionOrDelay){ 10 | var toast = $mdToast.simple() 11 | .textContent(message) 12 | .position('top right'); 13 | 14 | // Check if action is a string 15 | if(_.isString(actionOrDelay)){ 16 | toast.action(actionOrDelay) 17 | .highlightAction(true) 18 | .highlightClass('md-accent') 19 | .hideDelay(0); 20 | }else{ 21 | toast.hideDelay(parseInt(actionOrDelay * 1) || 3000); 22 | } 23 | 24 | $mdToast.show(toast); 25 | } 26 | }) 27 | 28 | .run(function (NotifyService, ipcRenderer){ 29 | ipcRenderer.on(null, 'notify', (evt, title, message, actionOrDelay) => { 30 | NotifyService.notify(title, message, actionOrDelay); 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /helpers/CheckPackageUpdate.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const request = require('request') 3 | 4 | const NPM_BASE = 'http://registry.npmjs.org' 5 | 6 | /* 7 | * Checks for update for a specific package 8 | * this will never send back errors. Only no-update available (null) 9 | */ 10 | module.exports = function CheckPackageUpdate(package, next) { 11 | let {name, version} = package 12 | let url = `${NPM_BASE}/${name}` 13 | 14 | request({ 15 | url, 16 | json: true, 17 | }, function (error, response, body) { 18 | if (error) { 19 | return next && next(null, null) 20 | } 21 | 22 | if (response.statusCode != 200) { 23 | return next && next(null, null) 24 | } 25 | 26 | // Check version 27 | let newVersion = body['dist-tags'].latest 28 | let hasUpdate = semver.gt(newVersion, version) 29 | 30 | // Send back data 31 | return next && next(null, hasUpdate ? newVersion : null) 32 | }) 33 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode7.3 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | language: c 7 | 8 | matrix: 9 | include: 10 | - os: osx 11 | - os: linux 12 | env: CC=clang CXX=clang++ npm_config_clang=1 13 | compiler: clang 14 | 15 | cache: 16 | directories: 17 | - node_modules 18 | - public/.components 19 | - $HOME/.electron 20 | - $HOME/.cache 21 | 22 | addons: 23 | apt: 24 | packages: 25 | - graphicsmagick 26 | - libgnome-keyring-dev 27 | - icnsutils 28 | - xz-utils 29 | 30 | before_install: 31 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 32 | 33 | install: 34 | - nvm install 7.4 35 | - npm install 36 | 37 | script: 38 | - npm run release 39 | 40 | branches: 41 | only: 42 | - deploy 43 | -------------------------------------------------------------------------------- /public/views/servers.panel.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | {{server}} 7 | 8 |
9 | 10 | Port {{serversPorts[server]}} 11 | 12 |
13 | 14 |
15 | 19 | 20 | 21 | 25 | 26 |
27 | 28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /public/app/GenericFileModel.js: -------------------------------------------------------------------------------- 1 | angular.module('GenericFileModel', [ 2 | 3 | ]) 4 | 5 | .service('GenericFileModel', function (ipcRenderer) { 6 | var service = this; 7 | 8 | service.subscribe = function ($scope, type, callback) { 9 | ipcRenderer.on($scope, 'GenericFileModel:update:'+type, callback); 10 | } 11 | 12 | service.list = function (type) { 13 | return ipcRenderer.sendSync('GenericFileModel:list', type); 14 | } 15 | 16 | service.get = function (type, id) { 17 | return ipcRenderer.sendSync('GenericFileModel:get', type, id); 18 | } 19 | 20 | service.getPath = function (type, id) { 21 | return ipcRenderer.sendSync('GenericFileModel:getPath', type, id); 22 | } 23 | 24 | service.save = function (type, id, data) { 25 | return ipcRenderer.sendSync('GenericFileModel:save', type, id, data); 26 | } 27 | 28 | service.remove = function (type, id) { 29 | return ipcRenderer.sendSync('GenericFileModel:remove', type, id); 30 | } 31 | 32 | window.GFM = service; 33 | }) 34 | -------------------------------------------------------------------------------- /config/squirrel.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const spawn = require('child_process').spawn; 3 | 4 | function run(args, done){ 5 | const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe') 6 | 7 | console.log(`Spawning ${updateExe} with args ${args}`, updateExe, args) 8 | 9 | spawn(updateExe, args, { 10 | detached: true 11 | }).on('close', done) 12 | } 13 | 14 | module.exports = function handleStartupEvent(){ 15 | if (process.platform !== 'win32') { 16 | return false 17 | } 18 | 19 | const cmd = process.argv[1] 20 | 21 | console.log(`Processing squirrel command ${cmd}`, cmd) 22 | 23 | const target = path.basename(process.execPath) 24 | if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') { 25 | run(['--createShortcut=' + target + ''], eApp.quit); 26 | return true; 27 | } else if (cmd === '--squirrel-uninstall') { 28 | run(['--removeShortcut=' + target + ''], eApp.quit); 29 | return true; 30 | } else if (cmd === '--squirrel-obsolete') { 31 | eApp.quit() 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivan Seidel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('config.menu'); 3 | 4 | var Menu = require('electron').Menu; 5 | 6 | module.exports = function config(app, next){ 7 | console.log(TAG, 'init') 8 | 9 | var template = [{ 10 | label: 'Application', 11 | submenu: [ 12 | { label: 'About Application', selector: 'orderFrontStandardAboutPanel:' }, 13 | { type: 'separator' }, 14 | { label: 'Quit', accelerator: 'Command+Q', click: function() { eApp.quit(); }} 15 | ]}, { 16 | label: 'Edit', 17 | submenu: [ 18 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 19 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 20 | { type: 'separator' }, 21 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 22 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 23 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 24 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } 25 | ]} 26 | ]; 27 | 28 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 29 | 30 | next(); 31 | } -------------------------------------------------------------------------------- /controllers/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('App'); 3 | 4 | var electron = require('electron') 5 | 6 | const BrowserWindow = electron.BrowserWindow 7 | 8 | exports.init = function (){ 9 | 10 | // Quit when all windows are closed. 11 | eApp.on('window-all-closed', function () { 12 | // On OS X it is common for applications and their menu bar 13 | // to stay active until the user quits explicitly with Cmd + Q 14 | if (process.platform !== 'darwin') { 15 | eApp.quit() 16 | } 17 | }) 18 | 19 | // Open MainWindow on Activate 20 | eApp.on('activate', app.controllers.MainWindow.launch) 21 | 22 | // Initialize Settings 23 | app.controllers.Settings.init(); 24 | 25 | // Initialize GenericFileModel 26 | app.controllers.GenericFileModel.init(); 27 | 28 | // Initialize extensions manager 29 | app.controllers.ExtensionManager.init(); 30 | 31 | // Initialize ServerRunner 32 | app.controllers.ServerRunner.init(); 33 | 34 | // Change badge count on server change 35 | app.controllers.ServerRunner.onStateChange = (states) => { 36 | let runningServers = _.without(_.values(states), false).length; 37 | eApp.setBadgeCount(runningServers); 38 | } 39 | 40 | // By Default, open Main Window on init 41 | app.controllers.MainWindow.launch(); 42 | 43 | // Initialize Autoupdater 44 | app.controllers.AutoUpdater.init(); 45 | } 46 | -------------------------------------------------------------------------------- /public/assets/app-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 13 | 15 | 17 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /controllers/MainWindow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('MainWindow'); 3 | 4 | const BrowserWindow = electron.BrowserWindow 5 | 6 | let mainWindow = null 7 | 8 | exports.getWindow = function (){ 9 | return mainWindow; 10 | } 11 | 12 | exports.launch = function (){ 13 | // Skip re-opening window on re-launch 14 | if(mainWindow) 15 | return 16 | 17 | // Create the browser window. 18 | mainWindow = new BrowserWindow({ 19 | minWidth: 850, 20 | minHeight: 600, 21 | width: 850, 22 | height: 600, 23 | icon: app.config.icon, 24 | }) 25 | 26 | // and load the index.html of the app. 27 | mainWindow.loadURL(`file://${__dirname}/../public/index.html`) 28 | 29 | // Open the DevTools. 30 | if(app.helpers.isDev()) 31 | mainWindow.webContents.openDevTools({detached: true}) 32 | 33 | // Emitted when the window is closed. 34 | mainWindow.on('closed', function () { 35 | mainWindow = null 36 | console.log(TAG, chalk.red('Closed mainWindow')) 37 | }) 38 | 39 | console.log(TAG, chalk.cyan('Launching mainWindow')) 40 | } 41 | 42 | exports.notify = function notify(title, message, stick) { 43 | if (!mainWindow) { 44 | return; 45 | } 46 | 47 | mainWindow.webContents.send('notify', title, message, stick); 48 | } 49 | 50 | exports.send = function send() { 51 | if (!mainWindow) { 52 | return; 53 | } 54 | 55 | mainWindow.webContents.send.apply(mainWindow.webContents, arguments); 56 | } -------------------------------------------------------------------------------- /public/views/extensions.panel.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 6 | 7 | 8 |
9 | Installed: 10 | {{ extensions.length }} 11 | 12 |
13 | 14 |
15 | 16 | 17 | Configure 18 | 19 |
20 | 21 | 22 |
23 | 24 | 26 | 27 |
28 |
{{ extension.name }} {{ extension.update ? '(Update Available)' : ''}}
29 |
{{ extension.description }}
30 |
31 | 32 | 33 |
34 | v{{ extension.version }} 35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /public/app/Panel.js: -------------------------------------------------------------------------------- 1 | angular.module('Panel', [ 2 | 3 | ]) 4 | 5 | // Manage windows 6 | .service('PanelService', function ($rootScope) { 7 | var service = this; 8 | service.panels = []; 9 | 10 | // Subscribe for changes 11 | service.subscribe = function (scope, callback){ 12 | if(!scope || !callback) 13 | return console.error('Cannot subscribe with invalid scope and callback!'); 14 | 15 | var handler = $rootScope.$on('PanelService:update', callback); 16 | scope.$on('$destroy', handler); 17 | } 18 | 19 | // Adds a window to the window list 20 | service.open = function (menu){ 21 | if(!menu) 22 | return; 23 | 24 | // Defaults to closed 25 | menu.open = menu.open || false; 26 | 27 | // Check if window is already included 28 | if(service.panels.indexOf(menu) >= 0) 29 | return; 30 | 31 | // Append window 32 | service.panels.push(menu); 33 | 34 | $rootScope.$emit('PanelService:update'); 35 | } 36 | 37 | // Close a window 38 | service.close = function (win){ 39 | // Check if window exists 40 | var idx = service.panels.indexOf(menu) 41 | if(idx < 0) 42 | return; 43 | 44 | // Remove element from the list 45 | service.panels.splice(idx, 1); 46 | 47 | $rootScope.$emit('PanelService:update'); 48 | } 49 | }) 50 | 51 | .controller('PanelCtrl', function ($scope, PanelService) { 52 | 53 | console.log('PanelCtrl'); 54 | 55 | $scope.panels = null; 56 | 57 | PanelService.subscribe($scope, updatePanels) 58 | 59 | updatePanels(); 60 | 61 | function updatePanels(){ 62 | $scope.panels = PanelService.panels; 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = 'Tournamenter'; 3 | 4 | /** 5 | * Module dependencies 6 | */ 7 | var async = require('async'); 8 | 9 | /** 10 | * Global App Object 11 | */ 12 | var app = { 13 | }; 14 | 15 | /** 16 | * Define Globals 17 | */ 18 | global.app = app; 19 | 20 | global._ = require('lodash'); 21 | global.chalk = require('chalk'); 22 | global.electron = require('electron'); 23 | global.eApp = global.electron.app; 24 | global._TAG = function (tag){ 25 | return chalk.yellow(`[${tag}]`); 26 | } 27 | 28 | /** 29 | * Squirrel Bootstrap 30 | * (Used while Installing/Updating...) 31 | */ 32 | if(require('./config/squirrel')()){ 33 | return; 34 | } 35 | 36 | /* 37 | * Bootstrap Process 38 | */ 39 | var configSetps = [ 40 | // Load configuration options 41 | require('./config/config'), 42 | 43 | // Bootstrap Helpers 44 | require('./config/helpers'), 45 | 46 | // Bootstrap Models 47 | // require('./config/models'), 48 | 49 | // Bootstrap Controllers 50 | require('./config/controllers'), 51 | 52 | // Initialize electron 53 | require('./config/electron'), 54 | 55 | // Initialize application menu 56 | require('./config/menu'), 57 | ]; 58 | 59 | // Configure steps and initialize 60 | async.eachSeries(configSetps, function (config, next){ 61 | config(app, next); 62 | }, function (err){ 63 | // Check if an error ocurred during initialization 64 | if(err){ 65 | console.error(TAG, 'Failed to initialize BajaSync: %s', err); 66 | throw err; 67 | return 68 | } 69 | 70 | // App started OK. 71 | console.log(TAG, chalk.green('App started')); 72 | 73 | // 74 | // Launch app 75 | // 76 | app.controllers.App.init(); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /public/views/dashboard.menu.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | New Server 14 | 15 | 16 | 17 | 18 | 19 | Start all servers 20 | 21 | 22 | 23 | 24 | Stop all servers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | Open Developer Tools 42 | 43 | 44 | 45 | 46 | 47 | Open AppData Folder 48 | 49 | 50 | 51 | 52 | 53 | Open Extensions Folder 54 | 55 | 56 | 57 | 58 | 59 | Reload Window 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## Tournamenter History 2 | > Tournamenter is the 5th generation of `Scoring Systems` that I developed. 3 | 4 | It started while organizing [FLL](http://www.firstlegoleague.org/) tournaments 5 | in Brazil. The main purpose of the first one, was to simplify the scoring system 6 | and allow Live Twitter feeds mixed with Scorings. 7 | 8 | Although the first one was reliable, it wasn't generic enough. I then built with 9 | other friends, a second version really complete, however still specific for the 10 | FLL and it was dependent on Internet + MySQL DB. 11 | 12 | Preparing the third version was a way to fix the past ones, keeping it 13 | [KISS](https://en.wikipedia.org/wiki/KISS_principle). It was a single HTML file 14 | that loaded a `.csv` file from the same folder, and didn't required any `terminal` 15 | knowledge to use (until Chrome added a safety precaution to loading files 16 | and complicated the life for using it). 17 | 18 | It was in 2013-2014, that I was engaged in a project for RoboCup: Build the software 19 | that would manage all the 23 simultaneous competitions. The main challenge was to 20 | create something that could adapt to different realities in each League. The 21 | concept was abstracted, generalized, and Tournamenter came to the World 22 | (Still hosted (here)[https://github.com/RoboCupDev/tournamenter]). 23 | 24 | After some years, a few people have used it, and mostly because they knew how to 25 | use *"THE terminal"*. In the time I built it, I used `Sails` framework, and really 26 | didn't like how it worked **(it broke tournamenter after some years because of version conflicts)**. 27 | 28 | *Finally*, came to the world `Tournamenter`, with uppercase `T`, a refactored 29 | version of original `tournamenter`, but without `Sails`, and more a few features, and it's 30 | father: `Tournamenter Manager`, the Desktop application to manage multiple tournamenter 31 | instances easily. THE END 32 | 33 | **TL;DR;**: I wasted my time 3 times before doing something that I would never 34 | need to do again, and then I re-did it to never have to do it again (again). 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![TournamenterApp](midia/Tournamenter.png) 2 | 3 | [![Build Status][travis-image]][travis-link] [![Build status][appveyor-image]][appveyor-link] [![Release][release-version-image]][release-version-link] [![Tournamenter Version][tournamenter-version-image]][tournamenter-version-link] 4 | 5 | [appveyor-image]: https://ci.appveyor.com/api/projects/status/kip5669pxyqr23jg/branch/deploy?svg=true 6 | [appveyor-link]: https://ci.appveyor.com/project/TendaDigital/tournamenterapp/branch/deploy 7 | [travis-image]: https://travis-ci.org/TendaDigital/TournamenterApp.svg?branch=deploy 8 | [travis-link]: https://travis-ci.org/TendaDigital/TournamenterApp 9 | [tournamenter-version-image]: https://img.shields.io/npm/v/tournamenter.svg?style=flat&label=Tournamenter 10 | [tournamenter-version-link]: https://www.npmjs.com/package/tournamenter 11 | [release-version-image]: https://img.shields.io/github/release/TendaDigital/TournamenterApp.svg 12 | [release-version-link]: https://tournamenter.herokuapp.com 13 | 14 | This project is an wrapper for the [Tournamenter](https://github.com/TendaDigital/tournamenter) 15 | app. It is a service manager that can launch and manage Tournamenter servers locally. 16 | 17 | ![TournamenterApp](midia/screenshot.png) 18 | 19 | ## Tournamenter is 20 | A system that allows you to manage your tournament or event, built with 21 | [Node.JS](https://nodejs.org) and [Electron](https://electron.atom.io). 22 | 23 | It allows you to run events with a easy to use interface that let's you: 24 | * Manage Teams 25 | * Create Groups (Like Soccer Groups) 26 | * Create Tables (With custom ranking options and Columns) 27 | * Create `Views` that will be displayed in TV Screens and Projectors 28 | (With custom `Pages` that can be customized) 29 | 30 | ## Download and Install 31 | The App is in BETA, and should work fine on Mac and Windows. Download the latest version for your OS: 32 | * [Windows x64 (Latest)](http://tournamenter.herokuapp.com/download/windows) 33 | * [Mac OSX (Latest)](http://tournamenter.herokuapp.com/download/osx) 34 | * [Linux (Latest)](http://tournamenter.herokuapp.com/download/linux) 35 | 36 | ## What it does 37 | It allows you to create instances ("run servers") of Tournamenter without knowing 38 | a bit of `Terminal`. You can run multiple instances of Tournamenter simultaneously. 39 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tournamenter-app", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Ivan Seidel " 6 | ], 7 | "description": "Node boilerplate for servers", 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "angular": "^1.5.6", 18 | "angular-material": "^1.1.3", 19 | "jquery": "^2.2.4", 20 | "gsap": "^1.18.5", 21 | "underscore": "^1.8.3", 22 | "angular-aria": "^1.5.6", 23 | "angular-animate": "^1.5.6", 24 | "angular-ui-router": "^0.3.1", 25 | "ngFx": "^1.1.0", 26 | "mdi": "^1.6.50", 27 | "angular-scroll-glue": "^2.0.7" 28 | }, 29 | "install": { 30 | "path": "public/components", 31 | "sources": { 32 | "jquery": { 33 | "mapping": [ 34 | { 35 | "public/.components/jquery/dist/jquery.min.js": "jquery.min.js" 36 | }, 37 | { 38 | "public/.components/jquery/dist/jquery.min.map": "jquery.min.map" 39 | } 40 | ] 41 | }, 42 | "mdi": { 43 | "mapping": [ 44 | { 45 | "public/.components/mdi/fonts/materialdesignicons-webfont.eot": "fonts/materialdesignicons-webfont.eot" 46 | }, 47 | { 48 | "public/.components/mdi/fonts/materialdesignicons-webfont.svg": "fonts/materialdesignicons-webfont.svg" 49 | }, 50 | { 51 | "public/.components/mdi/fonts/materialdesignicons-webfont.ttf": "fonts/materialdesignicons-webfont.ttf" 52 | }, 53 | { 54 | "public/.components/mdi/fonts/materialdesignicons-webfont.woff": "fonts/materialdesignicons-webfont.woff" 55 | }, 56 | { 57 | "public/.components/mdi/fonts/materialdesignicons-webfont.woff2": "fonts/materialdesignicons-webfont.woff2" 58 | }, 59 | { 60 | "public/.components/mdi/css/materialdesignicons.css": "css/materialdesignicons.css" 61 | }, 62 | { 63 | "public/.components/mdi/css/materialdesignicons.min.css": "css/materialdesignicons.min.css" 64 | } 65 | ] 66 | } 67 | } 68 | }, 69 | "resolutions": { 70 | "angular": "^1.5.6", 71 | "angular-animate": "^1.5.6", 72 | "gsap": "~1.11.6" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tournamenter-app", 3 | "author": { 4 | "name": "Ivan Seidel", 5 | "email": "ivan@tendadigital.net", 6 | "url": "https://github.com/ivanseidel" 7 | }, 8 | "productName": "Tournamenter", 9 | "description": "Manage instances of Tournamenter", 10 | "homepage": "https://github.com/ivanseidel/TournamenterApp", 11 | "repository": "https://github.com/ivanseidel/TournamenterApp", 12 | "license": "MIT", 13 | "version": "1.7.3", 14 | "README": "none", 15 | "engines": { 16 | "node": "6.9.1" 17 | }, 18 | "main": "index.js", 19 | "scripts": { 20 | "test": "make test", 21 | "start": "electron ./", 22 | "postinstall": "bower install && bower-installer", 23 | "pack": "build --dir", 24 | "dist": "build", 25 | "release": "build" 26 | }, 27 | "build": { 28 | "appId": "com.tournamenter", 29 | "asar": true, 30 | "mac": { 31 | "category": "public.app-category.productivity", 32 | "target": [ 33 | "dmg", 34 | "zip" 35 | ], 36 | "icon": "build/icon.icns" 37 | }, 38 | "dmg": { 39 | "background": "build/background.png", 40 | "iconSize": 120, 41 | "contents": [ 42 | { 43 | "x": 610, 44 | "y": 150, 45 | "type": "link", 46 | "path": "/Applications" 47 | }, 48 | { 49 | "x": 150, 50 | "y": 150, 51 | "type": "file" 52 | } 53 | ] 54 | }, 55 | "win": { 56 | "target": [ 57 | "squirrel" 58 | ], 59 | "icon": "build/icon.ico" 60 | }, 61 | "linux": { 62 | "description": "Tournamenter Manager App", 63 | "target": [ 64 | "AppImage", 65 | "deb:x64", 66 | "deb:armv7l" 67 | ] 68 | }, 69 | "squirrelWindows": { 70 | "iconUrl": "https://raw.githubusercontent.com/ivanseidel/TournamenterApp/master/build/icon.ico", 71 | "msi": false 72 | } 73 | }, 74 | "dependencies": { 75 | "async": "^2.3.0", 76 | "chalk": "^1.1.3", 77 | "forever-monitor": "^1.7.0", 78 | "ip": "^1.1.5", 79 | "kerberos": "0.0.21", 80 | "lodash": "^4.13.1", 81 | "npm": "^3.10.5", 82 | "request": "^2.81.0", 83 | "semver": "^5.3.0", 84 | "tournamenter": "2.4.3" 85 | }, 86 | "devDependencies": { 87 | "bower": "latest", 88 | "bower-installer": "latest", 89 | "electron": "^1.6.2", 90 | "electron-builder": "^16.6.0", 91 | "electron-builder-squirrel-windows": "^16.6.0", 92 | "electron-prebuilt": "^1.2.6", 93 | "rimraf": "^2.5.3" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /controllers/AutoUpdater.js: -------------------------------------------------------------------------------- 1 | const TAG = _TAG('AutoUpdater'); 2 | 3 | const { autoUpdater, BrowserWindow } = require('electron'); 4 | const os = require('os'); 5 | 6 | const UPDATE_SERVER_HOST = 'tournamenter.herokuapp.com'; 7 | const UPDATABLE_PLATFORMS = ['darwin', 'win32']; 8 | const DELAY_BEFORE_CHECKING = 10000; 9 | 10 | const VERSION = eApp.getVersion() 11 | const FEED_URL = `http://${UPDATE_SERVER_HOST}/update/${os.platform()}_${os.arch()}/${VERSION}` 12 | 13 | exports.init = function init(window) { 14 | 15 | if(app.helpers.isDev()) { 16 | console.log(TAG, chalk.dim('Dev Mode. Skip autoDownload')); 17 | return; 18 | } 19 | 20 | var supportUpdates = (UPDATABLE_PLATFORMS.indexOf(os.platform()) > -1) 21 | if(!supportUpdates) { 22 | console.log(TAG, chalk.dim('Platform does AutoUpdater. running on ' + os.platform())); 23 | 24 | // Manual Check if update is available 25 | setTimeout(exports.manualCheckUpdate, DELAY_BEFORE_CHECKING) 26 | return; 27 | } 28 | 29 | autoUpdater.addListener('update-available', (event) => { 30 | console.log(TAG, chalk.green('A new update is available')); 31 | app.controllers.MainWindow.notify('Updater', 'New Update available. Downloading...', 8000); 32 | }) 33 | 34 | autoUpdater.addListener('update-downloaded', (event, releaseNotes, releaseName, releaseDate, updateURL) => { 35 | app.controllers.MainWindow.notify( 36 | 'Updater Ready', 37 | `Version ${releaseName} is downloaded and will be automatically installed on Quit`, 38 | 'OK' 39 | ); 40 | }) 41 | 42 | autoUpdater.addListener('error', (error) => { 43 | console.error(TAG, error) 44 | app.controllers.MainWindow.notify('Update Error', 'Failed to download Update: ' + error, 'OK'); 45 | }) 46 | 47 | autoUpdater.addListener('checking-for-update', (event) => { 48 | console.log(TAG, 'checking-for-update') 49 | app.controllers.MainWindow.notify('Updater', 'Checking for updates...'); 50 | }) 51 | 52 | autoUpdater.addListener('update-not-available', () => { 53 | console.log(TAG, 'update-not-available') 54 | app.controllers.MainWindow.notify('Updater', 'Tournamenter is up to date! Swweeeeeeet'); 55 | }) 56 | 57 | autoUpdater.setFeedURL(FEED_URL) 58 | 59 | setTimeout(() => { 60 | autoUpdater.checkForUpdates(); 61 | }, DELAY_BEFORE_CHECKING); 62 | } 63 | 64 | exports.manualCheckUpdate = function () { 65 | app.helpers.CheckAppUpdate(FEED_URL, function (err, update) { 66 | if (err) { 67 | console.error(TAG, err) 68 | app.controllers.MainWindow.notify('Update Error', 'Failed to check for updates'); 69 | return 70 | } 71 | 72 | if (!update) { 73 | app.controllers.MainWindow.notify('Updater', 'Tournamenter is up to date! Swweeeeeeet'); 74 | return 75 | } 76 | 77 | // Update is available. Notify window 78 | app.controllers.MainWindow.notify( 79 | 'Updater Available', 80 | `Version ${update.version} is available for download.`, 81 | 'OK' 82 | ); 83 | 84 | app.controllers.MainWindow.send('newUpdate', update); 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /public/app/Common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Common', [ 4 | 5 | ]) 6 | 7 | .service('Dialogs', function ($mdDialog){ 8 | var service = this; 9 | 10 | this.alert = function(text, textOk, callback) { 11 | $mdDialog.show( 12 | $mdDialog.alert() 13 | .clickOutsideToClose(true) 14 | .title('Atenção') 15 | .textContent(text) 16 | .ariaLabel('Alert Dialog') 17 | .ok(textOk) 18 | .targetEvent(callback) 19 | ); 20 | }; 21 | 22 | this.confirm = function(text, textOk, textCancel, callback) { 23 | var confirm = $mdDialog.confirm() 24 | .title('Confirmação') 25 | .textContent(text) 26 | .ariaLabel('Confirm Dialog') 27 | .ok(textOk) 28 | .cancel(textCancel) 29 | .targetEvent(callback) 30 | $mdDialog.show(confirm).then(function() { 31 | callback && callback(true); 32 | }, function() { 33 | callback && callback(false); 34 | }); 35 | }; 36 | 37 | service.prompt = function(text, placeholder, textDefault, callback) { 38 | var confirm = $mdDialog.prompt() 39 | .title('Pergunta...') 40 | .textContent(text) 41 | .ariaLabel('Prompt Dialog') 42 | .placeholder(textDefault) 43 | // .initialValue(placeholder) 44 | .ok('OK') 45 | .cancel('Cancelar') 46 | .targetEvent(callback) 47 | $mdDialog.show(confirm).then(function(result) { 48 | callback && callback(result); 49 | }, function() { 50 | callback && callback(null); 51 | }); 52 | }; 53 | }) 54 | 55 | .factory('THREELoader', function (){ 56 | return new Loader(); 57 | }) 58 | 59 | .service('DragNDrop', function (){ 60 | var service = this; 61 | 62 | // Call this method to register drag'n drop 63 | // within the element. 64 | // 65 | // onStateChange will be called with a boolean flag indicating 66 | // if drop is being made. 67 | // 68 | // onDrop will be called once a file has been dropped 69 | service.onDrop = function (handler, onDrop, onStateChange) { 70 | if(!handler) 71 | return console.error('Invalid element to register DragnDrop', handler); 72 | 73 | onStateChange = onStateChange || null; 74 | 75 | // Throttle onStateChange to avoid too many events 76 | onStateChange = _.throttle(onStateChange, 200, {}, true, false); 77 | 78 | handler.ondragover = function () { 79 | // Notify drop state 80 | onStateChange && onStateChange(true); 81 | 82 | return true; 83 | } 84 | 85 | handler.ondragenter = handler.ondragover = function (e) { 86 | // Notify drop state 87 | onStateChange && onStateChange(true); 88 | 89 | e.preventDefault(); 90 | } 91 | 92 | handler.ondragleave = handler.ondragend = function () { 93 | // Notify drop state 94 | onStateChange && onStateChange(false); 95 | 96 | return false; 97 | }; 98 | 99 | handler.ondrop = function (e) { 100 | console.log('dropped'); 101 | 102 | // Notify drop state 103 | onStateChange && onStateChange(false); 104 | 105 | // Callback method 106 | onDrop && onDrop(e.dataTransfer); 107 | 108 | // Prevent default action 109 | e.preventDefault(); 110 | e.dataTransfer.dropEffect = 'copy'; 111 | 112 | return false; 113 | }; 114 | } 115 | }) 116 | -------------------------------------------------------------------------------- /public/styles/common.css: -------------------------------------------------------------------------------- 1 | /* 2 | Modifies Angular List to be more condensed 3 | */ 4 | md-list-item.md-condensed { 5 | min-height: 28px; 6 | } 7 | 8 | md-list-item.md-condensed ._md-list-item-inner { 9 | min-height: 28px; 10 | } 11 | 12 | md-list-item.md-condensed .md-button { 13 | min-height: 28px; 14 | line-height: 28px; 15 | } 16 | 17 | md-list-item.md-condensed md-icon { 18 | margin: 0 !important; 19 | } 20 | 21 | /* 22 | Modifies padding in form input blocks 23 | */ 24 | md-input-container{ 25 | margin: 18px 0 0 0; 26 | } 27 | 28 | /* 29 | Removes warning messages in servers-window 30 | */ 31 | .servers-window .md-errors-spacer{ 32 | display: none; 33 | } 34 | 35 | /* 36 | Dropping area 37 | */ 38 | .drop-area { 39 | position: absolute; 40 | top: 0; 41 | right: 0; 42 | bottom: 0; 43 | left: 0; 44 | background-color: rgba(0,0,0,0.5); 45 | color: #FFF; 46 | font-size: 32px; 47 | font-weight: 300; 48 | } 49 | 50 | /* 51 | Nicer look to links 52 | */ 53 | .discrete-link{ 54 | color: #457afb; 55 | text-decoration: none; 56 | } 57 | 58 | 59 | /* 60 | Label 61 | */ 62 | .label { 63 | padding: 4px 8px; 64 | border-radius: 4px; 65 | 66 | color: #FFF; 67 | background-color: #555; 68 | } 69 | 70 | /* 71 | Compacter button 72 | */ 73 | .md-compact { 74 | margin: 0; 75 | height: 24px; 76 | line-height: 24px; 77 | min-height: auto; 78 | font-size: 12px; 79 | } 80 | 81 | /* 82 | Log bar 83 | */ 84 | .logger-bar { 85 | transition: 0.2s background ease-in-out; 86 | box-shadow: 0 -1px rgba(255, 255, 255, 0.15); 87 | background-color: rgba(48,48,48,0.95) !important; 88 | } 89 | 90 | .logger-bar:not(.open):hover { 91 | background-color: rgba(48,48,48, 0.9) !important; 92 | } 93 | 94 | .logger-bar .opener{ 95 | box-shadow: 0 -1px rgba(255, 255, 255, 0.10); 96 | cursor: pointer; 97 | z-index: 1; 98 | } 99 | 100 | .logger-bar .opener:focus{ 101 | outline: 0; 102 | } 103 | 104 | .logger-bar .opener-info { 105 | font-size: 10px; 106 | color: #999; 107 | } 108 | 109 | .logger-bar .logs{ 110 | transition: 0.6s height ease-in-out; 111 | } 112 | 113 | .logger-bar .logs:not(.opened){ 114 | height: 0px !important; 115 | } 116 | 117 | .logger-bar .logs-container { 118 | padding: 16px; 119 | } 120 | 121 | .logger-bar .logs-container > pre{ 122 | margin: 0; 123 | white-space: pre-wrap; /* Since CSS 2.1 */ 124 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 125 | white-space: -pre-wrap; /* Opera 4-6 */ 126 | white-space: -o-pre-wrap; /* Opera 7 */ 127 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 128 | } 129 | 130 | .logger-bar .logs-container > pre.error{ 131 | color: #ff5151; 132 | font-weight: bold; 133 | } 134 | 135 | .logger-bar .logs-container > pre.server{ 136 | color: #51ff51; 137 | } 138 | 139 | .logger-bar .logs-container > pre.warn{ 140 | color: #ffdc51; 141 | } 142 | 143 | .logger-bar .logs-container > pre.debug{ 144 | color: #d2d2d2; 145 | } 146 | 147 | .logger-bar .lock-down{ 148 | position: absolute; 149 | bottom: 32px; 150 | right: 8px; 151 | font-size: 24px; 152 | background-color: #defb4c; 153 | border-radius: 3px; 154 | width: 24px; 155 | height: 24px; 156 | line-height: 20px; 157 | } 158 | 159 | .logger-bar .lock-down > md-icon{ 160 | color: #525252; 161 | } 162 | -------------------------------------------------------------------------------- /public/styles/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | Dashboard 3 | */ 4 | #content { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | } 11 | 12 | 13 | /* 14 | Menu bar 15 | */ 16 | .menu-bar md-menu-bar{ 17 | padding-left: 10px; 18 | } 19 | 20 | .menu-bar { 21 | min-height: auto; 22 | box-shadow: 0 1px rgba(0, 0, 0, 0.15); 23 | } 24 | 25 | 26 | /* 27 | Main Window 28 | */ 29 | .window-area md-tabs-content-wrapper { 30 | /*display: none;*/ 31 | } 32 | 33 | .window-area md-tabs-wrapper, 34 | .window-area .md-nav-bar { 35 | background-color: #2C80D2 !important; 36 | border: none !important; 37 | } 38 | 39 | .window-area md-tabs-wrapper md-tab-item{ 40 | color: #E0E0E0 !important; 41 | } 42 | 43 | .window-area md-tabs-wrapper md-tab-item.md-active{ 44 | color: #FFF !important; 45 | } 46 | 47 | .window-area md-tabs-wrapper md-next-button > md-icon { 48 | color: #E0E0E0 !important; 49 | } 50 | 51 | .window-area md-tab-content > div { 52 | height: 100%; 53 | min-height: 100%; 54 | } 55 | 56 | .window-area md-tabs-wrapper md-ink-bar { 57 | background-color: rgb(255,82,82); 58 | } 59 | 60 | 61 | /* 62 | Sub-menu 63 | */ 64 | .sub-menu { 65 | width: 280px; 66 | background-color: #F0F0F0; 67 | box-shadow: -1px 0 rgba(0, 0, 0, 0.2); 68 | } 69 | 70 | .sub-menu > .menu-group { 71 | background-color: #EEE; 72 | border-bottom: 1px solid #ccc; 73 | } 74 | 75 | .sub-menu > .menu-group > .menu-toolbar{ 76 | height: 32px; 77 | font-size: 14px; 78 | cursor: pointer; 79 | } 80 | 81 | .sub-menu > .menu-group > .menu-toolbar:focus { 82 | outline: 0; 83 | } 84 | 85 | .menu-toolbar md-icon.menu-icon { 86 | margin-left: 8px; 87 | height: 32px; 88 | font-size: 16px; 89 | line-height: 32px; 90 | } 91 | 92 | .menu-toolbar .toggle-open > md-icon { 93 | line-height: 24px; 94 | } 95 | 96 | .sub-menu > .menu-group > .menu-content { 97 | /*padding: 8px;*/ 98 | box-shadow: 0 -1px rgba(0, 0, 0, 0.05); 99 | } 100 | 101 | 102 | /* 103 | Status bar 104 | */ 105 | .status-bar { 106 | height: 24px; 107 | background-color: #E0E0E0; 108 | /*border-top: solid 1px #D0D0D0;*/ 109 | box-shadow: 0 -1px rgba(0, 0, 0, 0.1); 110 | 111 | color: #888; 112 | line-height: 24px; 113 | font-size: 12px; 114 | padding-left: 16px; 115 | padding-right: 16px; 116 | } 117 | 118 | 119 | /* 120 | Update bar 121 | */ 122 | .update-bar { 123 | background-color: rgb(120, 27, 243); 124 | /*border-top: solid 1px #D0D0D0;*/ 125 | 126 | color: #FFF; 127 | line-height: 24px; 128 | font-size: 16px; 129 | padding: 8px 16px; 130 | } 131 | 132 | 133 | /* 134 | Extension panel 135 | */ 136 | .installing > .extensions-panel{ 137 | opacity: 0.5; 138 | } 139 | 140 | .extensions-panel h5{ 141 | margin: 8px 0; 142 | } 143 | 144 | .extensions-panel .description{ 145 | margin-top: 8px; 146 | font-size: 12px; 147 | color: #949494; 148 | } 149 | 150 | /* 151 | Extension window 152 | */ 153 | md-progress-circular.white path{ 154 | stroke: #999; 155 | } 156 | 157 | .extension-card { 158 | margin: 16px; 159 | padding: 16px; 160 | border-radius: 4px; 161 | 162 | border: 1px solid #CCC; 163 | background-color: #F0F0F0; 164 | } 165 | 166 | .extension-card:hover { 167 | background-color: #E9E9E9; 168 | cursor: pointer; 169 | } 170 | 171 | .extension-card h3{ 172 | margin: 0; 173 | color: #2c80d2; 174 | } 175 | 176 | .extension-card p{ 177 | font-size: 12px; 178 | color: #999; 179 | } 180 | 181 | .extension-card .version{ 182 | text-align: right; 183 | font-size: 12px; 184 | color: #666; 185 | } 186 | 187 | .extension-card .author{ 188 | font-size: 12px; 189 | color: #2c80d2; 190 | } 191 | -------------------------------------------------------------------------------- /public/app/Window.js: -------------------------------------------------------------------------------- 1 | angular.module('Window', [ 2 | 3 | ]) 4 | 5 | // Manage Menu bar (Specific for each window) 6 | .service('MenuService', function ($rootScope) { 7 | var service = this; 8 | service.menuTemplate = null; 9 | 10 | // Subscribe for changes 11 | service.subscribe = function (scope, callback){ 12 | if(!scope || !callback) 13 | return console.error('Cannot subscribe with invalid scope and callback!'); 14 | 15 | var handler = $rootScope.$on('MenuService:update', callback); 16 | scope.$on('$destroy', handler); 17 | } 18 | 19 | service.setMenuTemplate = function (menuTemplate){ 20 | service.menuTemplate = menuTemplate; 21 | $rootScope.$emit('MenuService:update'); 22 | } 23 | }) 24 | 25 | 26 | // Controller for menu bar 27 | .controller('MenuCtrl', function ($rootScope) { 28 | 29 | // Broadcast the action clicked 30 | this.action = function (action, data) { 31 | $rootScope.$broadcast(action, data); 32 | } 33 | 34 | }) 35 | 36 | // Manage windows 37 | .service('WindowService', function ($rootScope) { 38 | var service = this; 39 | 40 | service.windows = []; 41 | service.selectedWindow = -1; 42 | 43 | // Generate Windows Id for a window 44 | service._windowId = function _windowId(win){ 45 | return win.id + ':' + (win.userData || {}).id; 46 | } 47 | // Get list of window ids (join both window ID and userData id) 48 | service._windowIds = function _windowIds(){ 49 | return _.map(service.windows, service._windowId); 50 | } 51 | 52 | // Subscribe for changes 53 | service.subscribe = function (scope, callback){ 54 | if(!scope || !callback) 55 | return console.error('Cannot subscribe with invalid scope and callback!'); 56 | 57 | var handler = $rootScope.$on('WindowService:update', callback); 58 | scope.$on('$destroy', handler); 59 | } 60 | 61 | // Adds a window to the window list 62 | service.open = function (win){ 63 | if(!win) 64 | return; 65 | 66 | // Set defaults on userData 67 | win.userData = win.userData || {}; 68 | 69 | // Check if window is already included, and open that window 70 | var idx = service._windowIds().indexOf(service._windowId(win)); 71 | if(idx >= 0){ 72 | service.selectedWindow = idx; 73 | $rootScope.$emit('WindowService:update'); 74 | return; 75 | } 76 | 77 | // Append window 78 | service.windows.push(win); 79 | service.selectedWindow = service.windows.length - 1; 80 | 81 | $rootScope.$emit('WindowService:update'); 82 | } 83 | 84 | // Close a window 85 | service.close = function (win){ 86 | // Check if window exists 87 | var idx = service._windowIds().indexOf(service._windowId(win)) 88 | if(idx < 0) 89 | return; 90 | 91 | // Remove element from the list 92 | service.windows.splice(idx, 1); 93 | 94 | // Change selected window if it's the last one 95 | var toGo = Math.min(service.selectedWindow, service.windows.length - 1); 96 | service.selectedWindow = toGo; 97 | 98 | $rootScope.$emit('WindowService:update'); 99 | } 100 | 101 | }) 102 | 103 | .controller('WindowCtrl', function ($scope, $rootScope, WindowService, MenuService) { 104 | 105 | // console.log('WindowCtrl'); 106 | 107 | $scope.win = null; 108 | $scope.windows = null; 109 | $scope.selectedWindow = 0; 110 | $scope._windowId = WindowService._windowId; 111 | 112 | updateWindows(); 113 | WindowService.subscribe($scope, updateWindows) 114 | 115 | $scope.open = WindowService.open; 116 | 117 | function updateWindows(){ 118 | $scope.windows = WindowService.windows; 119 | $scope.selectedWindow = WindowService.selectedWindow; 120 | // $scope.win = $scope.windows[$scope.selectedWindow]; 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /controllers/Settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('Settings') 3 | 4 | const fs = require('fs'); 5 | const EventEmitter = require('events'); 6 | 7 | var _settings = {}; 8 | 9 | var Settings = exports; 10 | 11 | // Set Default values in settings 12 | Settings.setDefaults = function () { 13 | // TODO 14 | } 15 | 16 | // Initialize Settings by loading it, then piping and connecting to Electron IPC 17 | Settings.init = function (){ 18 | // Load settings 19 | app.controllers.Settings.load(); 20 | 21 | // Set Defaults 22 | Settings.setDefaults(); 23 | 24 | // Get IPC 25 | var ipc = require('electron').ipcMain; 26 | 27 | // Pipe Settings to IPC 28 | app.controllers.Settings.subscribe(() => { 29 | // Get main window to emit even 30 | var _window = app.controllers.MainWindow.getWindow(); 31 | _window && _window.webContents.send('Settings:updated', _settings) 32 | }); 33 | 34 | // Listen for Settings requests 35 | ipc.on('Settings:set', (event, key, value) => { 36 | event.returnValue = Settings.set(key, value); 37 | }) 38 | 39 | ipc.on('Settings:get', (event, key) => { 40 | event.returnValue = Settings.get(key); 41 | }) 42 | } 43 | 44 | // Event emitter 45 | Settings.eventEmitter = new EventEmitter(); 46 | 47 | // Expose subscribe method publicly 48 | Settings.subscribe = function (next){ 49 | Settings.eventEmitter.on('Settings:updated', next); 50 | } 51 | 52 | // Persists _settings to file 53 | Settings.save = function () { 54 | var data = JSON.stringify(_settings); 55 | var status = fs.writeFileSync(app.config.settings_file, data); 56 | console.log(TAG, chalk.green('Saved settings file')) 57 | return status; 58 | } 59 | 60 | // Prevent saving repeatedly, and only saves after a period of time 61 | Settings.saveThrottled = _.throttle(Settings.save, 2000, {}, false, true); 62 | 63 | // Load settings from file 64 | Settings.load = function (avoidCheck) { 65 | // Check if settings file exists 66 | try{ 67 | fs.statSync(app.config.settings_file); 68 | }catch (e){ 69 | // Override settings and saves it 70 | console.log(TAG, chalk.cyan('Settings file not found. Creating...')) 71 | _settings = {}; 72 | Settings.save(); 73 | } 74 | 75 | // Load Settings in Text 76 | var data = fs.readFileSync(app.config.settings_file); 77 | 78 | try { 79 | data = JSON.parse(data); 80 | } catch (err) { 81 | console.error(TAG, chalk.red('Failed to load settings. Reseting JSON')) 82 | 83 | _settings = data = {}; 84 | Settings.save(); 85 | } 86 | 87 | // Update data 88 | Settings._update(data); 89 | } 90 | 91 | // Sets values without notifying 92 | Settings._update = function (object) { 93 | for(var k in object){ 94 | if(_settings[k] == object[k]) 95 | continue; 96 | 97 | // Update and notify 98 | _settings[k] = object[k]; 99 | } 100 | 101 | // Remove inexistent keys 102 | for(var k in _settings){ 103 | if(k in object) 104 | continue; 105 | 106 | delete object[k]; 107 | } 108 | // Notify update (even when none ocurred) 109 | Settings.eventEmitter.emit('Settings:updated', _settings); 110 | } 111 | 112 | // Sets a single value into key 113 | Settings.set = function (key, value){ 114 | console.log(TAG, chalk.cyan('Set'), chalk.yellow(key), 'to', value) 115 | 116 | // Skip updating if didn't change 117 | if(_settings[key] == value) 118 | return; 119 | 120 | // Save to local data 121 | _settings[key] = value; 122 | Settings.eventEmitter.emit('Settings:updated', _settings); 123 | 124 | // Persists to file 125 | Settings.saveThrottled(); 126 | } 127 | 128 | // Gets the entire settings, or, a single key 129 | Settings.get = function (key){ 130 | // Return entire settings object if no key passed 131 | if(!key) 132 | return _settings; 133 | 134 | // Return specific key 135 | if(key in _settings) 136 | return _settings[key] 137 | 138 | return null; 139 | } 140 | -------------------------------------------------------------------------------- /public/views/extensions.window.html: -------------------------------------------------------------------------------- 1 | 4 | 6 |

7 | Installed extensions: 8 | {{extensions.length}} 9 |

10 | 11 |
12 | 13 |
14 |
15 | Executing command... 16 |
17 | 19 | 20 |
21 |
22 | 23 | 24 | 26 | 27 | 29 | 30 | 31 |
32 |
{{::log[2]}}
33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
45 |
46 | 47 | 48 | Logs ({{logs.length}}) 49 |
50 |
51 |
52 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 |
62 | 65 | Install 66 | 67 |
68 |
69 | 70 | 71 | 72 |

73 | You have no installed extensions, yet. 74 |

75 |
76 | 77 | 78 | 79 | 81 | 82 |
83 |

{{ extension.name }}

84 |

{{ extension.description }}

85 |
86 | {{ extension.author.name ? 'by '+extension.author.name : ''}} 87 |
88 |
89 | 90 | 91 |
92 |
93 | New Update: 94 | 95 | v{{ extension.version }} 96 | -> v{{ extension.update }} 97 |
98 |
99 |
100 | 104 | Update 105 | 106 | 109 | Uninstall 110 | 111 |
112 |
113 |
114 |
115 | -------------------------------------------------------------------------------- /public/app/ExtensionManager.js: -------------------------------------------------------------------------------- 1 | angular.module('ExtensionManager', [ 2 | 'Panel', 3 | 'Window', 4 | 'Common', 5 | ]) 6 | 7 | .run(function ($rootScope, PanelService) { 8 | // Opens Servers panel 9 | PanelService.open({ 10 | name: 'Extensions', 11 | icon: 'mdi-puzzle', 12 | template: 'views/extensions.panel.html', 13 | open: true, 14 | }) 15 | }) 16 | 17 | .service('ExtensionManagerService', function (ipcRenderer) { 18 | var service = this; 19 | 20 | service.subscribe = function ($scope, channel, callback) { 21 | ipcRenderer.on($scope, 'ExtensionManager:'+channel, callback); 22 | } 23 | 24 | service.list = function () { 25 | return ipcRenderer.sendSync('ExtensionManager:list'); 26 | } 27 | 28 | service.get = function (id) { 29 | return ipcRenderer.sendSync('ExtensionManager:get', id); 30 | } 31 | 32 | service.isExecuting = function (id) { 33 | return ipcRenderer.sendSync('ExtensionManager:executing'); 34 | } 35 | 36 | service.install = function (extension) { 37 | return ipcRenderer.sendSync('ExtensionManager:install', extension); 38 | } 39 | 40 | service.remove = function (extension) { 41 | return ipcRenderer.sendSync('ExtensionManager:remove', extension); 42 | } 43 | 44 | window.EMS = service; 45 | }) 46 | 47 | 48 | .controller('ExtensionsWindowCtrl', function ($scope, ExtensionManagerService){ 49 | const MAX_LOG_COUNT = 1000; 50 | 51 | // `extensions` holds the list of all servers 52 | $scope.extensions = null; 53 | 54 | // `executing` keeps track if there is an running process (install/uninstall 55 | $scope.executing = null; 56 | 57 | // Keep logs here 58 | $scope.logs = []; 59 | 60 | // A private counter for logs 61 | var counter = 1; 62 | 63 | // Module to be installed model-input box 64 | $scope.extensionName = ''; 65 | 66 | // Update list of servers and it's states on update 67 | update(); 68 | ExtensionManagerService.subscribe($scope, 'update', update); 69 | ExtensionManagerService.subscribe($scope, 'executing', update); 70 | 71 | // Listen to logs 72 | ExtensionManagerService.subscribe($scope, 'log', readLogs); 73 | function readLogs(evt, type, message){ 74 | $scope.logs.push([counter++, type, message]); 75 | 76 | // Limit logs 77 | if($scope.logs.length > MAX_LOG_COUNT) 78 | $scope.logs.splice(0, logs.length - MAX_LOG_COUNT); 79 | 80 | // Apply changes to scope if not in digest phase 81 | if(!$scope.$$phase) 82 | $scope.$apply(); 83 | } 84 | 85 | // Remove extension 86 | $scope.removeExtension = function (extensionId){ 87 | // Clear logs 88 | $scope.logs = []; 89 | 90 | ExtensionManagerService.remove(extensionId); 91 | } 92 | 93 | // Remove extension 94 | $scope.installExtension = function (extensionId){ 95 | // Clear logs 96 | $scope.logs = []; 97 | 98 | ExtensionManagerService.install(extensionId); 99 | } 100 | 101 | function update(){ 102 | console.log('update'); 103 | $scope.extensions = ExtensionManagerService.list(); 104 | $scope.executing = ExtensionManagerService.isExecuting(); 105 | 106 | // Apply changes to scope if not in digest phase 107 | if(!$scope.$$phase) 108 | $scope.$apply(); 109 | } 110 | 111 | }) 112 | 113 | 114 | .controller('ExtensionsPanelCtrl', function ($scope, $rootScope, 115 | ExtensionManagerService, WindowService){ 116 | 117 | // `extensions` holds the list of all servers 118 | $scope.extensions = null; 119 | 120 | // `executing` keeps track if there is an running process (install/uninstall 121 | $scope.executing = null; 122 | 123 | // Opens a new window for the Server 124 | $scope.openExtensionManager = function () { 125 | WindowService.open({ 126 | id: 'ExtensionManager', 127 | name: `Extension Manager`, 128 | template: 'views/extensions.window.html', 129 | }); 130 | } 131 | $scope.openExtensionManager() 132 | 133 | // Update list of servers and it's states on update 134 | update(); 135 | ExtensionManagerService.subscribe($scope, 'update', update); 136 | ExtensionManagerService.subscribe($scope, 'executing', update); 137 | 138 | function update(){ 139 | $scope.extensions = ExtensionManagerService.list(); 140 | $scope.executing = ExtensionManagerService.isExecuting(); 141 | 142 | // Apply changes to scope if not in digest phase 143 | if(!$scope.$$phase) 144 | $scope.$apply(); 145 | } 146 | }) 147 | -------------------------------------------------------------------------------- /public/app/App.js: -------------------------------------------------------------------------------- 1 | // Get IPC in electron 2 | var ipc = require('electron').ipcRenderer; 3 | 4 | angular.module('App', [ 5 | 'ServerRunner', 6 | 'ExtensionManager', 7 | 8 | 'Panel', 9 | 'Window', 10 | 'Notify', 11 | 'Common', 12 | 'GenericFileModel', 13 | 14 | 'luegg.directives', 15 | 'ngMaterial', 16 | 'ngAnimate', 17 | 'ui.router', 18 | 'ngFx', 19 | ]) 20 | 21 | // Configure theme 22 | .config( function ($mdThemingProvider) { 23 | 24 | $mdThemingProvider.theme('default') 25 | .primaryPalette('blue', { 26 | 'default': '700' 27 | }) 28 | .accentPalette('pink') 29 | 30 | $mdThemingProvider.theme('dark', 'default') 31 | .primaryPalette('yellow') 32 | .dark(); 33 | }) 34 | 35 | .config( function ($stateProvider, $urlRouterProvider) { 36 | // Setup routes 37 | $stateProvider 38 | .state('dashboard', { 39 | url: '/dashboard', 40 | templateUrl: '/views/dashboard.html', 41 | controller: 'DashboardCtrl', 42 | }); 43 | 44 | // $urlRouterProvider.otherwise('/dashboard') 45 | }) 46 | 47 | // Set menu bindings 48 | .run( function ($rootScope) { 49 | 50 | // Open DevTools 51 | $rootScope.$on('App:openDevTools', function (){ 52 | require('electron').remote.getCurrentWindow().toggleDevTools(); 53 | }) 54 | 55 | // Reload Page 56 | $rootScope.$on('App:reload', function (){ 57 | location.reload(); 58 | }) 59 | 60 | // Open AppData folder 61 | $rootScope.$on('App:openAppData', function () { 62 | var AppData = require('electron').remote.app.getPath('userData') 63 | require('electron').shell.showItemInFolder(AppData) 64 | }) 65 | 66 | // Open Extensions folder 67 | $rootScope.$on('App:openExtensions', function () { 68 | var AppData = require('electron').remote.app.getPath('userData') 69 | var Extensions = require('path').join(AppData, '/extensions/node_modules') 70 | require('electron').shell.showItemInFolder(Extensions) 71 | }) 72 | }) 73 | 74 | // Safelly provides binding/unbinding to ipcRenderer of Electron 75 | .service('ipcRenderer', function (){ 76 | var service = this; 77 | 78 | // Override 'on' method to listen to $scope and stop listening on destroy 79 | service.on = function ($scope, channel, callback) { 80 | ipc.on(channel, callback) 81 | 82 | // Set destroy handler only if scope is defined 83 | $scope && $scope.$on('$destroy', handler); 84 | 85 | // Remove listener 86 | function handler(){ 87 | ipc.removeListener(channel, callback) 88 | } 89 | 90 | return handler; 91 | } 92 | 93 | // Expose the same methods for sending 94 | service.send = ipc.send.bind(ipc) 95 | service.sendSync = ipc.sendSync.bind(ipc) 96 | }) 97 | 98 | // Safelly provides binding/unbinding to ipcRenderer of Electron 99 | .service('NetworkInterfaces', function (){ 100 | var service = this; 101 | const ip = require('ip') 102 | 103 | service.list = function () { 104 | let ips = [] 105 | 106 | // Add internal IP 107 | ips.push({ip: 'localhost', type: 'private'}) 108 | 109 | // Get public IP 110 | let public = ip.address('public') 111 | if (public) { 112 | ips.push({ip: public, type: 'public'}) 113 | } 114 | 115 | return ips 116 | } 117 | }) 118 | 119 | // Keep Settings in sync with main process 120 | .service('Settings', function (ipcRenderer){ 121 | var service = this; 122 | console.log('Settings started'); 123 | 124 | // Load Settings 125 | service.settings = ipc.sendSync('Settings:get', null); 126 | 127 | // Listen for changes in settings and saves to service 128 | ipc.on('Settings:updated', updateSettings); 129 | 130 | // Update current settings 131 | function updateSettings (event, settings) { 132 | // Remove keys that doesn't exist 133 | for(var k in service.settings) 134 | if(!(k in settings)) 135 | delete settings[k]; 136 | 137 | // Set keys that exists 138 | for(var k in settings) 139 | service.settings[k] = settings[k]; 140 | } 141 | 142 | // Set settings 143 | service.set = function (key, value){ 144 | ipc.send('Settings:set', key, value); 145 | } 146 | 147 | }) 148 | 149 | 150 | .controller('AppCtrl', function ($timeout, $scope) { 151 | $scope._loaded = false; 152 | $scope.version = require('electron').remote.app.getVersion(); 153 | $scope.versionTournamenter = require('tournamenter/package.json').version; 154 | $scope.newUpdate = require('electron').remote.require('./helpers/CheckAppUpdate.js').newUpdate; 155 | 156 | $scope.openExternal = function openExternal(link){ 157 | const {shell} = require('electron'); 158 | shell.openExternal(link); 159 | } 160 | 161 | $scope.showUpdateInfo = function showUpdateInfo(){ 162 | require('electron').remote.dialog.showMessageBox({ 163 | type: 'info', 164 | title: $scope.newUpdate.name, 165 | message: $scope.newUpdate.notes, 166 | }) 167 | } 168 | 169 | $timeout(function (){ 170 | $scope._loaded = true; 171 | }, 1000); 172 | 173 | // Update newUpdate once fetch is done 174 | ipc.on('newUpdate', function (update) { 175 | $scope.newUpdate = update 176 | 177 | // Apply changes to scope if not in digest phase 178 | if(!$scope.$$phase) 179 | $scope.$apply(); 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /controllers/GenericFileModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('GenericFileModel'); 3 | // 4 | // This Controller works like a simple DB of Files. 5 | // + list(type) 6 | // will list json files within the directory. 7 | // 8 | // + get(type, id) 9 | // will load the json file and return it 10 | // 11 | // + save(type, id, json) 12 | // will save the json within the type dir with id as name 13 | // 14 | // + remove(type, id) 15 | // will remove the file within dir with id as name 16 | // 17 | var fs = require('fs'); 18 | var path = require('path'); 19 | 20 | var Settings = require('./Settings'); 21 | var dirName = app.config.fileModelsDir; 22 | 23 | /* 24 | * Stores collection types in a mapped object. 25 | * Each key represents an type of object that can be stored, and it's 26 | * content is an object with possible methods: 27 | * { 28 | * // If set, will set defaults to the object always before being saved 29 | * beforeSave: [Function(id, data)], 30 | * } 31 | */ 32 | exports.types = {}; 33 | 34 | exports.init = function (){ 35 | var fileModels = path.join(electron.app.getPath('userData'), dirName) 36 | var ipc = electron.ipcMain 37 | 38 | // Create the FileModels directory 39 | try { 40 | fs.mkdirSync(fileModels) 41 | }catch (e){} 42 | 43 | // Pipe Settings to IPC 44 | ipc.on('GenericFileModel:list', function (event, type) { 45 | event.returnValue = exports.list(type); 46 | }) 47 | 48 | ipc.on('GenericFileModel:get', function (event, type, id) { 49 | event.returnValue = exports.get(type, id); 50 | }) 51 | 52 | ipc.on('GenericFileModel:getPath', function (event, type, id) { 53 | event.returnValue = exports.getPath(type, id); 54 | }) 55 | 56 | ipc.on('GenericFileModel:save', function (event, type, id, data) { 57 | event.returnValue = exports.save(type, id, data); 58 | }) 59 | 60 | ipc.on('GenericFileModel:remove', function (event, type, id) { 61 | event.returnValue = exports.remove(type, id); 62 | }) 63 | } 64 | 65 | // Emits to electron ipc 66 | exports.emit = function (type, id){ 67 | // Emits the event to the target main window 68 | var _window = app.controllers.MainWindow.getWindow(); 69 | _window && _window.webContents.send('GenericFileModel:update:'+type, id); 70 | } 71 | 72 | // Creates a new collection (creates the collection folder, and register in `types`) 73 | exports.createCollection = function (type, configs) { 74 | var modelsFolder = path.join(electron.app.getPath('userData'), dirName, type) 75 | 76 | // Save configs 77 | exports.types[type] = configs || {}; 78 | 79 | try { 80 | fs.mkdirSync(modelsFolder); 81 | console.log(TAG, chalk.cyan('createCollection'), chalk.red(type)); 82 | }catch (e){} 83 | } 84 | 85 | // List file names (id's) 86 | exports.list = function (type){ 87 | 88 | var dir = path.join(electron.app.getPath('userData'), dirName, type) 89 | 90 | var ids = []; 91 | fs.readdirSync(dir).forEach( (file) => { 92 | if (file.indexOf('.json') < 0) 93 | return; 94 | 95 | ids.push(path.basename(file, '.json')); 96 | }); 97 | 98 | console.log(TAG, chalk.cyan('list'), chalk.red(type), chalk.dim(ids.length)); 99 | 100 | return ids; 101 | } 102 | 103 | // Get the object by id 104 | exports.get = function (type, id) { 105 | console.log(TAG, chalk.cyan('get'), chalk.red(type), chalk.red(id)) 106 | 107 | var dir = path.join(electron.app.getPath('userData'), dirName, type) 108 | var file = path.join(dir, id + '.json') 109 | 110 | // Check if file exists 111 | try{ 112 | fs.statSync(file) 113 | }catch (e){ 114 | // File doesn't exists. Return null 115 | return null 116 | } 117 | 118 | // Load Settings in Text 119 | var data = fs.readFileSync(file); 120 | 121 | try { 122 | return JSON.parse(data); 123 | } catch (err) { 124 | console.error(TAG, 'Invalid file', err); 125 | } 126 | 127 | return {}; 128 | } 129 | 130 | // Get the object by id 131 | exports.getPath = function (type, id) { 132 | console.log(TAG, chalk.cyan('getPath'), chalk.red(type), chalk.red(id)) 133 | 134 | var dir = path.join(electron.app.getPath('userData'), dirName, type) 135 | var file = path.join(dir, id + '.json') 136 | 137 | // Check if file exists 138 | try{ 139 | fs.statSync(file) 140 | }catch (e){ 141 | // File doesn't exists. Return null 142 | return null 143 | } 144 | 145 | return file; 146 | } 147 | 148 | // Save object as file with id in type's folder 149 | exports.save = function (type, id, json) { 150 | console.log(TAG, chalk.cyan('save'), chalk.red(type), chalk.red(id)) 151 | 152 | var dir = path.join(eApp.getPath('userData'), dirName, type) 153 | var file = path.join(dir, id + '.json') 154 | 155 | // Execute `beforeSave` hook 156 | var typeConfigs = exports.types[type] 157 | if (typeConfigs.beforeSave) { 158 | json = typeConfigs.beforeSave(id, json) 159 | } 160 | 161 | // Ensure it's an object 162 | json = json || {} 163 | 164 | // Save File 165 | fs.writeFileSync(file, JSON.stringify(json)); 166 | 167 | // Notify ipc 168 | exports.emit(type, id); 169 | 170 | return json; 171 | } 172 | 173 | // Remove an object 174 | exports.remove = function (type, id) { 175 | console.log(TAG, chalk.cyan('remove'), chalk.red(type), chalk.red(id)) 176 | 177 | var dir = path.join(electron.app.getPath('userData'), dirName, type) 178 | var file = path.join(dir, id + '.json') 179 | 180 | try{ 181 | fs.unlinkSync(file); 182 | }catch (e){ 183 | // Prevent emiting. Didn't delete (doesn't exists) 184 | return false; 185 | } 186 | 187 | // Notify ipc 188 | exports.emit(type, id); 189 | return id; 190 | } 191 | -------------------------------------------------------------------------------- /public/views/servers.window.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 12 | {{statusString}} {{needsSave ? '(Save before running)' : ''}} 13 | 14 |
15 | 16 | 17 | 18 | Open App 19 | 20 | 21 | 22 | 23 | 24 | Loading Interfaces... 25 | 26 | 27 | 28 | 29 | 30 |
31 |

{{interface.ip}}:{{configs.env.PORT}}

32 | 37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | Save 45 | 46 |
47 | 48 | 49 | 51 | 52 | 54 | 55 | 56 |
57 |
{{::log[2]}}
58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 |
70 |
71 | 72 | 73 | Logs ({{logs.length}}) 74 |
75 |
76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 |
92 | 93 | 94 | 96 | 97 | 98 | 99 | 101 | Show password 102 | 103 | 104 |
105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 114 | 115 | Select 116 | 117 | 118 |
119 | 120 |
121 | 122 | 123 | 124 | 125 | 126 | 128 | 129 | Select 130 | 131 | 132 |
133 | 134 |
135 |
Use Extensions
136 | 137 | 143 | {{ extension.name }} 144 | 145 |
146 | 147 |
148 |
149 | 150 | 151 | Delete Server 152 | 153 |
154 | 155 |
156 | 157 |
158 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tournamenter Manager 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 |
36 |
37 | 39 | 40 | 41 |
Tournamenter v{{::version}}
42 |
43 |
44 | 45 |
46 | 47 | 48 |
49 |
50 | New Update available: 51 | 52 | {{newUpdate.version}} 53 | 54 |
55 |
56 | 57 | 60 | Info 61 | 62 | 63 | 66 | Download 67 | 68 |
69 | 70 | 71 | 72 |
73 | 74 | 75 | 77 | 78 | 79 | 80 |
Tournamenter
81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 93 | 94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 | 102 | 103 |
105 | 106 | 108 | 111 |
113 |
114 |
115 |
116 | 117 |
118 | 119 | 120 | 122 |
123 | 124 | 125 |
126 |
127 | Tournamenter Manager v{{::version}} | Tournamenter v{{::versionTournamenter}} 128 |
129 |
130 |
131 | Made with by nerds in 132 | - by 133 | Ivan Seidel 134 |
135 |
136 | 137 |
138 | 139 | 140 | 141 | 142 | 143 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /controllers/ServerRunner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('ServerRunner'); 3 | 4 | var path = require('path'); 5 | var forever = require('forever-monitor'); 6 | 7 | // Single Callback for global state change 8 | exports.onStateChange = null; 9 | 10 | // Current running instances of Tournamenter 11 | exports._instances = {}; 12 | 13 | // Initialize deamon configurations 14 | exports.init = function (){ 15 | var ipc = electron.ipcMain 16 | 17 | // Pipe Settings to IPC 18 | ipc.on('ServerRunner:start', function (event, id) { 19 | exports.start(id); 20 | }) 21 | 22 | ipc.on('ServerRunner:stop', function (event, id) { 23 | exports.stop(id) 24 | }) 25 | 26 | ipc.on('ServerRunner:state', function (event, id) { 27 | event.returnValue = exports.state(id); 28 | }) 29 | 30 | // Binds beforeExit to process in order to stop all running instances 31 | eApp.on('before-quit', () => { 32 | // Kills all processes 33 | for(var k in exports._instances){ 34 | let instance = exports._instances[k]; 35 | if(!instance || !instance.running) 36 | continue; 37 | 38 | console.log(TAG, chalk.red(`Killing ${k}`)); 39 | instance.kill(true); 40 | } 41 | }) 42 | 43 | // Initialize collections 44 | app.controllers.GenericFileModel.createCollection('servers', { 45 | 46 | // Called on save, to apply defaults 47 | beforeSave: function (id, data) { 48 | data = data || {} 49 | 50 | // Defalts on process options 51 | data = _.defaults(data, { 52 | extensions: {}, 53 | }) 54 | 55 | // Defaults on Environment vars 56 | data.env = _.defaults(data.env, { 57 | APP_NAME: id, 58 | APP_LOGO: '', 59 | PASSWORD: '', 60 | DB_FOLDER: path.join(eApp.getPath('userData'), id + '.db'), 61 | // TODO: Improve default port selection 62 | PORT: 3000 + Math.round(Math.random(1000)), 63 | }) 64 | console.log('new data', data) 65 | 66 | return data 67 | } 68 | }); 69 | } 70 | 71 | // Starts a server 72 | exports.start = function (serverId, cb) { 73 | const ExtensionManager = app.controllers.ExtensionManager; 74 | 75 | var isStarted = serverId in exports._instances && exports._instances[serverId]; 76 | 77 | if(isStarted) 78 | return console.log(TAG, `Server ${serverId} already stared. Skipping`); 79 | 80 | console.log(TAG, `Starting new server: ${serverId}`); 81 | 82 | // Load instance configurations from files db 83 | let optsDb = app.controllers.GenericFileModel.get('servers', serverId) || {}; 84 | 85 | // Find out tournamenter location 86 | let electronCmd = path.join(__dirname, '../node_modules/.bin/electron'); 87 | let tournamenterModule = require.resolve('tournamenter'); 88 | let tournamenterScript = path.basename(tournamenterModule); 89 | let tournamenterDir = path.dirname(tournamenterModule); 90 | 91 | // Emit a debug log 92 | exports.emitLog(serverId, 'server', `Starting new server: ${serverId}`); 93 | 94 | // Gatter extensions used and join paths with `:` 95 | let extensions = optsDb.extensions || {} 96 | extensions = _.pickBy(extensions, v => v) 97 | extensions = _.keys(extensions) 98 | 99 | // Log it 100 | if(extensions.length > 0) 101 | exports.emitLog(serverId, 'server', `Use: ${extensions.join(',')}`); 102 | 103 | extensions = ExtensionManager.getExtensionsPaths(extensions); 104 | extensions = extensions.join(':'); 105 | 106 | // Prepare instance options 107 | let opts = { 108 | max: 5, 109 | uid: `Tournamenter ${serverId}`, 110 | silent: true, 111 | killTree: true, 112 | // command: 'node', 113 | // fork: true, 114 | // cwd: path.join(__dirname, '../'), 115 | 116 | minUptime: 5000, 117 | spinSleepTime: 2000, 118 | 119 | env: _.defaults(optsDb.env, { 120 | APP_NAME: 'Tournamenter', 121 | APP_UID: serverId, 122 | TMP_PATH: path.join(eApp.getPath('temp'), 'tournamenter_' + serverId), 123 | ELECTRON_RUN_AS_NODE: 1, 124 | TOURNAMENTER_EXTENSIONS: extensions, 125 | }), 126 | }; 127 | 128 | // Launch server 129 | let child = new (forever.Monitor)(tournamenterModule, opts); 130 | 131 | // Save spawned process to instances array 132 | exports._instances[serverId] = child; 133 | 134 | // Bind events 135 | exports._bindEvents(serverId, child); 136 | 137 | // Binds cb event if needed 138 | cb && child.on('start', cb); 139 | 140 | // Start server 141 | child.start(); 142 | 143 | // Emit general update 144 | exports.emitUpdate(serverId); 145 | } 146 | 147 | 148 | // Stops a server 149 | exports.stop = function (serverId) { 150 | if(serverId in exports._instances){ 151 | // Emit a debug log 152 | exports.emitLog(serverId, 'server', `Killing server: ${serverId}`); 153 | 154 | // Flag indicating stop will occur 155 | exports._instances[serverId].willStop = true; 156 | exports._instances[serverId].kill(true); 157 | } 158 | } 159 | 160 | 161 | // Gets a list of states for the servers 162 | exports.states = function (){ 163 | let states = {}; 164 | 165 | for(let k in exports._instances) 166 | states[k] = exports._instances[k].running; 167 | 168 | return states; 169 | } 170 | 171 | 172 | // Gets the state of a server or for all of them if not specified 173 | exports.state = function (serverId) { 174 | if(!serverId) 175 | return exports.states(); 176 | 177 | if(!(serverId in exports._instances)) 178 | return null; 179 | 180 | return exports._instances[serverId].running; 181 | } 182 | 183 | 184 | // Bind events to the process (links state and emits events) 185 | exports._bindEvents = function (serverId, child) { 186 | // child.STATE = null; 187 | 188 | child.on('start', () => { 189 | // child.STATE = 'START'; 190 | exports.emitUpdate(serverId); 191 | }) 192 | 193 | child.on('stop', () => { 194 | // child.STATE = 'STOP'; 195 | exports.emitUpdate(serverId); 196 | }) 197 | 198 | child.on('restart', () => { 199 | // child.STATE = 'START'; 200 | exports.emitUpdate(serverId); 201 | 202 | // Notify application 203 | if (!child.willStop) { 204 | eApp.dock && eApp.dock.bounce() 205 | 206 | app.controllers.MainWindow.notify( 207 | 'Server Crashed', 208 | `WARNING: Server '${serverId}' crashed for the ${child.times} time. Re-spawning...`, 209 | 'OK'); 210 | } 211 | }) 212 | 213 | child.on('exit', (code) => { 214 | // child.STATE = null; 215 | // Destroy instance 216 | delete exports._instances[serverId]; 217 | 218 | // Emits the destroyed instance 219 | exports.emitUpdate(serverId); 220 | 221 | // If it did crash, notify application 222 | if (!child.willStop) { 223 | eApp.dock && eApp.dock.bounce('critical') 224 | app.controllers.MainWindow.notify( 225 | 'Server Crashed', 226 | `WARNING: Server '${serverId}' crashed. Open the server logs for more info.`, 227 | 'OK'); 228 | } 229 | }) 230 | 231 | // Logs handling 232 | child.on('stdout', (message) => { 233 | exports.emitLog(serverId, 'debug', message); 234 | }); 235 | 236 | child.on('stderr', (message) => { 237 | exports.emitLog(serverId, 'error', message); 238 | }); 239 | } 240 | 241 | 242 | // Notifies changes in UI (about changes to an server STATE) 243 | exports.emitUpdate = function (serverId) { 244 | // Emits the event to the target main window 245 | let state = exports.state(serverId); 246 | let _window = app.controllers.MainWindow.getWindow(); 247 | _window && _window.webContents.send('ServerRunner:update:'+serverId, state); 248 | 249 | console.log(TAG, `${chalk.cyan(serverId)} -> ${chalk.green(state)}`); 250 | exports.emitUpdates(); 251 | } 252 | 253 | 254 | // Notifies about new console messages 255 | exports.emitLog = function (serverId, type, message) { 256 | let _window = app.controllers.MainWindow.getWindow(); 257 | _window && _window.webContents.send('ServerRunner:log:'+serverId, type, message.toString()); 258 | } 259 | 260 | 261 | // Emit the states of the servers 262 | exports.emitUpdates = function (){ 263 | let _window = app.controllers.MainWindow.getWindow(); 264 | let states = exports.states() 265 | _window && _window.webContents.send('ServerRunner:update', states); 266 | 267 | // Notify application 268 | exports.onStateChange && exports.onStateChange(states); 269 | } 270 | -------------------------------------------------------------------------------- /controllers/ExtensionManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var TAG = _TAG('ExtensionManager'); 3 | // 4 | // Manages (extra) extensions for Tournamenter 5 | // 6 | // It can: 7 | // + list currently installed extensions 8 | // + install extension (from: URL, or NPM name) 9 | // (Install it's dependencies) 10 | // + remove extension 11 | // (And it's installed dependencies) 12 | // 13 | const fs = require('fs'); 14 | const path = require('path'); 15 | const async = require('async'); 16 | const fork = require('child_process').fork; 17 | const readline = require('readline'); 18 | 19 | const emit = app.helpers.emit; 20 | 21 | // 22 | // Cache variables 23 | // 24 | exports._cachedUpdates = null 25 | exports._cachedExtensions = null 26 | 27 | // 28 | // Initialize module 29 | // 30 | exports.init = function () { 31 | var ipc = electron.ipcMain 32 | var extensionsFolder = exports.getInstallPath(); 33 | 34 | try { 35 | // Create extensions folder 36 | fs.mkdirSync(extensionsFolder); 37 | console.log(TAG, chalk.cyan('Initialize extensions folder')); 38 | }catch (e){ 39 | console.log(TAG, chalk.gray('Extensions folder already created')); 40 | } 41 | 42 | // Link IPC to actions 43 | 44 | // Pipe Settings to IPC 45 | ipc.on('ExtensionManager:list', function (event) { 46 | event.returnValue = exports.list(); 47 | }) 48 | 49 | ipc.on('ExtensionManager:get', function (event, id) { 50 | event.returnValue = exports.get(id); 51 | }) 52 | 53 | ipc.on('ExtensionManager:install', function (event, id) { 54 | event.returnValue = !!exports.install(id); 55 | }) 56 | 57 | ipc.on('ExtensionManager:remove', function (event, id) { 58 | event.returnValue = !!exports.remove(id); 59 | }) 60 | 61 | ipc.on('ExtensionManager:executing', function (event, id) { 62 | event.returnValue = exports.isExecuting(); 63 | }) 64 | 65 | // Delay check for updates a bit 66 | setTimeout(exports.checkUpdates, 4000); 67 | } 68 | 69 | 70 | // 71 | // Ge the installation where all extensions are 72 | // 73 | exports.getInstallPath = function () { 74 | return path.join(electron.app.getPath('userData'), 'extensions'); 75 | } 76 | 77 | // 78 | // Given a list of extension names, returns a list of paths to the extensions 79 | // 80 | exports.getExtensionsPaths = function (extensions) { 81 | // Filter extensions 82 | if(!_.isArray(extensions)) 83 | extensions = []; 84 | 85 | let paths = []; 86 | extensions.forEach(ext => { 87 | ext = exports.get(ext); 88 | 89 | // Skip if didn't found extension 90 | if(!ext) return; 91 | 92 | // Add to the paths list 93 | paths.push(ext.path); 94 | }) 95 | 96 | return paths; 97 | } 98 | 99 | // 100 | // List packages with it's `package.js` information 101 | // 102 | exports.list = function () { 103 | const installPath = path.join(exports.getInstallPath(), 'node_modules'); 104 | 105 | // Return cached extensions if already saved 106 | if(exports._cachedExtensions != null) 107 | return exports._cachedExtensions; 108 | 109 | // Read all files from that path and load into modules 110 | let folders; 111 | try{ 112 | folders = fs.readdirSync(installPath).filter((file) => { 113 | if(file.startsWith('.')) 114 | return false; 115 | 116 | if(fs.statSync(path.join(installPath, file)).isDirectory()) 117 | return true; 118 | 119 | return false; 120 | }); 121 | }catch(e){ 122 | // node_modules does not exists yet? 123 | return []; 124 | } 125 | 126 | // Check if folders contains a package.json 127 | let extensions = folders.map((folder) => { 128 | // Read file and parse json 129 | try{ 130 | let folderPath = path.join(installPath, folder); 131 | let filePath = path.join(folderPath, 'package.json'); 132 | var json = fs.readFileSync(filePath).toString(); 133 | json = JSON.parse(json); 134 | 135 | // Include folderPath in object (saves the folder location) 136 | json.path = folderPath; 137 | }catch(e){ 138 | return null; 139 | } 140 | return json; 141 | }) 142 | 143 | // Filter out dependencies that are not installed by the USER 144 | // Read: https://github.com/Microsoft/nodejstools/issues/603 145 | extensions = extensions.filter((extension) => { 146 | // Filter out json parsing errors from previous step 147 | if(extension === null) 148 | return false; 149 | 150 | // No _requiredBy. Set as a true root dependency 151 | if(!('_requiredBy' in extension)) 152 | return true; 153 | 154 | // It's a root dependency (installed by `npm install `) 155 | if(extension._requiredBy.indexOf('#USER') >= 0) 156 | return true; 157 | 158 | // By default, it's a dependency's dependency 159 | return false; 160 | }) 161 | 162 | // Select only important keys in each extension 163 | extensions = extensions.map((extension) => { 164 | return _.pick(extension, [ 165 | 'path', 166 | 167 | 'name', 168 | 'author', 169 | 'gitHead', 170 | 'version', 171 | 'description', 172 | 173 | '_resolved', 174 | ]) 175 | }) 176 | 177 | // Set updates into objects 178 | extensions = extensions.map((extension) => { 179 | extension.update = exports.getUpdate(extension.name) 180 | return extension 181 | }) 182 | 183 | // Save cache 184 | exports._cachedExtensions = extensions; 185 | 186 | return extensions; 187 | } 188 | 189 | // 190 | // Get an extension (by it's id) 191 | // 192 | exports.get = function (extension) { 193 | let extensions = exports.list(); 194 | 195 | let ext = extensions.find((ext) => { 196 | if(ext.name == extension) 197 | return ext; 198 | }) || null; 199 | 200 | return ext; 201 | } 202 | 203 | // 204 | // Flag indicating that a instalation is already in progress 205 | // 206 | exports._isExecuting = false; 207 | exports.isExecuting = function () { 208 | return exports._isExecuting; 209 | } 210 | exports.setExecuting = function (state) { 211 | exports._isExecuting = !!state; 212 | 213 | // Notify listeners 214 | emit('ExtensionManager:executing', exports._isExecuting); 215 | } 216 | 217 | 218 | // 219 | // Install an extension 220 | // 221 | exports.install = function (extension, cb) { 222 | console.log(TAG, chalk.green(`Installing ${extension}...`)); 223 | emit('ExtensionManager:log', 'server', `Installing ${extension}`); 224 | 225 | var proc = exports.runNpm([ 226 | 'install', extension, 227 | '--no-progress', 228 | ], cb); 229 | 230 | if(!proc) 231 | return; 232 | 233 | // Bind stdout and stderr read events and pipes to ipc 234 | app.helpers.bindProcessLogsToIPC(proc, 'ExtensionManager', { 235 | error: /ERR!/, 236 | skip: /npm (verb|http|info)/ 237 | }); 238 | } 239 | 240 | 241 | // 242 | // Removes a extension 243 | // 244 | exports.remove = function (extension, cb){ 245 | console.log(TAG, chalk.green(`Removing ${extension}...`)); 246 | emit('ExtensionManager:log', 'server', `Removing ${extension}`); 247 | 248 | var proc = exports.runNpm([ 249 | 'remove', extension, 250 | '--no-progress', 251 | ], cb); 252 | 253 | if(!proc) 254 | return; 255 | 256 | // Bind stdout and stderr read events and pipes to ipc 257 | app.helpers.bindProcessLogsToIPC(proc, 'ExtensionManager', { 258 | error: /ERR!/, 259 | skip: /npm (verb|http|info)/ 260 | }); 261 | } 262 | 263 | // 264 | // Check for updates on all dependencies 265 | // 266 | exports.checkUpdates = function (next) { 267 | let packages = exports.list() 268 | async.mapLimit(packages, 2, app.helpers.CheckPackageUpdate, (err, updates) => { 269 | // Join the 'names' with the versions, to create a map table from module name to version 270 | let versions = _.zipObject(_.map(packages, 'name'), updates) 271 | exports._cachedUpdates = versions 272 | 273 | // Clear Extensions cache 274 | exports._cachedExtensions = null 275 | 276 | // Notify update 277 | emit('ExtensionManager:update', true) 278 | }) 279 | } 280 | 281 | // 282 | // Get an update for a given module name string. Returns null if up to date 283 | // 284 | exports.getUpdate = function (name) { 285 | return exports._cachedUpdates && exports._cachedUpdates[name] || null 286 | } 287 | 288 | // 289 | // Low level call for NPM 290 | // 291 | exports.runNpm = function (params, cb){ 292 | const npmMain = require.resolve('npm'); 293 | const npmRoot = npmMain.split('lib')[0]; 294 | const npmCli = path.join(npmRoot, 'bin/npm-cli.js'); 295 | const installPath = exports.getInstallPath(); 296 | const regError = /npm ERR!/g; 297 | const regExit = /npm verb exit\s+\[\s*(\d+)\,\s*true\s*\]/gi; 298 | 299 | var parsedCode = null; 300 | 301 | // Inject --prefix instalation path into params 302 | params.push('--prefix', installPath, '--verbose', '--_exit', 'true'); 303 | 304 | // Check if it's already installing something 305 | if(exports.isExecuting()){ 306 | // Callback with error 307 | cb && cb('Cannot run npm command before finishing previous one'); 308 | 309 | // Return null 310 | return null; 311 | } 312 | 313 | // Set installing flag to `true` 314 | console.log(TAG, chalk.green(`npm run ${params[0]} ${params[1]}...`)); 315 | exports.setExecuting(true); 316 | 317 | // Stores errors came from stdout 318 | let errors = []; 319 | 320 | var proc = fork(npmCli, params, { 321 | silent: true, 322 | detached: true, 323 | stdio: [ 'pipe', 'pipe', 'pipe', 'ipc'], 324 | }); 325 | 326 | // Wait process to exit 327 | proc.on('exit', (code, signal) => { 328 | code = parsedCode !== null ? parsedCode : code; 329 | let failed = (code != 0); 330 | 331 | // Release cache 332 | exports._cachedExtensions = null; 333 | 334 | // Lower flag of installing 335 | exports.setExecuting(false); 336 | console.log(TAG, chalk.green(`npm run ${params[0]} ${params[1]}... finish: ${code}`)); 337 | emit('ExtensionManager:log', 'server', `Install finished. Code ${code}`); 338 | 339 | // Clear extension update cache 340 | exports._cachedUpdates = null 341 | 342 | // Check for updates 343 | exports.checkUpdates() 344 | 345 | // Callback with error 346 | cb && cb(failed ? errors && errors.join('\r\n') : null); 347 | }) 348 | 349 | // Pipe error interface to electron's IPC 350 | // Join error messages that matches pattern (for possible error throw) 351 | readline.createInterface({ 352 | input: proc.stderr, terminal: false 353 | }).on('line', function(line) { 354 | // Push to errors if matches NPM error log output 355 | if(regError.test(line)) 356 | errors.push(line); 357 | 358 | // Check if it's the end of script 359 | let matches = regExit.exec(line) 360 | if(matches){ 361 | parsedCode = parseInt(matches[1]); 362 | proc.kill(); 363 | } 364 | }); 365 | 366 | return proc; 367 | } 368 | -------------------------------------------------------------------------------- /public/app/ServerRunner.js: -------------------------------------------------------------------------------- 1 | angular.module('ServerRunner', [ 2 | 'Panel', 3 | 'Window', 4 | 'Common', 5 | 6 | 'ExtensionManager', 7 | ]) 8 | 9 | .run(function ($rootScope, PanelService, ServerService, GenericFileModel, Dialogs) { 10 | // Opens Servers panel 11 | PanelService.open({ 12 | name: 'Created Servers', 13 | icon: 'mdi-server', 14 | template: 'views/servers.panel.html', 15 | open: true, 16 | }) 17 | 18 | // Listens to create action 19 | $rootScope.$on('Servers:create', function (event, id) { 20 | Dialogs.prompt('Create the server identifier: (ex: myserver)', 'ID', '', (name) => { 21 | if(!name) 22 | return; 23 | 24 | // Filter name 25 | name = name.toLowerCase(); 26 | 27 | // Creates server 28 | GenericFileModel.save('servers', name); 29 | }); 30 | }); 31 | 32 | // Listens to stop all servers action 33 | $rootScope.$on('Servers:stopAll', function (event, id) { 34 | let state = ServerService.state(); 35 | for(let serverId in state) 36 | ServerService.stop(serverId); 37 | }); 38 | 39 | // Listens to start all servers action 40 | $rootScope.$on('Servers:startAll', function (event, id) { 41 | let servers = GenericFileModel.list('servers'); 42 | for(let k in servers) 43 | ServerService.start(servers[k]); 44 | }); 45 | }) 46 | 47 | // Manage Servers 48 | .service('ServerService', function ($rootScope, ipcRenderer) { 49 | var service = this; 50 | 51 | // Subscribe to general servers update or a specific one 52 | service.subscribe = function (scope, serverId, callback){ 53 | // Accepts only two parameters (second is callback) 54 | if(!callback){ 55 | callback = serverId; 56 | serverId = null; 57 | } 58 | 59 | if(!scope || !callback) 60 | return console.error('Cannot subscribe with invalid scope and callback!'); 61 | 62 | let eventName = 'ServerRunner:update' + (serverId ? ':' + serverId : '' ); 63 | ipcRenderer.on(scope, eventName, callback); 64 | } 65 | 66 | // Subscribe to logs on a specific server 67 | service.subscribeLogs = function (scope, serverId, callback){ 68 | if(!scope || !callback) 69 | return console.error('Cannot subscribe with invalid scope and callback!'); 70 | 71 | let eventName = 'ServerRunner:log:' + serverId; 72 | ipcRenderer.on(scope, eventName, callback); 73 | } 74 | 75 | service.start = function (serverId) { 76 | ipcRenderer.send('ServerRunner:start', serverId); 77 | } 78 | 79 | service.stop = function (serverId) { 80 | ipcRenderer.send('ServerRunner:stop', serverId); 81 | } 82 | 83 | service.state = function (serverId) { 84 | return ipcRenderer.sendSync('ServerRunner:state', serverId); 85 | } 86 | }) 87 | 88 | 89 | .controller('ServersPanelCtrl', function ($scope, $rootScope, 90 | ServerService, WindowService, GenericFileModel){ 91 | 92 | // `servers` holds the list of all servers 93 | $scope.servers = {}; 94 | $scope.serversPorts = {}; 95 | 96 | // `serversState` holds the list of running servers 97 | $scope.serversState = {}; 98 | 99 | // Opens a new window for the Server 100 | $scope.openServerWindow = function (serverId) { 101 | WindowService.open({ 102 | id: 'Server:'+serverId, 103 | name: `Server [${serverId}]`, 104 | template: 'views/servers.window.html', 105 | userData: { 106 | serverId: serverId, 107 | } 108 | }); 109 | } 110 | 111 | // Update list of servers and it's states on update 112 | update(); 113 | GenericFileModel.subscribe($scope, 'servers', update); 114 | ServerService.subscribe($scope, update); 115 | 116 | function update(){ 117 | $scope.servers = GenericFileModel.list('servers'); 118 | $scope.serversState = ServerService.state(); 119 | 120 | // Update servers ports 121 | $scope.serversPorts = {}; 122 | for(var k in $scope.servers){ 123 | let id = $scope.servers[k]; 124 | let server = GenericFileModel.get('servers', id) || {}; 125 | $scope.serversPorts[id] = server.env ? server.env.PORT : '[No Port]'; 126 | } 127 | 128 | // Apply changes to scope if not in digest phase 129 | if(!$scope.$$phase) 130 | $scope.$apply(); 131 | } 132 | }) 133 | 134 | 135 | // 136 | // Configures a server and enables to start/stop 137 | // 138 | .controller('ServerWindowCtrl', function ($scope, GenericFileModel, 139 | ServerService, Dialogs, WindowService, ExtensionManagerService, 140 | NetworkInterfaces, $timeout) { 141 | const MAX_LOG_COUNT = 2000; 142 | 143 | var win = null; 144 | 145 | $scope.serverUp = false; 146 | $scope.serverUpColor = 'red'; 147 | 148 | $scope.statusState = null; 149 | $scope.statusString = null; 150 | 151 | $scope.configs = {}; 152 | $scope.serverId = null; 153 | $scope.networkInterfaces = [] 154 | 155 | $scope.needsSave = false; 156 | 157 | // Holds list of extensions 158 | $scope.extensions = null; 159 | 160 | // Keep logs here 161 | $scope.logs = []; 162 | let logsBuffer = []; 163 | 164 | // Called from view to set this window data 165 | $scope.init = function (_win){ 166 | win = _win; 167 | $scope.serverId = win.userData.serverId; 168 | 169 | // Load data 170 | $scope.load(); 171 | } 172 | 173 | // Loads and overrides all configs into local state vars 174 | $scope.load = function load(){ 175 | updateInstanceState(); 176 | updateServerConfig(); 177 | 178 | // Keep track of the state of this instance 179 | ServerService.subscribe($scope, $scope.serverId, updateInstanceState); 180 | ServerService.subscribeLogs($scope, $scope.serverId, updateLogs); 181 | } 182 | 183 | // Saves data into a persistent storage 184 | $scope.save = function save(){ 185 | GenericFileModel.save('servers', $scope.serverId, $scope.configs); 186 | $scope.needsSave = false; 187 | } 188 | 189 | // Opens a external browser window with the app 190 | $scope.openApp = function openApp(ip) { 191 | let url = `http://${ip || 'localhost'}:${$scope.configs.env.PORT}` 192 | require('electron').shell.openExternal(url); 193 | } 194 | 195 | $scope.openAppMenu = function openAppMenu($mdMenu) { 196 | $timeout(() => { 197 | $mdMenu.open() 198 | }, 100) 199 | $scope.networkInterfaces = NetworkInterfaces.list() 200 | } 201 | 202 | // Removes this server 203 | $scope.deleteServer = function deleteServer(confirm) { 204 | if(confirm) 205 | return Dialogs.confirm('Tem certeza de que deseja deletar este servidor?', 206 | 'Sim', 'Cancelar', (confirm) => { 207 | confirm && $scope.deleteServer(); 208 | }); 209 | 210 | // Make sure server is stopped 211 | ServerService.stop($scope.serverId); 212 | 213 | // Remove data 214 | GenericFileModel.remove('servers', $scope.serverId); 215 | WindowService.close(win); 216 | } 217 | 218 | // Opens up a image file and sets to the logo 219 | $scope.openImage = function openImage() { 220 | const {dialog} = require('electron').remote; 221 | 222 | let imagePath = dialog.showOpenDialog({ 223 | title: 'Select an server`s Logo', 224 | defaultPath: $scope.configs.env.APP_LOGO || undefined, 225 | properties: ['openFile'], 226 | filters: [ 227 | {name: 'Images', extensions: ['jpg', 'png', 'gif']}, 228 | {name: 'All Files', extensions: ['*']} 229 | ], 230 | }); 231 | 232 | // Select imagePath 233 | imagePath = imagePath ? imagePath[0] || null : null; 234 | 235 | // If imagePath is set, update the config 236 | if(!imagePath) 237 | return; 238 | 239 | $scope.configs.env.APP_LOGO = imagePath; 240 | } 241 | 242 | // Open up a path and sets to the DB_FOLDER 243 | $scope.openDBPath = function openDBPath() { 244 | const {dialog} = require('electron').remote; 245 | 246 | let imagePath = dialog.showOpenDialog({ 247 | title: 'Select a place to store the DB', 248 | defaultPath: $scope.configs.env.DB_FOLDER || undefined, 249 | properties: ['openDirectory', 'openFile'], 250 | }); 251 | 252 | // Select imagePath 253 | imagePath = imagePath ? imagePath[0] || null : null; 254 | 255 | // If imagePath is set, update the config 256 | if(!imagePath) 257 | return; 258 | 259 | $scope.configs.env.DB_FOLDER = imagePath; 260 | } 261 | 262 | // Change the server state (start/stop) 263 | $scope.changeServerState = function (){ 264 | // console.log($scope.serverUp); 265 | if($scope.serverUp){ 266 | // Clear logs prior to starting 267 | $scope.logs = []; 268 | logsBuffer = []; 269 | 270 | ServerService.start($scope.serverId); 271 | }else{ 272 | ServerService.stop($scope.serverId); 273 | } 274 | } 275 | 276 | // Listen for changes in the config, and set flag of invalidation 277 | $scope.$watch('configs', function(obj, old) { 278 | if(obj !== old) 279 | $scope.needsSave = true; 280 | }, true); 281 | 282 | function updateInstanceState(){ 283 | let serverUp = $scope.statusState = ServerService.state($scope.serverId); 284 | $scope.serverUp = serverUp; 285 | 286 | if(serverUp){ 287 | $scope.statusString = 'Running'; 288 | $scope.serverUpColor = 'green'; 289 | }else{ 290 | $scope.statusString = 'Stopped'; 291 | $scope.serverUpColor = 'red'; 292 | } 293 | 294 | // Apply changes to scope if not in digest phase 295 | if(!$scope.$$phase) 296 | $scope.$apply(); 297 | } 298 | 299 | function updateServerConfig(){ 300 | const {app} = require('electron').remote; 301 | let configs = GenericFileModel.get('servers', $scope.serverId); 302 | 303 | // Set as not saved if has no data yet 304 | if(!configs.env) 305 | $scope.needsSave = true; 306 | 307 | // Save configs to scope 308 | $scope.configs = configs || {} 309 | } 310 | 311 | // Update logs (pushes to log array and limits it's content) 312 | let counter = 1; 313 | 314 | var refreshLogs = _.throttle(()=>{ 315 | $scope.logs.push(...logsBuffer); 316 | logsBuffer = []; 317 | 318 | // Limit logs 319 | if($scope.logs.length > MAX_LOG_COUNT) 320 | $scope.logs.splice(0, $scope.logs.length - MAX_LOG_COUNT); 321 | 322 | // Apply changes to scope if not in digest phase 323 | if(!$scope.$$phase) 324 | $scope.$apply(); 325 | }, 100) 326 | 327 | function updateLogs(evt, type, message){ 328 | logsBuffer.push([counter++, type, message]); 329 | 330 | refreshLogs(); 331 | } 332 | 333 | // Update extension listing 334 | updateExtensions() 335 | ExtensionManagerService.subscribe($scope, 'update', updateExtensions) 336 | ExtensionManagerService.subscribe($scope, 'executing', updateExtensions) 337 | 338 | function updateExtensions(){ 339 | $scope.extensions = ExtensionManagerService.list(); 340 | 341 | // Apply changes to scope if not in digest phase 342 | if(!$scope.$$phase) 343 | $scope.$apply(); 344 | } 345 | 346 | }) 347 | --------------------------------------------------------------------------------