├── 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 |
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 |
12 |
--------------------------------------------------------------------------------
/app/assets/img/btn_close.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
24 | );
25 | }
26 | })
27 |
28 | module.exports = TitleBar;
29 |
--------------------------------------------------------------------------------
/app/assets/img/btn_create.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------