├── .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 |
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 |
2 |
3 |
4 | Vizier
5 |
6 | New
7 | Deploy
8 |
9 |
10 | {{settingsLabel}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
57 |
58 |
66 |
--------------------------------------------------------------------------------
/src/renderer/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{settingsLabel}}
4 |
5 |
6 |
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 |
2 |
21 |
22 |
23 |
80 |
81 |
123 |
--------------------------------------------------------------------------------
/src/renderer/components/ProjectListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
{{ project.title }}
10 | {{ project.path }}
11 | Last deployed {{ displayDeployedDate }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
79 |
80 |
139 |
--------------------------------------------------------------------------------
/src/renderer/components/SettingsForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
57 |
58 |
59 |
115 |
116 |
152 |
--------------------------------------------------------------------------------
/src/renderer/components/SettingsInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
19 |
20 |
63 |
64 |
67 |
--------------------------------------------------------------------------------
/src/renderer/components/SettingsTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
35 |
36 |
38 |
--------------------------------------------------------------------------------
/src/renderer/components/Toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
35 |
36 |
58 |
--------------------------------------------------------------------------------
/src/renderer/components/ToolbarButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
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 |
--------------------------------------------------------------------------------