├── .babelrc ├── .electron-vue ├── build.js ├── dev-client.js ├── dev-runner.js ├── webpack.main.config.js ├── webpack.renderer.config.js └── webpack.web.config.js ├── .eslintignore ├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── appveyor.yml ├── build └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 512x512.png │ ├── 64x64.png │ ├── IconLarge.png │ ├── IconSmall.png │ ├── icon.icns │ ├── icon.ico │ └── make_icons.sh ├── dist ├── electron │ └── .gitkeep └── web │ └── .gitkeep ├── do_release.sh ├── example.vizappconfig ├── package-lock.json ├── package.json ├── src ├── index.ejs ├── lib │ ├── embed_code.js │ └── index.js ├── main │ ├── actions.js │ ├── autoupdate.js │ ├── default_data.js │ ├── dialogs.js │ ├── index.dev.js │ ├── index.js │ ├── install_ai_plugin.js │ ├── ipc.js │ ├── menus │ │ ├── InputContextMenu.js │ │ ├── Menubar.js │ │ └── ProjectContextMenu.js │ ├── storage.js │ └── workers.js ├── renderer │ ├── App.vue │ ├── Settings.vue │ ├── assets │ │ ├── .gitkeep │ │ └── logo.png │ ├── components │ │ ├── List.vue │ │ ├── ProjectListItem.vue │ │ ├── SettingsForm.vue │ │ ├── SettingsInput.vue │ │ ├── SettingsTextarea.vue │ │ ├── Toolbar.vue │ │ └── ToolbarButton.vue │ ├── main.js │ ├── mixins.js │ └── store │ │ ├── index.js │ │ ├── ipc_plugin.js │ │ └── modules │ │ ├── Projects.js │ │ ├── Settings.js │ │ └── index.js └── worker │ ├── index.js │ └── tasks │ ├── index.js │ ├── project_create.js │ └── project_deploy.js ├── static ├── .gitkeep ├── template.ai └── templates │ ├── ai2html.js.ejs │ ├── embed.html.ejs │ ├── embed.js.ejs │ ├── embed_code.html.ejs │ ├── meta_tags.html.ejs │ ├── oembed.json.ejs │ └── preview.html.ejs └── test ├── .eslintrc ├── e2e ├── index.js ├── specs │ └── Launch.spec.js └── utils.js └── unit ├── index.js ├── karma.conf.js └── specs └── LandingPage.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 8.9 } 8 | }], 9 | "stage-0" 10 | ], 11 | "plugins": ["istanbul"] 12 | }, 13 | "main": { 14 | "presets": [ 15 | ["env", { 16 | "targets": { "node": 8.9 } 17 | }], 18 | "stage-0" 19 | ] 20 | }, 21 | "renderer": { 22 | "presets": [ 23 | ["env", { 24 | "targets": { "browsers": ["chrome 61"] }, 25 | "modules": false 26 | }], 27 | "stage-0" 28 | ] 29 | }, 30 | "web": { 31 | "presets": [ 32 | ["env", { 33 | "modules": false 34 | }], 35 | "stage-0" 36 | ] 37 | } 38 | }, 39 | "plugins": ["transform-runtime"] 40 | } 41 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | //const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | const webConfig = require('./webpack.web.config') 15 | 16 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 17 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 18 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 19 | const isCI = process.env.CI || false 20 | 21 | if (process.env.BUILD_TARGET === 'clean') clean() 22 | else if (process.env.BUILD_TARGET === 'web') web() 23 | else build() 24 | 25 | function clean () { 26 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 27 | console.log(`\n${doneLog}\n`) 28 | process.exit() 29 | } 30 | 31 | function build () { 32 | greeting() 33 | 34 | del.sync(['dist/electron/*', '!.gitkeep']) 35 | 36 | const tasks = ['main', 'renderer'] 37 | const m = new Multispinner(tasks, { 38 | preText: 'building', 39 | postText: 'process' 40 | }) 41 | 42 | let results = '' 43 | 44 | m.on('success', () => { 45 | process.stdout.write('\x1B[2J\x1B[0f') 46 | console.log(`\n\n${results}`) 47 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 48 | process.exit() 49 | }) 50 | 51 | pack(mainConfig).then(result => { 52 | results += result + '\n\n' 53 | m.success('main') 54 | }).catch(err => { 55 | m.error('main') 56 | console.log(`\n ${errorLog}failed to build main process`) 57 | console.error(`\n${err}\n`) 58 | process.exit(1) 59 | }) 60 | 61 | pack(rendererConfig).then(result => { 62 | results += result + '\n\n' 63 | m.success('renderer') 64 | }).catch(err => { 65 | m.error('renderer') 66 | console.log(`\n ${errorLog}failed to build renderer process`) 67 | console.error(`\n${err}\n`) 68 | process.exit(1) 69 | }) 70 | } 71 | 72 | function pack (config) { 73 | return new Promise((resolve, reject) => { 74 | webpack(config, (err, stats) => { 75 | if (err) reject(err.stack || err) 76 | else if (stats.hasErrors()) { 77 | let err = '' 78 | 79 | stats.toString({ 80 | chunks: false, 81 | colors: true 82 | }) 83 | .split(/\r?\n/) 84 | .forEach(line => { 85 | err += ` ${line}\n` 86 | }) 87 | 88 | reject(err) 89 | } else { 90 | resolve(stats.toString({ 91 | chunks: false, 92 | colors: true 93 | })) 94 | } 95 | }) 96 | }) 97 | } 98 | 99 | function web () { 100 | del.sync(['dist/web/*', '!.gitkeep']) 101 | webpack(webConfig, (err, stats) => { 102 | if (err || stats.hasErrors()) console.log(err) 103 | 104 | console.log(stats.toString({ 105 | chunks: false, 106 | colors: true 107 | })) 108 | 109 | process.exit() 110 | }) 111 | } 112 | 113 | function greeting () { 114 | const cols = process.stdout.columns 115 | let text = '' 116 | 117 | if (cols > 85) text = 'lets-build' 118 | else if (cols > 60) text = 'lets-|build' 119 | else text = false 120 | 121 | if (text && !isCI) { 122 | say(text, { 123 | colors: ['yellow'], 124 | font: 'simple3d', 125 | space: false 126 | }) 127 | } else console.log(chalk.yellow.bold('\n lets-build')) 128 | console.log() 129 | } 130 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.plugin('compilation', compilation => { 52 | compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.plugin('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | before (app, ctx) { 68 | app.use(hotMiddleware) 69 | ctx.middleware.waitUntilValid(() => { 70 | resolve() 71 | }) 72 | } 73 | } 74 | ) 75 | 76 | server.listen(9080) 77 | }) 78 | } 79 | 80 | function startMain () { 81 | return new Promise((resolve, reject) => { 82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 83 | 84 | const compiler = webpack(mainConfig) 85 | 86 | compiler.plugin('watch-run', (compilation, done) => { 87 | logStats('Main', chalk.white.bold('compiling...')) 88 | hotMiddleware.publish({ action: 'compiling' }) 89 | done() 90 | }) 91 | 92 | compiler.watch({}, (err, stats) => { 93 | if (err) { 94 | console.log(err) 95 | return 96 | } 97 | 98 | logStats('Main', stats) 99 | 100 | if (electronProcess && electronProcess.kill) { 101 | manualRestart = true 102 | process.kill(electronProcess.pid) 103 | electronProcess = null 104 | startElectron() 105 | 106 | setTimeout(() => { 107 | manualRestart = false 108 | }, 5000) 109 | } 110 | 111 | resolve() 112 | }) 113 | }) 114 | } 115 | 116 | function startElectron () { 117 | electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')]) 118 | 119 | electronProcess.stdout.on('data', data => { 120 | electronLog(data, 'blue') 121 | }) 122 | electronProcess.stderr.on('data', data => { 123 | electronLog(data, 'red') 124 | }) 125 | 126 | electronProcess.on('close', () => { 127 | if (!manualRestart) process.exit() 128 | }) 129 | } 130 | 131 | function electronLog (data, color) { 132 | let log = '' 133 | data = data.toString().split(/\r?\n/) 134 | data.forEach(line => { 135 | log += ` ${line}\n` 136 | }) 137 | if (/[0-9A-z]+/.test(log)) { 138 | console.log( 139 | chalk[color].bold('┏ Electron -------------------') + 140 | '\n\n' + 141 | log + 142 | chalk[color].bold('┗ ----------------------------') + 143 | '\n' 144 | ) 145 | } 146 | } 147 | 148 | function greeting () { 149 | const cols = process.stdout.columns 150 | let text = '' 151 | 152 | if (cols > 104) text = 'electron-vue' 153 | else if (cols > 76) text = 'electron-|vue' 154 | else text = false 155 | 156 | if (text) { 157 | say(text, { 158 | colors: ['yellow'], 159 | font: 'simple3d', 160 | space: false 161 | }) 162 | } else console.log(chalk.yellow.bold('\n electron-vue')) 163 | console.log(chalk.blue(' getting ready...') + '\n') 164 | } 165 | 166 | function init () { 167 | greeting() 168 | 169 | Promise.all([startRenderer(), startMain()]) 170 | .then(() => { 171 | startElectron() 172 | }) 173 | .catch(err => { 174 | console.error(err) 175 | }) 176 | } 177 | 178 | init() 179 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies, version } = require('../package.json') 7 | const webpack = require('webpack') 8 | const crypto = require('crypto') 9 | const fs = require('fs') 10 | 11 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 12 | 13 | let mainConfig = { 14 | entry: { 15 | main: path.join(__dirname, '../src/main/index.js'), 16 | worker: path.join(__dirname, '../src/worker/index.js') 17 | }, 18 | externals: [ 19 | ...Object.keys(dependencies || {}) 20 | ], 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js)$/, 25 | enforce: 'pre', 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'eslint-loader', 29 | options: { 30 | formatter: require('eslint-friendly-formatter') 31 | } 32 | } 33 | }, 34 | { 35 | test: /\.js$/, 36 | use: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.node$/, 41 | use: 'node-loader' 42 | }, 43 | ] 44 | }, 45 | node: { 46 | __dirname: process.env.NODE_ENV !== 'production', 47 | __filename: process.env.NODE_ENV !== 'production' 48 | }, 49 | output: { 50 | filename: '[name].js', 51 | libraryTarget: 'commonjs2', 52 | path: path.join(__dirname, '../dist/electron') 53 | }, 54 | plugins: [ 55 | new webpack.NoEmitOnErrorsPlugin() 56 | ], 57 | resolve: { 58 | extensions: ['.js', '.json', '.node'] 59 | }, 60 | target: 'electron-main' 61 | } 62 | 63 | /** 64 | * Adjust mainConfig for production settings 65 | */ 66 | if (process.env.NODE_ENV === 'production') { 67 | mainConfig.plugins.push( 68 | new BabiliWebpackPlugin(), 69 | new webpack.DefinePlugin({ 70 | 'process.env.NODE_ENV': '"production"' 71 | }) 72 | ) 73 | } 74 | 75 | let channel = 'latest' 76 | if ( version.indexOf('beta') >= 0 ) channel = 'beta' 77 | else if (version.indexOf('alpha') >= 0 ) channel = 'alpha' 78 | 79 | mainConfig.plugins.push( 80 | new webpack.DefinePlugin({ 81 | 'AUTOUPDATE_CHANNEL': `"${channel}"` 82 | }) 83 | ) 84 | 85 | module.exports = mainConfig 86 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | 14 | /** 15 | * List of node_modules to include in webpack bundle 16 | * 17 | * Required for specific packages like Vue UI libraries 18 | * that provide pure *.vue files that need compiling 19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 20 | */ 21 | let whiteListedModules = ['vue'] 22 | 23 | let rendererConfig = { 24 | devtool: '#cheap-module-eval-source-map', 25 | entry: { 26 | renderer: path.join(__dirname, '../src/renderer/main.js'), 27 | }, 28 | externals: [ 29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 30 | ], 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.(js|vue)$/, 35 | enforce: 'pre', 36 | exclude: /node_modules/, 37 | use: { 38 | loader: 'eslint-loader', 39 | options: { 40 | formatter: require('eslint-friendly-formatter') 41 | } 42 | } 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: ExtractTextPlugin.extract({ 47 | fallback: 'style-loader', 48 | use: ['css-loader', 'postcss-loader'] 49 | }) 50 | }, 51 | { 52 | test: /\.(scss|sass)$/, 53 | use: ExtractTextPlugin.extract({ 54 | fallback: 'style-loader', 55 | use: ['css-loader', 'sass-loader'] 56 | }) 57 | }, 58 | { 59 | test: /\.html$/, 60 | use: 'vue-html-loader' 61 | }, 62 | { 63 | test: /\.js$/, 64 | use: 'babel-loader', 65 | exclude: /node_modules/ 66 | }, 67 | { 68 | test: /\.node$/, 69 | use: 'node-loader' 70 | }, 71 | { 72 | test: /\.vue$/, 73 | use: { 74 | loader: 'vue-loader', 75 | options: { 76 | extractCSS: process.env.NODE_ENV === 'production', 77 | loaders: { 78 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 79 | scss: 'vue-style-loader!css-loader!sass-loader' 80 | } 81 | } 82 | } 83 | }, 84 | { 85 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 86 | use: { 87 | loader: 'url-loader', 88 | query: { 89 | limit: 10000, 90 | name: 'imgs/[name]--[folder].[ext]' 91 | } 92 | } 93 | }, 94 | { 95 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 96 | loader: 'url-loader', 97 | options: { 98 | limit: 10000, 99 | name: 'media/[name]--[folder].[ext]' 100 | } 101 | }, 102 | { 103 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 104 | use: { 105 | loader: 'url-loader', 106 | query: { 107 | limit: 10000, 108 | name: 'fonts/[name]--[folder].[ext]' 109 | } 110 | } 111 | }, 112 | ] 113 | }, 114 | node: { 115 | __dirname: process.env.NODE_ENV !== 'production', 116 | __filename: process.env.NODE_ENV !== 'production' 117 | }, 118 | plugins: [ 119 | new ExtractTextPlugin('styles.css'), 120 | new HtmlWebpackPlugin({ 121 | filename: 'index.html', 122 | template: path.resolve(__dirname, '../src/index.ejs'), 123 | chunks: ['renderer'], 124 | minify: { 125 | collapseWhitespace: true, 126 | removeAttributeQuotes: true, 127 | removeComments: true 128 | }, 129 | nodeModules: process.env.NODE_ENV !== 'production' 130 | ? path.resolve(__dirname, '../node_modules') 131 | : false 132 | }), 133 | new webpack.HotModuleReplacementPlugin(), 134 | new webpack.NoEmitOnErrorsPlugin() 135 | ], 136 | output: { 137 | filename: '[name].js', 138 | libraryTarget: 'commonjs2', 139 | path: path.join(__dirname, '../dist/electron') 140 | }, 141 | resolve: { 142 | alias: { 143 | '@': path.join(__dirname, '../src/renderer'), 144 | 'vue$': 'vue/dist/vue.esm.js' 145 | }, 146 | modules: [ 147 | 'node_modules' 148 | ], 149 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 150 | }, 151 | target: 'electron-renderer' 152 | } 153 | 154 | /** 155 | * Adjust rendererConfig for development settings 156 | */ 157 | if (process.env.NODE_ENV !== 'production') { 158 | rendererConfig.plugins.push( 159 | new webpack.DefinePlugin({ 160 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 161 | }) 162 | ) 163 | } 164 | 165 | /** 166 | * Adjust rendererConfig for production settings 167 | */ 168 | if (process.env.NODE_ENV === 'production') { 169 | rendererConfig.devtool = '' 170 | 171 | rendererConfig.plugins.push( 172 | new BabiliWebpackPlugin(), 173 | new CopyWebpackPlugin([ 174 | { 175 | from: path.join(__dirname, '../static'), 176 | to: path.join(__dirname, '../dist/electron/static'), 177 | ignore: ['.*'] 178 | } 179 | ]), 180 | new webpack.DefinePlugin({ 181 | 'process.env.NODE_ENV': '"production"' 182 | }), 183 | new webpack.LoaderOptionsPlugin({ 184 | minimize: true 185 | }) 186 | ) 187 | } 188 | 189 | module.exports = rendererConfig 190 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | 13 | let webConfig = { 14 | devtool: '#cheap-module-eval-source-map', 15 | entry: { 16 | web: path.join(__dirname, '../src/renderer/main.js') 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|vue)$/, 22 | enforce: 'pre', 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'eslint-loader', 26 | options: { 27 | formatter: require('eslint-friendly-formatter') 28 | } 29 | } 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: ExtractTextPlugin.extract({ 34 | fallback: 'style-loader', 35 | use: 'css-loader' 36 | }) 37 | }, 38 | { 39 | test: /\.html$/, 40 | use: 'vue-html-loader' 41 | }, 42 | { 43 | test: /\.js$/, 44 | use: 'babel-loader', 45 | include: [ path.resolve(__dirname, '../src/renderer') ], 46 | exclude: /node_modules/ 47 | }, 48 | { 49 | test: /\.vue$/, 50 | use: { 51 | loader: 'vue-loader', 52 | options: { 53 | extractCSS: true, 54 | loaders: { 55 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 56 | scss: 'vue-style-loader!css-loader!sass-loader' 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | use: { 64 | loader: 'url-loader', 65 | query: { 66 | limit: 10000, 67 | name: 'imgs/[name].[ext]' 68 | } 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | use: { 74 | loader: 'url-loader', 75 | query: { 76 | limit: 10000, 77 | name: 'fonts/[name].[ext]' 78 | } 79 | } 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new ExtractTextPlugin('styles.css'), 85 | new HtmlWebpackPlugin({ 86 | filename: 'index.html', 87 | template: path.resolve(__dirname, '../src/index.ejs'), 88 | minify: { 89 | collapseWhitespace: true, 90 | removeAttributeQuotes: true, 91 | removeComments: true 92 | }, 93 | nodeModules: false 94 | }), 95 | new webpack.DefinePlugin({ 96 | 'process.env.IS_WEB': 'true' 97 | }), 98 | new webpack.HotModuleReplacementPlugin(), 99 | new webpack.NoEmitOnErrorsPlugin() 100 | ], 101 | output: { 102 | filename: '[name].js', 103 | path: path.join(__dirname, '../dist/web') 104 | }, 105 | resolve: { 106 | alias: { 107 | '@': path.join(__dirname, '../src/renderer'), 108 | 'vue$': 'vue/dist/vue.esm.js' 109 | }, 110 | extensions: ['.js', '.vue', '.json', '.css'] 111 | }, 112 | target: 'web' 113 | } 114 | 115 | /** 116 | * Adjust webConfig for production settings 117 | */ 118 | if (process.env.NODE_ENV === 'production') { 119 | webConfig.devtool = '' 120 | 121 | webConfig.plugins.push( 122 | new BabiliWebpackPlugin(), 123 | new CopyWebpackPlugin([ 124 | { 125 | from: path.join(__dirname, '../static'), 126 | to: path.join(__dirname, '../dist/web/static'), 127 | ignore: ['.*'] 128 | } 129 | ]), 130 | new webpack.DefinePlugin({ 131 | 'process.env.NODE_ENV': '"production"' 132 | }), 133 | new webpack.LoaderOptionsPlugin({ 134 | minimize: true 135 | }) 136 | ) 137 | } 138 | 139 | module.exports = webConfig 140 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/unit/coverage/** 2 | test/unit/*.js 3 | test/e2e/*.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "voxproduct", 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "allowImportExportEverywhere": false, 7 | "ecmaVersion": 8 8 | } 9 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | globals: { 12 | __static: true 13 | }, 14 | plugins: [ 15 | 'html' 16 | ], 17 | 'rules': { 18 | // allow debugger during development 19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/web/* 4 | build/* 5 | !build/icons 6 | coverage 7 | node_modules/ 8 | npm-debug.log 9 | npm-debug.log.* 10 | thumbs.db 11 | static/project-template/ai2html-output/* 12 | static/project-template/src/config.yml 13 | !.gitkeep 14 | yarn-error.log 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | osx_image: xcode8.3 4 | sudo: required 5 | dist: trusty 6 | language: c 7 | matrix: 8 | include: 9 | - os: osx 10 | - os: linux 11 | env: CC=clang CXX=clang++ npm_config_clang=1 12 | compiler: clang 13 | cache: 14 | directories: 15 | - node_modules 16 | - "$HOME/.electron" 17 | - "$HOME/.cache" 18 | addons: 19 | apt: 20 | packages: 21 | - libgnome-keyring-dev 22 | - icnsutils 23 | #- xvfb 24 | before_install: 25 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 26 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 27 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 28 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 29 | install: 30 | #- export DISPLAY=':99.0' 31 | #- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 32 | - nvm install 7 33 | - curl -o- -L https://yarnpkg.com/install.sh | bash 34 | - source ~/.bashrc 35 | - npm install -g xvfb-maybe 36 | - yarn 37 | script: 38 | #- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js 39 | #- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e 40 | - yarn run build 41 | branches: 42 | only: 43 | - master 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Viz.app 2 | 3 | ## 1.0.0-beta.4 4 | 5 | * Fix deploy not updating html files sometimes 6 | * Add custom icon 7 | * Autoupdate fixes 8 | 9 | ## 1.0.0-beta.3 10 | 11 | * Support for opening existing projects 12 | * Drag project folders to the project list to add a project 13 | * Added Mac OS X menu option File -> Open 14 | * Add validation to make sure no projects are created or added with and existing 15 | title or project path 16 | * Handle accidental drag and drops to other parts of the GUI 17 | * Enable autoupdate through S3 18 | 19 | ## 1.0.0-beta.2 20 | 21 | * Safari bugfixes for the embed. changes in layout.ejs 22 | 23 | ## 1.0.0-beta.1 24 | 25 | * Inital release 26 | * Mac OS X support 27 | * Incomplete Windows support 28 | * Experimental autoupdate support 29 | * Install custom included ai2html script to local Adobe Illustrator install 30 | * ai2html script install or update detection, prompting 31 | * Create new project: scaffold out a project folder 32 | * Delete a project: remove from app and/or delete local files 33 | * Building a project from ai2html output and scaffolded layout.ejs 34 | * Deploy a project to S3 35 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please mark your issue as `bug`, `enhancement` and complete the sections below. If you have a question 2 | and are not reporting a bug or requesting an enhancement, mark this issue as `question` and write 3 | whatever you want in here. 4 | 5 | ### EXPLANATION 6 | 7 | Is this a bug or a feature request? Provide a simple explanation of the bug or feature here. 8 | 9 | ### HOW TO REPRODUCE THE BUG 10 | 11 | Please provide steps to reproduce the bug. Please include screenshots or video clips to demonstrate this bug. Please include log output at the end of this description. You can view the log via the `Help` menu in the application. 12 | 13 | If this is a feature request, remove this section. 14 | 15 | ### WHAT IS THE CURRENT BEHAVIOR AND HOW SHOULD IT CHANGE 16 | 17 | Provide a description of how the application currently behaves and how it should behave. 18 | 19 | If this is a bug, you can remove this section. 20 | 21 | ### WHAT IS THE MOTIVATION OR USE CASE FOR THIS NEW FEATURE 22 | 23 | Provide a use case or argument for making this change or new feature. 24 | 25 | If this is a bug, you can remove this section. 26 | 27 | ### ABOUT YOUR COMPUTER 28 | 29 | VIZIER VERSION: 30 | OPERATING SYSTEM AND VERSION: 31 | ADOBE ILLUSTRATOR VERSION: 32 | 33 | ### LOG OUTPUT 34 | 35 | If this is a bug report, include the recent log contents below: 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Vox Media, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vizier 2 | 3 | A GUI for ai2html projects. Vizier makes it easy to use the [New York Times' ai2html plugin for Adobe Illustrator](http://ai2html.org/). 4 | 5 | Screenshot of the main UI window 6 | 7 | #### How to use it 8 | 9 | - [Download the most recent release](https://github.com/voxmedia/viz-app/releases). 10 | - Make sure you adjust your [Mac OS X Gatekeeper settings to allow applications from anywhere](https://support.apple.com/en-us/HT202491). 11 | - The first time you run the app, you will be prompted to install ai2html. This will replace any 12 | previously installed ai2html.js script. If you installed Illustrator to a non-standard location 13 | you may be asked to find the install folder. 14 | - Open the preference panel to specify the default project folder (where Vizier 15 | will try to create new projects). You will also need to provide AWS settings if 16 | you plan to publish anything. 17 | - Click new project. You will be prompted to specify a name and save the new 18 | project. This will create a new folder containing an ai file and a `src` folder. 19 | - Open the ai file by double clicking the project in Vizier. 20 | - Make an awesome graphic. 21 | - Run ai2html to export your graphic. File > Scripts > ai2html (If you don't see an ai2html option, you may need to restart illustrator). 22 | - By default ai2html will export every artboard. Ai2html looks at the pixel size 23 | of every artboard and the artboard will be displayed if the window is large enough. 24 | If you want to exempt an artboard from export, adjust the artboard name to begin 25 | with a minus `-artboardname`. 26 | - Once the ai2html export completes, return to Vizier, highlight the project 27 | and click deploy. 28 | - Once deploy is complete (green check will appear), right click on the project 29 | and click copy embed code. 30 | - Paste the embed code into your story and publish! 31 | - If your CMS supports oembed urls, you can use the preview link to automatically 32 | discover the embed code! 33 | 34 | #### Caveats 35 | 36 | Out of the box, Vizier and the ai2html script it provides only supports Arial and Georgia fonts. If you want to use non-standard web fonts, you will need to create a `.vizappconfig` file and load it in the program. 37 | 38 | If you notice a standard web font is missing or not working, please open a github issue about it. We won't add non-standard web fonts to the included fonts, even if it's free. 39 | 40 | #### Customizing 41 | 42 | You can write site config files for Vizier which include font data and css to customize the graphic preview, embed and ai2html script. 43 | 44 | The config file is a valid `YAML` document with the extension `.vizappconfig`. [Take a look at the example](https://github.com/voxmedia/viz-app/blob/master/example.vizappconfig). 45 | 46 | #### Developing 47 | 48 | This app uses Electron, Vue.js. 49 | 50 | Clone this repo, then: 51 | 52 | ``` bash 53 | # install dependencies 54 | npm install 55 | 56 | # serve with hot reload at localhost:9080 57 | npm run dev 58 | 59 | # build electron application for production 60 | npm run build 61 | 62 | # run unit & end-to-end tests 63 | npm test 64 | 65 | # lint all JS/Vue component files in `src/` 66 | npm run lint 67 | ``` 68 | 69 | #### Contributing 70 | 71 | Fork this repo, create a new branch on your fork, and make your changes there. 72 | Open a pull request on this repo for consideration. 73 | 74 | If its a small bugfix, feel free making the changes and opening a PR. If it's a 75 | feature addition or a more substantial change, please open a github issue 76 | outlining the feature or change. This is just to save you time and make sure 77 | your efforts can get aligned with other folks' plans. 78 | 79 | --- 80 | 81 | This project was generated with [electron-vue](https://github.com/SimulatedGREG/electron-vue)@[7c4e3e9](https://github.com/SimulatedGREG/electron-vue/tree/7c4e3e90a772bd4c27d2dd4790f61f09bae0fcef) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about the original structure can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html). 82 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2017 10 | platform: 11 | - x64 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 8 x64 24 | - choco install yarn --ignore-dependencies 25 | - git reset --hard HEAD 26 | - yarn 27 | - node --version 28 | 29 | build_script: 30 | #- yarn test 31 | - yarn build 32 | 33 | test: off 34 | -------------------------------------------------------------------------------- /build/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/1024x1024.png -------------------------------------------------------------------------------- /build/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/128x128.png -------------------------------------------------------------------------------- /build/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/16x16.png -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/32x32.png -------------------------------------------------------------------------------- /build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/512x512.png -------------------------------------------------------------------------------- /build/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/64x64.png -------------------------------------------------------------------------------- /build/icons/IconLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/IconLarge.png -------------------------------------------------------------------------------- /build/icons/IconSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/IconSmall.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/build/icons/icon.ico -------------------------------------------------------------------------------- /build/icons/make_icons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | rm -f 1024x1024.png 6 | rm -f 512x512.png 7 | rm -f 256x256.png 8 | rm -f 128x128.png 9 | rm -f 64x64.png 10 | rm -f 32x32.png 11 | rm -f 16x16.png 12 | 13 | cp IconLarge.png 1024x1024.png 14 | 15 | convert IconSmall.png -resize 16x16 16x16.png 16 | convert IconSmall.png -resize 32x32 32x32.png 17 | convert IconLarge.png -resize 64x64 64x64.png 18 | convert IconLarge.png -resize 128x128 128x128.png 19 | convert IconLarge.png -resize 256x256 256x256.png 20 | convert IconLarge.png -resize 512x512 512x512.png 21 | 22 | rm -f icon.icns 23 | rm -Rf icon.iconset 24 | mkdir icon.iconset 25 | 26 | cp 16x16.png icon.iconset/icon_16x16.png 27 | cp 32x32.png icon.iconset/icon_16x16@2x.png 28 | cp 32x32.png icon.iconset/icon_32x32.png 29 | cp 64x64.png icon.iconset/icon_32x32@2x.png 30 | cp 128x128.png icon.iconset/icon_128x128.png 31 | cp 256x256.png icon.iconset/icon_128x128@2x.png 32 | cp 256x256.png icon.iconset/icon_256x256.png 33 | cp 512x512.png icon.iconset/icon_256x256@2x.png 34 | cp 512x512.png icon.iconset/icon_512x512.png 35 | cp 1024x1024.png icon.iconset/icon_512x512@2x.png 36 | 37 | iconutil -c icns icon.iconset 38 | rm -R icon.iconset 39 | 40 | rm icon.ico 41 | convert 16x16.png 32x32.png 64x64.png 128x128.png 256x256.png icon.ico 42 | -------------------------------------------------------------------------------- /dist/electron/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/dist/electron/.gitkeep -------------------------------------------------------------------------------- /dist/web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/dist/web/.gitkeep -------------------------------------------------------------------------------- /do_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! git diff-index --quiet HEAD --; then 4 | echo You must commit all your changes before updating the version 5 | exit 1 6 | fi 7 | 8 | old_version=$(jq '.version' package.json | tr -d '"') 9 | 10 | if [ $# -ne 1 ]; then 11 | read -p "Current version is $old_version. Enter a new version: " version 12 | else 13 | version=$1 14 | fi 15 | 16 | if [ "$old_version" = "$version" ]; then 17 | echo Already at version $version 18 | exit 1 19 | fi 20 | 21 | echo Updating version to $version 22 | 23 | command -v jq >/dev/null 2>&1 || { echo >&2 "Missing jq. Please install jq."; exit 1; } 24 | 25 | { rm package.json && jq --arg version $version '.version |= $version' > package.json; } < package.json 26 | 27 | read -p "Do you wish to commit the new version, tag and push? [y/N] " tyn 28 | if echo "$tyn" | grep -iq "^y"; then 29 | git commit -am "bump to $version" && git tag v$version && git push && git push --tags 30 | 31 | read -p "Do you wish to build and publish the release? [y/N] " pyn 32 | if echo "$pyn" | grep -iq "^y"; then 33 | yarn run build:publish 34 | fi 35 | fi 36 | -------------------------------------------------------------------------------- /example.vizappconfig: -------------------------------------------------------------------------------- 1 | # REQUIRED config file version string. Only '1' is supported 2 | version: 1 3 | 4 | # OPTIONAL Deploy and s3 settings are optional and can be configured separately 5 | # through the app's preference screen. 6 | 7 | # The public URL where your graphics will be deployed to 8 | deployBaseUrl: https://graphics.example.com/ai2htmlgraphics 9 | # Only s3 is supported for deployType 10 | deployType: s3 11 | # The s3 bucket to upload the graphic to 12 | awsBucket: my-graphics-bucket 13 | # The path prefix or folder in the bucket to upload things to 14 | awsPrefix: ai2htmlgraphics 15 | # The AWS region where your S3 bucket lives 16 | awsRegion: us-east-1 17 | # The AWS access key and ID needed to upload to these places. (Be careful sending passwords around!) 18 | awsAccessKeyId: null 19 | awsSecretAccessKey: null 20 | 21 | # REQUIRED. The site or brand name to display to the user so they know what config is loaded 22 | siteConfigName: Example.com 23 | # OPTIONAL. Css to add to the preview graphic page. You can use this to customize the UI font. 24 | extraPreviewCss: |- 25 | @font-face { 26 | font-family: 'Fancy UI'; 27 | src: url('https://fonts.example.com/graphics/fancyui-regular.woff2') format('woff2'), 28 | url('https://fonts.example.com/graphics/fancyui-regular.woff') format('woff'); 29 | font-weight: 400; 30 | } 31 | body { font-family: 'Fancy UI', Helvetica, Arial, sans-serif; } 32 | # REQUIRED. This CSS is added to the graphic embed. Use it to load the brand fonts for your website. 33 | # The font names, weights and styles should match up with the data in the next `ai2htmlFonts` setting. 34 | extraEmbedCss: |- 35 | @font-face 36 | font-family: 'FancySans'; 37 | src: url('https://fonts.example.com/graphics/fancysans-book.woff') format('woff'), 38 | url('https://fonts.example.com/graphics/fancysans-book.woff2') format('woff2'); 39 | font-weight:400; 40 | font-style:normal; 41 | } 42 | 43 | @font-face 44 | font-family: 'FancySans'; 45 | src: url('https://fonts.example.com/graphics/fancysans-bold.woff') format('woff'), 46 | url('https://fonts.example.com/graphics/fancysans-bold.woff2') format('woff2'); 47 | font-weight:700; 48 | font-style:normal; 49 | } 50 | # REQUIRED. A list of comma-separated JSON objects to convert Adobe Illustrator font names to web 51 | # font names. Install the `aifontname.js` script from the ai2html project to your local AI installation 52 | # and run it to identify AI font names. 53 | ai2htmlFonts: |- 54 | {"aifont":"FancySans-Bold","family":"'FancySans', helvetica, sans-serif","weight":"700","style":""}, 55 | {"aifont":"FancySans-Book","family":"'FancySans', helvetica, sans-serif","weight":"400","style":""}, 56 | 57 | # OPTIONAL. Set the default `credit` field for ai2html 58 | ai2htmlCredit: by Example Media 59 | 60 | # OPTIONAL. Include a `provider_name` and `provider_url` in the oembed response 61 | oembedProviderName: Example Media 62 | oembedProviderUrl: https://www.example.com 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizier", 3 | "productName": "Vizier", 4 | "version": "1.1.0-beta.3", 5 | "author": "Ryan Mark ", 6 | "description": "Create, manage and publish ai2html projects", 7 | "license": "BSD-3-Clause", 8 | "main": "./dist/electron/main.js", 9 | "scripts": { 10 | "build": "node .electron-vue/build.js && electron-builder -mw", 11 | "build:win": "node .electron-vue/build.js && electron-builder --win", 12 | "build:mac": "node .electron-vue/build.js && electron-builder --mac", 13 | "build:linux": "node .electron-vue/build.js && electron-builder --linux", 14 | "build:all": "node .electron-vue/build.js && electron-builder -mwl", 15 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 16 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 17 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 18 | "build:publish": "node .electron-vue/build.js && electron-builder -mw -p always", 19 | "electronbuild": "electron-builder", 20 | "dev": "node .electron-vue/dev-runner.js", 21 | "e2e": "npm run pack && mocha test/e2e", 22 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test", 23 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test", 24 | "pack": "npm run pack:main && npm run pack:renderer", 25 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 26 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 27 | "test": "npm run unit && npm run e2e", 28 | "unit": "karma start test/unit/karma.conf.js", 29 | "postinstall": "npm run lint:fix" 30 | }, 31 | "build": { 32 | "productName": "Vizier", 33 | "appId": "com.voxmedia.vizier", 34 | "generateUpdatesFilesForAllChannels": true, 35 | "directories": { 36 | "output": "build" 37 | }, 38 | "files": [ 39 | "dist/electron/**/*" 40 | ], 41 | "mac": { 42 | "icon": "build/icons/icon.icns", 43 | "target": "zip" 44 | }, 45 | "win": { 46 | "icon": "build/icons/icon.ico", 47 | "target": "nsis" 48 | }, 49 | "linux": { 50 | "icon": "build/icons", 51 | "target": "appimage" 52 | }, 53 | "publish": [ 54 | { 55 | "provider": "github", 56 | "owner": "voxmedia", 57 | "repo": "viz-app" 58 | }, 59 | { 60 | "provider": "s3", 61 | "bucket": "apps.voxmedia.com", 62 | "path": "vizapp" 63 | } 64 | ] 65 | }, 66 | "dependencies": { 67 | "at-ui": "^1.3.3", 68 | "at-ui-style": "^1.5.1", 69 | "aws-sdk": "^2.229.1", 70 | "electron-json-storage": "^4.1.6", 71 | "electron-log": "^3.0.1", 72 | "electron-updater": "^4.0.0", 73 | "glob": "^7.1.2", 74 | "glob-copy": "^0.1.0", 75 | "js-yaml": "^3.13.1", 76 | "lodash": "^4.17.11", 77 | "luxon": "^1.0.0", 78 | "node-fs-extra": "^0.8.2", 79 | "s3-client-control": "^4.5.2", 80 | "sudo-prompt": "^8.2.5", 81 | "underscore.string": "^3.3.5", 82 | "uuid": "^3.2.1", 83 | "vue": "^2.3.3", 84 | "vue-electron": "^1.0.6", 85 | "vuex": "^2.3.1" 86 | }, 87 | "devDependencies": { 88 | "asar": "^0.14.3", 89 | "babel-core": "^6.25.0", 90 | "babel-eslint": "^7.2.3", 91 | "babel-loader": "^7.1.1", 92 | "babel-plugin-istanbul": "^4.1.1", 93 | "babel-plugin-transform-runtime": "^6.23.0", 94 | "babel-preset-env": "^1.6.0", 95 | "babel-preset-stage-0": "^6.24.1", 96 | "babel-register": "^6.24.1", 97 | "babili-webpack-plugin": "^0.1.2", 98 | "cfonts": "^1.1.3", 99 | "chai": "^4.0.0", 100 | "chalk": "^2.1.0", 101 | "copy-webpack-plugin": "^4.0.1", 102 | "cross-env": "^5.0.5", 103 | "css-loader": "^0.28.11", 104 | "del": "^3.0.0", 105 | "devtron": "^1.4.0", 106 | "electron": "^3.0.2", 107 | "electron-builder": "^20.44.2", 108 | "electron-debug": "^1.4.0", 109 | "electron-devtools-installer": "^2.2.0", 110 | "electron-publisher-s3": "^20.9.0", 111 | "eslint": "^4.4.1", 112 | "eslint-friendly-formatter": "^3.0.0", 113 | "eslint-loader": "^1.9.0", 114 | "eslint-plugin-html": "^3.1.1", 115 | "extract-text-webpack-plugin": "^3.0.0", 116 | "file-loader": "^0.11.2", 117 | "html-webpack-plugin": "^2.30.1", 118 | "inject-loader": "^3.0.0", 119 | "karma": "^4.1.0", 120 | "karma-chai": "^0.1.0", 121 | "karma-coverage": "^1.1.1", 122 | "karma-electron": "^5.1.1", 123 | "karma-mocha": "^1.2.0", 124 | "karma-sourcemap-loader": "^0.3.7", 125 | "karma-spec-reporter": "^0.0.31", 126 | "karma-webpack": "^2.0.1", 127 | "mocha": "^6.1.4", 128 | "multispinner": "^0.2.1", 129 | "node-loader": "^0.6.0", 130 | "node-sass": "^4.12.0", 131 | "postcss-loader": "^2.1.3", 132 | "require-dir": "^0.3.0", 133 | "sass-loader": "^6.0.7", 134 | "spectron": "^3.7.1", 135 | "style-loader": "^0.18.2", 136 | "url-loader": "^1.1.2", 137 | "vue-html-loader": "^1.2.4", 138 | "vue-loader": "^13.0.5", 139 | "vue-style-loader": "^3.0.1", 140 | "vue-template-compiler": "^2.4.2", 141 | "webpack": "^3.5.2", 142 | "webpack-dev-server": "^2.11.5", 143 | "webpack-hot-middleware": "^2.18.2", 144 | "webpack-merge": "^4.1.0" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vizier 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/embed_code.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import yaml from 'js-yaml' 4 | import { template } from 'lodash' 5 | import { slugify } from 'underscore.string' 6 | 7 | import { getProjectConfig, render, getEmbedMeta } from './index' 8 | 9 | export default function renderEmbedCode({project, settings}) { 10 | const config = getProjectConfig(project) 11 | const slug = slugify(project.title) 12 | const deploy_url = `${settings.deployBaseUrl}/${slug}/` 13 | const embedMeta = getEmbedMeta(config) 14 | const fallbacks = embedMeta.fallbacks 15 | const fallback_img_url = deploy_url + fallbacks[0].name 16 | const fallback_img_width = fallbacks[0].width 17 | const fallback_img_height = fallbacks[1].height 18 | 19 | return render('embed_code.html.ejs', { 20 | slug, 21 | deploy_url, 22 | fallback_img_url, 23 | fallback_img_width, 24 | fallback_img_height, 25 | height: embedMeta.height, 26 | resizable: embedMeta.resizable, 27 | }).replace(/\s+/g, ' ').trim() 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import yaml from 'js-yaml' 4 | import { template } from 'lodash' 5 | import { pipeline } from 'stream' 6 | 7 | const HOMEDIR = process.env[process.platform == 'win32' ? 'HOMEPATH' : 'HOME'] 8 | 9 | export function expandHomeDir (p, homedir) { 10 | if (!p) return p; 11 | if (!homedir) homedir = HOMEDIR 12 | if (process.platform == 'win32') { 13 | return p.replace('%HOMEPATH%', homedir) 14 | } else { 15 | return p.replace(/^~\//, `${homedir}/`) 16 | } 17 | } 18 | 19 | export function compactHomeDir (p, homedir) { 20 | if (!p) return p; 21 | if (!homedir) homedir = HOMEDIR 22 | if (process.platform == 'win32') { 23 | return p 24 | } else { 25 | return p.replace(homedir, '~') 26 | } 27 | } 28 | 29 | export function getStaticPath() { 30 | let ret 31 | if (process.env.ELECTRON_STATIC) 32 | return path.resolve(process.env.ELECTRON_STATIC) 33 | else if (process.env.NODE_ENV !== 'development') 34 | ret = path.join(__dirname, 'static') 35 | else 36 | ret = path.join(__dirname, '..', '..', 'static') 37 | return ret.replace(/\\/g, '\\\\') 38 | } 39 | 40 | export function getEmbedMeta(config) { 41 | const ext = config.image_format == 'jpg' ? 'jpg' : 'png' 42 | let fallbacks 43 | if ( config.fallback_image_height ) { 44 | fallbacks = [{name: `fallback.${ext}`, width: config.fallback_image_width, height: config.fallback_image_height}] 45 | } else { 46 | const artboards = config.artboards.split(',').map(a => a.trim()) 47 | fallbacks = artboards.map((ab) => { 48 | return { 49 | name: `fallback-${ab}.${ext}`, 50 | width: config[`artboard_${ab}_width`], 51 | height: config[`artboard_${ab}_height`], 52 | mime: 'image/' + (ext == 'jpg' ? 'jpeg' : 'png') 53 | } 54 | }).sort((a, b) => a.width - b.width) 55 | } 56 | 57 | // collect all the artboard heights from the config file 58 | const heights = [] 59 | for ( let k in config ) { 60 | const m = k.match(/^artboard_(.+)_height$/) 61 | if (m) heights.push(config[k]) 62 | } 63 | 64 | // if all the artboards are the same height, we can just set the height and 65 | // disable the responsive resizable stuff, set the iframe height to the min height 66 | let resizable = true 67 | let height = 150 68 | if (heights.length > 0) { 69 | resizable = !heights.every(h => h === heights[0]) 70 | height = Math.min(...heights) 71 | } 72 | 73 | return { height, resizable, fallbacks } 74 | } 75 | 76 | export function getProjectConfig(project) { 77 | const projectPath = expandHomeDir(project.path) 78 | const configFile = path.join(projectPath, 'config.yml') 79 | if ( !fs.existsSync(configFile) ) 80 | throw new Error('Missing project config.yml') 81 | 82 | return yaml.safeLoad(fs.readFileSync(configFile, 'utf8')) 83 | } 84 | 85 | export function render(templateName, data) { 86 | const tmpl = template(fs.readFileSync(path.join(getStaticPath(), 'templates', templateName), 'utf8')) 87 | return tmpl(Object.assign({render}, data)) 88 | } 89 | 90 | export function streamCopyFile(src, dest) { 91 | const from = path.normalize(src) 92 | const to = path.normalize(dest) 93 | 94 | const opts = {highWaterMark: Math.pow(2,16)} 95 | 96 | return new Promise((resolve, reject) => { 97 | pipeline( 98 | fs.createReadStream(from, opts), 99 | fs.createWriteStream(to, opts), 100 | (err) => { 101 | if (err) reject(err) 102 | else resolve() 103 | } 104 | ) 105 | }) 106 | } 107 | 108 | export function settingsLabel() { 109 | return process.platform === 'darwin' ? 'Preferences' : 'Settings' 110 | } 111 | -------------------------------------------------------------------------------- /src/main/actions.js: -------------------------------------------------------------------------------- 1 | import { dialog, BrowserWindow, shell, app, clipboard, Notification } from 'electron' 2 | import uuid from 'uuid' 3 | import path from 'path' 4 | import fs from 'fs-extra' 5 | import { slugify } from 'underscore.string' 6 | import yaml from 'js-yaml' 7 | import log from 'electron-log' 8 | import { autoUpdater } from 'electron-updater' 9 | 10 | import { dispatch, resetState } from './ipc' 11 | import state from './index' 12 | import { install } from './install_ai_plugin' 13 | import { run } from './workers' 14 | import { error, alert, confirm } from './dialogs' 15 | import storage from './storage' 16 | import defaultData from './default_data' 17 | 18 | import { expandHomeDir, compactHomeDir } from '../lib' 19 | import renderEmbedCode from '../lib/embed_code' 20 | 21 | export function newProject() { 22 | const projectsDir = expandHomeDir(state.data.Settings.projectDir, app.getPath('home')) 23 | if ( !fs.existsSync(projectsDir) ) fs.ensureDirSync(projectsDir) 24 | dialog.showSaveDialog( 25 | state.mainWindow, 26 | { 27 | title: 'Create a new project', 28 | defaultPath: projectsDir, 29 | nameFieldLabel: 'Project:', // TODO: why doesn't 'Project name:' display correctly? 30 | buttonLabel: 'Create' 31 | }, 32 | (filename) => { 33 | if ( !filename ) return 34 | 35 | addProjects([filename]) 36 | }) 37 | } 38 | 39 | export function addProjects(filenames) { 40 | for ( const filename of filenames ) { 41 | const title = path.basename(filename) 42 | const ppath = compactHomeDir(filename, app.getPath('home')) 43 | 44 | if ( fs.existsSync(filename) ) { 45 | const stats = fs.statSync(filename) 46 | if ( !stats.isDirectory() ) { 47 | return error({ 48 | parentWin: state.mainWindow, 49 | message: 'This type of file is not supported', 50 | details: `File was ${filename}`, 51 | }) 52 | } 53 | 54 | if ( !fs.existsSync(path.join(filename, `${title}.ai`)) ) { 55 | return error({ 56 | parentWin: state.mainWindow, 57 | message: `This is not a project folder. It is missing the Illustrator file "${title}.ai".`, 58 | details: `Folder was ${filename}`, 59 | }) 60 | } 61 | } 62 | 63 | const dupe = state.data.Projects.find(p => { 64 | return slugify(p.title) === slugify(title) || p.path === ppath 65 | }) 66 | 67 | if ( dupe ) { 68 | return error({ 69 | parentWin: state.mainWindow, 70 | message: `There is already a project with the name ${title}` 71 | }) 72 | } 73 | } 74 | 75 | for ( const filename of filenames ) { 76 | const project = { 77 | id: uuid(), 78 | title: path.basename(filename), 79 | path: compactHomeDir(filename, app.getPath('home')), 80 | status: "new", 81 | deployedDate: null, 82 | errorMessage: null, 83 | focus: false, 84 | } 85 | 86 | dispatch( 'project_create', project ) 87 | 88 | if ( !fs.existsSync(filename) ) { 89 | run('project_create', { project, settings: state.data.Settings }) 90 | .then((p) => { 91 | log.debug("Project created successfully!") 92 | }, (err) => { 93 | dispatch( 'project_error', [project.id, err.toString()] ) 94 | }) 95 | } 96 | } 97 | } 98 | 99 | export function openProject() { 100 | dialog.showOpenDialog( 101 | state.mainWindow, 102 | { 103 | defaultPath: expandHomeDir(state.data.Settings.projectDir, app.getPath('home')), 104 | message: 'Select an existing project folder to add.', 105 | properties: [ 'openDirectory', 'multiSelections' ] 106 | }, (filePaths) => { 107 | if (!filePaths || filePaths.length === 0) return; 108 | 109 | addProjects(filePaths) 110 | }) 111 | } 112 | 113 | export function deployProject() { 114 | if ( !state.selectedProject ) return log.debug('deployProject: No selected project!') 115 | const project = state.selectedProject 116 | dispatch( 'project_status', [project.id, 'deploying'] ) 117 | run('project_deploy', { project, settings: state.data.Settings }) 118 | .then((p) => { 119 | dispatch( 'project_status', [project.id, 'deployed'] ) 120 | }, (err) => { 121 | dispatch( 'project_error', [project.id, err] ) 122 | error({parentWin: state.mainWindow, message: err}) 123 | }) 124 | } 125 | 126 | export function openFolder() { 127 | if ( !state.selectedProject ) return log.debug('openFolder: No selected project!') 128 | const projectPath = expandHomeDir(state.selectedProject.path, app.getPath('home')) 129 | if (fs.existsSync(projectPath)) 130 | shell.openItem(projectPath) 131 | else 132 | error({ 133 | parentWin: state.mainWindow, 134 | message: `Project folder is missing.\r\n\r\nIt should be here:\r\n${projectPath}` 135 | }) 136 | } 137 | 138 | export function openInIllustrator() { 139 | if ( !state.selectedProject ) return log.debug('openInIllustrator: No selected project!') 140 | const p = state.selectedProject 141 | const projectPath = expandHomeDir(state.selectedProject.path, app.getPath('home')) 142 | const filepath = path.join(projectPath, `${p.title}.ai`) 143 | if (fs.existsSync(filepath)) 144 | shell.openItem(filepath) 145 | else 146 | error({ 147 | parentWin: state.mainWindow, 148 | message: `Illustrator file is missing.\r\n\r\nIt should be here:\r\n${filepath}` 149 | }) 150 | } 151 | 152 | export function openPreview() { 153 | const slug = slugify(state.selectedProject.title) 154 | const deployUrl = `${state.data.Settings.deployBaseUrl}/${slug}/preview.html` 155 | shell.openExternal(deployUrl) 156 | } 157 | 158 | export function copyEmbedCode() { 159 | try { 160 | clipboard.writeText( 161 | renderEmbedCode({project: state.selectedProject, settings: state.data.Settings}), 162 | 'text/html') 163 | } catch(e) { 164 | log.error('copyEmbedCode: ' + e.message) 165 | if ( e.message == 'Missing project config.yml' ) { 166 | error({ 167 | parentWin: state.mainWindow, 168 | message: 'Project ai2html output is missing.\r\n\r\nRun ai2html from the File > Scripts menu in Illustrator, then try again.' 169 | }) 170 | } 171 | } 172 | } 173 | 174 | export function copyPreviewLink() { 175 | if ( !state.selectedProject ) return log.debug('copyLink: No selected project!') 176 | const project = state.selectedProject 177 | if ( project.status !== 'deployed') { 178 | error({parentWin: state.mainWindow, message: 'Project has not been deployed.\r\n\r\nDeploy the project before attempting to copy the link'}) 179 | return log.debug('copyLink: The project has not been deployed.') 180 | } 181 | const slug = slugify(state.selectedProject.title) 182 | const deployUrl = `${state.data.Settings.deployBaseUrl}/${slug}/preview.html` 183 | clipboard.writeText(deployUrl, 'text/html') 184 | } 185 | 186 | export function removeFromList() { 187 | if ( !state.selectedProject ) return log.debug('removeFromList: No selected project!') 188 | dispatch('project_remove', state.selectedProject.id) 189 | } 190 | 191 | export function removeFromServer() { 192 | const p = state.selectedProject 193 | if ( !p ) return log.debug('removeFromServer: No selected project!') 194 | 195 | dialog.showMessageBox( 196 | state.mainWindow, 197 | { 198 | buttons: ['Cancel', 'Delete from internet'], 199 | defaultId: 0, 200 | title: `Permanently delete ${p.title}`, 201 | message: "This will delete the project from the internet.\r\n\r\nAre you sure you want to do this?", 202 | }, (resp) => { 203 | if ( resp === 0 ) return 204 | 205 | log.debug('removeFromServer') 206 | dispatch( 'project_status', [project.id, 'deploying'] ) 207 | run('project_undeploy', { project, settings: state.data.Settings, userData: app.getPath('userData') }) 208 | .then((p) => { 209 | dispatch( 'project_status', [project.id, 'new'] ) 210 | }, (err) => { 211 | dispatch( 'project_error', [project.id, err] ) 212 | error({parentWin: state.mainWindow, message: err}) 213 | }) 214 | } 215 | ) 216 | } 217 | 218 | export function deleteAll() { 219 | const p = state.selectedProject 220 | if ( !p ) return log.debug('deleteAll: No selected project!') 221 | const projectPath = expandHomeDir(state.selectedProject.path, app.getPath('home')) 222 | 223 | dialog.showMessageBox( 224 | state.mainWindow, 225 | { 226 | buttons: ['Cancel', 'Delete all'], 227 | defaultId: 0, 228 | title: `Permanently delete ${p.title}`, 229 | message: "This will delete the project from your hard drive; there is no undo!\r\n\r\nAre you sure you want to do this?", 230 | }, (resp) => { 231 | if ( resp === 0 ) return 232 | fs.remove(projectPath, (err) => { 233 | if (err) dispatch('project_error', [p.id, err.toString()]) 234 | else dispatch('project_remove', p.id) 235 | }) 236 | } 237 | ) 238 | } 239 | 240 | export function editSettings() { 241 | const winURL = process.env.NODE_ENV === 'development' 242 | ? `http://localhost:9080/#settings` 243 | : `file://${__dirname}/index.html#settings` 244 | 245 | const winWidth = process.platform === 'win32' ? 580 : 520 246 | const winHeight = 512 247 | 248 | state.settingsWindow = new BrowserWindow({ 249 | //parent: state.mainWindow, 250 | //modal: true, 251 | title: (process.platform == 'darwin') ? 'Preferences' : 'Settings', 252 | center: true, 253 | useContentSize: true, 254 | titleBarStyle: 'hidden', 255 | maximizable: false, 256 | minimizable: false, 257 | fullscreenable: false, 258 | alwaysOnTop: true, 259 | width: winWidth, 260 | minWidth: winWidth, 261 | maxWidth: winWidth, 262 | height: winHeight, 263 | minHeight: winHeight, 264 | maxHeight: winHeight, 265 | show: false, 266 | webPreferences: { 267 | webgl: false, 268 | webaudio: false, 269 | textAreasAreResizeable: false, 270 | }, 271 | }) 272 | 273 | state.settingsWindow.loadURL(winURL) 274 | 275 | if (process.platform === 'darwin') 276 | state.settingsWindow.setSheetOffset(22) 277 | 278 | state.settingsWindow.on('closed', () => { 279 | state.settingsWindow = null 280 | }) 281 | 282 | state.settingsWindow.on('ready-to-show', () => { 283 | state.settingsWindow.show() 284 | }); 285 | } 286 | 287 | export function installAi2html(parentWin) { 288 | install({parentWin, forceInstall: true}) 289 | } 290 | 291 | export function clearState() { 292 | storage.clear(() => { 293 | storage.load((err, data) => { 294 | log.debug(data) 295 | state.data = data 296 | resetState(data) 297 | }) 298 | }) 299 | } 300 | 301 | export function resetSettings() { 302 | confirm({ 303 | parentWin: state.settingsWindow, 304 | message: 'Do you wish to reset and clear your settings?', 305 | confirmLabel: 'Reset settings' 306 | }).then(() => { 307 | state.installedAi2htmlHash = null 308 | state.newAi2htmlHash = null 309 | dispatch('resetSettings', defaultData.Settings) 310 | }) 311 | } 312 | 313 | const ALLOWED_KEYS = [ 314 | 'deployBaseUrl', 'deployType', 315 | 'awsBucket', 'awsPrefix', 'awsRegion', 'awsAccessKeyId', 'awsSecretAccessKey', 316 | 'siteConfigName', 'extraPreviewCss', 'extraEmbedCss', 317 | 'ai2htmlFonts', 'ai2htmlCredit', 'oembedProviderName', 'oembedProviderUrl' 318 | ] 319 | 320 | export function importSettings() { 321 | dialog.showOpenDialog( state.settingsWindow, { 322 | message: 'Select a config file to load.', 323 | filters: [{name: 'Viz Config', extensions: ['vizappconfig']}], 324 | properties: [ 'openFile' ] 325 | }, (filePaths) => { 326 | if (!filePaths || filePaths.length === 0) return; 327 | 328 | const configFile = filePaths[0] 329 | const configContent = fs.readFileSync(configFile, 'utf8') 330 | const data = yaml.safeLoad(configContent) 331 | const configVersion = data.version || 1 332 | 333 | if ( configVersion != 1 ) { 334 | error({ 335 | parentWin: state.settingsWindow, 336 | message: 'This config file is for a different version of the app.' 337 | }) 338 | } else { 339 | const newSettings = {} 340 | for ( const k of ALLOWED_KEYS ) { 341 | if ( k in data && data[k] ) newSettings[k] = data[k] 342 | } 343 | dispatch('updateSettings', newSettings) 344 | } 345 | }) 346 | } 347 | 348 | export function openLog() { 349 | const filename = path.join(app.getPath('logs'), 'log.log') 350 | if ( fs.existsSync(filename) ) { 351 | shell.openItem(filename) 352 | } else { 353 | alert({message: 'No log to open.'}) 354 | } 355 | } 356 | 357 | export function checkForUpdates({alertNoUpdates = false} = {}) { 358 | return autoUpdater.checkForUpdates() 359 | } 360 | -------------------------------------------------------------------------------- /src/main/autoupdate.js: -------------------------------------------------------------------------------- 1 | import { Notification, app } from 'electron' 2 | import { autoUpdater } from 'electron-updater' 3 | import log from 'electron-log' 4 | 5 | import { error, alert, confirm } from './dialogs' 6 | import state from './index' 7 | 8 | // Configure the autoupdater. 9 | // Set the update channel. TODO: make a setting 10 | autoUpdater.channel = AUTOUPDATE_CHANNEL 11 | autoUpdater.allowDowngrade = false 12 | autoUpdater.autoInstallOnAppQuit = true 13 | autoUpdater.autoDownload = false 14 | // Use electron-log 15 | autoUpdater.logger = log 16 | 17 | // Setup auto update handling 18 | autoUpdater.on('update-available', (eve) => { 19 | confirm({ 20 | message: 'A new update is available. Do you wish to download and install it?', 21 | confirmLabel: 'Install update' 22 | }).then(() => { 23 | state.mainWindow.setProgressBar(2) 24 | autoUpdater.downloadUpdate() 25 | }, () => { 26 | log.info('User declined update') 27 | }) 28 | }) 29 | 30 | autoUpdater.on('download-progress', (eve) => { 31 | state.mainWindow.setProgressBar(eve.percent / 100) 32 | }) 33 | 34 | autoUpdater.on('update-downloaded', (eve) => { 35 | state.mainWindow.setProgressBar(-1) 36 | new Notification({ 37 | title: "Update is downloaded and ready to install", 38 | body: `${app.getName()} version ${eve.version} will be automatically installed on exit` 39 | }).show() 40 | }) 41 | 42 | autoUpdater.on('error', (eve) => { 43 | state.mainWindow.setProgressBar(-1) 44 | error({ 45 | message: 'Update download failed. Please check your internet connection and try again.' 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/main/default_data.js: -------------------------------------------------------------------------------- 1 | import { expandHomeDir } from '../lib' 2 | import { app } from 'electron' 3 | 4 | /* 5 | * This data object is loaded as the application state.data on first run, or if 6 | * the saved application state is removed. Is a useful reference for the 7 | * state.data schema. 8 | */ 9 | const data = { 10 | "Projects": [], 11 | "Settings": { 12 | "disableAi2htmlStartupCheck": false, 13 | "scriptInstallPath": null, 14 | "projectDir": process.platform === 'win32' ? app.getPath('home') + "\\Projects" : "~/Projects", 15 | "deployBaseUrl": null, 16 | "deployType": 's3', 17 | "awsBucket": null, 18 | "awsPrefix": null, 19 | "awsRegion": 'us-east-1', 20 | "awsAccessKeyId": null, 21 | "awsSecretAccessKey": null, 22 | "extraPreviewCss": null, 23 | "extraEmbedCss": null, 24 | "ai2htmlFonts": null, 25 | "ai2htmlCredit": null, 26 | "oembedProviderName": null, 27 | "oembedProviderUrl": null 28 | } 29 | } 30 | 31 | // if we're running in dev mode, provide some dummy projects for testing 32 | if (process.env.NODE_ENV === 'development') 33 | data.Projects = [ 34 | { 35 | "id": 1, 36 | "title": "My project", 37 | "path": "/Users/ryanmark/Projects/my-project", 38 | "status": "new", 39 | "deployedDate": null, 40 | "errorMessage": null, 41 | "focus": false 42 | }, 43 | { 44 | "id": 2, 45 | "title": "Deploying project", 46 | "path": "~/Projects/deploying-project", 47 | "status": "deploying", 48 | "deployedDate": null, 49 | "errorMessage": null, 50 | "focus": false 51 | }, 52 | { 53 | "id": 3, 54 | "title": "Finished project", 55 | "path": "~/Projects/finished-project", 56 | "status": "deployed", 57 | "deployedDate": "Today at 2:03pm", 58 | "errorMessage": null, 59 | "focus": false 60 | }, 61 | { 62 | "id": 4, 63 | "title": "My project", 64 | "path": "~/Projects/my-project", 65 | "status": "error", 66 | "deployedDate": null, 67 | "errorMessage": "Failed to deploy to Autotune!", 68 | "focus": false 69 | }, 70 | { 71 | "id": 5, 72 | "title": "Deploying project", 73 | "path": "~/Projects/deploying-project", 74 | "status": "deploying", 75 | "deployedDate": null, 76 | "errorMessage": null, 77 | "focus": false 78 | }, 79 | { 80 | "id": 6, 81 | "title": "Finished project", 82 | "path": "~/Projects/finished-project", 83 | "status": "error", 84 | "deployedDate": "Today at 2:03pm", 85 | "errorMessage": "Can't find project on disk!", 86 | "focus": false 87 | }, 88 | { 89 | "id": 7, 90 | "title": "My project", 91 | "path": "~/Projects/my-project", 92 | "status": "new", 93 | "deployedDate": null, 94 | "errorMessage": null, 95 | "focus": false 96 | }, 97 | { 98 | "id": 8, 99 | "title": "Deploying project", 100 | "path": "~/Projects/deploying-project", 101 | "status": "deploying", 102 | "deployedDate": null, 103 | "errorMessage": null, 104 | "focus": false 105 | }, 106 | { 107 | "id": 9, 108 | "title": "Finished project", 109 | "path": "~/Projects/finished-project", 110 | "status": "deployed", 111 | "deployedDate": "Today at 2:03pm", 112 | "errorMessage": null, 113 | "focus": false 114 | } 115 | ] 116 | 117 | export default data 118 | -------------------------------------------------------------------------------- /src/main/dialogs.js: -------------------------------------------------------------------------------- 1 | import { dialog } from 'electron' 2 | 3 | export function error({parentWin, message, details}) { 4 | return new Promise(resolve => { 5 | dialog.showMessageBox( 6 | parentWin || null, 7 | { 8 | type: 'error', 9 | title: 'An error occurred', 10 | message, 11 | details, 12 | }, 13 | resolve 14 | ) 15 | }) 16 | } 17 | 18 | export function alert({parentWin, message, details}) { 19 | return new Promise(resolve => { 20 | dialog.showMessageBox( 21 | parentWin || null, 22 | { 23 | type: 'none', 24 | title: 'Alert', 25 | message, 26 | details, 27 | }, 28 | resolve 29 | ) 30 | }) 31 | } 32 | 33 | export function confirm({parentWin, message, details, confirmLabel, defaultCancel}) { 34 | return new Promise((resolve, reject) => { 35 | dialog.showMessageBox( 36 | parentWin || null, 37 | { 38 | type: 'question', 39 | buttons: ['Cancel', confirmLabel || 'OK'], 40 | defaultId: defaultCancel ? 0 : 1, 41 | title: 'Confirm', 42 | message, 43 | details, 44 | }, (resp) => { 45 | if ( resp === 0 ) return reject() 46 | resolve() 47 | } 48 | ) 49 | }) 50 | } 51 | 52 | export function chooseFolder({ parentWin, title, defaultPath }) { 53 | return new Promise((resolve, reject) => { 54 | dialog.showOpenDialog(parentWin, { 55 | title, 56 | defaultPath, 57 | properties: ['openDirectory',], 58 | }, (filePaths) => { 59 | if ( filePaths.length === 1 ) resolve(filePaths[0]) 60 | else reject() 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Set environment for development 11 | process.env.NODE_ENV = 'development' 12 | 13 | // Install `electron-debug` with `devtron` 14 | require('electron-debug')({ showDevTools: true }) 15 | 16 | // Install `vue-devtools` 17 | require('electron').app.on('ready', () => { 18 | let installExtension = require('electron-devtools-installer') 19 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 20 | .then(() => {}) 21 | .catch(err => { 22 | console.log('Unable to install `vue-devtools`: \n', err) 23 | }) 24 | }) 25 | 26 | // Require `main` process to boot app 27 | require('./index') 28 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, ipcMain, dialog } from 'electron' 2 | import log from 'electron-log' 3 | import Menubar from './menus/Menubar' 4 | import storage from './storage' 5 | import worker from './workers' 6 | import { dispatch } from './ipc' 7 | import { checkOnLaunch } from './install_ai_plugin' 8 | import { checkForUpdates } from './actions' 9 | import { getStaticPath } from '../lib' 10 | 11 | import './autoupdate' 12 | 13 | log.catchErrors() 14 | 15 | // Global State struct for the app. 16 | const state = { 17 | ready: false, 18 | quitting: false, 19 | mainWindow: null, 20 | settingsWindow: null, 21 | selectedProject: null, 22 | data: null, 23 | staticPath: getStaticPath() 24 | } 25 | export default state 26 | 27 | // Set the main window URL 28 | const winURL = process.env.NODE_ENV === 'development' 29 | ? `http://localhost:9080` 30 | : `file://${__dirname}/index.html` 31 | 32 | function createWindow () { 33 | if ( state.mainWindow ) return 34 | 35 | /** 36 | * Initial window options 37 | */ 38 | state.mainWindow = new BrowserWindow({ 39 | useContentSize: true, 40 | titleBarStyle: 'hidden', 41 | maximizable: false, 42 | fullscreenable: false, 43 | width: 320, 44 | minWidth: 240, 45 | maxWidth: 620, 46 | height: 563, 47 | minHeight: 240, 48 | show: false, 49 | webPreferences: { 50 | webgl: false, 51 | webaudio: false, 52 | textAreasAreResizeable: false, 53 | }, 54 | }) 55 | 56 | state.mainWindow.loadURL(winURL) 57 | 58 | if (process.platform == 'darwin') { 59 | state.mainWindow.setSheetOffset(22) 60 | } else { 61 | state.mainWindow.setMenu( Menubar() ) 62 | } 63 | 64 | state.mainWindow.on('close', (eve) => { 65 | if (process.platform === 'darwin' && !state.quitting) { 66 | eve.preventDefault() 67 | state.mainWindow.hide() 68 | } 69 | }) 70 | 71 | state.mainWindow.once('show', () => { 72 | // Setup autoupdates 73 | if (process.env.NODE_ENV === 'production') 74 | checkForUpdates() 75 | 76 | checkOnLaunch() 77 | }) 78 | 79 | state.mainWindow.on('closed', () => state.mainWindow = null) 80 | state.mainWindow.on('ready-to-show', () => state.mainWindow.show()) 81 | } 82 | 83 | function setupEventHandlers() { 84 | app.on('before-quit', () => state.quitting = true) 85 | 86 | app.on('window-all-closed', () => { 87 | if (process.platform !== 'darwin') app.quit() 88 | }) 89 | 90 | app.on('activate', () => { 91 | if (!state.ready || !state.data) return; 92 | if (state.mainWindow) state.mainWindow.show() 93 | else if (state.mainWindow === null) createWindow() 94 | }) 95 | 96 | app.on('ready', () => { 97 | state.ready = true 98 | 99 | // Load app preferences then open the main window 100 | storage.load((error, data) => { 101 | state.data = data 102 | state.selectedProject = data.Projects.find(p => p.focus) 103 | createWindow() 104 | if ( process.platform === 'darwin' ) 105 | Menu.setApplicationMenu( Menubar() ) 106 | }) 107 | }) 108 | } 109 | 110 | // MacOS prevents multiple instances of the app running, but for other OSes 111 | // we have to manage it ourselves. 112 | if ( process.platform === 'darwin' ) { 113 | setupEventHandlers() 114 | } else { 115 | const singleInstanceLock = app.requestSingleInstanceLock() 116 | if ( !singleInstanceLock ) { 117 | // App is already running, so quit. 118 | app.quit() 119 | } else { 120 | setupEventHandlers() 121 | // If a second instance attempts to run, restore and focus this instance's 122 | // main window. 123 | app.on('second-instance', () => { 124 | if ( state.mainWindow.isMinimized() ) { 125 | state.mainWindow.restore() 126 | } 127 | state.mainWindow.focus() 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/install_ai_plugin.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import {dialog, app} from 'electron' 4 | import log from 'electron-log' 5 | import crypto from 'crypto' 6 | import sudo from 'sudo-prompt' 7 | 8 | import state from './index' 9 | import { dispatch } from './ipc' 10 | import { alert, confirm, error, chooseFolder } from './dialogs' 11 | import { render } from '../lib' 12 | import PACKAGE_INFO from '../../package.json' 13 | 14 | // Known install paths of Adobe Illustrator. We'll check these to see if AI is 15 | // installed. 16 | const PATHS = { 17 | 'darwin': [ 18 | '/Applications/Adobe Illustrator CC 2019', 19 | '/Applications/Adobe Illustrator CC 2018', 20 | '/Applications/Adobe Illustrator CC 2017', 21 | '/Applications/Adobe Illustrator CC 2015', 22 | '/Applications/Adobe Illustrator CC 2014', 23 | '~/Applications/Adobe Illustrator CC 2019', 24 | '~/Applications/Adobe Illustrator CC 2018', 25 | '~/Applications/Adobe Illustrator CC 2017', 26 | '~/Applications/Adobe Illustrator CC 2015', 27 | '~/Applications/Adobe Illustrator CC 2014', 28 | ], 29 | 'win32': [ 30 | 'C:\\Program Files\\Adobe\\Adobe Illustrator CC 2019', 31 | 'C:\\Program Files\\Adobe\\Adobe Illustrator CC 2018', 32 | 'C:\\Program Files\\Adobe\\Adobe Illustrator CC 2017', 33 | 'C:\\Program Files\\Adobe\\Adobe Illustrator CC 2015', 34 | 'C:\\Program Files\\Adobe\\Adobe Illustrator CC 2014', 35 | ] 36 | } 37 | 38 | // Hash algorithm used to compute a hash of the ai2html plugin to detect if 39 | // and update is needed 40 | const HASH_ALGO = 'sha1' 41 | 42 | // For Mac OS X the sudo prompt likes an icon, when in production, we need to 43 | // generate a path to the installed app package icon. 44 | let MACOS_SUDO_ICON = null 45 | if ( process.env.NODE_ENV === 'production' ) { 46 | MACOS_SUDO_ICON = path.join(path.dirname(app.getAppPath()), 'Vizier.icns') 47 | } else { 48 | MACOS_SUDO_ICON = PACKAGE_INFO.build.mac.icon 49 | } 50 | const SUDO_OPTS = { 51 | name: PACKAGE_INFO.build.productName, 52 | icns: MACOS_SUDO_ICON 53 | } 54 | 55 | let DEFAULT_PROGRAMS_DIR = null 56 | let SCRIPTS_DIR = null 57 | let COPY_CMD = null 58 | if ( process.platform === 'darwin' ) { 59 | DEFAULT_PROGRAMS_DIR = '/Applications' 60 | SCRIPTS_DIR = 'Presets.localized/en_US/Scripts' 61 | COPY_CMD = 'cp' 62 | } else if ( process.platform === 'win32' ) { 63 | DEFAULT_PROGRAMS_DIR = 'C:\\Program Files\\' 64 | SCRIPTS_DIR = 'Presets\\en_US\\Scripts' 65 | COPY_CMD = 'copy' 66 | } 67 | 68 | // Try to guess the path to Adobe Illustrator 69 | function guessAppPath() { 70 | if ( process.platform in PATHS ) { 71 | let appPath = PATHS[process.platform].find((path) => fs.existsSync(path)) 72 | if ( appPath ) { 73 | return Promise.resolve(appPath) 74 | } else { 75 | return Promise.reject() 76 | } 77 | } else { 78 | return Promise.reject() 79 | } 80 | } 81 | 82 | // Ask the user to identify the installed path to Adobe Illustrator 83 | function chooseAppPath(parentWin) { 84 | const message = "Can't find the Adobe Illustrator install location.\n\nClick 'Choose Illustrator Folder' to specify the install location yourself, or cancel installation." 85 | return confirm({parentWin, message, confirmLabel: 'Choose Illustrator Folder'}) 86 | .then(() => { 87 | return chooseFolder({ 88 | parentWin, 89 | title: 'Choose Illustrator Folder', 90 | defaultPath: DEFAULT_PROGRAMS_DIR, 91 | }) 92 | }) 93 | } 94 | 95 | // Find the scripts subdirectory of the installed adobe illustrator 96 | function findScriptsPath(appPath) { 97 | const scriptsPath = path.join(appPath, SCRIPTS_DIR) 98 | 99 | if ( !fs.existsSync(scriptsPath) || !fs.statSync(scriptsPath).isDirectory() ) { 100 | log.error("Can't find Adobe Illustrator scripts folder. Looked here: ", scriptsPath) 101 | return Promise.reject(new Error('Adobe Illustrator Scripts folder is missing.')) 102 | } 103 | 104 | return Promise.resolve(scriptsPath) 105 | } 106 | 107 | // Generate the ai2html script from a template 108 | function renderAi2htmlScript() { 109 | return render('ai2html.js.ejs', {settings: state.data.Settings}) 110 | } 111 | 112 | // Save the generated ai2html script to a temp location, then try to copy into 113 | // Adobe Illustrator with escalated permissions. 114 | function copyScript(scriptsPath) { 115 | const output = renderAi2htmlScript() 116 | const tempDest = path.join(app.getPath('userData'), 'ai2html.js') 117 | const dest = path.join(scriptsPath, 'ai2html.js') 118 | fs.writeFileSync(tempDest, output) 119 | return new Promise((resolve, reject) => { 120 | const command = `${COPY_CMD} "${tempDest}" "${dest}"` 121 | sudo.exec(command, SUDO_OPTS, (error, stdout, stderr) => { 122 | if (error) { 123 | log.error('Sudo install ai2html plugin failed', error, stdout, stderr) 124 | reject(new Error("Failed to install AI2HTML plugin")) 125 | } else { 126 | resolve(scriptsPath) 127 | } 128 | }) 129 | }) 130 | } 131 | 132 | // Calculate the hash of a file 133 | function calcHash(filename) { 134 | return new Promise((resolve, reject) => { 135 | if ( !fs.existsSync(filename) ) return reject(`File not found ${filename}`) 136 | const hash = crypto.createHash(HASH_ALGO) 137 | hash.on('readable', () => { 138 | const data = hash.read() 139 | if ( data ) resolve(data.toString('hex')) 140 | }) 141 | hash.on('error', reject) 142 | fs.createReadStream(filename).pipe(hash) 143 | }) 144 | } 145 | 146 | // Calculate the hash of the installed ai2html script 147 | export function calcInstalledHash() { 148 | const installPath = state.data.Settings.scriptInstallPath 149 | if ( !installPath || !fs.existsSync(installPath) ) return null 150 | const scriptPath = path.join(installPath, 'ai2html.js') 151 | if ( !fs.existsSync(scriptPath) ) return null 152 | const hash = crypto.createHash(HASH_ALGO) 153 | hash.update(fs.readFileSync(scriptPath, 'utf8')) 154 | return hash.digest('hex') 155 | } 156 | 157 | // Calculate the hash of a freshly generated ai2html script 158 | export function calcNewHash() { 159 | const hash = crypto.createHash(HASH_ALGO) 160 | hash.update(renderAi2htmlScript()) 161 | return hash.digest('hex') 162 | } 163 | 164 | // Do all the things to get ai2html installed 165 | export function install({parentWin = null, forceInstall = false} = {}) { 166 | const startupCheck = state.data.Settings.disableAi2htmlStartupCheck 167 | const installPath = state.data.Settings.scriptInstallPath 168 | 169 | // We don't recalculate hashes here because they should be accurate 170 | const installedHash = state.installedAi2htmlHash 171 | const newHash = state.newAi2htmlHash 172 | 173 | let verb 174 | if(!installedHash) verb = 'Install' 175 | else if (installedHash != newHash) verb = 'Update' 176 | else if (forceInstall) verb = 'Reinstall' 177 | else return; 178 | 179 | dialog.showMessageBox(parentWin, { 180 | type: 'question', 181 | title: `${verb} ai2html`, 182 | message: `Would you like to ${verb.toLowerCase()} ai2html?`, 183 | defaultId: 1, 184 | buttons: ['No', `${verb} ai2html`], 185 | checkboxLabel: "Always check on startup", 186 | checkboxChecked: !startupCheck, 187 | }, (res, checkboxChecked) => { 188 | dispatch('updateSettings', {disableAi2htmlStartupCheck: !checkboxChecked}) 189 | 190 | if ( res === 0 ) return; 191 | 192 | let prom 193 | if (!installPath) { 194 | prom = guessAppPath() 195 | .then(findScriptsPath) 196 | .catch(() => chooseAppPath(parentWin).then(findScriptsPath)) 197 | .then(copyScript) 198 | } else { 199 | prom = copyScript(installPath) 200 | } 201 | 202 | prom.then( 203 | (path) => { 204 | alert({parentWin, message: 'The ai2html script has been installed.'}) 205 | state.installedAi2htmlHash = newHash 206 | dispatch('updateSettings', {scriptInstallPath: path}) 207 | }, 208 | (err) => { 209 | log.error('install script failed', err) 210 | if ( err ) { 211 | if ( err.code && (err.code == 'EACCES' || err.code == 'EPERM') ) { 212 | error({ 213 | parentWin, 214 | message: `The ai2html script install failed.\n\nYou do not have permission to install the plugin.\n\nPlease give yourself write access to ${path.dirname(err.path)}`, 215 | details: err.toString() 216 | }) 217 | } else { 218 | error({parentWin, message: `The ai2html script install failed.\n\n${err.toString()}`}) 219 | } 220 | } else { 221 | error({parentWin, message: `The ai2html script install failed.`}) 222 | } 223 | } 224 | ) 225 | }) 226 | } 227 | 228 | // Check if ai2html needs an update and prompt the user 229 | export function checkOnLaunch() { 230 | // Calculate and stash these hashes at launch 231 | state.installedAi2htmlHash = calcInstalledHash() 232 | state.newAi2htmlHash = calcNewHash() 233 | 234 | if ( state.data.Settings.disableAi2htmlStartupCheck === true ) return; 235 | install({parentWin: state.mainWindow}) 236 | } 237 | -------------------------------------------------------------------------------- /src/main/ipc.js: -------------------------------------------------------------------------------- 1 | import { ipcMain, BrowserWindow } from 'electron' 2 | import ProjectContextMenu from './menus/ProjectContextMenu' 3 | import InputContextMenu from './menus/InputContextMenu' 4 | import state from './index' 5 | import storage from './storage' 6 | import { newProject, addProjects, deployProject, editSettings, installAi2html, openInIllustrator, importSettings, resetSettings } from './actions' 7 | import { calcInstalledHash, calcNewHash } from './install_ai_plugin' 8 | 9 | // Sync messages 10 | ipcMain.on( 'get-state', (eve) => { 11 | eve.returnValue = state.data 12 | } ) 13 | 14 | ipcMain.on( 'has-focus', (eve) => { 15 | eve.returnValue = eve.sender.isFocused() 16 | } ) 17 | 18 | ipcMain.on( 'get-hashes', (eve) => { 19 | eve.returnValue = { 20 | installedHash: state.installedAi2htmlHash, 21 | newHash: state.newAi2htmlHash 22 | } 23 | } ) 24 | 25 | 26 | // Async messages 27 | ipcMain.on( 'project-context-menu', (event, arg) => { 28 | const menu = ProjectContextMenu(arg) 29 | const win = BrowserWindow.fromWebContents(event.sender) 30 | menu.popup({window: win, async: true}) 31 | event.sender.send('context-menu-close', arg) 32 | } ) 33 | 34 | ipcMain.on( 'input-context-menu', (event, arg) => { 35 | const menu = InputContextMenu(arg) 36 | const win = BrowserWindow.fromWebContents(event.sender) 37 | menu.popup({window: win, async: true}) 38 | event.sender.send('context-menu-close', arg) 39 | } ) 40 | 41 | ipcMain.on( 'store-mutate', (eve, arg) => { 42 | if ( !arg.state ) 43 | return console.error('State is missing in store-mutate ipc', arg.mutation, arg.state) 44 | 45 | // Parse and cache current state 46 | const oldData = state.data 47 | state.data = JSON.parse( arg.state ) 48 | 49 | // Recalculate the ai2html script hash if necessary 50 | if ( state.data.Settings.ai2htmlFonts != oldData.Settings.ai2htmlFonts ) 51 | state.newAi2htmlHash = calcNewHash() 52 | 53 | // Make sure other windows have same state 54 | const srcWin = BrowserWindow.fromWebContents(eve.sender) 55 | BrowserWindow.getAllWindows().forEach((win) => { 56 | if ( srcWin.id !== win.id ) 57 | win.webContents.send( 'store-replace-state', state.data ) 58 | }) 59 | 60 | // Adjust selectedProject based on mutation 61 | if ( arg.mutation.type == 'PROJECT_BLUR' ) { 62 | if ( state.selectedProject && state.selectedProject.id == arg.mutation.payload ) 63 | state.selectedProject = null 64 | } else if ( arg.mutation.type == 'PROJECT_FOCUS' ) { 65 | state.selectedProject = state.data.Projects.find(p => p.id == arg.mutation.payload) 66 | } 67 | 68 | // Store application state 69 | storage.pushState( state.data ) 70 | } ) 71 | 72 | ipcMain.on( 'new-project', (eve, arg) => { 73 | newProject() 74 | } ) 75 | 76 | ipcMain.on( 'add-projects', (eve, arg) => { 77 | addProjects(arg) 78 | } ) 79 | 80 | ipcMain.on( 'deploy-project', (eve, arg) => { 81 | deployProject() 82 | } ) 83 | 84 | ipcMain.on( 'settings', (eve, arg) => { 85 | editSettings() 86 | } ) 87 | 88 | ipcMain.on( 'project-open-ai', (eve, arg) => { 89 | openInIllustrator() 90 | } ) 91 | 92 | ipcMain.on( 'install-ai2html', (eve, arg) => { 93 | if ( arg.from == 'settings-window' ) 94 | installAi2html(state.settingsWindow) 95 | else 96 | installAi2html() 97 | } ) 98 | 99 | ipcMain.on( 'import-settings', (eve, arg) => { 100 | if ( arg.from == 'settings-window' ) 101 | importSettings(state.settingsWindow) 102 | else 103 | importSettings() 104 | } ) 105 | 106 | ipcMain.on( 'reset-settings', (eve, arg) => { 107 | if ( arg.from == 'settings-window' ) 108 | resetSettings(state.settingsWindow) 109 | else 110 | resetSettings() 111 | } ) 112 | 113 | // Senders 114 | export function dispatch(action, payload) { 115 | BrowserWindow.getAllWindows().forEach((win) => { 116 | win.webContents.send( 'store-action', {action, payload} ) 117 | }) 118 | } 119 | 120 | export function resetState(data) { 121 | BrowserWindow.getAllWindows().forEach((win) => { 122 | win.webContents.send( 'store-replace-state', data ) 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /src/main/menus/InputContextMenu.js: -------------------------------------------------------------------------------- 1 | import {app, Menu, shell} from 'electron' 2 | import * as actions from '../actions' 3 | 4 | const INPUT_CONTEXT_MENU_TEMPLATE = [ 5 | {role: 'undo'}, 6 | {role: 'redo'}, 7 | {type: 'separator'}, 8 | {role: 'cut'}, 9 | {role: 'copy'}, 10 | {role: 'paste'}, 11 | {role: 'delete'}, 12 | {role: 'selectall'} 13 | ] 14 | 15 | export default function InputContextMenu () { 16 | return Menu.buildFromTemplate( INPUT_CONTEXT_MENU_TEMPLATE ) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/menus/Menubar.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import {app, Menu, shell} from 'electron' 4 | import { newProject, openProject, editSettings, installAi2html, clearState, importSettings, openLog, checkForUpdates } from '../actions' 5 | import { alert } from '../dialogs' 6 | import state from '../index' 7 | import storage from '../storage' 8 | 9 | const MACOS_APP_MENU_TEMPLATE = { 10 | label: app.getName(), 11 | submenu: [ 12 | {role: 'about'}, 13 | {type: 'separator'}, 14 | {label: 'Preferences', click(eve) { editSettings() }}, 15 | {label: 'Import preferences', click(eve) { importSettings() }}, 16 | {label: 'Install ai2html', click(eve) { installAi2html() }}, 17 | {label: 'Check for updates', click(eve) { checkForUpdates({alertNoUpdates: true}) }}, 18 | {type: 'separator'}, 19 | {role: 'services', submenu: []}, 20 | {type: 'separator'}, 21 | {role: 'hide'}, 22 | {role: 'hideothers'}, 23 | {role: 'unhide'}, 24 | {type: 'separator'}, 25 | {role: 'quit'} 26 | ] 27 | } 28 | 29 | const MACOS_FILE_MENU_TEMPLATE = { 30 | label: 'File', 31 | submenu: [ 32 | {label: 'New', accelerator: "CmdOrCtrl+N", click(eve) { newProject() }}, 33 | {label: 'Open', accelerator: "CmdOrCtrl+O", click(eve) { openProject() }}, 34 | ] 35 | } 36 | 37 | const FILE_MENU_TEMPLATE = { 38 | label: 'File', 39 | submenu: [ 40 | {label: 'New', accelerator: "CmdOrCtrl+N", click(eve) { newProject() }}, 41 | {label: 'Open', accelerator: "CmdOrCtrl+O", click(eve) { openProject() }}, 42 | {type: 'separator'}, 43 | {label: 'Settings', click(eve) { editSettings() }}, 44 | {label: 'Import settings', click(eve) { importSettings() }}, 45 | {label: 'Install ai2html', click(eve) { installAi2html() }}, 46 | {label: 'Check for updates', click(eve) { checkForUpdates({alertNoUpdates: true}) }}, 47 | {type: 'separator'}, 48 | {role: 'quit'} 49 | ] 50 | } 51 | 52 | const MACOS_EDIT_MENU_TEMPLATE = { 53 | label: 'Edit', 54 | submenu: [ 55 | {role: 'undo'}, 56 | {role: 'redo'}, 57 | {type: 'separator'}, 58 | {role: 'cut'}, 59 | {role: 'copy'}, 60 | {role: 'paste'}, 61 | {role: 'delete'}, 62 | {role: 'selectall'}, 63 | {type: 'separator'}, 64 | { 65 | label: 'Speech', 66 | submenu: [ 67 | {role: 'startspeaking'}, 68 | {role: 'stopspeaking'} 69 | ] 70 | } 71 | ] 72 | } 73 | 74 | const EDIT_MENU_TEMPLATE = { 75 | label: 'Edit', 76 | submenu: [ 77 | {role: 'undo'}, 78 | {role: 'redo'}, 79 | {type: 'separator'}, 80 | {role: 'cut'}, 81 | {role: 'copy'}, 82 | {role: 'paste'}, 83 | {role: 'delete'}, 84 | {role: 'selectall'} 85 | ] 86 | } 87 | 88 | const DEV_MENU_TEMPLATE = { 89 | label: 'Dev', 90 | submenu: [ 91 | {role: 'reload'}, 92 | {role: 'forcereload'}, 93 | {role: 'toggledevtools'}, 94 | {type: 'separator'}, 95 | { 96 | label: 'Clear storage', 97 | click() { clearState() } 98 | }, 99 | ] 100 | } 101 | 102 | const MACOS_WINDOW_MENU_TEMPLATE = { 103 | role: 'window', 104 | submenu: [ 105 | {role: 'close'}, 106 | {role: 'minimize'}, 107 | {role: 'zoom'}, 108 | {type: 'separator'}, 109 | { 110 | label: app.getName(), 111 | accelerator: "CmdOrCtrl+1", 112 | click() { 113 | state.mainWindow.show() 114 | } 115 | }, 116 | {type: 'separator'}, 117 | {role: 'front'} 118 | ] 119 | } 120 | 121 | const MACOS_HELP_MENU_TEMPLATE = { 122 | role: 'help', 123 | submenu: [ 124 | { 125 | label: 'Learn More', 126 | click () { shell.openExternal('https://github.com/voxmedia/viz-app') } 127 | }, 128 | { 129 | label: 'Open log', 130 | click () { openLog() } 131 | } 132 | ] 133 | } 134 | 135 | const HELP_MENU_TEMPLATE = { 136 | role: 'help', 137 | submenu: [ 138 | { 139 | label: 'Learn More', 140 | click () { shell.openExternal('https://github.com/voxmedia/viz-app') } 141 | }, 142 | { 143 | label: 'Open log', 144 | click () { openLog() } 145 | }, 146 | {type: 'separator'}, 147 | {role: 'about'}, 148 | ] 149 | } 150 | 151 | let MENUBAR_TEMPLATE 152 | let MACOSX_MENUBAR_TEMPLATE 153 | 154 | if (process.env.NODE_ENV === 'development') { 155 | MENUBAR_TEMPLATE = [ 156 | FILE_MENU_TEMPLATE, 157 | DEV_MENU_TEMPLATE, 158 | HELP_MENU_TEMPLATE 159 | ] 160 | MACOSX_MENUBAR_TEMPLATE = [ 161 | MACOS_APP_MENU_TEMPLATE, 162 | MACOS_FILE_MENU_TEMPLATE, 163 | MACOS_EDIT_MENU_TEMPLATE, 164 | DEV_MENU_TEMPLATE, 165 | MACOS_WINDOW_MENU_TEMPLATE, 166 | MACOS_HELP_MENU_TEMPLATE 167 | ] 168 | } else { 169 | MENUBAR_TEMPLATE = [ 170 | FILE_MENU_TEMPLATE, 171 | HELP_MENU_TEMPLATE 172 | ] 173 | MACOSX_MENUBAR_TEMPLATE = [ 174 | MACOS_APP_MENU_TEMPLATE, 175 | MACOS_FILE_MENU_TEMPLATE, 176 | MACOS_EDIT_MENU_TEMPLATE, 177 | MACOS_WINDOW_MENU_TEMPLATE, 178 | MACOS_HELP_MENU_TEMPLATE 179 | ] 180 | } 181 | 182 | export default function Menubar () { 183 | if (process.platform === 'darwin') { 184 | return Menu.buildFromTemplate( MACOSX_MENUBAR_TEMPLATE ) 185 | } else { 186 | return Menu.buildFromTemplate( MENUBAR_TEMPLATE ) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/menus/ProjectContextMenu.js: -------------------------------------------------------------------------------- 1 | import {app, Menu, shell} from 'electron' 2 | import * as actions from '../actions' 3 | 4 | const PROJECT_CONTEXT_MENU_TEMPLATE = [ 5 | {label: 'Open in Illustrator', click() { actions.openInIllustrator() }}, 6 | {label: 'Open folder', click() { actions.openFolder() }}, 7 | {label: 'Open preview in browser', click() { actions.openPreview() }}, 8 | {type: 'separator'}, 9 | {label: 'Copy embed code', click() { actions.copyEmbedCode() }}, 10 | {label: 'Copy preview link', click() { actions.copyPreviewLink() }}, 11 | {type: 'separator'}, 12 | //{label: 'Edit', click() { console.log('edit clicked') }}, 13 | {label: 'Deploy', click() { actions.deployProject() }}, 14 | {type: 'separator'}, 15 | {label: 'Remove from list', click() { actions.removeFromList() }}, 16 | //{label: 'Delete from servers', click() { actions.removeFromServer() }}, 17 | {label: 'Delete permanently', click() { actions.deleteAll() }}, 18 | ] 19 | 20 | export default function ProjectContextMenu () { 21 | return Menu.buildFromTemplate( PROJECT_CONTEXT_MENU_TEMPLATE ) 22 | } 23 | -------------------------------------------------------------------------------- /src/main/storage.js: -------------------------------------------------------------------------------- 1 | import storage from 'electron-json-storage' 2 | import isEqual from 'lodash/isequal' 3 | import defaultData from './default_data' 4 | 5 | const HISTORY_MAX = 20 6 | const SAVE_FILENAME = 'autosave' 7 | 8 | const history = [] 9 | let index = 0 10 | 11 | function load(cb) { 12 | storage.get( SAVE_FILENAME, (error, data) => { 13 | if ( error ) { 14 | console.error( error ) 15 | if ( cb ) cb(error, null) 16 | return 17 | } 18 | 19 | if ( Object.keys(data).length === 0 ) { 20 | data = defaultData 21 | } 22 | 23 | history.push(data) 24 | index = history.length - 1 25 | 26 | if ( cb ) cb(null, data) 27 | } ) 28 | } 29 | 30 | function clear(cb) { 31 | storage.clear((error) => { 32 | if (error) throw error 33 | if (cb) cb() 34 | }) 35 | } 36 | 37 | function save(data) { 38 | return storage.set(SAVE_FILENAME, data, (err) => { if (err) throw err }) 39 | } 40 | 41 | function pushState(newState) { 42 | if ( isEqual( newState, state() ) ) return; 43 | 44 | if ( index < history.length - 1 ) 45 | history.splice( index + 1 ) 46 | 47 | history.push( newState ) 48 | index = history.length - 1 49 | save( newState ) 50 | 51 | if ( HISTORY_MAX > history.length ) 52 | history.splice( 0, history.length - HISTORY_MAX ) 53 | } 54 | 55 | function state() { 56 | return history[index] 57 | } 58 | 59 | function back() { 60 | if ( index <= 0 ) return null 61 | let ret = history[--index] 62 | save( ret ) 63 | return ret 64 | } 65 | 66 | function forward() { 67 | if ( index >= history.length - 1 ) return null 68 | let ret = history[++index] 69 | save( ret ) 70 | return ret 71 | } 72 | 73 | export default { load, clear, pushState, state, back, forward } 74 | -------------------------------------------------------------------------------- /src/main/workers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import log from 'electron-log' 3 | import { app } from 'electron' 4 | import { fork } from 'child_process' 5 | import { join } from 'path' 6 | import EventEmitter from 'events' 7 | import state from './index' 8 | 9 | // How many workers can we have at once 10 | const WORKER_MAX = 5 11 | // How many times can we use a worker process? 12 | const WORKER_USE_LIMIT = 10 13 | // An array to hold the worker objects 14 | const WORKERS = [] 15 | // Path to the worker js script 16 | let WORKER_PATH 17 | 18 | // Figure out the WORKER_PATH 19 | if ( process.env.NODE_ENV === 'development' ) { 20 | WORKER_PATH = join(__dirname, '..', '..', 'dist', 'electron', 'worker') 21 | } else { 22 | // When running in production, we have to copy the worker script to a different 23 | // location to run. We can't fork it directly. 24 | const asarPath = join(__dirname, 'worker.js') 25 | WORKER_PATH = join(app.getPath('userData'), 'tmpworker.js') 26 | fs.writeFileSync(WORKER_PATH, fs.readFileSync(asarPath)) 27 | } 28 | 29 | function createWorker() { 30 | const workerForkOpts = {} 31 | if ( process.env.NODE_ENV === 'development' ) { 32 | workerForkOpts.env = Object.assign( 33 | { 34 | ELECTRON_STATIC: state.staticPath 35 | }, process.env) 36 | } else { 37 | // We also need to make sure it knows where the node_modules are 38 | workerForkOpts.env = Object.assign( 39 | { 40 | ELECTRON_STATIC: state.staticPath, 41 | NODE_PATH: join(__dirname, '..', '..', 'node_modules') 42 | }, process.env) 43 | } 44 | 45 | const proc = fork( WORKER_PATH, [], workerForkOpts ) 46 | const emitter = new EventEmitter() 47 | let status = 'new' // new, ready, working, dead 48 | let uses = 0 49 | 50 | proc.on('message', (args) => { 51 | if ( args === 'ready' ) status = 'ready' 52 | else if ( typeof args === 'object' && ( args[0] === 'done' || args[0] === 'fail' ) ) { 53 | emitter.emit('job-finish', args[0], args[1]) 54 | if ( uses > WORKER_USE_LIMIT ) proc.kill() 55 | else status = 'ready' 56 | } else { 57 | log.error("Unknown message", args) 58 | } 59 | }) 60 | 61 | proc.on('exit', (code) => { 62 | status = 'dead' 63 | }) 64 | 65 | proc.on('error', (err) => { 66 | log.error('Error in worker', err); 67 | proc.kill() 68 | if ( status === 'working' ) 69 | emitter.emit('job-finish', 'fail', err) 70 | }) 71 | 72 | return { 73 | is(s) { 74 | const args = Array.prototype.slice.call(arguments) 75 | return args.reduce((m, s) => m || status === s, false) 76 | }, 77 | on(name, cb) { 78 | emitter.on(name, cb) 79 | }, 80 | run(task, payload) { 81 | if ( status !== 'ready' ) return Promise.reject(new Error('Worker not available')) 82 | status = 'working' 83 | uses++ 84 | const ret = new Promise((resolve, reject) => { 85 | emitter.once('job-finish', (res, data) => { 86 | if ( res === 'done' ) resolve(data) 87 | else if ( res === 'fail' ) reject(data) 88 | }) 89 | }) 90 | 91 | proc.send([task, payload]) 92 | 93 | return ret 94 | }, 95 | } 96 | } 97 | 98 | // state from index.js is undefined on load, so we have to wait till index.js runs 99 | setTimeout(() => { WORKERS.push(createWorker()) }, 0) 100 | 101 | // Cleanup dead or broken workers 102 | function cleanup() { 103 | const idx = [] 104 | for(let i=0; i < WORKERS.length; i++) { 105 | if (!WORKERS[i].is('dead', 'error')) continue; 106 | idx.push(i) 107 | } 108 | idx.forEach(i => WORKERS.splice(i, 1)) 109 | } 110 | 111 | // Send a job to a worker to be run 112 | export function run(name, payload) { 113 | cleanup() 114 | 115 | // Look for a worker to send this job to 116 | for(let i=0; i < WORKERS.length; i++) { 117 | const worker = WORKERS[i] 118 | if (worker.is('ready')) return worker.run(name, payload) 119 | } 120 | 121 | // We didn't find an availble worker, spawn a new one 122 | if (WORKERS.length < WORKER_MAX) WORKERS.push(createWorker()) 123 | 124 | // Retry this job in 400-600 ms 125 | return new Promise((resolve, reject) => { 126 | setTimeout(() => resolve(run(name, payload)), 400 + Math.round(Math.random()*200)) 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 57 | 58 | 66 | -------------------------------------------------------------------------------- /src/renderer/Settings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 40 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/src/renderer/assets/.gitkeep -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/components/List.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 80 | 81 | 123 | -------------------------------------------------------------------------------- /src/renderer/components/ProjectListItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 79 | 80 | 139 | -------------------------------------------------------------------------------- /src/renderer/components/SettingsForm.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 115 | 116 | 152 | -------------------------------------------------------------------------------- /src/renderer/components/SettingsInput.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /src/renderer/components/SettingsTextarea.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /src/renderer/components/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 58 | -------------------------------------------------------------------------------- /src/renderer/components/ToolbarButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 76 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import Vue from 'vue' 3 | import 'at-ui-style/src/index.scss' 4 | //import './assets/base.scss' 5 | import './mixins' 6 | 7 | import App from './App' 8 | import Settings from './Settings' 9 | import store from './store' 10 | 11 | if (!process.env.IS_WEB) Vue.use(require('vue-electron')) 12 | Vue.config.productionTip = false 13 | 14 | //Vue.use(AtComponents) 15 | 16 | let app 17 | if ( typeof window !== 'undefined' && window.location.hash === '#settings' ) { 18 | /* eslint-disable no-new */ 19 | app = new Vue({ 20 | components: { Settings }, 21 | store, 22 | template: '' 23 | }).$mount('#app') 24 | } else { 25 | /* eslint-disable no-new */ 26 | app = new Vue({ 27 | components: { App }, 28 | store, 29 | template: '' 30 | }).$mount('#app') 31 | } 32 | 33 | if ( typeof window !== 'undefined' ) { 34 | window.app = app 35 | 36 | function updateWindowFocus() { 37 | const hasFocus = ipcRenderer.sendSync('has-focus') 38 | if ( hasFocus ) { 39 | let cls = document.body.className 40 | cls = cls.replace(/no-focus/g, '').trim() 41 | document.body.className = `${cls} focus`.trim() 42 | } else { 43 | let cls = document.body.className 44 | cls = cls.replace(/focus/g, '').trim() 45 | document.body.className = `${cls} no-focus` 46 | } 47 | } 48 | 49 | window.addEventListener('blur', updateWindowFocus) 50 | window.addEventListener('focus', updateWindowFocus) 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/mixins.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | let tabindex = 1 4 | Vue.mixin({ 5 | methods: { 6 | tabindex () { return tabindex++ }, 7 | isMac () { return process.platform === 'darwin' }, 8 | notMac () { return process.platform !== 'darwin' }, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import modules from './modules' 5 | import ipcPlugin from './ipc_plugin' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | modules, 11 | strict: process.env.NODE_ENV !== 'production', 12 | plugins: [ipcPlugin] 13 | }) 14 | -------------------------------------------------------------------------------- /src/renderer/store/ipc_plugin.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | export default function ipcPlugin(store) { 4 | ipcRenderer.on('store-action', (event, arg) => { 5 | store.dispatch(arg.action, arg.payload) 6 | }) 7 | 8 | ipcRenderer.on('store-replace-state', (event, arg) => { 9 | store.replaceState( arg ) 10 | }) 11 | 12 | store.subscribe((mutation, state) => { 13 | ipcRenderer.send('store-mutate', { mutation, state: JSON.stringify(state) }) 14 | }) 15 | 16 | store.replaceState( ipcRenderer.sendSync('get-state') ) 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Projects.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | 3 | const state = [ 4 | // { 5 | // id: 1, 6 | // title: "My project", 7 | // path: "/Users/ryanmark/Projects/my-project", 8 | // status: "new", 9 | // deployedDate: null, 10 | // errorMessage: null, 11 | // focus: false, 12 | // }, 13 | ] 14 | 15 | const getters = { 16 | getById: (state) => (id) => { 17 | return state.find(p => p.id === id) 18 | }, 19 | getSelected: (state) => { 20 | return state.find(p => p.focus) 21 | }, 22 | hasSelected: (state, getters) => { 23 | return getters.getSelected != undefined 24 | }, 25 | } 26 | 27 | const mutations = { 28 | PROJECT_FOCUS ( state, id ) { 29 | const proj = state.find(p => p.id === id) 30 | proj.focus = true 31 | }, 32 | PROJECT_BLUR ( state, id ) { 33 | const proj = state.find(p => p.id === id) 34 | proj.focus = false 35 | }, 36 | PROJECT_STATUS ( state, [id, status] ) { 37 | const proj = state.find(p => p.id === id) 38 | proj.status = status 39 | if ( status === 'deployed' ) 40 | proj.deployedDate = DateTime.local().toString() 41 | }, 42 | PROJECT_ERROR ( state, [id, error] ) { 43 | const proj = state.find(p => p.id === id) 44 | proj.status = 'error' 45 | proj.errorMessage = error 46 | }, 47 | PROJECT_ADD ( state, project ) { 48 | state.unshift(project) 49 | }, 50 | PROJECT_UPDATE ( state, [id, data] ) { 51 | for ( let i=0; i < state.length; i++ ) { 52 | const p = state[i] 53 | if ( p.id === id ) { 54 | for ( let k in data ) { 55 | if ( ! k in p ) throw new Error(`Invalid project field '${k}'`) 56 | if ( !_.isEqual(data[k], p[k]) ) p[k] = data[k] 57 | } 58 | break 59 | } 60 | } 61 | const proj = state.find(p => p.id === project.id) 62 | }, 63 | PROJECT_REMOVE ( state, id ) { 64 | const proj = state.find(p => p.id === id) 65 | const idx = state.indexOf( proj ) 66 | state.splice(idx, 1) 67 | } 68 | } 69 | 70 | const actions = { 71 | project_focus ( { commit, getters }, id ) { 72 | if ( getters.getSelected ) commit('PROJECT_BLUR', getters.getSelected.id) 73 | commit('PROJECT_FOCUS', id) 74 | }, 75 | project_blur ( { commit, getters } ) { 76 | if ( getters.getSelected ) commit('PROJECT_BLUR', getters.getSelected.id) 77 | }, 78 | project_status ( { commit }, payload ) { 79 | commit('PROJECT_STATUS', payload) 80 | }, 81 | project_error ( { commit }, payload ) { 82 | commit('PROJECT_ERROR', payload) 83 | }, 84 | project_update ( { commit }, payload ) { 85 | commit('PROJECT_UPDATE', payload) 86 | }, 87 | project_create ( { commit }, project ) { 88 | commit('PROJECT_ADD', project) 89 | }, 90 | project_remove ( { commit }, id ) { 91 | commit('PROJECT_REMOVE', id) 92 | }, 93 | } 94 | 95 | export default { 96 | state, 97 | mutations, 98 | actions, 99 | getters 100 | } 101 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Settings.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const state = { 4 | // disableAi2htmlStartupCheck: false, 5 | // scriptInstallPath: null, 6 | // projectDir: '/Users/ryanmark/Projects', 7 | // deployBaseUrl: null, 8 | // deployType: 's3', 9 | // awsBucket: null, 10 | // awsPrefix: null, 11 | // awsRegion: null, 12 | // awsAccessKeyId: null, 13 | // awsSecretAccessKey: null, 14 | // siteConfigName: null, 15 | // extraPreviewCss: null, 16 | // extraEmbedCss: null, 17 | // ai2htmlFonts: null, 18 | // ai2htmlCredit: null, 19 | // oembedProviderName: null, 20 | // oembedProviderUrl: null 21 | } 22 | 23 | const mutations = { 24 | SETTINGS_SET ( state, newSettings ) { 25 | for (const key in newSettings) { 26 | if ( key in state ) state[key] = newSettings[key] 27 | else Vue.set(state, key, newSettings[key]) 28 | } 29 | }, 30 | SETTINGS_RESET ( state, defaults ) { 31 | for ( const k in state ) { 32 | if ( k in defaults ) state[k] = defaults[k] 33 | else state[k] = null 34 | } 35 | }, 36 | } 37 | 38 | const actions = { 39 | set ({commit}, { key, val }) { 40 | const args = {} 41 | args[key] = val 42 | commit('SETTINGS_SET', args) 43 | }, 44 | updateSettings ({commit}, newSettings) { 45 | commit('SETTINGS_SET', newSettings) 46 | }, 47 | resetSettings ({commit}, defaults) { 48 | commit('SETTINGS_RESET', defaults) 49 | } 50 | } 51 | 52 | export default { 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = require.context('.', false, /\.js$/) 7 | const modules = {} 8 | 9 | files.keys().forEach(key => { 10 | if (key === './index.js') return 11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 12 | }) 13 | 14 | export default modules 15 | -------------------------------------------------------------------------------- /src/worker/index.js: -------------------------------------------------------------------------------- 1 | import tasks from './tasks' 2 | import log from 'electron-log' 3 | 4 | export function done(result) { 5 | process.send(['done', result]) 6 | } 7 | 8 | export function fail(error) { 9 | let ret = error 10 | if ( error.message ) { 11 | ret = `Programming error, please report this.\r\n\r\n${error.name}: ${error.message}` 12 | log.error(error.stack) 13 | } else if ( typeof(error) !== 'string' ) { 14 | log.error(error) 15 | ret = 'Unknown error occured' 16 | } 17 | process.send(['fail', ret]) 18 | } 19 | 20 | process.on('message', ([ task, payload ]) => { 21 | if ( ! task in tasks ) fail(`${task} is not a task`) 22 | try { 23 | tasks[task](payload).then(done, fail) 24 | } catch (e) { 25 | fail(e) 26 | } 27 | }); 28 | 29 | process.send('ready'); 30 | -------------------------------------------------------------------------------- /src/worker/tasks/index.js: -------------------------------------------------------------------------------- 1 | const files = require.context('.', false, /\.js$/) 2 | const modules = {} 3 | 4 | files.keys().forEach(key => { 5 | if (key === './index.js') return 6 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 7 | }) 8 | 9 | export default modules 10 | -------------------------------------------------------------------------------- /src/worker/tasks/project_create.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import { expandHomeDir, getStaticPath, streamCopyFile } from '../../lib' 4 | 5 | export default function createProject({ project, settings }) { 6 | return new Promise((resolve, reject) => { 7 | const projectPath = expandHomeDir(project.path) 8 | fs.mkdirSync(projectPath) 9 | 10 | streamCopyFile( 11 | path.join(getStaticPath(), 'template.ai'), 12 | path.join(projectPath, project.title + '.ai') 13 | ).then(resolve).catch(reject) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/worker/tasks/project_deploy.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import { slugify } from 'underscore.string' 5 | //import { deploy } from '../../lib/s3deploy' 6 | import s3 from 's3-client-control' 7 | import cp from 'glob-copy' 8 | 9 | import { expandHomeDir, getStaticPath, getEmbedMeta, getProjectConfig, render } from '../../lib' 10 | import renderEmbedCode from '../../lib/embed_code' 11 | 12 | function projectBuild({ project, settings }) { 13 | return new Promise((resolve, reject) => { 14 | const projectPath = expandHomeDir(project.path) 15 | const dest = path.join(projectPath, 'build') 16 | 17 | if (!fs.existsSync(dest)) fs.mkdirSync(dest) 18 | 19 | const expected = 3 20 | const errors = [] 21 | let count = 0 22 | function end(error) { 23 | if (error) errors.push(error) 24 | if (++count >= expected) { 25 | if (errors.length > 0) reject(errors) 26 | else resolve(project) 27 | } 28 | } 29 | 30 | const src = path.join( 31 | projectPath, 32 | 'ai2html-output', 33 | '*.{gif,jpg,png,svg,jpeg,json,mp4,mp3,webm,webp}' 34 | ) 35 | cp(src, dest, end) 36 | 37 | const contentFile = path.join(projectPath, 'ai2html-output', 'index.html') 38 | 39 | // Template data 40 | const slug = slugify(project.title) 41 | const deploy_url = `${settings.deployBaseUrl}/${slug}/` 42 | const config = getProjectConfig(project) 43 | const content = fs.readFileSync(contentFile, 'utf8') 44 | const embed_code = renderEmbedCode({ project, settings }) 45 | const embed_meta = getEmbedMeta(config) 46 | const extra_preview_css = settings.extraPreviewCss || '' 47 | const extra_embed_css = settings.extraEmbedCss || '' 48 | 49 | fs.writeFile( 50 | path.join(dest, 'index.html'), 51 | render('embed.html.ejs', { config, content, project, embed_meta, slug, deploy_url, extra_embed_css }), 52 | end) 53 | 54 | fs.writeFile( 55 | path.join(dest, 'preview.html'), 56 | render('preview.html.ejs', { config, embed_code, project, embed_meta, slug, deploy_url, extra_preview_css }), 57 | end) 58 | 59 | fs.writeFile( 60 | path.join(dest, 'embed.js'), 61 | render('embed.js.ejs', { id: slug + '__graphic', url: deploy_url }), 62 | end) 63 | 64 | fs.writeFile( 65 | path.join(dest, 'oembed.json'), 66 | render('oembed.json.ejs', { config, embed_code, project, embed_meta, slug, deploy_url, settings }), 67 | end) 68 | }) 69 | } 70 | 71 | export default function projectDeploy({ project, settings }) { 72 | return new Promise((resolve, reject) => { 73 | if (settings.deployType !== 's3') 74 | return reject(`Deploy type ${settings.deployType} is not implemented`) 75 | 76 | if (!settings.deployBaseUrl) 77 | return reject('Base deploy URL is missing. Please set this in settings.') 78 | 79 | if (!settings.awsRegion) 80 | return reject('AWS Region is missing. Please set this in settings.') 81 | 82 | if (!settings.awsBucket) 83 | return reject('AWS S3 bucket is missing. Please set this in settings.') 84 | 85 | if (!settings.awsPrefix) 86 | return reject('AWS S3 file path is missing. Please set this in settings.') 87 | 88 | if (!settings.awsAccessKeyId && !process.env.AWS_ACCESS_KEY_ID) 89 | return reject('AWS Access Key ID is missing. Please set this in settings.') 90 | 91 | if (!settings.awsSecretAccessKey && !process.env.AWS_SECRET_ACCESS_KEY) 92 | return reject('AWS Secret Access Key is missing. Please set this in settings.') 93 | 94 | const projectPath = expandHomeDir(project.path) 95 | 96 | if (!fs.existsSync(projectPath)) 97 | return reject(`Project folder is missing.\r\n\r\nIt should be here:\r\n${projectPath}`) 98 | 99 | if (!fs.existsSync(path.join(projectPath, 'ai2html-output'))) 100 | return reject('Project ai2html output is missing.\r\n\r\nRun ai2html from the File > Scripts menu in Illustrator, then try again.') 101 | 102 | const localDir = path.join(projectPath, 'build') 103 | 104 | const client = s3.createClient({ 105 | s3Options: { 106 | region: settings.awsRegion || process.env.AWS_REGION, 107 | accessKeyId: settings.awsAccessKeyId || process.env.AWS_ACCESS_KEY_ID, 108 | secretAccessKey: settings.awsSecretAccessKey || process.env.AWS_SECRET_ACCESS_KEY, 109 | } 110 | }) 111 | 112 | const s3Params = { 113 | Bucket: settings.awsBucket, 114 | Prefix: `${settings.awsPrefix}/${slugify(project.title.trim())}`, 115 | CacheControl: 'max-age=60', 116 | ACL: 'public-read' 117 | } 118 | 119 | projectBuild({ project, settings }) 120 | .then(() => { 121 | return new Promise((resolve, reject) => { 122 | const uploader = client.uploadDir({ localDir, s3Params }) 123 | uploader.on('error', (err) => reject(err)) 124 | uploader.on('end', () => resolve()) 125 | }) 126 | }) 127 | .then(() => { 128 | resolve(project) 129 | }, err => { 130 | reject(err) 131 | }) 132 | 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/static/.gitkeep -------------------------------------------------------------------------------- /static/template.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxmedia/viz-app/9b7278f4efb880fd84cfa5d1871adfdd9262ebdd/static/template.ai -------------------------------------------------------------------------------- /static/templates/embed.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%=config.headline || project.title%> 7 | 8 | 9 | 10 | 11 | 12 | <%=render('meta_tags.html.ejs', {slug, deploy_url, embed_meta, config, project}) %> 13 | 14 | 20 | 21 | 22 | 81 | 82 | 83 | 84 | <%=content%> 85 | 86 | 87 | -------------------------------------------------------------------------------- /static/templates/embed.js.ejs: -------------------------------------------------------------------------------- 1 | (function() { 2 | var l = function() { 3 | new pym.Parent( 4 | '<%=id %>', 5 | '<%=url %>'); 6 | }; 7 | if(typeof(pym) === 'undefined') { 8 | var h = document.getElementsByTagName('head')[0], 9 | s = document.createElement('script'); 10 | s.type = 'text/javascript'; 11 | s.src = 'https://pym.nprapps.org/pym.v1.min.js'; 12 | s.onload = l; 13 | h.appendChild(s); 14 | } else { 15 | l(); 16 | } 17 | })(); 18 | -------------------------------------------------------------------------------- /static/templates/embed_code.html.ejs: -------------------------------------------------------------------------------- 1 |
data-iframe-resizable<% } %>>
10 | 11 | -------------------------------------------------------------------------------- /static/templates/meta_tags.html.ejs: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /static/templates/oembed.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "type": "rich", 4 | <% if ( settings.oembedProviderName ) { %>"provider_name": "<%=settings.oembedProviderName %>",<% } %> 5 | <% if ( settings.oembedProviderUrl ) { %>"provider_url": "<%=settings.oembedProviderUrl %>",<% } %> 6 | "title": "<%=config.headline || project.title%>", 7 | "author_name": "<%=config.credit%>", 8 | "html": "<%=embed_code.replace(/"/g, '\\"')%>", 9 | "width": "100%", 10 | "height": <%=embed_meta.height %>, 11 | "thumbnail_url": "<%=deploy_url + embed_meta.fallbacks[0].name %>", 12 | "thumbnail_width": <%=embed_meta.fallbacks[0].width %>, 13 | "thumbnail_height": <%=embed_meta.fallbacks[0].height %> 14 | } 15 | -------------------------------------------------------------------------------- /static/templates/preview.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preview: <%=config.headline || project.title %> 6 | 7 | 8 | 9 | <%=render('meta_tags.html.ejs', {slug, deploy_url, embed_meta, config, project}) %> 10 | 11 | 98 | 99 | 100 |
101 | 106 |
107 |
Viewport: &emdash; x &emdash;
108 |
Graphic/artboard width: &emdash;
109 |
110 |
111 |

Expedita perspiciatis eaque corporis dicta nesciunt debitis quod. Quos atque fugit nam quibusdam cum Consequuntur.

112 |

<%=embed_code %>

113 |

Dolor sit praesentium sint hic doloremque vero. Eos delectus perferendis facere cum voluptatum asperiores nostrum?

114 |
115 |
116 | 117 | 118 | 119 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "__static": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Set BABEL_ENV to use proper env config 4 | process.env.BABEL_ENV = 'test' 5 | 6 | // Enable use of ES6+ on required files 7 | require('babel-register')({ 8 | ignore: /node_modules/ 9 | }) 10 | 11 | // Attach Chai APIs to global scope 12 | const { expect, should, assert } = require('chai') 13 | global.expect = expect 14 | global.should = should 15 | global.assert = assert 16 | 17 | // Require all JS files in `./specs` for Mocha to consume 18 | require('require-dir')('./specs') 19 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import utils from '../utils' 2 | 3 | describe('Launch', function () { 4 | beforeEach(utils.beforeEach) 5 | afterEach(utils.afterEach) 6 | 7 | it('shows the proper application title', function () { 8 | return this.app.client.getTitle() 9 | .then(title => { 10 | expect(title).to.equal('vizier') 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import { Application } from 'spectron' 3 | 4 | export default { 5 | afterEach () { 6 | this.timeout(10000) 7 | 8 | if (this.app && this.app.isRunning()) { 9 | return this.app.stop() 10 | } 11 | }, 12 | beforeEach () { 13 | this.timeout(10000) 14 | this.app = new Application({ 15 | path: electron, 16 | args: ['dist/electron/main.js'], 17 | startTimeout: 10000, 18 | waitTimeout: 10000 19 | }) 20 | 21 | return this.app.start() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.devtools = false 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('webpack-merge') 5 | const webpack = require('webpack') 6 | 7 | const baseConfig = require('../../.electron-vue/webpack.renderer.config') 8 | const projectRoot = path.resolve(__dirname, '../../src/renderer') 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = 'test' 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: '#inline-source-map', 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': '"testing"' 18 | }) 19 | ] 20 | }) 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry 24 | delete webpackConfig.externals 25 | delete webpackConfig.output.libraryTarget 26 | 27 | // apply vue option to apply isparta-loader on js 28 | webpackConfig.module.rules 29 | .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' 30 | 31 | module.exports = config => { 32 | config.set({ 33 | browsers: ['visibleElectron'], 34 | client: { 35 | useIframe: false 36 | }, 37 | coverageReporter: { 38 | dir: './coverage', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | customLaunchers: { 45 | 'visibleElectron': { 46 | base: 'Electron', 47 | flags: ['--show'] 48 | } 49 | }, 50 | frameworks: ['mocha', 'chai'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | reporters: ['spec', 'coverage'], 56 | singleRun: true, 57 | webpack: webpackConfig, 58 | webpackMiddleware: { 59 | noInfo: true 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/specs/LandingPage.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LandingPage from '@/components/LandingPage' 3 | 4 | describe('LandingPage.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: h => h(LandingPage) 9 | }).$mount() 10 | 11 | expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!') 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------