├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── app ├── AppBridge.js ├── AppCapturer.js ├── IconGenerator.js ├── ImageUtils.js ├── WebsiteManager.js ├── builder.js ├── index.js ├── is.js ├── preload.js ├── preload_embedded.js └── wrapper.js ├── assets ├── MacInstaller.PNG ├── atom.ico ├── electron.icns └── icon.png ├── package-lock.json ├── package.json ├── renderer ├── css │ └── font-awesome.min.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 └── index.html ├── src ├── Overview.vue ├── app.js ├── assets │ ├── asset-overview.vue │ ├── icon-generator.vue │ ├── screenshot-container.vue │ └── screenshot-tool.vue ├── installer-view.vue ├── launchfox-embedded.vue ├── mocks │ └── website.js ├── more-tools.vue ├── publish │ ├── publish-form-github.vue │ ├── publish-form-launchfox.vue │ ├── publish-form-s3.vue │ └── publish-overview.vue ├── storage.js ├── store.js ├── toolkit.js ├── website-builder-view.vue └── widgets │ ├── fx-toast.vue │ ├── loading-button.vue │ ├── server-error.vue │ └── sticky-footer.vue └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | renderer/toolkit.js 61 | 62 | /dev.config.js 63 | /config.json 64 | 65 | publish-checklist.txt 66 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | /dev.config.js 62 | 63 | /src 64 | 65 | .babelrc 66 | webpack.config.js 67 | config.json 68 | 69 | publish-checklist.txt 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 1. License for electron-toolkit 2 | 3 | MIT License 4 | Copyright (c) 2017 Philipp Langhans & Alina Sinelnikova / Hytag, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | 2. License for website template (inside Website Builder) 25 | 26 | Copyright (c) 2017 Hytag, Inc. 27 | 28 | The License grants you, free of charge, an ongoing, non-exclusive, worldwide license to use, copy, modify, manipulate the template to create a website for your commercial or non-commercial end product. You are not allowed to sublicense, and/or sell the template. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Toolkit 2 | 3 | "Command Line Gui Tools" to make launching Electron apps easier, faster and fun 4 | 5 | - __GUI for [electron-builder](https://github.com/electron-userland/electron-builder)__ - package and build your app 6 | - __App Icon Generator__ - generate app icons for all platforms 7 | + ```.ico ``` for Windows 8 | + ``` .icns ``` for macOS 9 | + ``` .png ``` for Linux 10 | - __Screen Capturer__ - create assets for mockups, store listings, online marketing... 11 | + Take screenshots 12 | + Record videos 13 | - __Website Builder__ - go public and launch a website for your app 14 | + Lean template for desktop apps 15 | + Responsive design 16 | + Custom styling options 17 | 18 | 19 | 20 | 21 | 22 | ## Screenshots 23 | 24 | 25 | 26 | Overview | 27 | :-------------------------:| 28 |  | 29 | 30 | 31 | 32 | Electron Builder GUI | Icon Generator 33 | :-------------------------:|:-------------------------: 34 |  |  35 | 36 | 37 | 38 | Screen Capturer | Website Builder 39 | :-------------------------:|:-------------------------: 40 |  |  | 41 | 42 | 43 | 44 | 45 | 46 | ## Getting Started 47 | 48 | 1. Install electron-toolkit inside your Electron app directory 49 | ``` 50 | npm install electron-toolkit --save-dev 51 | ``` 52 | 2. Add the electron-toolkit script to your package.json file 53 | ``` 54 | { 55 | ... 56 | "scripts": { 57 | "electron-toolkit": "electron ./node_modules/electron-toolkit" 58 | } 59 | } 60 | ``` 61 | 3. Make sure Electron and Electron Builder are installed. 62 | 63 | ``` 64 | npm install electron --save-dev 65 | npm install electron-builder --save-dev 66 | ``` 67 | 68 | 4. Now you can run electron-toolkit directly from your project directory 69 | ``` 70 | npm run electron-toolkit 71 | ``` 72 | 73 | 74 | 75 | 76 | ## Supported Platforms 77 | - Windows (32/64 bit) 78 | - macOS 79 | - Linux 80 | 81 | 82 | 83 | 84 | ## Usage 85 | 86 | See Wiki 87 | 88 | 89 | 90 | ## Security Checklist 91 | 92 | - [x] Only display secure (https) content: 93 | 94 | `` 95 | - [x] Disable the Node integration in all renderers that display remote content (setting nodeIntegration to false in webPreferences) 96 | - [x] Enable context isolation in all renderers that display remote content (setting contextIsolation to true in webPreferences) 97 | - [ ] Use ses.setPermissionRequestHandler() in all sessions that load remote content 98 | - [x] Do not disable webSecurity. Disabling it will disable the same-origin policy. 99 | - [ ] Define a Content-Security-Policy , and use restrictive rules (i.e. script-src 'self') 100 | - [ ] Override and disable eval , which allows strings to be executed as code. 101 | - [x] Do not set allowRunningInsecureContent to true. 102 | - [x] Do not enable experimentalFeatures or experimentalCanvasFeatures unless you know what you're doing. 103 | - [x] Do not use blinkFeatures unless you know what you're doing. 104 | - [x] WebViews: Do not add the nodeintegration attribute. 105 | - [x] WebViews: Do not use disablewebsecurity 106 | - [x] WebViews: Do not use allowpopups 107 | - [x] WebViews: Do not use insertCSS or executeJavaScript with remote CSS/JS. 108 | - [x] WebViews: Verify the options and params of all `` tags before they get attached using the will-attach-webview event 109 | ```javascript 110 | app.on('web-contents-created', (event, contents) => { 111 | contents.on('will-attach-webview', (event, webPreferences, params) => { 112 | // Strip away preload scripts if unused or verify their location is legitimate 113 | delete webPreferences.preload 114 | delete webPreferences.preloadURL 115 | 116 | // Disable node integration 117 | webPreferences.nodeIntegration = false 118 | 119 | // Verify URL being loaded 120 | if (!params.src.startsWith('https://yourapp.com/')) { 121 | event.preventDefault() 122 | } 123 | }) 124 | }) 125 | ``` 126 | 127 | 128 | 129 | ## Want to contribute? 130 | 131 | We welcome and encourage all sorts of contributions that help us make this project more awesome. 132 | Contact me philipplgh@gmail.com. 133 | 134 | 135 | 136 | ## License 137 | 138 | 1. License for electron-toolkit 139 | 140 | MIT License 141 | 142 | Copyright (c) 2017 Philipp Langhans & Alina Sinelnikova / Hytag,inc 143 | 144 | 145 | 146 | 2. License for website template (inside Website Builder) 147 | 148 | Copyright (c) 2017 Hytag, inc 149 | 150 | The License grants you, free of charge, an ongoing, non-exclusive, worldwide license to use, copy, modify, manipulate the template to create a website for your commercial or non-commercial end product. 151 | You are not allowed to sublicense, and/or sell the template. 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /app/AppBridge.js: -------------------------------------------------------------------------------- 1 | const { spawn, exec } = require("child_process"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const EventEmitter = require("events"); 5 | 6 | const { ipcMain } = require("electron"); 7 | 8 | const is = require("./is.js"); 9 | var devConfig = {}; 10 | try { devConfig = require('../dev.config.js') } catch (error) {} 11 | const PROJECT_DIR = is.dev ? devConfig.PROJECT_DIR : path.join(__dirname, '..', '..', '..'); 12 | 13 | class AppBridge { 14 | constructor() { 15 | 16 | ipcMain.on("spawn-builder", (event, arg) => { 17 | 18 | this.tryLaunch("node", "builder.js", true) 19 | /* 20 | .then(child => { 21 | return this.setupIpc(child); 22 | }) 23 | */ 24 | .then(child => { 25 | console.log("builder is ready"); 26 | 27 | var namespace = "builder_"; 28 | 29 | //TODO recieve this list from child 30 | var eventTypes = ["done", "error", "debug"]; 31 | 32 | const debugCallback = data => { 33 | event.sender.send(namespace + "debug", data.toString()); 34 | }; 35 | child.stdout.on("data", debugCallback) 36 | 37 | const sendCallback = (event, message) => { 38 | console.log("send to builder: ", message); 39 | child.send(message); 40 | }; 41 | ipcMain.on(namespace + "send", sendCallback); 42 | 43 | const unsubscribeAndKill = () => { 44 | child.stdin.pause(); 45 | child.stdout.pause(); 46 | child.stdout.removeListener("data", debugCallback); 47 | ipcMain.removeListener(namespace + "send", sendCallback); 48 | child.removeAllListeners(); 49 | child.kill("SIGKILL"); 50 | } 51 | 52 | eventTypes.forEach(ev => { 53 | child.on("message", message => { 54 | console.log("forward message", message); 55 | event.sender.send(namespace + message.type, message.data); 56 | 57 | if (ev == "done") { 58 | unsubscribeAndKill(); 59 | } 60 | }) 61 | 62 | //ipc syntax 63 | /* 64 | child.on(ev, data => { 65 | console.log("forward builder event", ev); 66 | event.sender.send(namespace + ev, data); 67 | }); 68 | */ 69 | }); 70 | 71 | event.sender.send("spawn-builder-complete", namespace, eventTypes); 72 | 73 | }) 74 | .catch(err => { 75 | console.log("error in builder", err); 76 | }); 77 | 78 | 79 | }); 80 | 81 | ipcMain.on("spawn-wrapper", (event, arg) => { 82 | 83 | /** 84 | * launch child process, 85 | * then create convenience emitter interface (legacy) 86 | * subscribe on emitter, representing child process 87 | * forwrad event through ipc in their own namespace 88 | * listen on backchannel 89 | */ 90 | this.launchWrapper().then(child => { 91 | 92 | //TODO recieve this list from child 93 | var eventTypes = ["title-available", "closed", "blur", "focus", "show", "hide", "maximize", "unmaximize", "minimize", "resize"]; 94 | var namespace = "c1_"; 95 | 96 | eventTypes.forEach(ev => { 97 | child.on(ev, (data) => { 98 | //forward to renderer 99 | //console.log("forward child event to renderer", ev, data) 100 | event.sender.send((namespace + ev), data); 101 | }) 102 | }) 103 | 104 | ipcMain.on(namespace + "send", (event, data) => { 105 | child.send(data) 106 | }) 107 | 108 | event.sender.send("spawn-complete", namespace, eventTypes); 109 | }); 110 | }); 111 | 112 | } 113 | tryLaunch(appPath, scriptPath, isDetached) { 114 | var self = this; 115 | return new Promise((resolve, reject) => { 116 | 117 | var child = spawn(appPath, [path.join(__dirname, scriptPath), PROJECT_DIR], { 118 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'], 119 | //stdio: ["pipe", "pipe", "pipe", "pipe"], 120 | //stdio: ["pipe", "pipe", "pipe"], 121 | // if you pass some value, even an empty object, 122 | // the electron process will always exit immediately on linux, works fine in OSX 123 | // env: {} 124 | // env: { 'DISPLAY': process.env.DISPLAY } 125 | cwd: self.projectDir, 126 | detached: false 127 | }) 128 | .on("error", error => { 129 | reject(error); 130 | }); 131 | 132 | child.unref() 133 | 134 | 135 | var init = function(data) { 136 | try { 137 | var message = data.toString().trim(); 138 | if (message === "~~~") { 139 | setTimeout(() => { child.stdout.removeListener("data", init); }, 500); 140 | resolve(child); 141 | } 142 | if (message === "!~~~") { 143 | reject(new Error("ipc could not be established")) 144 | } 145 | } catch (error) { 146 | console.error("error: ", error); 147 | } 148 | }; 149 | 150 | child.stdout.on("data", init); 151 | 152 | /* 153 | child.stdout.on("data", (data) => { 154 | console.log("debug child: ", data.toString()) 155 | }); 156 | */ 157 | 158 | 159 | }); 160 | } 161 | 162 | launchWrapper() { 163 | //https://github.com/electron/electron/issues/6656#issuecomment-236465350 164 | /* 165 | Also on macOS there two types of process, if you run the script in the main process, 166 | a new instance of your app will be started, if you run it in the renderer process, 167 | the script will be run in background. It is unclear which way would be expected. 168 | let paths = [ 169 | `${process.execPath}`, 170 | "electron", 171 | require.resolve( 172 | "electron", { 173 | paths: [this.projectDir] 174 | } 175 | )]; 176 | */ 177 | let appPath = process.execPath; 178 | 179 | return this.tryLaunch(appPath, "wrapper.js") 180 | .then(child => { 181 | return this.setupIpc(child); 182 | //return this.setupIpcFallback(child); 183 | }) 184 | .catch(error => { 185 | console.error("app could not be started", error); 186 | return; 187 | }); 188 | } 189 | 190 | setupIpc(child) { 191 | var emitter = new EventEmitter(); 192 | 193 | child 194 | .on("message", message => { 195 | emitter.emit(message.type, message.data); 196 | }) 197 | .on("disconnect", () => { 198 | //child.kill(); 199 | }); 200 | 201 | 202 | child.stdout.on("data", function(data) { 203 | console.log("child console: ", data.toString()) 204 | }) 205 | 206 | 207 | emitter.send = (type, data) => { 208 | //console.log("send to child", type, data); 209 | child.send({ 210 | type: type, 211 | data: data 212 | }); 213 | }; 214 | 215 | //provide convenience function to maximize user app 216 | emitter.maximize = () => { 217 | emitter.send("request-maximize"); 218 | }; 219 | 220 | return emitter; 221 | } 222 | 223 | // some configurations (os, webpack, node, electron) seem to have 224 | //issues with IPC and opening additional file descriptors 225 | //the problem occured on mac when child process was created in renderer 226 | //now, that this functionality is moved to the main process, ipc seems to be working fine and thi smethod is not needed 227 | //this method simulates regular ipc using stdout and stdin 228 | //we use emitter as facade to pass it to the renderer without leaking child 229 | //processes 230 | setupIpcFallback(child) { 231 | var emitter = new EventEmitter(); 232 | 233 | /*for debugging: 234 | child.stdout.on('data', function(data) { 235 | console.log('stdout: ' + data); 236 | }); 237 | */ 238 | 239 | //parse incoming message and emit as "regular" events 240 | child.stdout.on("data", function(data) { 241 | var message; 242 | try { 243 | message = data.toString("utf-8").trim(); 244 | 245 | //if child process sends event faster than we read we get 246 | // "{"type":"blur","data":null}{"type":"focus","data":null}" 247 | //which is not valid JSON so we transofmr it to 248 | //{"type":"blur","data":null}#{"type":"focus","data":null} 249 | //and handle them separately. 250 | //{"type":"blur","data":null}# becomes ["{"type":"blur","data":null}", ""] 251 | //we ignore empty strings 252 | var messages = message.split("#"); 253 | 254 | messages.forEach(msg => { 255 | if (!msg) { return; } 256 | 257 | //console.log("received message", msg) 258 | var msg = JSON.parse(msg); 259 | 260 | if (msg.type) { 261 | var mtype = msg.type.trim(); 262 | //console.log("emit: ", mtype, msg.data); 263 | emitter.emit(mtype, msg.data); 264 | } else { 265 | throw new Error("malformed message", m); 266 | } 267 | }); 268 | 269 | } catch (error) { 270 | //console.log("error", error, message); 271 | } 272 | }) 273 | 274 | //provide backchannel 275 | emitter.send = (type, data) => { 276 | //console.log("send to child", type, data); 277 | child.stdin.write(JSON.stringify({ 278 | type: type, 279 | data: data 280 | })); 281 | }; 282 | 283 | //provide convenience function to maximize user app 284 | emitter.maximize = () => { 285 | emitter.send("request-maximize"); 286 | }; 287 | 288 | return emitter; 289 | } 290 | } 291 | 292 | module.exports = AppBridge; -------------------------------------------------------------------------------- /app/AppCapturer.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const { desktopCapturer, screen, ipcRenderer } = require("electron"); 7 | 8 | class AppCapturer { 9 | constructor(projectDir) { 10 | this.recorder; 11 | this.recordedChunks = []; 12 | this.numRecordedChunks = 0; 13 | this.localStream; 14 | this.project_dir = projectDir; 15 | 16 | this.buildDir = path.join(projectDir, "build"); 17 | this.assetDir = path.join(this.buildDir, "assets"); 18 | } 19 | 20 | generateImageFilename() { 21 | var timestamp = new Date().getTime(); 22 | var name = "screenshot_" + timestamp + ".png"; 23 | return name; 24 | } 25 | 26 | generateVideoFilename() { 27 | var timestamp = new Date().getTime(); 28 | var name = "video_" + timestamp + ".webm"; 29 | return name; 30 | } 31 | 32 | launch() { 33 | return new Promise((resolve, reject) => { 34 | ipcRenderer.send("spawn-wrapper", "now"); 35 | 36 | ipcRenderer.removeAllListeners("spawn-complete"); 37 | ipcRenderer.once("spawn-complete", (event, namespace, events) => { 38 | var child = new EventEmitter() 39 | events.forEach(ev => { 40 | var channel = namespace + ev; 41 | ipcRenderer.on(channel, (evt, data) => { 42 | child.emit(ev, data); 43 | }); 44 | }) 45 | child.send = function(msg) { 46 | ipcRenderer.send(namespace + "send", msg) 47 | } 48 | child.maximize = function() { 49 | child.send("request-maximize") 50 | } 51 | resolve(child); 52 | }); 53 | }); 54 | } 55 | 56 | launchApp() { 57 | return this.launch().then(child => { 58 | this.childProcess = child; 59 | 60 | //find child window and extend protocol events with : 61 | //'ready' => ready to record & 62 | //'window-detection-failed' 63 | child.on("title-available", originalWindowTitle => { 64 | //console.log("received title available", originalWindowTitle); 65 | var detectionTitle = originalWindowTitle + "~"; 66 | 67 | this.getWindowByTitle(detectionTitle) 68 | .then(window => { 69 | var windowId = window.id; 70 | 71 | child.windowId = windowId; 72 | 73 | //tell child to reset window title 74 | child.send("detection-complete"); 75 | 76 | //notify app that we found the application window 77 | //and are ready to record => will reste the window title 78 | child.emit("ready", windowId); 79 | }) 80 | .catch(err => { 81 | console.log("error", err); 82 | child.emit("window-detection-failed"); 83 | }); 84 | }); 85 | 86 | return child; 87 | }); 88 | } 89 | 90 | getWindowByTitle(windowTitle, opts) { 91 | return new Promise((resolve, reject) => { 92 | let options = Object.assign({ types: ["window"] }, opts); 93 | desktopCapturer.getSources(options, (error, sources) => { 94 | if (error) { 95 | return reject(error); 96 | } 97 | for (let i = 0; i < sources.length; ++i) { 98 | //console.log("check: ", sources[i].name, " - ", windowTitle); 99 | if (sources[i].name === windowTitle) { 100 | return resolve(sources[i]); 101 | } 102 | } 103 | reject(new Error("window not found")); 104 | }); 105 | }); 106 | } 107 | 108 | //known issues: 109 | //no windows aero effects: https://github.com/electron/electron/blob/8359c72347372176e3cb27735caa19d967dae762/atom/browser/api/atom_api_desktop_capturer.cc#L60 110 | getWindowThumb(windowTitle, thumbSize) { 111 | thumbSize = thumbSize || this.determineScreenShotSize(); 112 | let options = { types: ["window"], thumbnailSize: thumbSize }; 113 | return this.getWindowByTitle(windowTitle, options) 114 | .then(window => window.thumbnail); 115 | } 116 | 117 | determineScreenShotSize() { 118 | const screenSize = screen.getPrimaryDisplay().workAreaSize; 119 | const maxDimension = Math.max(screenSize.width, screenSize.height); 120 | return { 121 | width: maxDimension * window.devicePixelRatio, 122 | height: maxDimension * window.devicePixelRatio 123 | }; 124 | } 125 | 126 | 127 | prepareAssetDir() { 128 | 129 | return new Promise((resolve, reject) => { 130 | this.prepareDirectory(this.buildDir) 131 | .then(data => { 132 | return this.prepareDirectory(this.assetDir) 133 | }) 134 | .then(data => { 135 | resolve(data); 136 | }) 137 | }); 138 | 139 | } 140 | 141 | prepareDirectory(dir) { 142 | return new Promise((resolve, reject) => { 143 | fs.mkdir(dir, err => { 144 | if (err) { 145 | if (err.code == "EEXIST") { 146 | return resolve(dir); 147 | } 148 | reject(err); 149 | } else { 150 | resolve(dir); 151 | } 152 | }); 153 | }); 154 | } 155 | 156 | takeScreenshotFromVideo(videoElement) { 157 | return new Promise((resolve, reject) => { 158 | 159 | /* 160 | if (!localStream) { 161 | resolve(null); 162 | } 163 | */ 164 | 165 | var canvas = document.createElement("canvas"); 166 | canvas.width = videoElement.videoWidth; 167 | canvas.height = videoElement.videoHeight; 168 | canvas.getContext("2d").drawImage(videoElement, 0, 0); 169 | var thumb = canvas.toDataURL("image/png"); 170 | 171 | //TODO ImageUtils.saveDataUri(fullPath, thumb) 172 | 173 | this.prepareAssetDir() 174 | .then(dir => { 175 | var scName = this.generateImageFilename(); 176 | var fullPath = path.join(dir, scName); 177 | 178 | var asset = { 179 | id: scName, 180 | filename: scName, 181 | data: thumb, 182 | state: "ready", 183 | role: scName.startsWith("main") ? "main" : "feature", 184 | type: "image", 185 | path: fullPath 186 | }; 187 | 188 | var base64Data = thumb.replace(/^data:image\/png;base64,/, ""); 189 | var binaryData = new Buffer(base64Data, "base64").toString("binary"); 190 | 191 | fs.writeFile(fullPath, binaryData, "binary", function(err) { 192 | if (err) return console.log(err); 193 | // console.log("screenshot saved"); 194 | 195 | resolve(asset); 196 | }); 197 | }) 198 | .catch(err => { 199 | reject(err); 200 | }); 201 | }); 202 | } 203 | 204 | initScreenCapture(windowId) { 205 | if (this.localStream) { 206 | this.closeStream(); 207 | } 208 | 209 | return new Promise((resolve, reject) => { 210 | if (!windowId) { 211 | return reject(new Error("no window id")); 212 | } 213 | 214 | navigator.webkitGetUserMedia({ 215 | audio: false, 216 | video: { 217 | mandatory: { 218 | chromeMediaSource: "desktop", 219 | chromeMediaSourceId: windowId, 220 | maxWidth: window.screen.width - 10, 221 | maxHeight: window.screen.height - 10 222 | } 223 | } 224 | }, 225 | stream => { 226 | this.localStream = stream; 227 | // console.log("stream created", stream.getTracks().length); 228 | resolve(URL.createObjectURL(stream)); 229 | }, 230 | err => { 231 | // console.log("The following error occurred: " + err.name); 232 | reject(err); 233 | } 234 | ); 235 | }); 236 | } 237 | 238 | closeStream() { 239 | // console.log("closing stream .."); 240 | if (!this.localStream) { 241 | return; 242 | } 243 | let tracks = this.localStream.getTracks(); 244 | tracks.forEach(function(track) { 245 | track.stop(); 246 | }); 247 | this.localStream = null; 248 | } 249 | 250 | startRecording() { 251 | 252 | if (!this.localStream) { 253 | //throw new Error("no stream available") 254 | // console.error("no stream available"); 255 | return; 256 | } 257 | 258 | try { 259 | // console.log("Start recording the stream."); 260 | this.recorder = new MediaRecorder(this.localStream); 261 | } catch (e) { 262 | console.assert(false, "Exception while creating MediaRecorder: " + e); 263 | return; 264 | } 265 | 266 | var state = new EventEmitter(); 267 | 268 | this.recorder.ondataavailable = event => { 269 | // console.log("data available fired"); 270 | if (event.data && event.data.size > 0) { 271 | this.recordedChunks.push(event.data); 272 | this.numRecordedChunks += event.data.byteLength; 273 | } 274 | 275 | if (this.recordedChunks.length == 0) { 276 | return; 277 | } 278 | 279 | let blob = new Blob(this.recordedChunks, { 280 | type: "video/webm" 281 | }); 282 | var url = URL.createObjectURL(blob); 283 | 284 | this.prepareAssetDir() 285 | .then(dir => { 286 | 287 | var vName = this.generateVideoFilename(); 288 | var p = path.join(dir, vName); 289 | 290 | var asset = { 291 | id: vName, 292 | filename: vName, 293 | data: url, 294 | role: vName.startsWith("main") ? "main" : "feature", 295 | type: "video", 296 | path: p 297 | }; 298 | 299 | state.emit("new-video", asset); 300 | 301 | this.saveVideo(vName, p); 302 | }) 303 | .catch(err => { 304 | state.emit("recoder-error", err); 305 | }); 306 | }; 307 | 308 | this.recorder.onstop = () => { 309 | // console.log("recorderOnStop fired"); 310 | }; 311 | 312 | this.recorder.start(); 313 | 314 | return state; 315 | } 316 | 317 | stopRecording() { 318 | if (!this.recorder || this.recorder.state == "inactive") { 319 | return; 320 | } 321 | this.recorder.stop(); 322 | } 323 | 324 | saveVideo(name, p) { 325 | var self = this; 326 | let blob = new Blob(this.recordedChunks, { type: "video/webm" }); 327 | 328 | var reader = new FileReader(); 329 | reader.onload = function() { 330 | var buffer = new Buffer(reader.result); 331 | 332 | fs.writeFile(p, buffer, {}, (err, res) => { 333 | self.resetVideoRecorder(); 334 | if (err) { 335 | console.error(err); 336 | return; 337 | } 338 | 339 | }); 340 | }; 341 | 342 | reader.readAsArrayBuffer(blob); 343 | } 344 | 345 | resetVideoRecorder() { 346 | // this.localStream = null; 347 | this.recordedChunks = []; 348 | this.numRecordedChunks = 0; 349 | } 350 | } 351 | 352 | module.exports = AppCapturer; -------------------------------------------------------------------------------- /app/IconGenerator.js: -------------------------------------------------------------------------------- 1 | const png2icons = require("png2icons"); 2 | const ImageUtils = require("./ImageUtils.js") 3 | const path = require("path") 4 | const fs = require("fs") 5 | 6 | class IconGenerator { 7 | 8 | constructor(projectDir) { 9 | 10 | this.buildDir = path.join(projectDir, "build"); 11 | this.pngDir = path.join(this.buildDir, "png"); 12 | 13 | } 14 | 15 | prepareIconDirs() { 16 | 17 | return new Promise((resolve, reject) => { 18 | this.prepareDirectory(this.buildDir) 19 | .then(data => { 20 | return this.prepareDirectory(this.pngDir) 21 | }) 22 | .then(data => { 23 | resolve(); 24 | 25 | }) 26 | }); 27 | 28 | } 29 | 30 | prepareDirectory(dir) { 31 | return new Promise((resolve, reject) => { 32 | fs.mkdir(dir, err => { 33 | if (err) { 34 | if (err.code == "EEXIST") { 35 | return resolve(dir); 36 | } 37 | reject(err); 38 | } else { 39 | resolve(dir); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | readFile(p) { 46 | return new Promise((resolve, reject) => { 47 | fs.readFile(p, function(error, data) { 48 | if (error) { 49 | return reject(error); 50 | } 51 | resolve(data); 52 | }); 53 | }); 54 | } 55 | 56 | writeFile(p, data) { 57 | return new Promise((resolve, reject) => { 58 | fs.writeFile(p, data, err => { 59 | if (err) { 60 | return reject(err); 61 | } 62 | resolve(p); 63 | }); 64 | }); 65 | } 66 | 67 | //create dir if not exists and return path 68 | getWritableDir() { 69 | return new Promise((resolve, reject) => {}); 70 | } 71 | 72 | getExtension(p) { 73 | return path.extname(p).toLowerCase(); 74 | } 75 | 76 | generatePng(imagePath) { 77 | return this.prepareIconDirs() 78 | .then(() => { 79 | if (ImageUtils.isPng(imagePath)) { 80 | return Promise.resolve(imagePath); 81 | } 82 | if (!ImageUtils.isSvg(imagePath)) { 83 | throw new Error("svg or png input required"); 84 | } 85 | 86 | const outPath = path.join(this.buildDir, "512x512.png"); 87 | 88 | return ImageUtils.svg2png(imagePath, { width: 512, height: 512 }) 89 | .then(buffer => this.writeFile(outPath, buffer)) 90 | .then(filePath => { 91 | 92 | // console.log("RETURNING OUT", outPath); 93 | return outPath; 94 | }) 95 | }) 96 | } 97 | 98 | generatePngBundle(pngPath) { 99 | if (!ImageUtils.isPng(pngPath)) { 100 | return Promise.reject(new Error("png input required")); 101 | } 102 | 103 | // console.log("png path", this); 104 | var OUT_DIR = this.pngDir; 105 | 106 | var scales = [16, 24, 32, 48, 64, 96, 128, 256, 512]; 107 | 108 | var promises = scales.map(scale => { return ImageUtils.resizePng(pngPath, scale, OUT_DIR) }); 109 | 110 | return Promise.all(promises).then(data => { 111 | return { 112 | //in: pngPath, 113 | //use 512px generated png from bundle as input for .ico & .icns 114 | //for performance 115 | in: path.join(OUT_DIR, "512x512.png"), 116 | out: OUT_DIR 117 | }; 118 | }); 119 | } 120 | 121 | generateIcns(pngPath) { 122 | var icnsPath = path.join(this.buildDir, "icon.icns"); 123 | if (!ImageUtils.isPng(pngPath)) { 124 | return Promise.reject(new Error("png input required")); 125 | } 126 | 127 | return this.readFile(pngPath).then(data => { 128 | var output = png2icons.PNG2ICNS(data, png2icons.BILINEAR, false, 0); 129 | if (!output) { 130 | throw new Error("conversion to Icns failed"); 131 | } 132 | return this.writeFile(icnsPath, output).then(filePath => { 133 | return { in: pngPath, 134 | out: icnsPath 135 | }; 136 | }); 137 | }); 138 | } 139 | 140 | generateIco(pngPath) { 141 | var icoPath = path.join(this.buildDir, "icon.ico"); 142 | 143 | if (!ImageUtils.isPng(pngPath)) { 144 | return Promise.reject(new Error("png input required")); 145 | } 146 | 147 | return this.readFile(pngPath).then(data => { 148 | //var output = png2icons.PNG2ICO_PNG(data, png2icons.BEZIER, false, 0); 149 | // Microsoft ICO using BMP icons with bicubic interpolation. 150 | var output = png2icons.PNG2ICO_BMP(data, png2icons.BICUBIC, false); 151 | if (!output) { 152 | throw new Error("conversion to Ico failed"); 153 | } 154 | return this.writeFile(icoPath, output).then(filePath => { 155 | return { in: pngPath, 156 | out: icoPath 157 | }; 158 | }); 159 | }); 160 | } 161 | } 162 | 163 | module.exports = IconGenerator; -------------------------------------------------------------------------------- /app/ImageUtils.js: -------------------------------------------------------------------------------- 1 | const jimp = require("jimp"); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const { nativeImage } = require('electron'); 5 | 6 | class ImageUtils { 7 | static isSvg(imagePath) { 8 | return path.extname(imagePath).toLowerCase() == ".svg"; 9 | } 10 | 11 | static isPng(imagePath) { 12 | return path.extname(imagePath).toLowerCase() == ".png"; 13 | } 14 | 15 | static resizePng(imagePath, scale, dir) { 16 | //console.log("resize", imagePath, scale, dir); 17 | return new Promise((resolve, reject) => { 18 | jimp 19 | .read(imagePath) 20 | .then(function(img) { 21 | img 22 | .resize(scale, scale) 23 | .write(`${dir}/${scale}x${scale}.png`, err => { 24 | if (err) return reject(); 25 | resolve(); 26 | }); 27 | }) 28 | .catch(function(err) { 29 | console.log(err); 30 | reject(); 31 | }); 32 | }); 33 | } 34 | 35 | //FIXME support more formats / mime types and async loading 36 | static loadAsDataUri(pngUri) { 37 | var content = fs.readFileSync(pngUri); 38 | var dataUri = "data:image/png;base64," + content.toString("base64"); 39 | return dataUri; 40 | } 41 | 42 | static dataUriToBuf(pngUri) { 43 | //alternative without native image 44 | //var base64Data = pngUri.replace(/^data:image\/png;base64,/, ""); 45 | //var binaryData = new Buffer(base64Data, "base64").toString("binary"); 46 | //return binaryData; 47 | var pngNative = nativeImage.createFromDataURL(pngUri); 48 | return pngNative.toPng(); 49 | } 50 | 51 | static svg2png(svgPath) { 52 | return new Promise((resolve, reject) => { 53 | var canvas = document.createElement("canvas"); 54 | canvas.width = 1024; 55 | canvas.height = 1024; 56 | var ctx = canvas.getContext("2d"); 57 | var img = new Image(); 58 | img.onload = function() { 59 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 60 | var pngUri = canvas.toDataURL("image/png"); 61 | // console.log("data uri is ready", pngUri.length) 62 | var buf = ImageUtils.dataUriToBuf(pngUri); 63 | resolve(buf); //resolve png buffer 64 | }; 65 | img.onerror = e => { 66 | reject(e); 67 | }; 68 | 69 | fs.readFile(svgPath, (err, content) => { 70 | if (err) return reject(err); 71 | var dataUri = "data:image/svg+xml;base64," + content.toString("base64"); 72 | img.src = dataUri; 73 | }); 74 | }); 75 | } 76 | } 77 | 78 | module.exports = ImageUtils; -------------------------------------------------------------------------------- /app/WebsiteManager.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const { desktopCapturer, screen } = require("electron"); 7 | 8 | const AppBridge = require("./AppBridge.js"); 9 | 10 | 11 | class WebsiteManager { 12 | constructor() { 13 | 14 | // this.project_dir = projectDir; 15 | } 16 | /* 17 | 18 | prepareDirectory() { 19 | return new Promise((resolve, reject) => { 20 | const assetsDir = path.join(this.project_dir, "website"); 21 | 22 | fs.mkdir(assetsDir, err => { 23 | if (err) { 24 | if (err.code == "EEXIST") { 25 | return resolve(assetsDir); 26 | } 27 | reject(err); 28 | } else { 29 | resolve(assetsDir); 30 | } 31 | }); 32 | }); 33 | } 34 | */ 35 | 36 | 37 | loadWebsiteData(){ 38 | return new Promise((resolve, reject) => { 39 | this.prepareDirectory() 40 | .then(dir => { 41 | var fullPath = path.join(dir, "website.json"); 42 | 43 | fs.readFile(fullPath, 'utf8', function (err, data) { 44 | if (err) { 45 | console.log(err); 46 | return resolve(null); 47 | 48 | }else{ 49 | var website = JSON.parse(data); 50 | return resolve(website); 51 | } 52 | 53 | }); 54 | 55 | }) 56 | .catch(err => { 57 | reject(err); 58 | }); 59 | }); 60 | } 61 | 62 | saveWebsiteData(website){ 63 | return new Promise((resolve, reject) => { 64 | this.prepareDirectory() 65 | .then(dir => { 66 | var fullPath = path.join(dir, "website.json"); 67 | var json = JSON.stringify(website); 68 | fs.writeFile(fullPath, json, function (err, data) { 69 | if (err) { 70 | console.log(err); 71 | reject(err); 72 | }else{ 73 | resolve(); 74 | } 75 | 76 | }); 77 | 78 | }) 79 | .catch(err => { 80 | reject(err); 81 | }); 82 | }); 83 | 84 | 85 | 86 | } 87 | 88 | 89 | 90 | 91 | 92 | 93 | } 94 | 95 | module.exports = WebsiteManager; 96 | 97 | -------------------------------------------------------------------------------- /app/builder.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | 5 | 6 | function killProcess() { 7 | try { 8 | process.removeAllListeners(); 9 | } catch (error) { 10 | //fs.appendFileSync("log.txt", JSON.stringify(error) + "\n"); 11 | } 12 | 13 | try { 14 | process.exit(1); 15 | } catch (error) { 16 | //fs.appendFileSync("log.txt", JSON.stringify(error) + "\n"); 17 | } 18 | 19 | try { 20 | process.kill(process.pid); 21 | } catch (error) { 22 | //fs.appendFileSync("log.txt", JSON.stringify(error) + "\n"); 23 | } 24 | } 25 | 26 | //we have basically 4 options to create the build process 27 | //1: spawn process of cli.js in the same way npm scripts are doing it 28 | //2: use electron-builder's programmatic api: https://www.electron.build/api/electron-builder 29 | // a) with patched package.json 30 | // b) with separate electron-builder.json 31 | // c) with config object 32 | 33 | function build(config) { 34 | var builder; 35 | try { 36 | builder = require(path.join(config.projectDir, "node_modules", "electron-builder", "out", "index.js")); 37 | } catch (error) { 38 | process.send({ type: "error", data: "electron builder not found.try: npm install electron-builder --save-dev" }); 39 | setTimeout(() => { 40 | process.exit(); 41 | }, 500); 42 | return; 43 | } 44 | const Platform = builder.Platform; 45 | //const builder = require(path.join(PROJECT_DIR, "node_modules", "electron-builder", "out", "cli", "cli.js")); 46 | 47 | /** 48 | missing settings... 49 | https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder-lib/src/configuration.ts 50 | 51 | 52 | afterPack?, 53 | apk?, 54 | appImage?, 55 | appx?, 56 | artifactName?, 57 | asarUnpack?, 58 | beforeBuild?, 59 | buildDependenciesFromSource?, 60 | buildVersion?, 61 | compression?, 62 | , deb?, 63 | detectUpdateChannel?, 64 | , dmg?, 65 | electronCompile?, electronDist?, 66 | electronDownload?, , extends?, 67 | extraFiles?, extraMetadata?, extraResources?, 68 | fileAssociations?, files?, forceCodeSigning?, freebsd?, 69 | generateUpdatesFilesForAllChannels?, icon?, linux?, mac?, 70 | mas?, msi?, muonVersion?, nodeGypRebuild?, npmArgs?, 71 | npmRebuild?, npmSkipBuildFromSource?, nsis?, nsisWeb?, p5p?, 72 | pacman?, pkg?, portable?, 73 | , protocols?, 74 | publish?, releaseInfo?, rpm?, snap?, squirrelWindows?, target?, win? 75 | */ 76 | 77 | /** 78 | https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder-lib/src/packagerApi.ts 79 | export interface PackagerOptions { 80 | targets?: Map>> 81 | 82 | mac?: Array 83 | linux?: Array 84 | win?: Array 85 | 86 | projectDir?: string | null 87 | 88 | cscLink?: string | null 89 | cscKeyPassword?: string | null 90 | 91 | cscInstallerLink?: string | null 92 | cscInstallerKeyPassword?: string | null 93 | 94 | platformPackagerFactory?: ((info: Packager, platform: Platform) => PlatformPackager) | null 95 | 96 | readonly config?: Configuration | string | null 97 | 98 | readonly effectiveOptionComputed?: (options: any) => Promise 99 | 100 | readonly prepackaged?: string | null 101 | } 102 | */ 103 | 104 | /** 105 | build expects CliOptions: 106 | 107 | interface CliOptions extends PackagerOptions, PublishOptions 108 | arch?: string 109 | x64?: boolean 110 | ia32?: boolean 111 | armv7l?: boolean 112 | dir?: boolean 113 | platform?: string 114 | project?: string 115 | */ 116 | 117 | //Without target configuration, electron-builder builds Electron app for current platform and current arch using default target. 118 | //macOS - DMG and ZIP for Squirrel.Mac. 119 | //Windows - NSIS. 120 | //Linux - AppImage. 121 | //config.targets = Platform.WINDOWS.createTarget(); 122 | 123 | builder 124 | .build(config) 125 | .then(result => { 126 | // handle result 127 | try { 128 | process.send({ 129 | type: "done" 130 | }) 131 | } catch (error) { 132 | //fs.appendFileSync("log.txt", JSON.stringify(error) + "\n"); 133 | } 134 | 135 | killProcess() 136 | 137 | }) 138 | .catch(error => { 139 | // handle error 140 | console.error("error during build", error); 141 | process.send({ 142 | type: "error", 143 | error: process.pid + JSON.stringify(error) 144 | }) 145 | 146 | //fs.appendFileSync("log.txt", JSON.stringify(error) + "\n"); 147 | 148 | killProcess() 149 | 150 | }); 151 | } 152 | 153 | 154 | if (typeof process.send === "function") { 155 | console.log("~~~"); //signal 'ready' 156 | 157 | process.on("message", message => { 158 | console.log("builder received message: " + message.type); 159 | 160 | if (message.type === 'build') { 161 | var config = message.data; 162 | /* 163 | console.log = (text) => { 164 | process.send({ 165 | type: 'console', 166 | data: text 167 | }) 168 | } 169 | */ 170 | build(config) 171 | } 172 | 173 | 174 | }); 175 | } else { 176 | 177 | process.send = () => {} //provide no-op for 'ipc-less' testing 178 | process.on = () => {} //provide no-op for 'ipc-less' testing 179 | 180 | console.log("!~~~"); //signal 'failed' 181 | } 182 | 183 | 184 | /* 185 | var testConfig = { 186 | /... 187 | } 188 | 189 | 190 | build(testConfig); 191 | */ -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, nativeImage } = require('electron') 2 | const { webContents } = require("electron"); 3 | const path = require('path') 4 | const url = require('url') 5 | 6 | //this can help if media stream is not working: window is black 7 | //app.disableHardwareAcceleration(); 8 | 9 | const AppBridge = require("./AppBridge.js"); 10 | const bridge = new AppBridge(); 11 | 12 | const is = require('./is.js') 13 | 14 | var port = is.dev ? (process.argv && process.argv.length > 2 ? process.argv[2].split('=')[1] : '3334') : ''; 15 | console.log("starting toolkit in ", (is.dev ? 'dev mode' : 'production mode')) 16 | 17 | // Keep a global reference of the window object, if you don't, the window will 18 | // be closed automatically when the JavaScript object is garbage collected. 19 | let win 20 | 21 | let willQuitApp = false; 22 | 23 | function createWindow() { 24 | 25 | var package = require("../package.json"); 26 | 27 | // Create the browser window. 28 | win = new BrowserWindow({ 29 | width: 800, 30 | height: 600, 31 | title: "Electron Toolkit v" + package.version, 32 | //icon: nativeImage.createFromPath(path.join(__dirname, '..', "icon.ico")), 33 | webPreferences: { 34 | //nodeIntegration: false, // => webview won't work 35 | preload: path.join(__dirname, "preload.js") 36 | } 37 | }); 38 | 39 | win.setMenu(null); 40 | 41 | if (is.dev) { 42 | win.loadURL("http://127.0.0.1:" + port + "/renderer/"); 43 | } else { 44 | win.loadURL(url.format({ 45 | pathname: path.join(__dirname, "..", "renderer", "index.html"), 46 | protocol: "file:", 47 | slashes: true 48 | })); 49 | } 50 | 51 | //ignore other title changes 52 | win.on("page-title-updated", function(ev) { 53 | ev.preventDefault(); 54 | }); 55 | 56 | //https://electron.atom.io/docs/api/browser-window/#event-close 57 | win.on('close', (e) => { 58 | //do not close: 59 | if (process.platform === 'darwin') { 60 | if (willQuitApp) { 61 | win = null 62 | } else { 63 | win.hide(); 64 | e.preventDefault(); 65 | } 66 | } else { 67 | win = null 68 | } 69 | }) 70 | 71 | var webContents = win.webContents; 72 | 73 | if (is.dev) { 74 | webContents.openDevTools() 75 | } 76 | 77 | var handleRedirect = (e, url) => { 78 | //console.log("before navigate: ", url, webContents.getURL()) 79 | if (url != webContents.getURL()) { 80 | e.preventDefault(); 81 | require("electron").shell.openExternal(url); 82 | } 83 | }; 84 | 85 | webContents.on("will-navigate", handleRedirect); 86 | webContents.on("new-window", handleRedirect); 87 | 88 | //Emitted when a new webContents is created. 89 | app.on('web-contents-created', (event, contents) => { 90 | 91 | contents.on('will-attach-webview', (event, webPreferences, params) => { 92 | // Strip away preload scripts if unused or verify their location is legitimate 93 | delete webPreferences.preload 94 | delete webPreferences.preloadURL 95 | 96 | // Disable node integration 97 | webPreferences.nodeIntegration = false 98 | 99 | // Verify URL being loaded 100 | if (!params.src.startsWith('https://launchfox.co/')) { 101 | event.preventDefault() 102 | } 103 | }) 104 | 105 | }) 106 | 107 | // Emitted when the window is closed. 108 | win.on('closed', () => { 109 | // Dereference the window object, usually you would store windows 110 | // in an array if your app supports multi windows, this is the time 111 | // when you should delete the corresponding element. 112 | //win = null 113 | }) 114 | 115 | } 116 | 117 | // This method will be called when Electron has finished 118 | // initialization and is ready to create browser windows. 119 | // Some APIs can only be used after this event occurs. 120 | app.on('ready', createWindow) 121 | 122 | // Quit when all windows are closed. 123 | app.on('window-all-closed', () => { 124 | // On macOS it is common for applications and their menu bar 125 | // to stay active until the user quits explicitly with Cmd + Q 126 | if (process.platform !== 'darwin') { 127 | app.quit() 128 | } 129 | }) 130 | 131 | app.on('activate', () => { 132 | // On OS X it's common to re-create a window in the app when the 133 | // dock icon is clicked and there are no other windows open. 134 | if (process.platform === "darwin") { 135 | if (win) { 136 | win.show(); 137 | } 138 | if (win === null) { 139 | createWindow(); 140 | } 141 | } else { 142 | createWindow(); 143 | } 144 | }) 145 | 146 | app.on("before-quit", () => (willQuitApp = true)); -------------------------------------------------------------------------------- /app/is.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prod: process.env["ELECTRON_ENV"] !== "development", 3 | dev: process.env["ELECTRON_ENV"] === "development" 4 | }; -------------------------------------------------------------------------------- /app/preload.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const os = require('os') 7 | 8 | const uuidv4 = require('uuid/v4'); 9 | 10 | const { dialog } = require("electron").remote; 11 | const { shell, ipcRenderer } = require("electron"); 12 | 13 | const ImageUtils = require("./ImageUtils"); 14 | const IconGenerator = require("./IconGenerator.js"); 15 | const AppCapturer = require("./AppCapturer.js"); 16 | 17 | const is = require("./is.js"); 18 | window.is = is; 19 | window._ = require('lodash'); 20 | 21 | var devConfig = {}; 22 | try { devConfig = require('../dev.config.js') } catch (error) {} 23 | 24 | const PROJECT_DIR = is.dev ? devConfig.PROJECT_DIR : path.join(__dirname, '..', '..', '..'); 25 | console.log("project dir", PROJECT_DIR) 26 | 27 | const isImage = (file) => { 28 | var ext = path.extname(file).toLowerCase(); 29 | return ['.png', '.svg', '.jpg', '.jpeg'].includes(ext); 30 | } 31 | 32 | const isVideo = (file) => { 33 | var ext = path.extname(file).toLowerCase(); 34 | return ['.mp4', '.webm'].includes(ext); 35 | } 36 | 37 | let _packageJson; 38 | 39 | 40 | function base64ToBuf(b64string) { 41 | var buf; 42 | if (typeof Buffer.from === "function") { 43 | buf = Buffer.from(b64string, "base64"); 44 | } else { 45 | buf = new Buffer(b64string, "base64"); 46 | } 47 | return buf; 48 | } 49 | 50 | //security measure 51 | /* 52 | window.eval = global.eval = function() { 53 | throw new Error( 54 | "Sorry, N1 does not support window.eval() for security reasons." 55 | ); 56 | }; 57 | */ 58 | 59 | window.Toolkit = { 60 | 61 | Config: { 62 | PROJECT_DIR: PROJECT_DIR, 63 | LAUNCHFOX_HOST: is.dev ? devConfig.LAUNCHFOX_HOST : "launchfox.co" 64 | }, 65 | 66 | IconGenerator: new IconGenerator(PROJECT_DIR), 67 | 68 | AppCapturer: new AppCapturer(PROJECT_DIR), 69 | 70 | 71 | saveZipAs: function(b64string, fileName) { 72 | return new Promise((resolve, reject) => { 73 | dialog.showSaveDialog({ 74 | title: "Save website", 75 | defaultPath: path.join(PROJECT_DIR, fileName), 76 | filters: [{ name: "application/zip", extensions: ["zip"] }] 77 | }, 78 | filepath => { 79 | if (filepath === undefined) { 80 | return reject(new Error("invalid path")); 81 | } 82 | var buf = base64ToBuf(b64string); 83 | fs.writeFile(filepath, buf, err => { 84 | if (err) { 85 | return reject(err); 86 | } 87 | resolve(filepath); 88 | }); 89 | } 90 | ); 91 | }); 92 | }, 93 | 94 | getSystemInfo: function() { 95 | return { 96 | 'electron': process.versions.electron, 97 | 'platform': os.platform(), 98 | 'arch': os.arch() 99 | } 100 | }, 101 | 102 | getProjectID: function() { 103 | var configPath = path.join(__dirname, "..", "config.json"); 104 | if (fs.existsSync(configPath)) { 105 | var data = fs.readFileSync(configPath, 'utf8'); 106 | if (data) { 107 | var id = JSON.parse(data).id; 108 | if (id) { 109 | return id; 110 | } 111 | } 112 | } 113 | var id = uuidv4(); 114 | var content = { 115 | "id": id 116 | } 117 | fs.writeFile(configPath, JSON.stringify(content), 'utf8', function(err) {}); 118 | return id; 119 | }, 120 | 121 | //safe function that checks file relative to project dir 122 | //should be preferred over absolute paths 123 | //TODO make sure that relative path p is not leaving dir: ../../.. 124 | base64: function(p) { 125 | var _p = path.join(PROJECT_DIR, p); 126 | return ImageUtils.loadAsDataUri(_p) 127 | }, 128 | base64Asset: function(p) { 129 | var _p = path.join(__dirname, '..', 'assets', p); 130 | return ImageUtils.loadAsDataUri(_p) 131 | }, 132 | base64_abs: function(p) { 133 | var type = path 134 | .extname(p) 135 | .toLowerCase() 136 | .substring(1); 137 | var data = ""; 138 | switch (type) { 139 | case "svg": 140 | data = "data:image/svg+xml;base64,"; 141 | break; 142 | default: 143 | data = "data:image/" + type + ";base64,"; 144 | break; 145 | } 146 | //var type = 'data:image/svg+xml;base64,' 147 | // console.log(type); 148 | //'data:image/png;base64,' 149 | var content = fs.readFileSync(p); 150 | return data + content.toString("base64"); 151 | }, 152 | 153 | readPackageJson: function() { 154 | console.log("read from ", PROJECT_DIR); 155 | 156 | return new Promise((resolve, reject) => { 157 | var pjPath = path.join(PROJECT_DIR, "package.json"); 158 | fs.readFile(pjPath, (err, data) => { 159 | if (err) { 160 | return reject(err); 161 | } 162 | var packageJson = JSON.parse(data); 163 | _packageJson = packageJson; 164 | resolve(packageJson); 165 | }); 166 | }); 167 | }, 168 | writePackageJson: function(data) { 169 | console.log("write to ", PROJECT_DIR); 170 | return new Promise((resolve, reject) => { 171 | var pjPath = path.join(PROJECT_DIR, "package.json"); 172 | fs.writeFile(pjPath, data, (err, data) => { 173 | if (err) { 174 | return reject(err); 175 | } 176 | resolve(); 177 | }); 178 | }); 179 | }, 180 | 181 | /* 182 | loadWebsiteData: function() { 183 | return new Promise((resolve, reject) => { 184 | this.WebsiteManager.loadWebsiteData().then(data => { 185 | resolve(data); 186 | }); 187 | }); 188 | }, 189 | */ 190 | 191 | generateBuildFile: function(data) { 192 | return new Promise((resolve, reject) => { 193 | var fullPath = path.join(PROJECT_DIR, "electron-builder.json"); 194 | var json = JSON.stringify(data); 195 | fs.writeFile(fullPath, json, function(err, data) { 196 | if (err) { return reject(err); } 197 | resolve(); 198 | }); 199 | }); 200 | }, 201 | 202 | generateArtifacts: function(model) { 203 | 204 | return new Promise((resolve, reject) => { 205 | 206 | var emitter = new EventEmitter(); 207 | 208 | //console.log("generate installer called") 209 | ipcRenderer.send("spawn-builder", "now"); 210 | 211 | ipcRenderer.removeAllListeners("spawn-builder-complete"); 212 | ipcRenderer.once("spawn-builder-complete", function(event, namespace, eventTypes) { 213 | 214 | emitter.emit('status', 'process started') 215 | 216 | ipcRenderer.send(namespace + "send", { 217 | type: 'build', 218 | data: model 219 | }); 220 | 221 | eventTypes.forEach(ev => { 222 | var nev = namespace + ev; 223 | ipcRenderer.on(nev, (event, data) => { 224 | //console.log("received builder event", nev, data) 225 | emitter.emit(ev, data) 226 | }) 227 | }); 228 | 229 | }) 230 | 231 | resolve(emitter) 232 | 233 | }); 234 | 235 | 236 | }, 237 | 238 | loadLogos: function() { 239 | return new Promise((resolve, reject) => { 240 | var logosPath = path.join(PROJECT_DIR, "build", "png"); 241 | fs.readdir(logosPath, (err, files) => { 242 | if (err) { 243 | return resolve([]); 244 | //return reject(err); 245 | } 246 | 247 | var logos = files.map(file => { 248 | var fullPath = path.join(logosPath, file); 249 | 250 | //FIXME 251 | var content = fs.readFileSync(fullPath); 252 | 253 | return { 254 | id: file, 255 | filename: file, 256 | data: "data:image/png;base64," + content.toString("base64"), //TODO use ImageUtils 257 | type: "image", 258 | path: fullPath, 259 | size: content.length 260 | }; 261 | }); 262 | 263 | logos.sort((a, b) => { 264 | return a.size - b.size; 265 | }); 266 | resolve(logos); 267 | }); 268 | }); 269 | }, 270 | loadAssets: function() { 271 | return new Promise((resolve, reject) => { 272 | var screenshotPath = path.join(PROJECT_DIR, "build", "assets"); 273 | 274 | fs.readdir(screenshotPath, (err, files) => { 275 | if (err) { 276 | // /console.log("error"); 277 | return resolve([]); 278 | // return reject(err); 279 | } 280 | 281 | var screens = files.map(file => { 282 | var fullPath = path.join(screenshotPath, file); 283 | //FIXME 284 | var content = fs.readFileSync(fullPath); 285 | 286 | //console.log(content); 287 | var blob = new Blob([new Uint8Array(content)]); 288 | var fileURL = URL.createObjectURL(blob); 289 | 290 | if (isImage(file)) { 291 | return { 292 | id: file, 293 | filename: file, 294 | data: "data:image/png;base64," + content.toString("base64"), //TODO use ImageUtils 295 | state: "ready", 296 | role: file.startsWith("main") ? "main" : "feature", 297 | type: "image", 298 | path: fullPath 299 | }; 300 | } else if (isVideo(file)) { 301 | return { 302 | id: file, 303 | filename: file, 304 | data: fileURL, 305 | state: "ready", 306 | role: file.startsWith("main") ? "main" : "feature", 307 | type: "video", 308 | path: fullPath 309 | }; 310 | } else { 311 | return null; 312 | } 313 | }); 314 | 315 | screens = screens.filter(function(screen) { 316 | return typeof screen != undefined && screen != null; 317 | }); 318 | 319 | resolve(screens); 320 | }); 321 | }); 322 | }, 323 | 324 | openLink: function(link) { 325 | shell.openExternal(link); 326 | }, 327 | 328 | openInExplorer: function(filePath) { 329 | shell.showItemInFolder(filePath); 330 | }, 331 | 332 | deleteAsset: function(filePath) { 333 | console.log(filePath); 334 | fs.unlink(filePath, function(err) { 335 | if (err) { 336 | console.log("delete did not work", filePath); 337 | } else { 338 | console.log("file deleted"); 339 | } 340 | }); 341 | } 342 | }; -------------------------------------------------------------------------------- /app/preload_embedded.js: -------------------------------------------------------------------------------- 1 | // In guest page. 2 | const { ipcRenderer } = require('electron') 3 | 4 | window.electron = { 5 | notifyHost: function() { 6 | setTimeout(() => { 7 | ipcRenderer.sendToHost('pong') 8 | }, 4 * 1000) 9 | } 10 | } -------------------------------------------------------------------------------- /app/wrapper.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | 7 | //ipc channel not working: try fallback 8 | var out; 9 | var inp; 10 | if (typeof process.send !== 'function') { 11 | 12 | out = fs.createWriteStream(null, { fd: 1 }) 13 | inp = fs.createReadStream(null, { fd: 0 }) 14 | 15 | process.send = function(data) { 16 | out.write(JSON.stringify(data) + "#"); 17 | }; 18 | 19 | inp.on("data", data => { 20 | try { 21 | var message = data.toString(); 22 | var m = JSON.parse(message); 23 | process.emit("message", m); 24 | } catch (error) { 25 | console.log("error", error); 26 | } 27 | }); 28 | 29 | } 30 | 31 | 32 | //notify parent that child process is ready 33 | console.log("~~~") 34 | process.send({ 35 | type: 'ready' 36 | }) 37 | 38 | 39 | app.on('browser-window-created', (event, win) => { 40 | 41 | 42 | let title = win.getTitle(); 43 | console.log("window-created: " + title) 44 | 45 | process.on('message', message => { 46 | //console.log("message", message) 47 | if (message.type === 'detection-complete') { 48 | //reset title 49 | win.setTitle(title); 50 | } 51 | 52 | if (message.type === 'request-maximize') { 53 | if (win.isMinimized()) { 54 | win.restore() 55 | } 56 | } 57 | 58 | }); 59 | 60 | var timer = setTimeout(() => { 61 | process.send({ 62 | 'type': 'title-available', 63 | 'data': win.getTitle() 64 | }) 65 | win.setTitle(title + "~"); 66 | }, 2000); 67 | 68 | win.webContents.on('page-title-updated', () => { 69 | 70 | clearTimeout(timer); 71 | 72 | title = win.getTitle(); 73 | 74 | process.send({ 75 | 'type': 'title-available', 76 | 'data': win.getTitle() 77 | }) 78 | 79 | win.setTitle(title + '~'); 80 | 81 | }) 82 | 83 | 84 | 85 | //the parent app is interested in the following window events: 86 | var eventTypes = ["closed", "blur", "focus", "show", "hide", "maximize", "unmaximize", "minimize", "resize"] 87 | 88 | eventTypes.forEach(ev => { 89 | win.on(ev, () => { 90 | process.send({ 91 | 'type': "" + ev, 92 | 'data': null 93 | }) 94 | }) 95 | }) 96 | 97 | win.on('closed', () => { 98 | console.log("window closed"); 99 | if (process.disconnect) { 100 | process.disconnect(); 101 | } 102 | //if fallback is used make sure streams on fds are closed 103 | if (out) { 104 | //fs.closeSync(0); 105 | //fs.closeSync(1); 106 | out.close(); 107 | // inp.close(); 108 | out.destroy(); 109 | // inp.destroy(); 110 | } 111 | app.quit(); 112 | win = null 113 | }) 114 | 115 | }) 116 | 117 | 118 | const PROJECT_DIR = process.argv[2]; 119 | const pjPath = path.join(PROJECT_DIR, "package.json"); 120 | const pjson = require(pjPath); 121 | const mainPath = pjson.main || "index.js"; 122 | const mainPathAbs = path.join(PROJECT_DIR, mainPath); 123 | require(mainPathAbs); -------------------------------------------------------------------------------- /assets/MacInstaller.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/assets/MacInstaller.PNG -------------------------------------------------------------------------------- /assets/atom.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/assets/atom.ico -------------------------------------------------------------------------------- /assets/electron.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/assets/electron.icns -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/assets/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-toolkit", 3 | "version": "1.0.25", 4 | "description": "Tools to make launching Electron apps easier", 5 | "main": "./app/index.js", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "webpack --config webpack.config.js", 10 | "devServer": "cross-env NODE_ENV=development webpack-dev-server --open --hot --port=3334 --host 127.0.0.1", 11 | "devElectron": "cross-env ELECTRON_ENV=development electron . --port=3334", 12 | "dev": "npm run devServer & npm run devElectron" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/PhilippLgh/electron-toolkit.git" 17 | }, 18 | "author": "Philipp Langhans", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/PhilippLgh/electron-toolkit/issues" 22 | }, 23 | "homepage": "https://github.com/PhilippLgh/electron-toolkit#readme", 24 | "dependencies": { 25 | "bootstrap": "^4.0.0-alpha.6", 26 | "jimp": "^0.2.28", 27 | "jquery": "^3.2.1", 28 | "lodash": "^4.17.4", 29 | "png2icons": "^0.9.1", 30 | "tether": "^1.4.3", 31 | "uuid": "^3.1.0", 32 | "vue": "^2.4.4", 33 | "vue-router": "^3.0.1" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.26.0", 37 | "babel-loader": "^7.1.2", 38 | "babel-preset-env": "^1.6.0", 39 | "cross-env": "^5.0.5", 40 | "css-loader": "^0.28.7", 41 | "file-loader": "^1.1.4", 42 | "style-loader": "^0.19.0", 43 | "vue-loader": "^13.0.5", 44 | "vue-template-compiler": "^2.4.4", 45 | "webpack": "^3.6.0", 46 | "webpack-dev-server": "^2.9.1" 47 | } 48 | } -------------------------------------------------------------------------------- /renderer/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} 5 | -------------------------------------------------------------------------------- /renderer/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/renderer/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /renderer/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/renderer/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /renderer/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/renderer/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /renderer/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/renderer/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /renderer/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippLgh/electron-toolkit/13661eddba022fc238f3383922511392af5125ac/renderer/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /src/Overview.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Assets 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Installer 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Publish 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Website 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Updates* 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | More 71 | 72 | 73 | 74 | 75 | 76 | *coming soon 77 | 78 | 79 | 80 | 81 | 97 | 98 | 141 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | class App { 2 | showUserInfo(content, callback) { 3 | window.vueApp.showToast({ 4 | content: content, 5 | callback: callback 6 | }) 7 | } 8 | 9 | showUserError(content) { 10 | window.vueApp.showToast({ 11 | content: content, 12 | isError: true 13 | }); 14 | } 15 | 16 | openLink(link) { 17 | window.Toolkit.openLink(link); 18 | } 19 | 20 | openExplorer(location) { 21 | window.Toolkit.openInExplorer(location); 22 | } 23 | } 24 | 25 | const app = new App(); 26 | export default app; -------------------------------------------------------------------------------- /src/assets/asset-overview.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | Assets 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | App Icon 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Screenshots 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 44 | 45 | 88 | -------------------------------------------------------------------------------- /src/assets/icon-generator.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tools 6 | Assets 7 | Icon Generator 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Icon Generator 18 | Please select a SVG or PNG file. 19 | 20 | 21 | 22 | .png 23 | .ico 24 | .icns 25 | 26 | 27 | 28 | 29 | 30 | 31 | Drop image here or click to browse 32 | {{ warning }} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Generate 41 | placeholder 42 | {{ imagePath }} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 223 | 224 | 423 | -------------------------------------------------------------------------------- /src/assets/screenshot-container.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 59 | 60 | 205 | -------------------------------------------------------------------------------- /src/assets/screenshot-tool.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | Assets 6 | Screenshot Tool 7 | 8 | 9 | 10 | 11 | Screen Capture Tool 12 | Record videos and take screenshots of your app. 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{ recordingTimeFormatted }} 24 | 25 | 26 | 27 | Start App Preview 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Take Screenshot 38 | 39 | 40 | Start Recording 41 | Stop Recording 42 | 43 | 44 | 45 | 46 | Gallery 47 | 48 | nothing captured yet 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {{ activeGalleryImage.filename}} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 379 | 380 | 583 | -------------------------------------------------------------------------------- /src/launchfox-embedded.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 197 | 198 | 235 | -------------------------------------------------------------------------------- /src/mocks/website.js: -------------------------------------------------------------------------------- 1 | 2 | const meta = { 3 | author: "", 4 | title: "", 5 | favicon: "", 6 | keywords: "" 7 | }; 8 | 9 | 10 | const style = { 11 | backgroundColor: { 12 | "h": 0, 13 | "s": 0, 14 | "l": 1, 15 | "a": 1 16 | }, 17 | backgroundColor2: { 18 | "h": 0, 19 | "s": 0, 20 | "l": 0.97, 21 | "a": 1 22 | }, 23 | fontColor: { 24 | "h": 0, 25 | "s": 0, 26 | "l": 0.2, 27 | "a": 1 28 | }, 29 | 30 | fontColor2: { 31 | "h": 0, 32 | "s": 0, 33 | "l": 0.52, 34 | "a": 1 35 | }, 36 | buttonColor: { 37 | "h": 197, 38 | "s": 0.48, 39 | "l": 0.63, 40 | "a": 1 41 | }, 42 | buttonFontColor: { 43 | "h": 0, 44 | "s": 0, 45 | "l": 1, 46 | "a": 1 47 | }, 48 | accentColor: { 49 | "h": 0, 50 | "s": 0, 51 | "l": 0.4, 52 | "a": 1 53 | }, 54 | fontFamily: "Open Sans", 55 | fontFamily2: "Abel" 56 | }; 57 | 58 | const website = { 59 | pro: false, 60 | availableInstallers: { 61 | Mac: true, 62 | Windows: true, 63 | Linux: true 64 | }, 65 | style: style, 66 | meta: meta, 67 | 68 | name: "Product name", 69 | company: "example Ltd.", 70 | year: (new Date()).getFullYear() + "", 71 | email: "example@company.com", 72 | 73 | logo: { 74 | "path":"", 75 | "type": "image", 76 | "id": "f5941a52-7dee-423e-85ca-7e2475b2af40", 77 | "title": "logo", 78 | "url": "https://launchfox.co/assets/placeholder_icon.png" 79 | }, 80 | 81 | mission: "Product mission statement", 82 | description: "Catchy product description text.", 83 | featuresHeadline: "What's inside?", 84 | testimonialHeadline: "What people are saying...", 85 | features: [{ 86 | "id": "89493662-9921-4a77-b829-1230c6bae3e6", 87 | "name": "Feature 1", 88 | "description": "What makes this feature awesome?", 89 | "asset": { 90 | "path":"", 91 | "type": "image", 92 | "id": "f5941a52-7dee-423e-85ca-7e2475b2daf40", 93 | "title": "Feature image", 94 | "url": "https://launchfox.co/assets/app_template_placeholder.png" 95 | } 96 | }, 97 | { 98 | "id": "89493662-9921-4a77-b829-1230c6bae3e5", 99 | "name": "Feature 2", 100 | "description": "What makes this feature awesome?", 101 | "asset": { 102 | "path":"", 103 | "type": "image", 104 | "id": "f5941a52-7dee-423e-85ca-7e2475db2af42", 105 | "title": "Feature image", 106 | "url": "https://launchfox.co/assets/app_template_placeholder.png" 107 | } 108 | }, 109 | { 110 | "id": "89493662-9921-4a77-b829-1230cd6bae3e4", 111 | "name": "Feature 3", 112 | "description": "What makes this feature awesome?", 113 | "asset": { 114 | "path":"", 115 | "type": "image", 116 | "id": "f5941a52-7dee-423e-85ca-7e2475b2af43", 117 | "title": "Feature image", 118 | "url": "https://launchfox.co/assets/app_template_placeholder.png" 119 | } 120 | }, 121 | { 122 | "id": "89493662-9921-4a77-b82d9-1230c6bae3e8", 123 | "name": "Feature 4", 124 | "description": "What makes this feature awesome?", 125 | "asset": { 126 | "path":"", 127 | "type": "image", 128 | "id": "f5941a52-7dee-423e-85cda-7e2475b2af40", 129 | "title": "Feature image", 130 | "url": "https://launchfox.co/assets/app_template_placeholder.png" 131 | } 132 | } 133 | ], 134 | testimonials: [{ 135 | "id": "1", 136 | "name": "John Doe", 137 | "text": "You have an amazing product. Keep up the good work!", 138 | "image": "", 139 | "src": "" 140 | }, 141 | { 142 | "id": "2", 143 | "name": "Jane Doe", 144 | "text": "You have an amazing product. Keep up the good work!", 145 | "image": "", 146 | "src": "" 147 | } 148 | ], 149 | social: [{ 150 | "id": "1", 151 | "name": "Twitter", 152 | "icon": "fa fa-twitter", 153 | "url": "twitter.com/" 154 | }, 155 | { 156 | "id": "2", 157 | "name": "Medium", 158 | "icon": "fa fa-medium", 159 | "url": "medium.com/" 160 | }, 161 | { 162 | "id": "3", 163 | "name": "GitHub", 164 | "icon": "fa fa-github", 165 | "url": "github.com/" 166 | }, 167 | { 168 | "id": "4", 169 | "name": "Google Plus", 170 | "icon": "fa fa-google-plus", 171 | "url": "plus.google.com/" 172 | } 173 | ], 174 | deviceTemplate: { 175 | url: "https://launchfox.co/assets/placeholder_trans.png", 176 | preview: "https://launchfox.co/assets/placeholder_trans.png" 177 | }, 178 | 179 | screenshot: { 180 | "path":"", 181 | "type": "image", 182 | "id": "6cdf248b-8b56-4dc2-8f55-17751f17252e", 183 | "title": "Release video", 184 | "url": "https://launchfox.co/assets/app_template_placeholder.png", 185 | }, 186 | downloadButtons: [{ 187 | "id": "1", 188 | "name": "Download now", 189 | "mac": "", 190 | "windows": "", 191 | "linux": "" 192 | }], 193 | componentVisibility: { 194 | "componentProductLogo": true, 195 | "componentProductCompany": true, 196 | "componentProductTitle": true, 197 | "componentProductDescription": true, 198 | "componentProductMission": true, 199 | "componentProductDownload": true, 200 | "componentProductScreenshots": true, 201 | "componentProductFeatures": true, 202 | "componentProductTestimonials": true, 203 | "componentProductSubscription": false, 204 | "componentProductSocial": true 205 | }, 206 | menuVisibility: false, 207 | pagesVisibility: { 208 | "landing": false, 209 | "changelog": false, 210 | "featurelist": false, 211 | "pricing": false, 212 | "terms": false 213 | } 214 | } 215 | 216 | 217 | module.exports = website; -------------------------------------------------------------------------------- /src/more-tools.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | More Tools 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/publish/publish-form-github.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tools 6 | Publish 7 | GitHub 8 | 9 | 10 | 11 | 12 | 13 | 14 | Access token to support auto-update (optional) 15 | 16 | 17 | 18 | 19 | Repository 20 | 21 | 22 | 23 | 24 | Owner 25 | 26 | 27 | 28 | 29 | Release type 30 | 31 | Draft 32 | Prerelease 33 | Release 34 | 35 | 36 | 37 | 38 | 39 | 40 | Use v-prefixed tag name 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Is private repo 49 | 50 | 51 | 52 | 53 | 54 | Save GitHub settings 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 155 | 156 | -------------------------------------------------------------------------------- /src/publish/publish-form-launchfox.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | Publish 6 | Launchfox 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | -------------------------------------------------------------------------------- /src/publish/publish-form-s3.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | Publish 6 | AWS S3 7 | 8 | 9 | 10 | 11 | 12 | To use Amazon S3 please add electron-publisher-s3 dependency to devDependencies (yarn add electron-publisher-s3 --dev). 13 | Define AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables. Or in the ~/.aws/credentials. 14 | 15 | 16 | 17 | Bucket name 18 | 19 | 20 | 21 | 22 | Region 23 | 24 | 25 | 26 | 27 | Access Control List (ACL) 28 | 29 | public 30 | private 31 | none 32 | 33 | 34 | 35 | 36 | Storage class 37 | 38 | Standard 39 | Reduced redundancy 40 | Standard IA 41 | 42 | 43 | 44 | 45 | Channel 46 | 47 | 48 | 49 | 50 | Endpoint 51 | 52 | 53 | 54 | 55 | Path 56 | 57 | 58 | 59 | 60 | Save S3 settings 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 157 | 158 | -------------------------------------------------------------------------------- /src/publish/publish-overview.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tools 5 | Publish 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Launchfox 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | GitHub 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Amazon S3 37 | 38 | 39 | 40 | 41 | 75 | 76 | 77 | 78 | 79 | 80 | 88 | 89 | 132 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | import store from "./store.js"; 2 | 3 | class Storage { 4 | 5 | 6 | constructor() { 7 | 8 | } 9 | 10 | getProperty(property) { 11 | 12 | var key = this.computeKey(property); 13 | ///console.log("storage storage", window.localStorage); 14 | 15 | //window.localStorage.removeItem(key); 16 | // console.log() 17 | var data = JSON.parse(window.localStorage.getItem(key)); 18 | // console.log("saving", value); 19 | if (property == "website" && data) { 20 | this.addDataUrls(data); 21 | } 22 | return data; 23 | } 24 | 25 | setProperty(property, value) { 26 | 27 | let val = JSON.parse(JSON.stringify(value)); 28 | if (property == "website") { 29 | this.removeDataUrls(val); 30 | } 31 | 32 | var key = this.computeKey(property); 33 | window.localStorage.setItem(key, JSON.stringify(val)); 34 | 35 | } 36 | 37 | computeKey(key) { 38 | var val = 'electron-toolkit-' + store.id + "-" + key + '-setting'; 39 | return val; 40 | } 41 | 42 | removeDataUrls(website) { 43 | this.replaceDataUrlWithPath(website.screenshot); 44 | this.replaceDataUrlWithPath(website.logo); 45 | _.forEach(website.features, (feature) => { 46 | this.replaceDataUrlWithPath(feature.asset); 47 | }) 48 | } 49 | 50 | replaceDataUrlWithPath(asset) { 51 | if (asset.url.startsWith("data:")) { 52 | asset.url = "data:" + asset.path; 53 | } 54 | } 55 | 56 | addDataUrls(website) { 57 | this.generateDataUrlFromPath(website.screenshot); 58 | this.generateDataUrlFromPath(website.logo); 59 | _.forEach(website.features, (feature) => { 60 | this.generateDataUrlFromPath(feature.asset); 61 | }) 62 | } 63 | 64 | generateDataUrlFromPath(asset) { 65 | if (asset.url.startsWith("data:")) { 66 | var path = asset.url.split("data:")[1]; 67 | // console.log(path); 68 | try { 69 | asset.url = Toolkit.base64_abs(path); 70 | } catch (error) { 71 | //path does not exist 72 | } 73 | // asset.url = "data:"+asset.path; 74 | } 75 | } 76 | } 77 | 78 | 79 | 80 | const storage = new Storage(); 81 | export default storage; -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | let store = { 2 | 3 | product: { 4 | name: "", 5 | version: "1.0.0", 6 | appId: "", 7 | author: "unknown", 8 | description: "" 9 | }, 10 | 11 | website: {}, 12 | websiteTemp: {}, 13 | websiteInit: false, 14 | packageJSON: {}, 15 | screenshots: [], 16 | logos: [], 17 | screens_initialized: false, 18 | screenPlaceholder: 'app_template_placeholder.png', 19 | iconPlaceholder: "placeholder_icon.png" 20 | } 21 | 22 | 23 | export default store; -------------------------------------------------------------------------------- /src/toolkit.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import "bootstrap/dist/css/bootstrap.min.css"; 5 | 6 | import Toast from "./widgets/fx-toast.vue"; 7 | 8 | import Overview from './Overview.vue' 9 | 10 | //Asset Views 11 | import AssetOverview from './assets/asset-overview.vue'; 12 | import IconGenerator from './assets/icon-generator.vue'; 13 | import ScreenshotTool from './assets/screenshot-tool.vue'; 14 | 15 | //Installer Views 16 | import InstallerView from './installer-view.vue'; 17 | 18 | //Publish Views 19 | import PublishOverview from './publish/publish-overview.vue'; 20 | import PublishLaunchfoxView from "./publish/publish-form-launchfox.vue"; 21 | import PublishGithubView from "./publish/publish-form-github.vue"; 22 | import PublishS3View from "./publish/publish-form-s3.vue"; 23 | 24 | //Website Views 25 | import WebsiteBuilderView from './website-builder-view.vue'; 26 | 27 | import MoreToolsView from './more-tools.vue'; 28 | 29 | const Toolkit = window.Toolkit; 30 | 31 | const LAUNCHFOX_HOST = Toolkit.Config.LAUNCHFOX_HOST; 32 | 33 | const routes = [ 34 | 35 | { path: '/', component: Overview }, 36 | 37 | { path: '/assets', component: AssetOverview }, 38 | { path: '/assets/appicon', component: IconGenerator }, 39 | { path: '/assets/screenshot', component: ScreenshotTool }, 40 | 41 | { path: '/installer', component: InstallerView }, 42 | 43 | { path: '/publish', component: PublishOverview }, 44 | { 45 | path: "/publish/detail/Launchfox", 46 | component: PublishLaunchfoxView, 47 | props: { 48 | host: LAUNCHFOX_HOST 49 | } 50 | }, 51 | { path: "/publish/detail/GitHub", component: PublishGithubView }, 52 | { path: "/publish/detail/AWS_S3", component: PublishS3View }, 53 | 54 | { path: '/website', component: WebsiteBuilderView, props: { host: LAUNCHFOX_HOST } }, 55 | 56 | { path: '/more', component: MoreToolsView }, 57 | 58 | ] 59 | 60 | Vue.use(VueRouter) 61 | 62 | const router = new VueRouter({ 63 | routes // short for `routes: routes` 64 | }) 65 | 66 | import store from "./store.js"; 67 | import website from './mocks/website.js' 68 | import storage from "./storage.js"; 69 | 70 | 71 | const app = new Vue({ 72 | components: { 73 | "fx-toast": Toast 74 | }, 75 | router, 76 | data: { 77 | product: store.product 78 | }, 79 | methods: { 80 | defaultWebsite: function() { 81 | return website; 82 | }, 83 | showToast: function(message) { 84 | this.$refs.toast.show(message) 85 | } 86 | }, 87 | mounted: function() { 88 | 89 | var logo = {}; 90 | var screens = []; 91 | 92 | store.id = Toolkit.getProjectID(); 93 | 94 | var data = storage.getProperty("website"); 95 | 96 | if (!data) { 97 | store.website = this.defaultWebsite(); 98 | } else { 99 | store.website = data; 100 | store.websiteInit = true; 101 | } 102 | store.websiteTemp = store.website; 103 | 104 | Toolkit.readPackageJson() 105 | .then(data => { 106 | this.product.name = data.name; 107 | this.product.version = data.version; 108 | this.product.author = data.author; 109 | this.product.repository = data.repository; 110 | this.product.build = data.build; 111 | }) 112 | 113 | Toolkit.loadAssets() 114 | .then(assets => { 115 | assets.forEach(sc => { 116 | store.screenshots.push( 117 | sc 118 | ); 119 | }); 120 | store.screens_initialized = true; 121 | }) 122 | .catch(err => { 123 | console.log("error loading assets: ", err) 124 | }) 125 | 126 | Toolkit.loadLogos() 127 | .then(logos => { 128 | store.logos = logos; 129 | }); 130 | 131 | } 132 | }).$mount('#app') 133 | 134 | window.vueApp = app; -------------------------------------------------------------------------------- /src/website-builder-view.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tools 6 | Website Builder 7 | Powered By Launchfox 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Save 16 | Export 17 | Publish 18 | 19 | 20 | 21 | 22 | 23 | 24 | 167 | 168 | 212 | -------------------------------------------------------------------------------- /src/widgets/fx-toast.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 47 | -------------------------------------------------------------------------------- /src/widgets/loading-button.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{loadingCaption}} 5 | 6 | 7 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /src/widgets/server-error.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ooooops 404! Internet connection required... 6 | 7 | 8 | 9 | 22 | 23 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/widgets/sticky-footer.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | var webpack = require("webpack"); 3 | 4 | module.exports = { 5 | entry: { 6 | toolkit: "./src/toolkit.js" 7 | }, 8 | output: { 9 | path: path.join(__dirname, "renderer"), 10 | publicPath: "/renderer/", 11 | filename: "[name].js" 12 | }, 13 | module: { 14 | rules: [{ 15 | test: /\.vue$/, 16 | loader: "vue-loader", 17 | options: { 18 | loaders: {} 19 | // other vue-loader options go here 20 | } 21 | }, 22 | /* 23 | { 24 | //test: /\.es6$/, 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | loader: 'babel-loader', 28 | query: { 29 | //presets: ['es2015'] deprecated: http://babeljs.io/env 30 | presets: ['env'] 31 | } 32 | }, 33 | */ 34 | { 35 | test: /\.css$/, 36 | use: ["style-loader", "css-loader"] 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: "file-loader", 41 | options: { 42 | name: "[name].[ext]?[hash]" 43 | } 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new webpack.ProvidePlugin({ 49 | $: "jquery", 50 | jQuery: "jquery", 51 | "window.jQuery": "jquery", 52 | Popper: ["popper.js", "default"], 53 | Tether: "tether" 54 | }) 55 | ], 56 | resolve: { 57 | alias: { 58 | vue$: "vue/dist/vue.esm.js" 59 | } 60 | }, 61 | devServer: { 62 | historyApiFallback: true, 63 | noInfo: true 64 | }, 65 | performance: { 66 | hints: false 67 | }, 68 | devtool: "#eval-source-map" 69 | }; --------------------------------------------------------------------------------
*coming soon
12 | To use Amazon S3 please add electron-publisher-s3 dependency to devDependencies (yarn add electron-publisher-s3 --dev). 13 | Define AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables. Or in the ~/.aws/credentials. 14 |