From 4802b779a6ad68b59332a3890bff75f80fac1f93 Mon Sep 17 00:00:00 2001 From: Ryan Mark Date: Wed, 17 Oct 2018 10:56:11 -0400 Subject: [PATCH] major refactor; remove all brand-specific stuff, implement a site config file format which customizes vizier to a specific brand or site without code changes --- .electron-vue/webpack.main.config.js | 1 - src/lib/index.js | 4 + src/main/actions.js | 52 ++++++- src/main/default_data.js | 5 +- src/main/install_ai_plugin.js | 139 ++++++++++-------- src/main/ipc.js | 28 +++- src/main/menus/Menubar.js | 3 +- src/renderer/App.vue | 1 + src/renderer/Settings.vue | 6 +- src/renderer/components/SettingsForm.vue | 58 +++++++- src/renderer/store/modules/Settings.js | 26 +++- .../{ai2html.js => templates/ai2html.js.ejs} | 24 +-- static/templates/embed.html.ejs | 114 -------------- static/templates/preview.html.ejs | 10 +- 14 files changed, 252 insertions(+), 219 deletions(-) rename static/{ai2html.js => templates/ai2html.js.ejs} (98%) diff --git a/.electron-vue/webpack.main.config.js b/.electron-vue/webpack.main.config.js index 7a657ea..bfb9755 100644 --- a/.electron-vue/webpack.main.config.js +++ b/.electron-vue/webpack.main.config.js @@ -78,7 +78,6 @@ else if (version.indexOf('alpha') >= 0 ) channel = 'alpha' mainConfig.plugins.push( new webpack.DefinePlugin({ - 'AI2HTML_HASH': `"${crypto.createHash('sha1').update(fs.readFileSync(path.join(__dirname, '../static/ai2html.js'))).digest('hex')}"`, 'AUTOUPDATE_CHANNEL': `"${channel}"` }) ) diff --git a/src/lib/index.js b/src/lib/index.js index bb8fc20..83e31c4 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -102,3 +102,7 @@ export function streamCopyFile(src, dest) { ) }) } + +export function settingsLabel() { + return process.platform === 'darwin' ? 'Preferences' : 'Settings' +} diff --git a/src/main/actions.js b/src/main/actions.js index 6a68f63..7d2f962 100644 --- a/src/main/actions.js +++ b/src/main/actions.js @@ -4,13 +4,15 @@ import path from 'path' import rmrf from 'rimraf' import fs from 'fs' import { slugify } from 'underscore.string' +import yaml from 'js-yaml' import { dispatch, resetState } from './ipc' import state from './index' import { install } from './install_ai_plugin' import { run } from './workers' -import { error } from './dialogs' +import { error, alert, confirm } from './dialogs' import storage from './storage' +import defaultData from './default_data' import { expandHomeDir, compactHomeDir } from '../lib' import renderEmbedCode from '../lib/embed_code' @@ -237,7 +239,7 @@ export function editSettings() { : `file://${__dirname}/index.html#settings` const winWidth = 520 - const winHeight = 630 + const winHeight = 512 state.settingsWindow = new BrowserWindow({ //parent: state.mainWindow, @@ -291,3 +293,49 @@ export function clearState() { }) }) } + +export function resetSettings() { + confirm({ + parentWin: state.settingsWindow, + message: 'Do you wish to reset and clear your settings?', + confirmLabel: 'Reset settings' + }).then(() => { + state.installedAi2htmlHash = null + state.newAi2htmlHash = null + dispatch('resetSettings', defaultData.Settings) + }) +} + +const ALLOWED_KEYS = [ + 'deployBaseUrl', 'deployType', + 'awsBucket', 'awsPrefix', 'awsRegion', 'awsAccessKeyId', 'awsSecretAccessKey', + 'siteConfigName', 'extraPreviewCss', 'extraEmbedCss', 'ai2htmlFonts' +] + +export function importSettings() { + dialog.showOpenDialog( state.settingsWindow, { + message: 'Select a config file to load.', + filters: [{name: 'Viz Config', extensions: ['vizappconfig']}], + properties: [ 'openFile' ] + }, (filePaths) => { + if (!filePaths || filePaths.length === 0) return; + + const configFile = filePaths[0] + const configContent = fs.readFileSync(configFile, 'utf8') + const data = yaml.safeLoad(configContent) + const configVersion = data.version || 1 + + if ( configVersion != 1 ) { + error({ + parentWin: state.settingsWindow, + message: 'This config file is for a different version of the app.' + }) + } else { + const newSettings = {} + for ( const k of ALLOWED_KEYS ) { + if ( k in data && data[k] ) newSettings[k] = data[k] + } + dispatch('updateSettings', newSettings) + } + }) +} diff --git a/src/main/default_data.js b/src/main/default_data.js index 8f3cbe7..743ea57 100644 --- a/src/main/default_data.js +++ b/src/main/default_data.js @@ -15,7 +15,10 @@ const data = { "awsPrefix": null, "awsRegion": 'us-east-1', "awsAccessKeyId": null, - "awsSecretAccessKey": null + "awsSecretAccessKey": null, + "extraPreviewCss": null, + "extraEmbedCss": null, + "ai2htmlFonts": null } } diff --git a/src/main/install_ai_plugin.js b/src/main/install_ai_plugin.js index 8a6a6da..32d164d 100644 --- a/src/main/install_ai_plugin.js +++ b/src/main/install_ai_plugin.js @@ -5,7 +5,7 @@ import state from './index' import crypto from 'crypto' import { dispatch } from './ipc' import { alert, confirm, error, chooseFolder } from './dialogs' -import { streamCopyFile } from '../lib' +import { render } from '../lib' const PATHS = { 'darwin': [ @@ -20,6 +20,8 @@ const PATHS = { ], } +const HASH_ALGO = 'sha1' + let DEFAULT_PROGRAMS_DIR = null let SCRIPTS_DIR = null if ( process.platform === 'darwin' ) { @@ -60,18 +62,25 @@ function findScriptsPath(appPath) { console.error("Can't find Adobe Illustrator scripts folder. Looked here: ", scriptsPath) return Promise.reject(new Error('Adobe Illustrator Scripts folder is missing.')) } + return Promise.resolve(scriptsPath) } +function renderAi2htmlScript() { + return render('ai2html.js.ejs', {settings: state.data.Settings}) +} + function copyScript(scriptsPath) { - const src = path.join(state.staticPath, 'ai2html.js') + const output = renderAi2htmlScript() const dest = path.join(scriptsPath, 'ai2html.js') - return streamCopyFile(src, dest).then(() => scriptsPath) + fs.writeFileSync(dest, output) + return Promise.resolve(scriptsPath) } -function calcHash(filename, type='sha1') { +function calcHash(filename) { return new Promise((resolve, reject) => { - const hash = crypto.createHash(type) + if ( !fs.existsSync(filename) ) return reject(`File not found ${filename}`) + const hash = crypto.createHash(HASH_ALGO) hash.on('readable', () => { const data = hash.read() if ( data ) resolve(data.toString('hex')) @@ -81,72 +90,86 @@ function calcHash(filename, type='sha1') { }) } -function isInstalled() { +export function calcInstalledHash() { const installPath = state.data.Settings.scriptInstallPath - return Promise.resolve(installPath && fs.existsSync(installPath)) + if ( !installPath || !fs.existsSync(installPath) ) return null + const scriptPath = path.join(installPath, 'ai2html.js') + if ( !fs.existsSync(scriptPath) ) return null + const hash = crypto.createHash(HASH_ALGO) + hash.update(fs.readFileSync(scriptPath, 'utf8')) + return hash.digest('hex') } -function isUpdated() { - const installPath = state.data.Settings.scriptInstallPath - if ( ! fs.existsSync(installPath) ) return Promise.resolve(null) - return calcHash(installPath).then((installedHash) => AI2HTML_HASH === installedHash) +export function calcNewHash() { + const hash = crypto.createHash(HASH_ALGO) + hash.update(renderAi2htmlScript()) + return hash.digest('hex') } export function install({parentWin = null, forceInstall = false} = {}) { const startupCheck = state.data.Settings.disableAi2htmlStartupCheck const installPath = state.data.Settings.scriptInstallPath - Promise.all([isInstalled(), isUpdated()]) - .then(([installed, updated]) => { - let verb - if(!installed) verb = 'Install' - else if (installed && !updated) verb = 'Update' - else if (forceInstall) verb = 'Reinstall' - else return; - - dialog.showMessageBox(parentWin, { - type: 'question', - title: `${verb} ai2html`, - message: `Would you like to ${verb.toLowerCase()} ai2html?`, - defaultId: 1, - buttons: ['No', `${verb} ai2html`], - checkboxLabel: "Always check on startup", - checkboxChecked: !startupCheck, - }, (res, checkboxChecked) => { - dispatch('set', {key: 'disableAi2htmlStartupCheck', val: !checkboxChecked}) - - if ( res === 0 ) return; - - let prom - if (!installed) { - prom = guessAppPath() - .then(findScriptsPath) - .catch(() => chooseAppPath(parentWin).then(findScriptsPath)) - .then(copyScript) + // We don't recalculate hashes here because they should be accurate + const installedHash = state.installedAi2htmlHash + const newHash = state.newAi2htmlHash + + let verb + if(!installedHash) verb = 'Install' + else if (installedHash != newHash) verb = 'Update' + else if (forceInstall) verb = 'Reinstall' + else return; + + dialog.showMessageBox(parentWin, { + type: 'question', + title: `${verb} ai2html`, + message: `Would you like to ${verb.toLowerCase()} ai2html?`, + defaultId: 1, + buttons: ['No', `${verb} ai2html`], + checkboxLabel: "Always check on startup", + checkboxChecked: !startupCheck, + }, (res, checkboxChecked) => { + dispatch('updateSettings', {disableAi2htmlStartupCheck: !checkboxChecked}) + + if ( res === 0 ) return; + + let prom + if (!installPath) { + prom = guessAppPath() + .then(findScriptsPath) + .catch(() => chooseAppPath(parentWin).then(findScriptsPath)) + .then(copyScript) + } else { + prom = copyScript(installPath) + } + + prom.then( + (path) => { + alert({parentWin, message: 'The ai2html script has been installed.'}) + state.installedAi2htmlHash = newHash + dispatch('updateSettings', {scriptInstallPath: path}) + }, + (err) => { + if ( err.code && err.code == 'EACCES' ) { + error({ + parentWin, + 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)}`, + details: err.toString() + }) } else { - prom = copyScript(path.dirname(installPath)) + console.error('install script failed', err) + error({parentWin, message: 'The ai2html script install failed.', details: err.toString()}) } - - prom.then( - (path) => { - alert({parentWin, message: 'The ai2html script has been installed.'}) - if (!installed) dispatch('set', {key: 'scriptInstallPath', val: path}) - }, - (err) => { - if ( err.code && err.code == 'EACCES' ) { - error({parentWin, 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)}`, details: err.toString()}) - } else { - console.error('install script failed', err) - error({parentWin, message: 'The ai2html script install failed.', details: err.toString()}) - } - } - ) - }) - - }) + } + ) + }) } export function checkOnLaunch() { + // Calculate and stash these hashes at launch + state.installedAi2htmlHash = calcInstalledHash() + state.newAi2htmlHash = calcNewHash() + if ( state.data.Settings.disableAi2htmlStartupCheck === true ) return; - install() + install({parentWin: state.mainWindow}) } diff --git a/src/main/ipc.js b/src/main/ipc.js index 81d08ec..c9303e8 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -2,7 +2,8 @@ import { ipcMain, BrowserWindow } from 'electron' import ProjectContextMenu from './menus/ProjectContextMenu' import state from './index' import storage from './storage' -import { newProject, addProjects, deployProject, editSettings, installAi2html, openInIllustrator } from './actions' +import { newProject, addProjects, deployProject, editSettings, installAi2html, openInIllustrator, importSettings, resetSettings } from './actions' +import { calcInstalledHash, calcNewHash } from './install_ai_plugin' // Sync messages ipcMain.on( 'get-state', (eve) => { @@ -13,6 +14,13 @@ ipcMain.on( 'has-focus', (eve) => { eve.returnValue = eve.sender.isFocused() } ) +ipcMain.on( 'get-hashes', (eve) => { + eve.returnValue = { + installedHash: state.installedAi2htmlHash, + newHash: state.newAi2htmlHash + } +} ) + // Async messages ipcMain.on( 'project-context-menu', (event, arg) => { @@ -27,8 +35,13 @@ ipcMain.on( 'store-mutate', (eve, arg) => { return console.error('State is missing in store-mutate ipc', arg.mutation, arg.state) // Parse and cache current state + const oldData = state.data state.data = JSON.parse( arg.state ) + // Recalculate the ai2html script hash if necessary + if ( state.data.Settings.ai2htmlFonts != oldData.Settings.ai2htmlFonts ) + state.newAi2htmlHash = calcNewHash() + // Make sure other windows have same state const srcWin = BrowserWindow.fromWebContents(eve.sender) BrowserWindow.getAllWindows().forEach((win) => { @@ -75,6 +88,19 @@ ipcMain.on( 'install-ai2html', (eve, arg) => { installAi2html() } ) +ipcMain.on( 'import-settings', (eve, arg) => { + if ( arg.from == 'settings-window' ) + importSettings(state.settingsWindow) + else + importSettings() +} ) + +ipcMain.on( 'reset-settings', (eve, arg) => { + if ( arg.from == 'settings-window' ) + resetSettings(state.settingsWindow) + else + resetSettings() +} ) // Senders export function dispatch(action, payload) { diff --git a/src/main/menus/Menubar.js b/src/main/menus/Menubar.js index bafe43a..c237b06 100644 --- a/src/main/menus/Menubar.js +++ b/src/main/menus/Menubar.js @@ -1,5 +1,5 @@ import {app, Menu, shell} from 'electron' -import { newProject, openProject, editSettings, installAi2html, clearState } from '../actions' +import { newProject, openProject, editSettings, installAi2html, clearState, importSettings } from '../actions' import state from '../index' import storage from '../storage' @@ -10,6 +10,7 @@ const MACOSX_MENUBAR_TEMPLATE = [ {role: 'about'}, {type: 'separator'}, {label: 'Preferences', click(eve) { editSettings() }}, + {label: 'Import preferences', click(eve) { importSettings() }}, {label: 'Install ai2html', click(eve) { installAi2html() }}, {type: 'separator'}, {role: 'services', submenu: []}, diff --git a/src/renderer/App.vue b/src/renderer/App.vue index 417b94a..5eafe62 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -59,6 +59,7 @@ body { font: caption; overflow:hidden; + user-select:none; } /* CSS */ diff --git a/src/renderer/Settings.vue b/src/renderer/Settings.vue index de2547d..ac86df3 100644 --- a/src/renderer/Settings.vue +++ b/src/renderer/Settings.vue @@ -8,14 +8,13 @@ diff --git a/src/renderer/store/modules/Settings.js b/src/renderer/store/modules/Settings.js index 0a50694..c89365a 100644 --- a/src/renderer/store/modules/Settings.js +++ b/src/renderer/store/modules/Settings.js @@ -11,20 +11,38 @@ const state = { // awsRegion: null, // awsAccessKeyId: null, // awsSecretAccessKey: null, + // siteConfigName: null, // extraPreviewCss: null, // extraEmbedCss: null, + // ai2htmlFonts: null, } const mutations = { - SETTINGS_SET ( state, { key, val } ) { - if ( key in state ) state[key] = val - else Vue.set(state, key, val) + SETTINGS_SET ( state, newSettings ) { + for (const key in newSettings) { + if ( key in state ) state[key] = newSettings[key] + else Vue.set(state, key, newSettings[key]) + } + }, + SETTINGS_RESET ( state, defaults ) { + for ( const k in state ) { + if ( k in defaults ) state[k] = defaults[k] + else state[k] = null + } }, } const actions = { set ({commit}, { key, val }) { - commit('SETTINGS_SET', {key, val}) + const args = {} + args[key] = val + commit('SETTINGS_SET', args) + }, + updateSettings ({commit}, newSettings) { + commit('SETTINGS_SET', newSettings) + }, + resetSettings ({commit}, defaults) { + commit('SETTINGS_RESET', defaults) } } diff --git a/static/ai2html.js b/static/templates/ai2html.js.ejs similarity index 98% rename from static/ai2html.js rename to static/templates/ai2html.js.ejs index cde88ee..bf31206 100644 --- a/static/ai2html.js +++ b/static/templates/ai2html.js.ejs @@ -411,26 +411,8 @@ var fonts = [ {"aifont":"Georgia","family":"georgia,'times new roman',times,serif","weight":"","style":""}, {"aifont":"Georgia-Bold","family":"georgia,'times new roman',times,serif","weight":"bold","style":""}, {"aifont":"Georgia-Italic","family":"georgia,'times new roman',times,serif","weight":"","style":"italic"}, -{"aifont":"Georgia-BoldItalic","family":"georgia,'times new roman',times,serif","weight":"bold","style":"italic"}, - -{"aifont":"Balto-Medium","family":"'Balto', helvetica, sans-serif","weight":"400","style":""}, -{"aifont":"Balto-MediumItalic","family":"'Balto', helvetica, sans-serif","weight":"400","style":"italic"}, -{"aifont":"Balto-Bold","family":"'Balto', helvetica, sans-serif","weight":"700","style":""}, -{"aifont":"Balto-BoldItalic","family":"'Balto', helvetica, sans-serif","weight":"700","style":"italic"}, -{"aifont":"Balto-Book","family":"'Balto', helvetica, sans-serif","weight":"400","style":""}, -{"aifont":"Balto-BookItalic","family":"'Balto', helvetica, sans-serif","weight":"400","style":"italic"}, -{"aifont":"Balto-Black","family":"'Balto', helvetica, sans-serif","weight":"900","style":""}, - -{"aifont":"Harriet-Bold","family":"'Harriet', helvetica, sans-serif","weight":"700","style":""}, -{"aifont":"Harriet-Book","family":"'Harriet', helvetica, sans-serif","weight":"400","style":""}, -{"aifont":"Harriet-BookItalic","family":"'Harriet', helvetica, sans-serif","weight":"400","style":"italic"}, - -{"aifont":"HarrietDisplay-Bold","family":"'Harriet Display', helvetica, sans-serif","weight":"700","style":""}, -{"aifont":"HarrietDisplay-BoldItalic","family":"'Harriet Display', helvetica, sans-serif","weight":"700","style":"italic"}, -{"aifont":"HarrietDisplay-Black","family":"'Harriet Display', helvetica, sans-serif","weight":"900","style":""}, - -{"aifont":"NittiGrotesk","family":"'Nitti',helvetica, sans-serif","weight":"400","style":""}, -{"aifont":"NittiGrotesk-Bold","family":"'Nitti-Bold','Nitti',helvetica, sans-serif","weight":"700","style":""} +{"aifont":"Georgia-BoldItalic","family":"georgia,'times new roman',times,serif","weight":"bold","style":"italic"}<%=settings.ai2htmlFonts ? ',' : ''%> +<%=settings.ai2htmlFonts%> ]; // CSS text-transform equivalents @@ -989,7 +971,7 @@ function testSimilarBounds(a, b, maxOffs) { function applyTemplate(template, replacements) { var keyExp = '([_a-zA-Z][\\w-]*)'; var mustachePattern = new RegExp("\\{\\{\\{? *" + keyExp + " *\\}\\}\\}?","g"); - var ejsPattern = new RegExp("<%=? *" + keyExp + " *%>","g"); + var ejsPattern = new RegExp("<"+"%=? *" + keyExp + " *%"+">","g"); var replace = function(match, name) { var lcname = name.toLowerCase(); if (name in replacements) return replacements[name]; diff --git a/static/templates/embed.html.ejs b/static/templates/embed.html.ejs index 18ef34a..e243a7f 100644 --- a/static/templates/embed.html.ejs +++ b/static/templates/embed.html.ejs @@ -12,120 +12,6 @@ <%=render('meta_tags.html.ejs', {slug, deploy_url, embed_meta, config, project}) %>