├── app ├── api │ └── .gitkeep ├── utils │ ├── .gitkeep │ ├── cycle.js │ └── front-end-dispatcher.js ├── appicon.ico ├── appicon.icns ├── appicon-512.png ├── assets │ ├── css │ │ ├── _keyframes.scss │ │ ├── _colours.scss │ │ ├── style.scss │ │ ├── _titlebar.scss │ │ ├── _logs.scss │ │ ├── _reset.scss │ │ ├── _optionsbar.scss │ │ ├── _emptysiteslist.scss │ │ ├── _siteswitch.scss │ │ └── _siteslist.scss │ └── img │ │ ├── btn_minimize.svg │ │ ├── btn_edit.svg │ │ ├── btn_close.svg │ │ ├── icn_rolling.svg │ │ ├── btn_forget.svg │ │ ├── btn_export.svg │ │ ├── btn_create.svg │ │ ├── icn_folder.svg │ │ ├── btn_preview.svg │ │ ├── btn_open.svg │ │ ├── btn_settings.svg │ │ └── btn_logs.svg ├── index.js ├── app.html ├── logs-index.html ├── logs-index.js ├── containers │ ├── MainRenderer.js │ └── DevTools.js ├── hot-dev-app.html ├── hot-dev-logs.html └── components │ ├── Log.js │ ├── Reporter.js │ ├── Logs-UI.js │ ├── Title-bar.js │ ├── simple-button.js │ ├── Options-bar.js │ ├── UI.js │ ├── Empty-sites-list.js │ ├── Logs-list.js │ ├── Sites-list.js │ └── Site.js ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── server.js ├── webpack.config.base.js ├── LICENSE ├── main.js ├── server ├── storage.js ├── windows.js ├── site-controller.js ├── dispatcher.js ├── sites-store.js ├── logger.js ├── process-controller.js └── menu.js ├── webpack.config.development.js ├── readme.md ├── webpack.config.production.js ├── install.js ├── package.js ├── package.json └── CHANGELOG.md /app/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L-A/Little-Jekyll/HEAD/app/appicon.ico -------------------------------------------------------------------------------- /app/appicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L-A/Little-Jekyll/HEAD/app/appicon.icns -------------------------------------------------------------------------------- /app/appicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L-A/Little-Jekyll/HEAD/app/appicon-512.png -------------------------------------------------------------------------------- /app/assets/css/_keyframes.scss: -------------------------------------------------------------------------------- 1 | @keyframes flash { 2 | from { 3 | background-color: $primary-color; 4 | } 5 | 6 | to { 7 | background-color: $switch-bg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["add-module-exports"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import UI from './components/UI.js'; 4 | import css from './assets/css/style.scss'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Little Jekyll 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/logs-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Little Jekyll 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/logs-index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import LogsUI from './components/Logs-UI.js'; 4 | import css from './assets/css/style.scss'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /app/containers/MainRenderer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Renderer from '../components/Renderer'; 3 | 4 | export default class MainRenderer extends Component { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/hot-dev-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Little Jekyll 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/hot-dev-logs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Little Jekyll 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/Log.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | var Log = React.createClass({ 4 | render: function () { 5 | return ( 6 |
  • {this.props.log.logData}

  • 7 | ); 8 | } 9 | }) 10 | 11 | module.exports = Log; 12 | -------------------------------------------------------------------------------- /app/components/Reporter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dispatcher from '../utils/front-end-dispatcher'; 3 | 4 | var displayReport = function(event, message) { 5 | console.log(message); 6 | } 7 | 8 | Dispatcher.createCallback('report', displayReport); 9 | Dispatcher.send('hello'); 10 | console.log('Reporter is up!'); 11 | -------------------------------------------------------------------------------- /app/assets/img/btn_minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /app/utils/cycle.js: -------------------------------------------------------------------------------- 1 | var cycleThrough = function(o, currentPos, delta) { 2 | var oLength = 0; 3 | if (o.length != undefined) { 4 | oLength = o.length; 5 | } else { 6 | return false; 7 | } 8 | if ((currentPos + delta) >= oLength) ( delta -= oLength ); 9 | if ((currentPos + delta) < 0) ( delta += oLength ); 10 | return currentPos + delta; 11 | } 12 | 13 | module.exports = cycleThrough; 14 | -------------------------------------------------------------------------------- /app/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "react/jsx-uses-react": 2, 11 | "react/jsx-uses-vars": 2, 12 | "react/react-in-jsx-scope": 2, 13 | 14 | "no-var": 0, 15 | "vars-on-top": 0, 16 | "comma-dangle": 0, 17 | "no-use-before-define": 0 18 | }, 19 | "plugins": [ 20 | "react" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /app/assets/img/btn_edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/img/btn_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/Logs-UI.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import TitleBar from './Title-bar'; 4 | import LogsList from './Logs-list'; 5 | import Dispatcher from '../utils/front-end-dispatcher'; 6 | 7 | var LogsUI = React.createClass({ 8 | getInitialState: function() { 9 | return {}; 10 | }, 11 | render: function() { 12 | return ( 13 |
    14 | 15 | 16 |
    17 | ); 18 | } 19 | }); 20 | 21 | // I keep forgetting to export. So here's a reminder. 22 | module.exports = LogsUI; 23 | -------------------------------------------------------------------------------- /app/assets/img/icn_rolling.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | node_modules 3 | 4 | # OS X 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | # If Sass goes crazy 32 | .sass-cache 33 | 34 | # Little-Jekyll specific 35 | dist/ 36 | release/ 37 | .install_cache/ 38 | jekyll/ 39 | -------------------------------------------------------------------------------- /app/assets/css/_colours.scss: -------------------------------------------------------------------------------- 1 | $primary-text-color: #61636b; 2 | $primary-color: #4350a6; 3 | $success-color: #068666; 4 | $error-color: #be1d49; 5 | 6 | $light-color: #e9eaf3; 7 | $switch-bg: #f1f1f1; 8 | $boring-gray:lighten($primary-text-color, 20); 9 | $options-bg-color: #4350a6; 10 | $options-contrast-color: lighten($options-bg-color, 40%); 11 | 12 | $console-bg-color: #3e4044; 13 | 14 | $options-gradient: linear-gradient(to bottom, $options-bg-color, darken($options-bg-color, 5%)); 15 | $options-reverse-gradient: linear-gradient(to top, $options-bg-color, darken($options-bg-color, 5%)); 16 | 17 | $knob-gradient: linear-gradient(to bottom, lighten($primary-color, 4), $primary-color); 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | addons: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | 18 | install: 19 | - export CXX="g++-4.8" 20 | - npm install 21 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 22 | 23 | before_script: 24 | - export DISPLAY=:99.0 25 | - sh -e /etc/init.d/xvfb start & 26 | - sleep 3 27 | 28 | script: 29 | - npm run lint 30 | - npm run test 31 | - npm run build 32 | - npm run test-e2e 33 | -------------------------------------------------------------------------------- /app/assets/img/btn_forget.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/utils/front-end-dispatcher.js: -------------------------------------------------------------------------------- 1 | import {ipcRenderer} from 'electron'; 2 | 3 | var dispatcher = { 4 | send: function(message, content) { 5 | content = content || null; 6 | if (content == null) { 7 | ipcRenderer.send(message); 8 | } else { 9 | ipcRenderer.send(message, content); 10 | } 11 | console.log('Sending ' + message); 12 | }, 13 | createCallback: function(channel, callback) { 14 | ipcRenderer.on(channel, callback); 15 | } 16 | } 17 | 18 | ipcRenderer.on("log", function(event, ...args) { 19 | console.log("--- Server event ---"); 20 | args.forEach(function(arg){ 21 | console.log(arg); 22 | }); 23 | console.log("--- Fin ---"); 24 | }); 25 | 26 | module.exports = dispatcher; 27 | -------------------------------------------------------------------------------- /app/assets/img/btn_export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/Title-bar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { remote } from 'electron'; 3 | 4 | var TitleBar = React.createClass({ 5 | minimize: function() { 6 | var window = remote.BrowserWindow.getFocusedWindow(); 7 | window.minimize(); 8 | }, 9 | close: function() { 10 | var window = remote.BrowserWindow.getFocusedWindow(); 11 | if (process.platform !== 'darwin') { 12 | window.close(); 13 | } else { 14 | window.hide(); 15 | } 16 | 17 | }, 18 | render: function () { 19 | return ( 20 |
    21 | 22 | 23 |
    24 | ); 25 | } 26 | }) 27 | 28 | module.exports = TitleBar; 29 | -------------------------------------------------------------------------------- /app/assets/img/btn_create.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/simple-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dispatcher from '../utils/front-end-dispatcher'; 3 | 4 | var SimpleButton = React.createClass({ 5 | getInitialState: function() { 6 | return {hintText: this.props.hintText}; 7 | }, 8 | reportHover: function() { 9 | Dispatcher.send('hint', this.state.hintText); 10 | }, 11 | endHover: function() { 12 | Dispatcher.send('endHint'); 13 | }, 14 | render: function() { 15 | return ( 16 | 20 | {this.props.textContent || ""} 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | }) 26 | 27 | module.exports = SimpleButton; 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0, no-console: 0 */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const express = require('express'); 6 | const webpack = require('webpack'); 7 | const config = require('./webpack.config.development'); 8 | 9 | const app = express(); 10 | const compiler = webpack(config); 11 | 12 | const PORT = 3000; 13 | 14 | app.use(require('webpack-dev-middleware')(compiler, { 15 | publicPath: config.output.publicPath, 16 | stats: { 17 | colors: true 18 | } 19 | })); 20 | 21 | app.use(require('webpack-hot-middleware')(compiler)); 22 | 23 | app.get('*', (req, res) => { 24 | res.sendFile(path.join(__dirname, 'app', 'hot-dev-app.html')); 25 | }); 26 | 27 | app.listen(PORT, 'localhost', err => { 28 | if (err) { 29 | console.log(err); 30 | return; 31 | } 32 | 33 | console.log(`Listening at http://localhost:${PORT}`); 34 | }); 35 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0 */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | module: { 8 | loaders: [{ 9 | test: /\.jsx?$/, 10 | loaders: ['babel-loader'], 11 | exclude: /node_modules/ 12 | }, 13 | { 14 | test: /\.scss$/, 15 | loaders: ["style", "css", "sass"] 16 | }, 17 | { 18 | test: /\.svg$/, 19 | loader: 'file-loader' 20 | }] 21 | }, 22 | output: { 23 | path: path.join(__dirname, 'dist'), 24 | filename: '[name].js', 25 | libraryTarget: 'commonjs2' 26 | }, 27 | resolve: { 28 | extensions: ['', '.js', '.jsx'], 29 | packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] 30 | }, 31 | plugins: [ 32 | 33 | ], 34 | externals: [ 35 | // put your node 3rd party libraries which can't be built with webpack here (mysql, mongodb, and so on..) 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /app/assets/img/icn_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | $title-bar-height: 36px; 2 | $options-bar-height: 48px; 3 | 4 | // Animation stuff 5 | $bouncy-ease: cubic-bezier(0.650, 1.650, 0.490, 0.970); 6 | $bouncy-ease-smoother: cubic-bezier(0.650, 1.250, 0.290, 0.970); 7 | $timing-smooth: 500ms; 8 | $timing-snappy: 250ms; 9 | $timing-snappy-btn: 100ms; 10 | 11 | @import 'colours'; 12 | @import 'reset'; 13 | @import 'keyframes'; 14 | @import 'titlebar'; 15 | @import 'siteslist'; 16 | @import 'siteswitch'; 17 | @import 'optionsbar'; 18 | @import 'logs'; 19 | @import 'emptysiteslist'; 20 | 21 | html { 22 | height: 100%; 23 | width: 100%; 24 | overflow: hidden; 25 | } 26 | 27 | body { 28 | color: $primary-text-color; 29 | font-family: -apple-system, Helvetica, Arial, sans-serif; 30 | margin: 0; 31 | height: 100%; 32 | } 33 | 34 | .ui-root, .logs-root { 35 | display: flex; 36 | flex-flow: column; 37 | height: 100%; 38 | margin: 0; 39 | } 40 | 41 | body>div { 42 | height: 100%; 43 | } 44 | -------------------------------------------------------------------------------- /app/components/Options-bar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dispatcher from '../utils/front-end-dispatcher'; 3 | import SimpleButton from './simple-button' 4 | 5 | var OptionsBar = React.createClass({ 6 | requestNewSite: function() { 7 | Dispatcher.send('addSite'); 8 | }, 9 | createNewSite: function() { 10 | Dispatcher.send('createSite'); 11 | }, 12 | render: function () { 13 | return ( 14 |
    15 | 16 | {this.props.hintText} 17 | 18 | {/* */} 19 |
    20 | ); 21 | } 22 | }) 23 | 24 | module.exports = OptionsBar; 25 | -------------------------------------------------------------------------------- /app/assets/css/_titlebar.scss: -------------------------------------------------------------------------------- 1 | .title-bar { 2 | flex: 0 0 $title-bar-height; 3 | justify-content: flex-start; 4 | 5 | -webkit-app-region: drag; 6 | -webkit-user-select: none; 7 | cursor: default; 8 | 9 | background-color: white; 10 | height: 36px; 11 | width: 100%; 12 | 13 | z-index: 2; 14 | 15 | box-shadow: 0 0 8px 0 #afb0b5; 16 | 17 | .btn-close, .btn-minimize { 18 | background: $boring-gray; 19 | cursor: default; 20 | display: inline-block; 21 | opacity: .4; 22 | margin-top: 10px; 23 | 24 | height: 18px; 25 | width: 18px; 26 | 27 | -webkit-app-region: no-drag; 28 | 29 | transition: background-color $timing-snappy-btn linear; 30 | 31 | &:hover { background: darken($boring-gray, 50%); } 32 | &:active { background: darken($boring-gray, 35%); } 33 | } 34 | 35 | .btn-close { 36 | -webkit-mask: url(../img/btn_close.svg) center no-repeat; 37 | margin-left: 10px; 38 | } 39 | 40 | .btn-minimize { 41 | -webkit-mask: url(../img/btn_minimize.svg) center no-repeat; 42 | margin-left: 6px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/assets/img/btn_preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/assets/css/_logs.scss: -------------------------------------------------------------------------------- 1 | .logs-root { 2 | background-color: white; 3 | color: $primary-text-color; 4 | font-family: monospace; 5 | 6 | 7 | .logs-list { 8 | flex: 1 0 1; 9 | margin: 0; 10 | position: relative; 11 | 12 | overflow-y: scroll; 13 | -webkit-overflow-scrolling: touch; 14 | z-index: 1; 15 | 16 | li { 17 | align-items: baseline; 18 | border-bottom: solid 1px $light-color; 19 | border-left: solid 4px $light-color; 20 | display: flex; 21 | flex-direction: row; 22 | 23 | padding: 6px 12px; 24 | 25 | .log-data { 26 | flex: 1 1 auto; 27 | padding-right: 1em; 28 | white-space: pre-line; 29 | } 30 | 31 | .time { 32 | color: darken($light-color, 20%); 33 | flex: 0 0 100px; 34 | font-size: 0.8em; 35 | } 36 | 37 | &.success { 38 | border-left-color: $success-color; 39 | color: $success-color; 40 | } 41 | 42 | &.err { 43 | border-left-color: $error-color; 44 | color: $error-color; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/assets/img/btn_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 L-A Labadie 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 | -------------------------------------------------------------------------------- /app/components/UI.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import TitleBar from './Title-bar'; 4 | import SitesList from './Sites-list'; 5 | import OptionsBar from './Options-bar'; 6 | import Reporter from './Reporter'; 7 | import Dispatcher from '../utils/front-end-dispatcher'; 8 | 9 | var UI = React.createClass({ 10 | getInitialState: function() { 11 | Dispatcher.createCallback('hint', this.handleChildHover); 12 | Dispatcher.createCallback('endHint', this.endChildHover); 13 | return {hintText: '', hintAvailable: false}; 14 | }, 15 | handleChildHover: function(event, hintText) { 16 | if (hintText == undefined) { hintText = "" }; 17 | this.setState({hintText: hintText, hintAvailable: true}); 18 | }, 19 | endChildHover: function () { 20 | this.setState({hintAvailable: false}); 21 | }, 22 | render: function() { 23 | return ( 24 |
    25 | 26 | 27 | 28 |
    29 | ); 30 | } 31 | }); 32 | 33 | // I keep forgetting to export. So here's a reminder. 34 | module.exports = UI; 35 | -------------------------------------------------------------------------------- /app/assets/img/btn_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0 */ 2 | 'use strict'; 3 | 4 | require('babel-core/register'); 5 | 6 | const electron = require('electron'); 7 | const app = electron.app; 8 | const crashReporter = electron.crashReporter; 9 | 10 | const Windows = require('./server/windows.js'); 11 | const appServer = require('./server/dispatcher.js'); 12 | 13 | let mainWindow = null; 14 | 15 | // crashReporter.start(); 16 | 17 | if (process.env.NODE_ENV === 'development') { 18 | require('electron-debug')(); 19 | } 20 | 21 | app.on('window-all-closed', () => { 22 | if (process.platform !== 'darwin') app.quit() 23 | else mainWindow = null; 24 | }); 25 | 26 | var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) { 27 | // Someone tried to run a second instance, we should focus our window 28 | if (mainWindow) { 29 | if (mainWindow.isMinimized()) mainWindow.restore(); 30 | mainWindow.focus(); 31 | } 32 | return true; 33 | }); 34 | 35 | if (shouldQuit) { 36 | app.quit(); 37 | return; 38 | } 39 | 40 | app.on('will-quit', function() { 41 | appServer.handleWillQuit(); 42 | }) 43 | 44 | app.on('ready', function() { mainWindow = Windows.initMain(appServer) }); 45 | app.on('activate', function() { 46 | if (mainWindow == null) { mainWindow = Windows.initMain(appServer) } 47 | else { (mainWindow.show()) } 48 | }); 49 | -------------------------------------------------------------------------------- /app/assets/css/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | * {box-sizing: border-box; cursor: default;} 7 | 8 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | font-size: 100%; 13 | font: inherit; 14 | vertical-align: baseline; } 15 | 16 | /* HTML5 display-role reset for older browsers */ 17 | 18 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 19 | display: block; } 20 | 21 | body { 22 | line-height: 1; } 23 | 24 | ol, ul { 25 | list-style: none; } 26 | 27 | blockquote, q { 28 | quotes: none; } 29 | 30 | blockquote { 31 | &:before, &:after { 32 | content: ''; 33 | content: none; } } 34 | 35 | q { 36 | &:before, &:after { 37 | content: ''; 38 | content: none; } } 39 | 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; } 43 | -------------------------------------------------------------------------------- /app/components/Empty-sites-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dispatcher from '../utils/front-end-dispatcher'; 3 | import SimpleButton from './simple-button' 4 | 5 | var EmptySitesList = React.createClass({ 6 | requestNewSite: function() { 7 | Dispatcher.send('addSite'); 8 | }, 9 | createNewSite: function() { 10 | Dispatcher.send('createSite'); 11 | }, 12 | render: function () { 13 | var listClass = this.props.isActive ? "empty-sites-list active" : "empty-sites-list"; 14 | var buttonsRow = ( 15 |
    16 | 17 |
    18 | Create 19 | 20 | 21 |
    22 | Open 23 | 24 |
    25 | ) 26 | 27 | if (this.props.sitesReceived) { 28 | return ( 29 |
    30 |

    Oh dear! This list is empty.

    31 | {buttonsRow} 32 |
    33 |
    34 | ) 35 | } else { 36 | return ( 37 |
    38 | ) 39 | } 40 | } 41 | }) 42 | 43 | module.exports = EmptySitesList; 44 | -------------------------------------------------------------------------------- /server/storage.js: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | var appDataPath = path.join(app.getPath('userData'), "sitesList.json") ; 6 | var appDataIsBeingWritten = false; 7 | 8 | var storableProperties = ["filePath", "id", "name"]; 9 | 10 | module.exports.attemptToOpenSitesList = function (callback, sender) { 11 | fs.readFile(appDataPath, 'utf8', function (err, data) { 12 | if (err) { 13 | callback([], sender); 14 | } else { 15 | callback(JSON.parse(data), sender); 16 | } 17 | }); 18 | } 19 | 20 | module.exports.updateSitesList = function (sitesList) { 21 | var sitesListString = createStorableList(sitesList); 22 | if (!appDataIsBeingWritten) { 23 | appDataIsBeingWritten = true; 24 | fs.writeFile(appDataPath, sitesListString, function(){ 25 | appDataIsBeingWritten = false; 26 | }); 27 | } 28 | } 29 | 30 | var createStorableList = function (sitesList) { 31 | var storableList = []; 32 | for (var i = 0; i < sitesList.length; i++) { 33 | storableList[i] = {}; 34 | for (var j = 0; j < storableProperties.length; j++) { 35 | var prop = storableProperties[j] 36 | storableList[i][prop] = sitesList[i][prop]; 37 | 38 | // stores whether to start a server on the next app load 39 | storableList[i].serverWorking = sitesList[i].serverActive; 40 | } 41 | } 42 | return JSON.stringify(storableList); 43 | } 44 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0 */ 2 | 'use strict'; 3 | 4 | const webpack = require('webpack'); 5 | const webpackTargetElectronRenderer = require('webpack-target-electron-renderer'); 6 | const baseConfig = require('./webpack.config.base'); 7 | 8 | 9 | const config = Object.create(baseConfig); 10 | 11 | config.debug = true; 12 | 13 | config.devtool = 'cheap-module-eval-source-map'; 14 | 15 | config.entry = { 16 | index: ['webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', './app/index.js'], 17 | logs: ['webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', './app/logs-index.js'] 18 | }; 19 | 20 | config.output.publicPath = 'http://localhost:3000/dist/'; 21 | 22 | config.module.loaders.push({ 23 | test: /^((?!\.module).)*\.css$/, 24 | loaders: [ 25 | 'style-loader', 26 | 'css-loader' 27 | ] 28 | }, { 29 | test: /\.module\.css$/, 30 | loaders: [ 31 | 'style-loader', 32 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!' 33 | ] 34 | }); 35 | 36 | 37 | config.plugins.push( 38 | new webpack.HotModuleReplacementPlugin(), 39 | new webpack.NoErrorsPlugin(), 40 | new webpack.DefinePlugin({ 41 | '__DEV__': true, 42 | 'process.env': { 43 | 'NODE_ENV': JSON.stringify('development') 44 | } 45 | }) 46 | ); 47 | 48 | config.target = webpackTargetElectronRenderer(config); 49 | 50 | module.exports = config; 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Little Jekyll 2 | 3 | ## **Development paused** (11/11/2016) 4 | 5 | Since Jekyll has recently moved to [gem-based themes](http://jekyllrb.com/news/2016/07/26/jekyll-3-2-0-released/), my method for including an install-less Jekyll conflicts with the direction they are taking. 6 | 7 | It also (in my opinion) adds more obstacles to the learning steps that a beginner might take in learning to build for the web, which goes against the general mission I gave myself with Little Jekyll. 8 | 9 | <3 10 | 11 | --- 12 | 13 | ### To use gem-based themes with Little-Jekyll 14 | 15 | Any gem-based theme can be converted to the "old" way of including theme files in your repo: [Converting gem-based themes to regular themes](https://jekyllrb.com/docs/themes/#converting-gem-based-themes-to-regular-themes) 16 | 17 | --- 18 | 19 | A desktop app to manage Jekyll websites, overview and control your Jekyll processes. 20 | 21 | ## Setup 22 | 23 | - `git clone` 24 | - `npm install` 25 | 26 | ## Development 27 | 28 | In two terminal sessions: 29 | 30 | - `npm run hot-server` for live-reloading 31 | - `npm run start-hot` for Electron to start in hot mode. The front-end components will auto-reload. 32 | 33 | ## Packaging 34 | - `npm run package` does a test packaging of the Darwin (OS X) distributable. 35 | - `npm run package-all` does Windows, Linux (x86, x64), and Darwin. 36 | 37 | ## License and acknowledgements 38 | 39 | License: [MIT](../master/LICENSE) 40 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0 */ 2 | 'use strict'; 3 | 4 | const webpack = require('webpack'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const webpackTargetElectronRenderer = require('webpack-target-electron-renderer'); 7 | const baseConfig = require('./webpack.config.base'); 8 | 9 | const config = Object.create(baseConfig); 10 | 11 | config.devtool = 'source-map'; 12 | 13 | config.entry = { 14 | index: './app/index', 15 | logs: './app/logs-index' 16 | }; 17 | 18 | config.output.publicPath = '../dist/'; 19 | 20 | config.module.loaders.push({ 21 | test: /^((?!\.module).)*\.css$/, 22 | loader: ExtractTextPlugin.extract( 23 | 'style-loader', 24 | 'css-loader' 25 | ) 26 | }, { 27 | test: /\.module\.css$/, 28 | loader: ExtractTextPlugin.extract( 29 | 'style-loader', 30 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 31 | ) 32 | }, { 33 | test: /\.module\.svg$/, 34 | loader: ExtractTextPlugin.extract( 35 | 'file-loader' 36 | ) 37 | }); 38 | 39 | config.plugins.push( 40 | new webpack.optimize.OccurenceOrderPlugin(), 41 | new webpack.DefinePlugin({ 42 | '__DEV__': false, 43 | 'process.env': { 44 | 'NODE_ENV': JSON.stringify('production') 45 | } 46 | }), 47 | new webpack.optimize.UglifyJsPlugin({ 48 | compressor: { 49 | screw_ie8: true, 50 | warnings: false 51 | } 52 | }), 53 | new ExtractTextPlugin('style.css', { allChunks: true }) 54 | ); 55 | 56 | config.target = webpackTargetElectronRenderer(config); 57 | 58 | module.exports = config; 59 | -------------------------------------------------------------------------------- /app/components/Logs-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Site from './Site'; 3 | import Log from './Log'; 4 | import Dispatcher from '../utils/front-end-dispatcher'; 5 | import { VelocityElement, VelocityTransitionGroup } from 'velocity-react'; 6 | 7 | var LogsList = React.createClass({ 8 | getInitialState: function () { 9 | Dispatcher.createCallback('setLogs', this.receiveLogs); 10 | Dispatcher.send('getLogs'); 11 | this.shouldScroll = true; 12 | return {logs:[]}; 13 | }, 14 | componentDidMount: function() { 15 | this.node = require('react-dom').findDOMNode(this); 16 | }, 17 | handleScroll: function(e) { 18 | var node = this.node; 19 | this.shouldScroll = node.scrollTop + node.offsetHeight === node.scrollHeight; 20 | console.log(this.shouldScroll); 21 | }, 22 | scrollDown: function() { 23 | console.log("I should scroll down"); 24 | }, 25 | receiveLogs: function (event, receivedLogs) { 26 | this.setState({logs: receivedLogs}); 27 | }, 28 | componentDidUpdate: function () { 29 | if(this.shouldScroll) { 30 | console.log("I should scroll down"); 31 | this.node.scrollTop = this.node.scrollHeight + 200; 32 | } 33 | }, 34 | render: function () { 35 | if (this.state.logs != null && this.state.logs.length > 0) { 36 | var logsNodes = this.state.logs.map( function(data, rank){ 37 | return ( 38 | 39 | ); 40 | }) 41 | } 42 | return( 43 |
      44 | {logsNodes} 45 |
    46 | ) 47 | } 48 | }) 49 | 50 | module.exports = LogsList; 51 | -------------------------------------------------------------------------------- /app/assets/css/_optionsbar.scss: -------------------------------------------------------------------------------- 1 | .options-bar { 2 | display: flex; 3 | flex: 0 0 $options-bar-height; 4 | flex-direction: row; 5 | align-self: flex-end; 6 | 7 | background-image: $options-gradient; 8 | color: $light-color; 9 | position: relative; 10 | height: $options-bar-height; 11 | width: 100%; 12 | 13 | transition: transform $timing-snappy ease-out; 14 | 15 | .btn-create, .btn-open, .btn-settings { 16 | align-self: center; 17 | background: $options-contrast-color; 18 | cursor: default; 19 | display: inline-block; 20 | margin: 10px 8px; 21 | 22 | transition: background-color $timing-snappy-btn linear; 23 | 24 | height: 24px; 25 | width: 24px; 26 | 27 | &:hover { background: lighten($options-contrast-color, 10%); } 28 | &:active { background: lighten($options-contrast-color, 20%); } 29 | } 30 | 31 | .hint-text { 32 | align-self: center; 33 | color: $options-contrast-color; 34 | opacity: 0; 35 | text-align: center; 36 | flex: 1 0 auto; 37 | font-weight: 300; 38 | font-size: 11px; 39 | transition: opacity $timing-snappy linear; 40 | transition-delay: 350ms; 41 | 42 | &.hint-available { 43 | opacity: 1; 44 | transition: opacity $timing-snappy-btn linear; 45 | } 46 | } 47 | 48 | .btn-create { 49 | -webkit-mask: url(../img/btn_create.svg) center no-repeat; 50 | } 51 | 52 | .btn-open { 53 | -webkit-mask: url(../img/btn_open.svg) center no-repeat; 54 | margin-right: 10px; 55 | } 56 | 57 | .btn-settings { 58 | -webkit-mask: url(../img/btn_settings.svg) center no-repeat; 59 | } 60 | 61 | } 62 | 63 | .empty-sites-list + .options-bar { 64 | transform: translateY($options-bar-height); 65 | } 66 | -------------------------------------------------------------------------------- /app/assets/img/btn_logs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /server/windows.js: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import { BrowserWindow, Menu, app } from 'electron'; 3 | 4 | const appPath = Path.join(app.getAppPath(), 'app'); 5 | const darwin = (process.platform === 'darwin'); 6 | const win32 = (process.platform === 'win32'); 7 | 8 | module.exports.initMain = function (appServer) { 9 | let menu; 10 | let template; 11 | var iconPath = ""; 12 | 13 | if (!darwin) { 14 | iconPath = Path.join( appPath, ( win32 ? "appicon.ico" : "appicon-512.png")); 15 | } 16 | console.log(iconPath); 17 | 18 | var mainWindow = new BrowserWindow({ 19 | frame: false, 20 | icon: (iconPath || null), 21 | width: 340, 22 | height: 260, 23 | minWidth: 340, 24 | minHeight: 230, 25 | acceptFirstMouse: true 26 | }); 27 | 28 | var url = Path.join("file://", appPath, (process.env.HOT ? '/hot-dev-app.html' : 'app.html' )); 29 | 30 | mainWindow.loadURL(url); 31 | 32 | mainWindow.on('closed', () => { 33 | mainWindow = null; 34 | }); 35 | 36 | // if (darwin) { 37 | // mainWindow.on('close', function(event) { 38 | // event.preventDefault(); 39 | // mainWindow.hide(); 40 | // }) 41 | // } 42 | 43 | if (darwin) { 44 | template = require("./menu.js").osxMenu(app, appServer, mainWindow); 45 | menu = Menu.buildFromTemplate(template); 46 | Menu.setApplicationMenu(menu); 47 | } else { 48 | template = require("./menu.js").winLinMenu(mainWindow, appServer); 49 | menu = Menu.buildFromTemplate(template); 50 | mainWindow.setMenu(menu); 51 | } 52 | return mainWindow; 53 | }; 54 | 55 | module.exports.initLogs = function () { 56 | var logsWindow = new BrowserWindow({ 57 | frame: false, 58 | width: 600, 59 | height: 320, 60 | minWidth: 400, 61 | minHeight: 320, 62 | acceptFirstMouse: true 63 | }); 64 | 65 | var url = Path.join("file://", appPath, (process.env.HOT ? '/hot-dev-logs.html' : 'logs-index.html' )); 66 | 67 | logsWindow.loadURL(url); 68 | 69 | logsWindow.on('closed', () => { 70 | logsWindow = null; 71 | }); 72 | 73 | // if (darwin) { 74 | // logsWindow.on('close', function(event) { 75 | // event.preventDefault(); 76 | // logsWindow.hide(); 77 | // }) 78 | // } 79 | 80 | return logsWindow; 81 | }; 82 | -------------------------------------------------------------------------------- /app/assets/css/_emptysiteslist.scss: -------------------------------------------------------------------------------- 1 | .empty-sites-list { 2 | display: flex; 3 | flex: 1 1 auto; 4 | flex-direction: column; 5 | justify-content: center; 6 | 7 | font-size: 14px; 8 | font-weight: 300; 9 | 10 | p, .buttons-row { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: center; 14 | } 15 | 16 | .buttons-row { 17 | transition: opacity $timing-snappy linear; 18 | } 19 | 20 | p { 21 | color: $primary-color; 22 | margin: 30px 0; 23 | } 24 | 25 | .btn-create, .btn-open { 26 | align-self: center; 27 | display: flex-row; 28 | flex: 0 0 auto; 29 | 30 | padding: 0 40px; 31 | 32 | span { 33 | color: lighten($primary-text-color, 20%); 34 | font-size: 12px; 35 | 36 | transition: color $timing-snappy-btn linear; 37 | } 38 | 39 | &:hover .icon { 40 | background-color: $primary-color; 41 | } 42 | 43 | &:hover span { 44 | color: $primary-text-color; 45 | } 46 | } 47 | 48 | .btn-create .icon, .btn-open .icon { 49 | background-color: lighten($primary-color, 20%); 50 | height: 34px; 51 | width: 34px; 52 | 53 | margin: 0 auto 6px; 54 | transition: background-color $timing-snappy-btn linear; 55 | 56 | } 57 | .btn-create .icon { 58 | -webkit-mask: url(../img/btn_create.svg) center no-repeat; 59 | -webkit-mask-size: 80%; 60 | } 61 | 62 | .btn-open .icon{ 63 | -webkit-mask: url(../img/btn_open.svg) center no-repeat; 64 | -webkit-mask-size: 95%; 65 | } 66 | 67 | .btn-create { 68 | border-right: 1px solid $light-color; 69 | } 70 | 71 | .activity-indicator { 72 | -webkit-mask: url(../img/icn_rolling.svg) center no-repeat; 73 | -webkit-mask-size: contain; 74 | background-color: transparent; 75 | 76 | position: absolute; 77 | left: 50%; 78 | 79 | height: 24px; 80 | width: 24px; 81 | margin-left: -12px; 82 | 83 | transition: background-color $timing-snappy linear, margin-top $timing-snappy $bouncy-ease; 84 | } 85 | 86 | &.active .activity-indicator { 87 | animation: working 1s $bouncy-ease 0s 10, giving-up linear 1s 10s forwards; 88 | background-color: $primary-color; 89 | margin-top: -40px; 90 | } 91 | 92 | &.active .btn-create, &.active .btn-open { 93 | opacity: 0.1; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/site-controller.js: -------------------------------------------------------------------------------- 1 | import sitesStore from './sites-store.js'; 2 | import processController from './process-controller.js'; 3 | 4 | module.exports.startServerOnSite = function(sender, siteID) { 5 | var site = sitesStore.siteById(siteID); 6 | var newServer = processController.newServer(sender, site.id, site.filePath); 7 | sitesStore.setSiteProperty(siteID, 'serverWorking', true); 8 | sitesStore.setSiteProperty(siteID, 'server', newServer); 9 | sitesStore.sendSitesList(sender); 10 | } 11 | 12 | module.exports.reportRunningServerOnSite = function(sender, siteID) { 13 | sitesStore.setSiteProperty(siteID, 'serverActive', true); 14 | sitesStore.sendSitesList(sender); 15 | } 16 | 17 | module.exports.reportWorkingServerOnSite = function(sender, siteID) { 18 | sitesStore.setSiteProperty(siteID, 'serverWorking', true); 19 | sitesStore.sendSitesList(sender); 20 | } 21 | 22 | module.exports.reportAvailableServerOnSite = function(sender, siteID) { 23 | sitesStore.setSiteProperty(siteID, 'serverWorking', false); 24 | sitesStore.sendSitesList(sender); 25 | } 26 | 27 | module.exports.reportErrorOnSite = function(sender, siteID) { 28 | sitesStore.setSiteProperty(siteID, 'hasError', true); 29 | sitesStore.sendSitesList(sender); 30 | } 31 | 32 | module.exports.reportSuccessOnSite = function(sender, siteID) { 33 | sitesStore.setSiteProperty(siteID, 'hasError', false); 34 | sitesStore.sendSitesList(sender); 35 | } 36 | 37 | module.exports.stopServerOnSite = function(sender, siteID, ignoreLogsWindow) { 38 | var server = sitesStore.siteById(siteID).server; 39 | 40 | if (server) { 41 | processController.stopServer(server, ignoreLogsWindow); 42 | sitesStore.setSiteProperty(siteID, 'serverActive', false); 43 | sitesStore.setSiteProperty(siteID, 'serverWorking', false); 44 | sitesStore.setSiteProperty(siteID, 'hasError', undefined); 45 | sitesStore.setSiteProperty(siteID, 'server', undefined); 46 | } 47 | 48 | if (sender) { sitesStore.sendSitesList(sender) }; 49 | } 50 | 51 | module.exports.removeSite = function (sender, siteID) { 52 | var site = sitesStore.siteById(siteID); 53 | if (site.server) { module.exports.stopServerOnSite(sender, siteID)} 54 | sitesStore.removeSite(siteID); 55 | 56 | sitesStore.sendSitesList(sender); 57 | } 58 | 59 | module.exports.openLogs = function (sender, siteID) { 60 | var site = sitesStore.siteById(siteID); 61 | if (site.server) { site.server.logger.openLogsWindow(siteID); } 62 | 63 | sitesStore.sendSitesList(sender); 64 | } 65 | 66 | module.exports.buildSite = function (sender, siteID) { 67 | sitesStore.buildSite(siteID); 68 | sitesStore.sendSitesList(sender); 69 | } 70 | -------------------------------------------------------------------------------- /app/components/Sites-list.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Site from './Site'; 3 | import Mousetrap from 'Mousetrap'; 4 | import EmptySitesList from './Empty-sites-list'; 5 | import Dispatcher from '../utils/front-end-dispatcher'; 6 | import Cycle from '../utils/cycle'; 7 | import { VelocityElement, VelocityTransitionGroup } from 'velocity-react'; 8 | 9 | var SitesList = React.createClass({ 10 | getInitialState: function() { 11 | Dispatcher.createCallback('updateSitesList', this.receiveSitesList); 12 | Dispatcher.createCallback('activityStarted', this.showActivity); 13 | Dispatcher.createCallback('activityStopped', this.stopActivity); 14 | return {sites: null, isActive: false, selectedSite: 0}; 15 | }, 16 | componentDidMount: function() { 17 | Dispatcher.send('getSitesList'); 18 | Mousetrap.bind('up', this.selectPrevious); 19 | Mousetrap.bind('down', this.selectNext); 20 | }, 21 | componentWillUnmount: function() { 22 | Mousetrap.unbind('up', this.selectNext); 23 | Mousetrap.unbind('down', this.selectPrevious); 24 | }, 25 | receiveSitesList: function( event, list ) { 26 | this.setState({sites: list}); 27 | }, 28 | showActivity: function() { 29 | this.setState({isActive:true}); 30 | }, 31 | stopActivity: function() { 32 | this.setState({isActive:false}) 33 | }, 34 | selectNext: function() { 35 | var nextIndex = Cycle(this.state.sites, this.state.selectedSite, 1); 36 | this.setSelection(nextIndex); 37 | }, 38 | selectPrevious: function() { 39 | var previousIndex = Cycle(this.state.sites, this.state.selectedSite, -1); 40 | this.setSelection(previousIndex); 41 | }, 42 | setSelection: function(index) { 43 | this.setState({selectedSite: index}); 44 | }, 45 | render: function () { 46 | if (this.state.sites != null && this.state.sites.length > 0) { 47 | var siteNodes = this.state.sites.map( function(data, mapIndex){ 48 | 49 | var selectMe = function (){this.setSelection(mapIndex)}; 50 | 51 | return ( 52 | 53 | ); 54 | }, this) 55 | return( 56 | 57 | {siteNodes} 58 | 59 | ) 60 | } else { 61 | return ( 62 | 63 | ); 64 | } 65 | } 66 | }) 67 | 68 | module.exports = SitesList; 69 | -------------------------------------------------------------------------------- /server/dispatcher.js: -------------------------------------------------------------------------------- 1 | import {ipcMain} from 'electron'; 2 | import {app} from 'electron'; 3 | import sitesStore from './sites-store.js'; 4 | import siteController from './site-controller.js'; 5 | import Logger from './logger.js'; 6 | 7 | var reporter = null; 8 | var nextLogs = null; 9 | 10 | // Not all events come from the front-end, but that's 11 | // where we want to display them if needed. 12 | module.exports.reporter = reporter; 13 | ipcMain.on('hello', function(event) { reporter = event.sender; }) 14 | 15 | // The best thing for testing packaged apps 16 | module.exports.report = function(message){ 17 | console.log(message); 18 | if ( reporter ) reporter.send('report', message); 19 | } 20 | 21 | module.exports.createCallback = function (channel, callback) { 22 | ipcMain.on(channel, callback); 23 | } 24 | 25 | module.exports.prepareLogs = function(logsToSend) { 26 | nextLogs = logsToSend; 27 | } 28 | 29 | module.exports.handleWillQuit = function() { 30 | sitesStore.stopAllServers(); 31 | } 32 | 33 | module.exports.createSite = function(sender) { 34 | if (sender || reporter) { 35 | sender = sender ? sender : reporter; 36 | sitesStore.createSite(sender); 37 | } 38 | } 39 | 40 | module.exports.addSite = function(sender) { 41 | if (sender || reporter) { 42 | sender = sender ? sender : reporter; 43 | sitesStore.addSite(sender); 44 | } 45 | } 46 | 47 | module.exports.sendActivityState = function(sender, activity) { 48 | if (sender || reporter) { 49 | sender = sender ? sender : reporter; 50 | if (activity) { 51 | sender.send('activityStarted'); 52 | } else { 53 | sender.send('activityStopped'); 54 | } 55 | } 56 | } 57 | 58 | ipcMain.on('getLogs', function(event) { 59 | if (nextLogs) { 60 | event.sender.send('setLogs', nextLogs); 61 | nextLogs = null; 62 | } 63 | }) 64 | 65 | ipcMain.on('getSitesList', function(event) { 66 | sitesStore.sendSitesList(event.sender); 67 | }); 68 | 69 | ipcMain.on('addSite', function(event) { 70 | module.exports.addSite(event.sender); 71 | }); 72 | 73 | ipcMain.on('createSite', function(event) { 74 | module.exports.createSite(event.sender); 75 | }); 76 | 77 | ipcMain.on('startServer', function(event, siteId) { 78 | siteController.startServerOnSite(event.sender, siteId); 79 | }); 80 | 81 | ipcMain.on('stopServer', function(event, siteId) { 82 | siteController.stopServerOnSite(event.sender, siteId); 83 | }); 84 | 85 | ipcMain.on('removeSiteFromList', function(event, siteId) { 86 | siteController.removeSite(event.sender, siteId); 87 | }); 88 | 89 | ipcMain.on('buildSite', function(event, siteId) { 90 | siteController.buildSite(event.sender, siteId); 91 | }); 92 | 93 | ipcMain.on('openServerLogs', function(event, siteId) { 94 | siteController.openLogs(event.sender, siteId); 95 | }); 96 | 97 | ipcMain.on('hint', function(event, hintText) { 98 | event.sender.send('hint', hintText); 99 | }); 100 | 101 | ipcMain.on('endHint', function(event) { 102 | event.sender.send('endHint'); 103 | }); 104 | -------------------------------------------------------------------------------- /app/assets/css/_siteswitch.scss: -------------------------------------------------------------------------------- 1 | // Variables pour le knob 2 | $switch-knob-length: 6px; // 0 is round 3 | $switch-movement-length: 6px; // 0 is no movement, also determines switch height; 4 | $switch-groove-border-radius: 11px; // Is used in length calculations 5 | $switch-knob-border-radius: 8px; // Is used in length calculations 6 | $switch-edge-difference: $switch-groove-border-radius - $switch-knob-border-radius; 7 | 8 | .site-serve-switch { 9 | display: block; 10 | flex: 0 0 38px; 11 | padding: 0 8px; 12 | 13 | &:hover .groove .knob { opacity: 0.8; } 14 | &:active .groove .knob { opacity: 0.6; } 15 | 16 | .groove { 17 | background-color: $switch-bg; 18 | border-radius: 11px; 19 | padding: $switch-edge-difference; 20 | height: ($switch-edge-difference * 2) + ($switch-knob-border-radius * 2) + $switch-knob-length + $switch-movement-length; 21 | overflow: hidden; 22 | width: ($switch-groove-border-radius * 2); 23 | transition: background-color 200ms linear; 24 | 25 | box-shadow: inset 0 2px 2px $light-color; 26 | 27 | .knob { 28 | border-radius: 8px; 29 | height: ($switch-knob-border-radius * 2) + $switch-knob-length; 30 | position: relative; 31 | margin-top: 0; 32 | width: $switch-knob-border-radius * 2; 33 | 34 | transition: background-color $timing-snappy linear, 35 | margin-top $timing-snappy $bouncy-ease, 36 | height $timing-snappy $bouncy-ease, 37 | opacity $timing-snappy linear; 38 | 39 | .activity-indicator { 40 | -webkit-mask: url(../img/icn_rolling.svg) center no-repeat; 41 | -webkit-mask-size: 10%; 42 | background-color: $boring-gray; 43 | height: ($switch-knob-border-radius * 2) + $switch-knob-length; 44 | 45 | transition: background-color $timing-snappy linear, 46 | -webkit-mask-size $timing-snappy ease; 47 | } 48 | } 49 | } 50 | 51 | &.switch-off .groove .knob { 52 | background-color: $boring-gray; 53 | margin-top: $switch-movement-length; 54 | } 55 | 56 | &.switch-on .groove { 57 | background-color: $switch-bg; 58 | 59 | .knob { 60 | background-color: $primary-color; 61 | margin-top: 0; 62 | 63 | .activity-indicator { 64 | -webkit-mask-size: 10%; 65 | background-color: $primary-color; 66 | } 67 | } 68 | } 69 | 70 | &.switch-on:active .groove .knob { opacity: 0.6; } 71 | 72 | &.switch-working .groove { 73 | .knob { 74 | background-color: $switch-bg; 75 | margin-top: $switch-movement-length/2; 76 | 77 | .activity-indicator { 78 | -webkit-mask-size: 100%; 79 | background-color: $primary-color; 80 | animation: working 1s $bouncy-ease 0s 10, giving-up linear 1s 10s forwards; 81 | } 82 | } 83 | } 84 | } 85 | 86 | @keyframes working { 87 | 0% { 88 | transform: rotate(0deg); 89 | } 90 | 100% { 91 | transform: rotate(360deg); 92 | } 93 | } 94 | 95 | @keyframes giving-up { 96 | 0% { 97 | opacity: 1; 98 | } 99 | 100% { 100 | opacity: 0.4; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Installs the appropriate Traveling Jekyll version in __dirname/jekyll 4 | 5 | const http = require('http'); 6 | const fs = require('fs'); 7 | const zlib = require('zlib'); 8 | const Path = require('path'); 9 | const Request = require('request'); 10 | const rimraf = require('rimraf'); 11 | 12 | const argv = require('minimist')(process.argv.slice(2)); 13 | 14 | const releases_URL = "https://github.com/L-A/Traveling-Jekyll/releases/download/"; 15 | const release_prefix = "/traveling-jekyll-"; 16 | const release_suffix = ".tar.gz"; 17 | const TJ_version = "3.4.3"; 18 | const cache_location = Path.join(__dirname, ".install_cache"); 19 | 20 | const releases = { 21 | "darwin" : { 22 | "x64" : "osx" 23 | }, 24 | "linux" : { 25 | "ia32" : "linux-x86", 26 | "x64" : "linux-x86_64" 27 | } 28 | } 29 | 30 | function downloadLJRelease(platform, arch, cb) { 31 | cb = cb || null; 32 | var fileURL = releaseURL(platform, arch); 33 | var localFile = localFilePath(platform, arch); 34 | 35 | var extractDownloadedArchive = function() { 36 | extract(platform, arch, cb); 37 | } 38 | 39 | fs.access(localFile, fs.F_OK, function(err) { 40 | if (!err) { 41 | console.log("Traveling Jekyll archive found. Delete it (in '/.install-cache') to download a new version instead. "); 42 | extractDownloadedArchive(); 43 | } else { 44 | console.log("Downloading " + fileURL); 45 | download(fileURL, localFile, extractDownloadedArchive); 46 | } 47 | }); 48 | } 49 | 50 | function download(url, dest, cb) { 51 | mkdirSync(cache_location); 52 | Request(url, cb) 53 | .pipe(fs.createWriteStream(dest)); 54 | } 55 | 56 | function extract(platform, arch, cb) { 57 | var localFile = localFilePath(platform, arch); 58 | var jekyllPath = Path.join(__dirname, 'jekyll'); 59 | rmdirSync(jekyllPath); 60 | 61 | var extractor = require('tar').Extract({ 62 | path: jekyllPath, 63 | strip: 1 64 | }); 65 | extractor.on('error', function(err) { 66 | console.log('Error: ' + err); 67 | }); 68 | extractor.on('end', function() { 69 | console.log("Traveling Jekyll for " + platform + " " + arch + " installed") 70 | if(cb) { cb() }; 71 | }); 72 | fs.createReadStream(localFile) 73 | .on('error', function(err) { 74 | console.log('Error: ' + err); 75 | }) 76 | .pipe(zlib.createGunzip()) 77 | .pipe(extractor); 78 | 79 | console.log("Extracting... "); 80 | } 81 | 82 | function mkdirSync (path) { 83 | try { 84 | fs.mkdirSync(path); 85 | } catch(e) { 86 | if ( e.code != 'EEXIST' ) throw e; 87 | } 88 | } 89 | 90 | function rmdirSync (path) { 91 | try { 92 | rimraf.sync(path); 93 | } catch(e) { 94 | if ( e.code != 'ENOENT' ) throw e; 95 | } 96 | } 97 | 98 | function platformFor(platform, arch) { 99 | return releases[platform][arch]; 100 | } 101 | 102 | function fileName(platform, arch) { 103 | return release_prefix + TJ_version + "-" + platformFor(platform, arch) + release_suffix; 104 | } 105 | 106 | function localFilePath(platform, arch) { 107 | return Path.join(cache_location, fileName(platform, arch)); 108 | } 109 | 110 | function releaseURL(platform, arch) { 111 | return releases_URL + TJ_version + fileName(platform, arch); 112 | } 113 | 114 | module.exports.installForTarget = function(platform, arch, cb){ 115 | downloadLJRelease(platform, arch, cb); 116 | } 117 | 118 | if(require.main === module) { 119 | var platform = argv.plat || process.platform; 120 | var arch = argv.arch || process.arch; 121 | downloadLJRelease(platform, arch); 122 | } 123 | -------------------------------------------------------------------------------- /server/sites-store.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {dialog} from 'electron'; 3 | import Dispatcher from './dispatcher'; 4 | import siteController from './site-controller'; 5 | import processController from './process-controller'; 6 | import storage from './storage'; 7 | 8 | var sitesList = []; 9 | var strippedList = []; 10 | var firstGetSitesList = true; 11 | 12 | var initSitesList = function(sitesData, sender) { 13 | if(sitesData) { 14 | sitesList = sitesData; 15 | for (var i = 0; i < sitesList.length; i++) { 16 | if (sitesList[i].serverWorking) { 17 | siteController.startServerOnSite(sender, sitesList[i].id); 18 | } 19 | } 20 | module.exports.sendSitesList(sender); 21 | } 22 | } 23 | 24 | module.exports.siteById = function(id) { 25 | for (var i=0; i < sitesList.length; i++) { 26 | if (sitesList[i].id === id) { 27 | return sitesList[i]; 28 | } 29 | } 30 | } 31 | 32 | module.exports.setSiteProperty = function(id, property, value) { 33 | var site = module.exports.siteById(id); 34 | site[property] = value; 35 | } 36 | 37 | module.exports.sendSitesList = function(sender) { 38 | if (firstGetSitesList) { 39 | storage.attemptToOpenSitesList(initSitesList, sender); 40 | firstGetSitesList = false; 41 | } else { 42 | if (sitesList) { 43 | storage.updateSitesList(sitesList); 44 | sender.send('updateSitesList', sitesList); 45 | } 46 | } 47 | } 48 | 49 | module.exports.addSite = function(sender, filePaths) { 50 | var filePaths = (typeof filePaths === "string" ? [filePaths] : filePaths) || dialog.showOpenDialog({ properties: [ 'openDirectory', 'multiSelections' ]}); 51 | 52 | if ( filePaths != undefined ) { 53 | for ( var i = (filePaths.length - 1); i >= 0 ; i-- ) { 54 | for ( var j = 0; j < sitesList.length; j++ ) { 55 | if(filePaths[i] == sitesList[j].filePath) { 56 | filePaths.splice(i, 1); 57 | } 58 | } 59 | } 60 | 61 | for (var i = 0; i < filePaths.length; i++) { 62 | var filePath = filePaths[i]; 63 | var automaticName = filePath.slice(filePath.lastIndexOf("/") + 1); 64 | var id = new Date().valueOf() + i; // I am an expert at unique IDs 65 | 66 | sitesList.unshift({ 67 | id: id, 68 | name: automaticName, 69 | filePath: filePath, 70 | serverActive: false, 71 | serverWorking: false, 72 | hasError: false, 73 | server: null 74 | }); 75 | } 76 | 77 | module.exports.sendSitesList(sender); 78 | } 79 | } 80 | 81 | module.exports.createSite = function(sender) { 82 | var folderPath = dialog.showSaveDialog({ properties: [ 'openDirectory' ]}); 83 | 84 | if ( folderPath != undefined ) { 85 | folderPath = folderPath.replace(/["']/g, ""); 86 | fs.mkdir(folderPath); 87 | processController.createNewSite(sender, folderPath); 88 | }; 89 | } 90 | 91 | module.exports.buildSite = function(siteID) { 92 | var buildPath = dialog.showSaveDialog({ properties: [ 'openDirectory' ]}); 93 | var sitePath = module.exports.siteById(siteID).filePath; 94 | 95 | if ( buildPath != undefined ) { 96 | buildPath = buildPath.replace(/["']/g, ""); 97 | processController.buildSite(sitePath, buildPath); 98 | }; 99 | } 100 | 101 | module.exports.removeSite = function(siteID) { 102 | var site = module.exports.siteById(siteID); 103 | sitesList.splice(sitesList.indexOf(site), 1); 104 | } 105 | 106 | module.exports.stopAllServers = function() { 107 | for (var i=0; i < sitesList.length; i++) { 108 | if (sitesList[i].serverActive) { 109 | siteController.stopServerOnSite(false, sitesList[i].id, true); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0, no-shadow: 0, no-unused-vars: 0, no-console: 0 */ 2 | 'use strict'; 3 | 4 | const os = require('os'); 5 | const webpack = require('webpack'); 6 | const cfg = require('./webpack.config.production.js'); 7 | const packager = require('electron-packager'); 8 | const del = require('del'); 9 | const exec = require('child_process').exec; 10 | const TJ = require('./install.js'); 11 | const execFileSync = require('child_process').execFileSync; 12 | const argv = require('minimist')(process.argv.slice(2)); 13 | const pkg = require('./package.json'); 14 | const devDeps = Object.keys(pkg.devDependencies); 15 | 16 | const appName = argv.name || argv.n || pkg.productName; 17 | const shouldUseAsar = argv.asar || argv.a || false; 18 | const shouldBuildAll = argv.all || false; 19 | 20 | const DEFAULT_OPTS = { 21 | 'app-version': pkg.version || null, 22 | dir: './', 23 | name: appName, 24 | asar: shouldUseAsar, 25 | overwrite: true, 26 | ignore: [ 27 | '/test($|/)', 28 | '/release($|/)', 29 | '/.install_cache($|/)' 30 | ].concat(devDeps.map(name => `/node_modules/${name}($|/)`)) 31 | }; 32 | 33 | const icon = argv.icon || argv.i || 'app/app.icns'; 34 | 35 | if (icon) { 36 | DEFAULT_OPTS.icon = icon; 37 | } 38 | 39 | const version = argv.version || argv.v; 40 | 41 | if (version) { 42 | DEFAULT_OPTS.version = version; 43 | startPack(); 44 | } else { 45 | // use the same version as the currently-installed electron-prebuilt 46 | exec('npm list electron-prebuilt', (err, stdout) => { 47 | if (err) { 48 | DEFAULT_OPTS.version = '0.37.2'; 49 | } else { 50 | DEFAULT_OPTS.version = stdout.split('electron-prebuilt@')[1].replace(/\s/g, ''); 51 | } 52 | startPack(); 53 | }); 54 | } 55 | 56 | function startPack() { 57 | console.log('start pack...'); 58 | webpack(cfg, (err, stats) => { 59 | if (err) return console.error(err); 60 | del('release') 61 | .then(paths => { 62 | if (shouldBuildAll) { 63 | // build for all platforms 64 | const platforms = ['linux', 'win32', 'darwin']; 65 | const archs = ['ia32', 'x64']; 66 | 67 | // Installs Traveling Jekyll versions between each pack(), 68 | // to pick the proper native components 69 | 70 | function packSeries(plat, arch) { 71 | if (plat >= platforms.length) { return; } 72 | else { 73 | if (arch >= archs.length) { packSeries (plat+1, 0) } 74 | else { 75 | pack(platforms[plat], archs[arch], function() { 76 | log(plat, arch); 77 | packSeries (plat, arch+1); 78 | }); 79 | } 80 | } 81 | } 82 | packSeries(0,0); 83 | 84 | } else { 85 | // build for current platform only 86 | pack(os.platform(), os.arch(), log(os.platform(), os.arch())); 87 | } 88 | }) 89 | .catch(err => { 90 | console.error(err); 91 | }); 92 | }); 93 | } 94 | 95 | function pack(plat, arch, cb) { 96 | if ((plat === 'darwin' && arch === 'ia32') || plat === 'win32') { 97 | console.log("Skipping build: " + plat + " " + arch ); 98 | cb(); 99 | return; 100 | }; // darwin 32 doesn't exist, and Windows support is planned if possible 101 | 102 | const opts = Object.assign({}, DEFAULT_OPTS, { 103 | platform: plat, 104 | arch, 105 | prune: true, 106 | out: `release/${plat}-${arch}` 107 | }); 108 | 109 | TJ.installForTarget( plat, arch, 110 | function() { packager(opts, cb) } 111 | ); 112 | 113 | } 114 | 115 | function log(plat, arch) { 116 | return (err, filepath) => { 117 | if (err) return console.error(err); 118 | console.log(`${plat}-${arch} finished!`); 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from './dispatcher'; 2 | import siteController from './site-controller'; 3 | import Windows from './windows'; 4 | 5 | let Logger = function () { 6 | var newLogger = { 7 | logs: [], 8 | window: null, 9 | addLog: function (server, logData, logType) { 10 | 11 | logData = logData.toString().replace(/(\[\d+m)/g, '').trim(); 12 | 13 | var logEntry = { 14 | logType: logType || "std", 15 | logData: logData, 16 | time: Date.now() 17 | } 18 | 19 | if (logEntry.logData.search("Error:") != -1) { logEntry.logType = "err" } 20 | if (logEntry.logData.search("done\ in ") != -1) { logEntry.logType = "success" } 21 | 22 | if (logEntry.logType === "err") { siteController.reportErrorOnSite(server.reportTo, server.siteID); } 23 | 24 | serverUpdate(server, logEntry); 25 | 26 | this.logs.push(logEntry); 27 | if (this.window != null) { this.sendLogs() }; 28 | 29 | }, 30 | setup: function (server) { 31 | server.jekyllProcess.stdout.on('data', 32 | function (data) { 33 | server.logger.addLog(server, data); 34 | } 35 | ) 36 | 37 | server.jekyllProcess.stderr.on('data', 38 | function (data) { 39 | server.logger.addLog(server, data, "err"); 40 | } 41 | ) 42 | 43 | server.jekyllProcess.on('close', 44 | function (data) { 45 | // ssssh 46 | } 47 | ) 48 | }, 49 | openLogsWindow: function() { 50 | if ( this.window ) { 51 | this.window.show(); 52 | } else { 53 | this.window = Windows.initLogs(); 54 | this.window.on('did-start-loading', function() { 55 | Dispatcher.prepareLogs(this.logs); 56 | }) 57 | Dispatcher.prepareLogs(this.logs); 58 | } 59 | }, 60 | sendLogs: function () { 61 | this.window.webContents.send('setLogs', this.logs); 62 | }, 63 | closeLogsWindow: function () { 64 | if (this.window) { this.window.close() }; 65 | } 66 | } 67 | return newLogger; 68 | }; 69 | 70 | var serverUpdate = function(server, logEntry) { 71 | var matched = false; 72 | var data = logEntry.logData; 73 | for (var i = 0; i < updateHandlers.length; i++) { 74 | if (data.search(updateHandlers[i].str) != -1) { 75 | matched = true; 76 | updateHandlers[i].handler(server, logEntry); 77 | } 78 | } 79 | if (matched == false) { console.log("no match: " + data) }; 80 | }; 81 | 82 | var updateHandlers = [ 83 | { 84 | str: "Configuration file:", 85 | handler: function (server, logEntry) { 86 | // Unused, maybe check if _config.yml is always under site root 87 | // var path = data.match("(/.*\.yml)"); 88 | } 89 | }, 90 | { 91 | str: "Generating...", 92 | handler: function (server, logEntry) { 93 | siteController.reportWorkingServerOnSite(server.reportTo, server.siteID); 94 | } 95 | }, 96 | { 97 | str: "Regenerating:", 98 | handler: function (server, logEntry) { 99 | siteController.reportWorkingServerOnSite(server.reportTo, server.siteID); 100 | } 101 | }, 102 | { 103 | str: "Source: ", 104 | handler: function (server, logEntry) { 105 | // Unused 106 | } 107 | }, 108 | { 109 | str: "done\ in ", 110 | handler: function (server, logEntry) { 111 | // Unused 112 | // var duration = data.match(/\d+\.?\d*/g); 113 | siteController.reportSuccessOnSite(server.reportTo, server.siteID); 114 | siteController.reportAvailableServerOnSite(server.reportTo, server.siteID); 115 | } 116 | }, 117 | { 118 | str: "Auto-regeneration:", 119 | handler: function (server, logEntry) { 120 | // Unused 121 | // var autoregen = ( data.match("(enabled)") >= 0 ); 122 | } 123 | }, 124 | { 125 | str: "Server address:", 126 | handler: function (server, logEntry) { 127 | // Unused yo 128 | } 129 | } 130 | ]; 131 | 132 | module.exports = Logger; 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "little-jekyll", 3 | "version": "0.1.4", 4 | "productName": "LittleJekyll", 5 | "description": "A desktop app to manage Jekyll", 6 | "author": "Louis-André Labadie", 7 | "repository": "l-a/little-jekyll", 8 | "license": "MIT", 9 | "main": "main.js", 10 | "scripts": { 11 | "test": "better-npm-run test", 12 | "test-watch": "npm test -- --watch", 13 | "test-e2e": "better-npm-run test-e2e", 14 | "lint": "eslint app test *.js", 15 | "hot-server": "node server.js", 16 | "build": "better-npm-run build", 17 | "start": "better-npm-run start", 18 | "start-hot": "better-npm-run start-hot", 19 | "package": "better-npm-run package", 20 | "package-all": "npm run package -- --all", 21 | "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node install.js" 22 | }, 23 | "betterScripts": { 24 | "start": { 25 | "command": "electron ./", 26 | "env": { 27 | "NODE_ENV": "production" 28 | } 29 | }, 30 | "start-hot": { 31 | "command": "electron ./", 32 | "env": { 33 | "HOT": 1, 34 | "NODE_ENV": "development" 35 | } 36 | }, 37 | "package": { 38 | "command": "node package.js -i 'app/appicon' -n 'Little Jekyll'", 39 | "env": { 40 | "NODE_ENV": "production" 41 | } 42 | }, 43 | "build": { 44 | "command": "webpack --config webpack.config.production.js --progress --profile --colors", 45 | "env": { 46 | "NODE_ENV": "production" 47 | } 48 | }, 49 | "test": { 50 | "command": "mocha --compilers js:babel-core/register --recursive --require ./test/setup.js test/**/*.spec.js", 51 | "env": { 52 | "NODE_ENV": "test" 53 | } 54 | }, 55 | "test-e2e": { 56 | "command": "mocha --compilers js:babel-core/register --require ./test/setup.js --require co-mocha ./test/e2e.js", 57 | "env": { 58 | "NODE_ENV": "test" 59 | } 60 | } 61 | }, 62 | "bin": { 63 | "electron": "./node_modules/.bin/electron" 64 | }, 65 | "license": "MIT", 66 | "devDependencies": { 67 | "asar": "^0.11.0", 68 | "babel-eslint": "^6.1.2", 69 | "better-npm-run": "0.0.9", 70 | "chai": "^3.3.0", 71 | "chromedriver": "^2.19.0", 72 | "co-mocha": "^1.1.2", 73 | "css-loader": "^0.23.1", 74 | "css-modules-require-hook": "^4.0.1", 75 | "del": "^2.0.1", 76 | "electron-packager": "^7.3.0", 77 | "electron-prebuilt": "^1.2.7", 78 | "electron-rebuild": "^1.0.0", 79 | "eslint": "^3.1.0", 80 | "eslint-config-airbnb": "^9.0.1", 81 | "eslint-plugin-react": "^5.2.2", 82 | "express": "^4.13.3", 83 | "extract-text-webpack-plugin": "^1.0.1", 84 | "fbjs-scripts": "^0.7.1", 85 | "file-loader": "^0.9.0", 86 | "jsdom": "^9.4.1", 87 | "mocha": "^2.3.0", 88 | "node-libs-browser": "^1.0.0", 89 | "node-sass": "^3.4.2", 90 | "postcss": "^5.0.13", 91 | "postcss-modules-extract-imports": "^1.0.0", 92 | "postcss-modules-local-by-default": "^1.0.1", 93 | "postcss-modules-scope": "^1.0.0", 94 | "postcss-modules-values": "^1.1.1", 95 | "react-addons-test-utils": "^15.2.1", 96 | "redux-devtools": "^3.0.1", 97 | "redux-devtools-dock-monitor": "^1.0.1", 98 | "redux-devtools-log-monitor": "^1.0.1", 99 | "redux-logger": "^2.3.1", 100 | "sass-loader": "^4.0.0", 101 | "selenium-webdriver": "^2.48.2", 102 | "sinon": "^1.17.2", 103 | "style-loader": "^0.13.0", 104 | "webpack": "^1.12.9", 105 | "webpack-dev-middleware": "^1.2.0", 106 | "webpack-hot-middleware": "^2.4.1", 107 | "webpack-target-electron-renderer": "^0.4.0" 108 | }, 109 | "dependencies": { 110 | "babel-core": "^6.3.26", 111 | "babel-loader": "^6.2.0", 112 | "babel-plugin-add-module-exports": "^0.2.1", 113 | "babel-polyfill": "^6.3.14", 114 | "babel-preset-es2015": "^6.3.13", 115 | "babel-preset-react": "^6.3.13", 116 | "babel-preset-react-hmre": "^1.0.1", 117 | "babel-preset-stage-0": "^6.3.13", 118 | "browser-sync": "^2.11.1", 119 | "consistent-path": "^2.0.3", 120 | "electron-debug": "^1.0.1", 121 | "history": "^3.0.0", 122 | "minimist": "^1.2.0", 123 | "mousetrap": "^1.5.3", 124 | "react": "^15.2.1", 125 | "react-dom": "^15.2.1", 126 | "react-redux": "^4.0.5", 127 | "react-router": "^2.5.2", 128 | "velocity-react": "^1.1.1" 129 | }, 130 | "devEngines": { 131 | "node": "5.x || 6.x", 132 | "npm": "2.x || 3.x" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/components/Site.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Dispatcher from '../utils/front-end-dispatcher'; 4 | import SimpleButton from './simple-button.js'; 5 | import Mousetrap from 'Mousetrap'; 6 | import {shell} from 'electron'; 7 | 8 | var Site = React.createClass({ 9 | getInitialState: function() { 10 | return {optionsShown: false}; 11 | }, 12 | componentDidMount: function() { 13 | if (this.props.selected) { 14 | this.ownKeyboardShortcuts(true); 15 | } 16 | }, 17 | componentWillReceiveProps: function(nextProps) { 18 | if (this.props.selected && !nextProps.selected) { 19 | this.ownKeyboardShortcuts(false); 20 | } 21 | if (!this.props.selected && nextProps.selected) { 22 | this.ownKeyboardShortcuts(true); 23 | ReactDOM.findDOMNode(this).scrollIntoViewIfNeeded(); 24 | } 25 | }, 26 | ownKeyboardShortcuts: function(shouldOwn) { 27 | var _bind = shouldOwn ? Mousetrap.bind : Mousetrap.unbind; 28 | 29 | _bind('space', this.toggleServerState); 30 | _bind(['del', 'meta+backspace'], this.removeSiteFromList); 31 | _bind('meta+b', this.buildSite); 32 | _bind('o', this.openLocalServer); 33 | _bind('meta+l', this.openServerLogs); 34 | _bind('meta+d', this.openFolder); 35 | }, 36 | toggleServerState: function() { 37 | var message = this.props.siteInfo.serverActive ? 'stopServer' : 'startServer'; 38 | Dispatcher.send(message, this.props.siteInfo.id); 39 | }, 40 | toggleOptionsPanel: function() { 41 | var newOptionsState = !this.state.optionsShown; 42 | this.setState({optionsShown: newOptionsState}); 43 | }, 44 | openLocalServer: function() { 45 | if(this.props.siteInfo.serverActive) { 46 | shell.openExternal(this.props.siteInfo.server.localURL); 47 | } 48 | }, 49 | openServerLogs: function() { 50 | Dispatcher.send('openServerLogs', this.props.siteInfo.id); 51 | }, 52 | openFolder: function() { 53 | shell.openItem(this.props.siteInfo.filePath); 54 | }, 55 | removeSiteFromList: function() { 56 | this.setState({optionsShown: false}); 57 | Dispatcher.send('removeSiteFromList', this.props.siteInfo.id); 58 | }, 59 | buildSite: function() { 60 | Dispatcher.send('buildSite', this.props.siteInfo.id); 61 | if (this.state.optionsShown) { 62 | this.toggleOptionsPanel(); 63 | } 64 | }, 65 | render: function () { 66 | var siteInfo = this.props.siteInfo; 67 | var cellClass = (this.state.optionsShown ? "site-cell options-shown" : "site-cell") + (siteInfo.hasError ? " error" : "") + (siteInfo.serverActive ? " logs-available" : "") + (this.props.selected ? " selected" : ""); 68 | var switchState = 'site-serve-switch ' + (siteInfo.serverWorking ? 'switch-working' : (siteInfo.serverActive ? 'switch-on' : 'switch-off')); 69 | return ( 70 |
  • 71 |
    72 | 73 |
    74 |
    75 |
    76 |
    77 |
    78 |
    79 |
    80 |

    {siteInfo.name}

    81 | 82 |
    83 |
    84 | 85 |
    86 |
    87 |
    88 | 89 | 90 | 91 |
    92 | 93 | 94 |
    95 |
  • 96 | ); 97 | } 98 | }) 99 | 100 | module.exports = Site; 101 | -------------------------------------------------------------------------------- /server/process-controller.js: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import childProcess from 'child_process'; 3 | import sitesStore from './sites-store'; 4 | import siteController from './site-controller'; 5 | import browsersync from 'browser-sync'; 6 | import Dispatcher from './dispatcher'; 7 | import Logger from './logger'; 8 | import path from 'path'; 9 | 10 | var jekyllDist = path.join(require('electron').app.getAppPath(), "jekyll", "jekyll"); 11 | var usedPorts = []; // Will fill with active servers' used ports (BrowserSync has trouble with two simultaneous inits) 12 | 13 | module.exports.newServer = function(requester, id, dir) { 14 | var server = { 15 | siteID: id, 16 | localPath: dir, 17 | reportTo: requester, 18 | jekyllProcess: undefined, 19 | browserSyncProcess: browsersync.create(), 20 | logger: Logger(), 21 | localURL: undefined, 22 | hasError: false, 23 | port: firstAvailablePort() 24 | }; 25 | 26 | var filePath = path.join(dir, "_site"); 27 | server.browserSyncProcess.init({ 28 | server: filePath, 29 | files: filePath, 30 | port: server.port, 31 | notify: false, 32 | ui: false, 33 | logLevel: "silent", // I trust you BrowserSync 34 | open: false, 35 | scrollRestoreTechnique: 'cookie', 36 | watchOptions: { 37 | awaitWriteFinish: { 38 | stabilityThreshold: 1200, 39 | pollInterval: 200 40 | } 41 | } 42 | }, function(err, bs) { 43 | server.jekyllProcess = startServer(dir); 44 | server.localURL = bs.options.getIn(["urls", "local"]); 45 | siteController.reportRunningServerOnSite(server.reportTo, server.siteID); 46 | server.logger.setup(server); 47 | }); 48 | 49 | return server; 50 | } 51 | 52 | module.exports.createNewSite = function(requester, dir) { 53 | Dispatcher.sendActivityState(requester, true); 54 | var creatorProcess = childProcess.spawn(jekyllDist, ["new", dir]); 55 | creatorProcess.stdout.on('data', 56 | function (data) { 57 | sitesStore.addSite(requester, dir); 58 | Dispatcher.sendActivityState(requester, false); 59 | } 60 | ); 61 | creatorProcess.stderr.on('data', 62 | function (data) { 63 | Dispatcher.report("Creator error: " + data); 64 | Dispatcher.sendActivityState(requester, false); 65 | } 66 | ); 67 | } 68 | 69 | module.exports.buildSite = function(sourcePath, buildPath) { 70 | var buildProcess = childProcess.spawn(jekyllDist, ["build", "--source", sourcePath, "--destination", buildPath]); 71 | buildProcess.stderr.on('data', 72 | function (data) { 73 | console.log("Creator error: " + data); 74 | } 75 | ); 76 | } 77 | 78 | var startServer = function(dir) { 79 | var destinationDir = path.join(dir, "_site"); // Needed, otherwise Jekyll may try writing to fs root 80 | var cmdLineArgs = ["build", "--source", dir, "--destination", destinationDir, "--watch"]; 81 | 82 | return childProcess.spawn(jekyllDist, cmdLineArgs) 83 | } 84 | 85 | module.exports.stopServer = function(server, ignoreLogsWindow) { 86 | for(var i = 0; i < usedPorts.length; i++) { 87 | if (server.port == usedPorts[i]) usedPorts.splice(i, 1); 88 | } 89 | server.browserSyncProcess.exit(); 90 | server.jekyllProcess.kill(); 91 | if (!ignoreLogsWindow) { server.logger.closeLogsWindow() }; 92 | } 93 | 94 | // There's probably some better organization to do here 95 | 96 | var updateHandlers = [ 97 | { 98 | str: "Configuration file:", 99 | handler: function (server, data) { 100 | // Unused, maybe check if _config.yml is always under site root 101 | // var path = data.match("(/.*\.yml)"); 102 | } 103 | }, 104 | { 105 | str: "Generating...", 106 | handler: function (server, data) { 107 | siteController.reportWorkingServerOnSite(server.reportTo, server.siteID); 108 | } 109 | }, 110 | { 111 | str: "Regenerating:", 112 | handler: function (server, data) { 113 | siteController.reportWorkingServerOnSite(server.reportTo, server.siteID); 114 | } 115 | }, 116 | { 117 | str: "Source: ", 118 | handler: function (server, data) { 119 | // Unused 120 | } 121 | }, 122 | { 123 | str: "done\ in ", 124 | handler: function (server, data) { 125 | // Unused 126 | // var duration = data.match(/\d+\.?\d*/g); 127 | siteController.reportAvailableServerOnSite(server.reportTo, server.siteID); 128 | } 129 | }, 130 | { 131 | str: "Auto-regeneration:", 132 | handler: function (server, data) { 133 | // Unused 134 | // var autoregen = ( data.match("(enabled)") >= 0 ); 135 | } 136 | }, 137 | { 138 | str: "Server address:", 139 | handler: function (server, data) { 140 | // Unused yo 141 | } 142 | } 143 | ]; 144 | 145 | 146 | 147 | var firstAvailablePort = function () { 148 | var port = 4000; 149 | 150 | for(var i = 0; i < usedPorts.length; i++) { 151 | if (port == usedPorts[i]) port++; 152 | } 153 | 154 | usedPorts.push(port); 155 | 156 | return port; 157 | } 158 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.7.1 (2015.12.27) 2 | 3 | #### Bug fixed 4 | 5 | - **Fixed npm script on windows 10:** #103. 6 | - **history and react-router version bump**: #109, #110. 7 | 8 | #### Improvements 9 | 10 | - **electron 0.36** 11 | 12 | 13 | 14 | # 0.7.0 (2015.12.16) 15 | 16 | #### Bug fixed 17 | 18 | - **Fixed process.env.NODE_ENV variable in webpack:** #74. 19 | - **add missing object-assign**: #76. 20 | - **packaging in npm@3:** #77. 21 | - **compatibility in windows:** #100. 22 | - **disable chrome debugger in production env:** #102. 23 | 24 | #### Improvements 25 | 26 | - **redux** 27 | - **css-modules** 28 | - **upgrade to react-router 1.x** 29 | - **unit tests** 30 | - **e2e tests** 31 | - **travis-ci** 32 | - **upgrade to electron 0.35.x** 33 | - **use es2015** 34 | - **check dev engine for node and npm** 35 | 36 | 37 | # 0.6.5 (2015.11.7) 38 | 39 | #### Improvements 40 | 41 | - **Bump style-loader to 0.13** 42 | - **Bump css-loader to 0.22** 43 | 44 | 45 | # 0.6.4 (2015.10.27) 46 | 47 | #### Improvements 48 | 49 | - **Bump electron-debug to 0.3** 50 | 51 | 52 | # 0.6.3 (2015.10.26) 53 | 54 | #### Improvements 55 | 56 | - **Initialize ExtractTextPlugin once:** #64. 57 | 58 | 59 | # 0.6.2 (2015.10.18) 60 | 61 | #### Bug fixed 62 | 63 | - **Babel plugins production env not be set properly:** #57. 64 | 65 | 66 | # 0.6.1 (2015.10.17) 67 | 68 | #### Improvements 69 | 70 | - **Bump electron to v0.34.0** 71 | 72 | 73 | # 0.6.0 (2015.10.16) 74 | 75 | #### Breaking Changes 76 | 77 | - **From react-hot-loader to react-transform** 78 | 79 | 80 | # 0.5.2 (2015.10.15) 81 | 82 | #### Improvements 83 | 84 | - **Run tests with babel-register:** #29. 85 | 86 | 87 | # 0.5.1 (2015.10.12) 88 | 89 | #### Bug fixed 90 | 91 | - **Fix #51:** use `path.join(__dirname` instead of `./`. 92 | 93 | 94 | # 0.5.0 (2015.10.11) 95 | 96 | #### Improvements 97 | 98 | - **Simplify webpack config** see [#50](https://github.com/chentsulin/electron-react-boilerplate/pull/50). 99 | 100 | #### Breaking Changes 101 | 102 | - **webpack configs** 103 | - **port changed:** changed default port from 2992 to 3000. 104 | - **npm scripts:** remove `start-dev` and `dev-server`. rename `hot-dev-server` to `hot-server`. 105 | 106 | 107 | # 0.4.3 (2015.9.22) 108 | 109 | #### Bug fixed 110 | 111 | - **Fix #45 zeromq crash:** bump version of `electron-prebuilt`. 112 | 113 | 114 | # 0.4.2 (2015.9.15) 115 | 116 | #### Bug fixed 117 | 118 | - **run start-hot breaks chrome refresh(CTRL+R) (#42)**: bump `electron-debug` to `0.2.1` 119 | 120 | 121 | # 0.4.1 (2015.9.11) 122 | 123 | #### Improvements 124 | 125 | - **use electron-prebuilt version for packaging (#33)** 126 | 127 | 128 | # 0.4.0 (2015.9.5) 129 | 130 | #### Improvements 131 | 132 | - **update dependencies** 133 | 134 | 135 | # 0.3.0 (2015.8.31) 136 | 137 | #### Improvements 138 | 139 | - **eslint-config-airbnb** 140 | 141 | 142 | # 0.2.10 (2015.8.27) 143 | 144 | #### Features 145 | 146 | - **custom placeholder icon** 147 | 148 | #### Improvements 149 | 150 | - **electron-renderer as target:** via [webpack-target-electron-renderer](https://github.com/chentsulin/webpack-target-electron-renderer) 151 | 152 | 153 | # 0.2.9 (2015.8.18) 154 | 155 | #### Bug fixed 156 | 157 | - **Fix hot-reload** 158 | 159 | 160 | # 0.2.8 (2015.8.13) 161 | 162 | #### Improvements 163 | 164 | - **bump electron-debug** 165 | - **babelrc** 166 | - **organize webpack scripts** 167 | 168 | 169 | # 0.2.7 (2015.7.9) 170 | 171 | #### Bug fixed 172 | 173 | - **defaultProps:** fix typos. 174 | 175 | 176 | # 0.2.6 (2015.7.3) 177 | 178 | #### Features 179 | 180 | - **menu** 181 | 182 | #### Bug fixed 183 | 184 | - **package.js:** include webpack build. 185 | 186 | 187 | # 0.2.5 (2015.7.1) 188 | 189 | #### Features 190 | 191 | - **NPM Script:** support multi-platform 192 | - **package:** `--all` option 193 | 194 | 195 | # 0.2.4 (2015.6.9) 196 | 197 | #### Bug fixed 198 | 199 | - **Eslint:** typo, [#17](https://github.com/chentsulin/electron-react-boilerplate/issues/17) and improve `.eslintrc` 200 | 201 | 202 | # 0.2.3 (2015.6.3) 203 | 204 | #### Features 205 | 206 | - **Package Version:** use latest release electron version as default 207 | - **Ignore Large peerDependencies** 208 | 209 | #### Bug fixed 210 | 211 | - **Npm Script:** typo, [#6](https://github.com/chentsulin/electron-react-boilerplate/pull/6) 212 | - **Missing css:** [#7](https://github.com/chentsulin/electron-react-boilerplate/pull/7) 213 | 214 | 215 | # 0.2.2 (2015.6.2) 216 | 217 | #### Features 218 | 219 | - **electron-debug** 220 | 221 | #### Bug fixed 222 | 223 | - **Webpack:** add `.json` and `.node` to extensions for imitating node require. 224 | - **Webpack:** set `node_modules` to externals for native module support. 225 | 226 | 227 | # 0.2.1 (2015.5.30) 228 | 229 | #### Bug fixed 230 | 231 | - **Webpack:** #1, change build target to `atom`. 232 | 233 | 234 | # 0.2.0 (2015.5.30) 235 | 236 | #### Features 237 | 238 | - **Ignore:** `test`, `tools`, `release` folder and devDependencies in `package.json`. 239 | - **Support asar** 240 | - **Support icon** 241 | 242 | 243 | # 0.1.0 (2015.5.27) 244 | 245 | #### Features 246 | 247 | - **Webpack:** babel, react-hot, ... 248 | - **Flux:** actions, api, components, containers, stores.. 249 | - **Package:** darwin (osx), linux and win32 (windows) platform. 250 | -------------------------------------------------------------------------------- /server/menu.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const shell = electron.shell; 3 | const browserWindow = electron.BrowserWindow; 4 | 5 | module.exports.osxMenu = function(app, appServer, mainWindow) { 6 | return [{ 7 | label: 'Little Jekyll', 8 | submenu: [{ 9 | label: 'About Little Jekyll', 10 | selector: 'orderFrontStandardAboutPanel:' 11 | }, { 12 | type: 'separator' 13 | }, { 14 | label: 'Services', 15 | submenu: [] 16 | }, { 17 | type: 'separator' 18 | }, { 19 | label: 'Hide Little Jekyll', 20 | accelerator: 'Command+H', 21 | selector: 'hide:' 22 | }, { 23 | label: 'Hide Others', 24 | accelerator: 'Command+Shift+H', 25 | selector: 'hideOtherApplications:' 26 | }, { 27 | label: 'Show All', 28 | selector: 'unhideAllApplications:' 29 | }, { 30 | type: 'separator' 31 | }, { 32 | label: 'Quit', 33 | accelerator: 'Command+Q', 34 | click() { 35 | app.quit(); 36 | } 37 | }] 38 | }, { 39 | label: 'File', 40 | submenu: [{ 41 | label: 'New', 42 | accelerator: 'Command+N', 43 | click() { 44 | appServer.createSite(); 45 | } 46 | }, { 47 | label: 'Open', 48 | accelerator: 'Command+O', 49 | click() { 50 | appServer.addSite(); 51 | } 52 | }] 53 | }, { 54 | label: 'Edit', 55 | submenu: [{ 56 | label: 'Undo', 57 | accelerator: 'Command+Z', 58 | selector: 'undo:' 59 | }, { 60 | label: 'Redo', 61 | accelerator: 'Shift+Command+Z', 62 | selector: 'redo:' 63 | }, { 64 | type: 'separator' 65 | }, { 66 | label: 'Cut', 67 | accelerator: 'Command+X', 68 | selector: 'cut:' 69 | }, { 70 | label: 'Copy', 71 | accelerator: 'Command+C', 72 | selector: 'copy:' 73 | }, { 74 | label: 'Paste', 75 | accelerator: 'Command+V', 76 | selector: 'paste:' 77 | }, { 78 | label: 'Select All', 79 | accelerator: 'Command+A', 80 | selector: 'selectAll:' 81 | }] 82 | }, { 83 | label: 'View', 84 | submenu: (process.env.NODE_ENV === 'development') ? [{ 85 | label: 'Reload', 86 | accelerator: 'Command+R', 87 | click() { 88 | mainWindow.restart(); 89 | } 90 | }, { 91 | label: 'Toggle Full Screen', 92 | accelerator: 'Ctrl+Command+F', 93 | click() { 94 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 95 | } 96 | }, { 97 | label: 'Toggle Developer Tools', 98 | accelerator: 'Alt+Command+I', 99 | click() { 100 | mainWindow.toggleDevTools(); 101 | } 102 | }] : [{ 103 | label: 'Toggle Full Screen', 104 | accelerator: 'Ctrl+Command+F', 105 | click() { 106 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 107 | } 108 | }] 109 | }, { 110 | label: 'Window', 111 | submenu: [{ 112 | label: 'Minimize', 113 | accelerator: 'Command+M', 114 | selector: 'performMiniaturize:' 115 | }, { 116 | label: 'Close', 117 | accelerator: 'Command+W', 118 | click() { 119 | if (browserWindow.getFocusedWindow() != null) { 120 | browserWindow.getFocusedWindow().hide(); 121 | } 122 | } 123 | }, { 124 | type: 'separator' 125 | }, { 126 | label: 'Bring All to Front', 127 | selector: 'arrangeInFront:' 128 | }] 129 | }, { 130 | label: 'Help', 131 | submenu: [{ 132 | label: 'Learn More', 133 | click() { 134 | shell.openExternal('http://electron.atom.io'); 135 | } 136 | }, { 137 | label: 'Documentation', 138 | click() { 139 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 140 | } 141 | }, { 142 | label: 'Community Discussions', 143 | click() { 144 | shell.openExternal('https://discuss.atom.io/c/electron'); 145 | } 146 | }, { 147 | label: 'Search Issues', 148 | click() { 149 | shell.openExternal('https://github.com/atom/electron/issues'); 150 | } 151 | }] 152 | }]; 153 | }; 154 | 155 | module.exports.winLinMenu = function(mainWindow, appServer) { 156 | return [{ 157 | label: '&File', 158 | submenu: [{ 159 | label: '&New', 160 | accelerator: 'Ctrl+N', 161 | click() { 162 | appServer.createSite(); 163 | } 164 | }, { 165 | label: '&Open', 166 | accelerator: 'Ctrl+O', 167 | click() { 168 | appServer.addSite(); 169 | } 170 | }, { 171 | label: '&Close', 172 | accelerator: 'Ctrl+W', 173 | click() { 174 | mainWindow.close(); 175 | } 176 | }] 177 | }, { 178 | label: '&View', 179 | submenu: (process.env.NODE_ENV === 'development') ? [{ 180 | label: '&Reload', 181 | accelerator: 'Ctrl+R', 182 | click() { 183 | mainWindow.restart(); 184 | } 185 | }, { 186 | label: 'Toggle &Full Screen', 187 | accelerator: 'F11', 188 | click() { 189 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 190 | } 191 | }, { 192 | label: 'Toggle &Developer Tools', 193 | accelerator: 'Alt+Ctrl+I', 194 | click() { 195 | mainWindow.toggleDevTools(); 196 | } 197 | }] : [{ 198 | label: 'Toggle &Full Screen', 199 | accelerator: 'F11', 200 | click() { 201 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 202 | } 203 | }] 204 | }, { 205 | label: 'Help', 206 | submenu: [{ 207 | label: 'Learn More', 208 | click() { 209 | shell.openExternal('http://electron.atom.io'); 210 | } 211 | }, { 212 | label: 'Documentation', 213 | click() { 214 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 215 | } 216 | }, { 217 | label: 'Community Discussions', 218 | click() { 219 | shell.openExternal('https://discuss.atom.io/c/electron'); 220 | } 221 | }, { 222 | label: 'Search Issues', 223 | click() { 224 | shell.openExternal('https://github.com/atom/electron/issues'); 225 | } 226 | }] 227 | }] 228 | }; 229 | -------------------------------------------------------------------------------- /app/assets/css/_siteslist.scss: -------------------------------------------------------------------------------- 1 | $options-panel-width: 146px; 2 | $options-panel-offset: 38px; 3 | $error-button-offset: 38px; 4 | 5 | .sites-list { 6 | flex: 1 1 auto; 7 | margin: 0; 8 | position: relative; 9 | 10 | overflow-y: scroll; 11 | -webkit-overflow-scrolling: touch; 12 | z-index: 1; 13 | 14 | .site-cell { 15 | position: relative; 16 | z-index: 2; 17 | border-bottom: 1px solid #f1f2f2; 18 | overflow: hidden; 19 | border-left: 4px solid transparent; 20 | transition: border-left-color $timing-snappy-btn ease-out; 21 | 22 | &.selected { 23 | border-left: 4px solid $options-contrast-color; 24 | } 25 | 26 | &:hover .main-panel, 27 | &.error .main-panel{ 28 | box-shadow: 2px 0 4px $light-color; 29 | } 30 | 31 | &.options-shown .main-panel, 32 | &.options-shown.error .main-panel{ 33 | margin-right: $options-panel-width - $error-button-offset; 34 | box-shadow: 2px 0 4px $light-color; 35 | } 36 | 37 | &.error .main-panel { 38 | margin-right: $options-panel-offset + $error-button-offset; 39 | } 40 | 41 | &.logs-available { 42 | &.options-shown .main-panel { 43 | margin-right: $options-panel-width; 44 | } 45 | 46 | .btn-logs.available { 47 | width: auto; 48 | .btn { 49 | top: 1px; 50 | width: 21px; 51 | } 52 | } 53 | } 54 | 55 | .main-panel{ 56 | background-color: white; 57 | display: flex; 58 | flex-direction: row; 59 | align-items: center; 60 | margin-right: $options-panel-offset; 61 | overflow: hidden; 62 | 63 | padding: 0 8px 0 0; 64 | 65 | position: relative; 66 | z-index: 5; 67 | 68 | transition: margin-right $timing-snappy $bouncy-ease-smoother, 69 | box-shadow $timing-snappy ease-out; 70 | 71 | .site-info { 72 | flex: 1 1 0; 73 | padding: 18px 0; 74 | transition: color $timing-snappy linear; 75 | overflow: hidden; 76 | 77 | h1 { 78 | color: $primary-text-color; 79 | font-size: 14px; 80 | font-weight: bold; 81 | margin-bottom: 0; 82 | 83 | overflow: hidden; 84 | text-overflow: ellipsis; 85 | white-space:nowrap; 86 | 87 | &.server-active { 88 | color: $primary-color; 89 | } 90 | } 91 | 92 | .site-folder { 93 | background: url('../img/icn_folder.svg') left no-repeat; 94 | color: $boring-gray; 95 | display: block; 96 | font-size: 11px; 97 | padding: 2px 6px 0 12px; 98 | min-width: 0; 99 | height: 1.3em; 100 | overflow: hidden; 101 | text-overflow: ellipsis; 102 | white-space:nowrap; 103 | 104 | transition: color 100ms linear; 105 | 106 | &:hover { color: $primary-text-color; opacity: 1;} 107 | } 108 | } 109 | 110 | .site-options { 111 | flex: 0 0 24px; 112 | padding: 4px 0 0; 113 | text-align: right; 114 | 115 | .btn-preview{ 116 | background: $primary-color; 117 | -webkit-mask: url('../img/btn_preview.svg') center no-repeat; 118 | -webkit-mask-size: contain; 119 | margin-right: 10px; 120 | opacity: 0; 121 | width: 0; 122 | display: inline-block; 123 | height: 24px; 124 | vertical-align: top; 125 | 126 | transition: background-color 100ms linear, 127 | margin-right $timing-snappy $bouncy-ease, 128 | opacity 100ms linear, 129 | width $timing-snappy $bouncy-ease; 130 | 131 | &.available { 132 | margin-right: 0px; 133 | opacity: 1; 134 | width: 18px; 135 | 136 | &:hover { background: darken($primary-color, 10%); } 137 | &:active { opacity: 0.8; } 138 | 139 | &.hold { 140 | background: $boring-gray; 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | .secondary-panel { 148 | background: white; 149 | height: 100%; // Apparently fixes a border flash? 150 | width: $options-panel-width; 151 | 152 | padding-top: 12px; 153 | text-align: right; 154 | 155 | position: absolute; 156 | top: 0; 157 | right: 0; 158 | 159 | z-index: 2; 160 | 161 | .btn-edit, .btn-remove, .btn-build, .btn-logs .btn { 162 | display: inline-block; 163 | vertical-align: middle; 164 | height: 40px; 165 | 166 | background: $boring-gray; 167 | margin-left: 10px; 168 | position: relative; 169 | width: 20px; 170 | 171 | transition: background-color $timing-snappy-btn linear; 172 | 173 | &:hover { background: darken($boring-gray, 30%); } 174 | &:active { background: darken($boring-gray, 20%); } 175 | } 176 | 177 | .btn-edit { 178 | float: right; 179 | -webkit-mask: url('../img/btn_edit.svg') center no-repeat; 180 | margin-right: 11px; 181 | } 182 | 183 | .btn-remove { 184 | -webkit-mask: url('../img/btn_forget.svg') center no-repeat; 185 | margin-top: 2px; 186 | } 187 | 188 | .btn-build { 189 | -webkit-mask: url('../img/btn_export.svg') center no-repeat; 190 | margin-right: 1px; 191 | } 192 | 193 | .btn-logs { 194 | display: inline-block; 195 | position: relative; 196 | padding-right: 1px; 197 | width: 0; 198 | 199 | .indicator { 200 | background-color: $error-color; 201 | border: 0px solid white; 202 | border-radius: 8px; 203 | content: ""; 204 | position: absolute; 205 | top: 10px; 206 | right: 0; 207 | width: 10px; 208 | height: 10px; 209 | transform: scale(0); 210 | transition: transform $timing-snappy $bouncy-ease, 211 | border-width $timing-snappy $bouncy-ease; 212 | 213 | box-shadow: 0 0 2px $light-color; 214 | } 215 | 216 | .btn { 217 | -webkit-mask: url('../img/btn_logs.svg') center no-repeat; 218 | -webkit-mask-size: contain; 219 | width: 0; 220 | margin-right: 4px; 221 | margin-top: 1px; 222 | 223 | transition: width $timing-snappy $bouncy-ease; 224 | } 225 | 226 | &.error .indicator { 227 | border-width: 2px; 228 | height: 10px; 229 | width: 10px; 230 | 231 | transform: scale(1); 232 | transition-delay: 120ms, 120ms; 233 | } 234 | } 235 | } 236 | } 237 | } 238 | --------------------------------------------------------------------------------