├── 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 |
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 | 
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 | 
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 |
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 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
70 |
71 |
72 |
73 |
Logs ({{logs.length}})
74 |
75 |
76 |
77 |
78 |
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 |
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 |
--------------------------------------------------------------------------------