├── .gitignore ├── README.md ├── build.sh ├── build ├── background.jpg ├── logo.icns └── logo.ico ├── data └── .gitkeep ├── github └── preview.png ├── logo.ico ├── logo_small.ico ├── main.js ├── package.json ├── public ├── assets │ ├── css │ │ └── app.css │ └── images │ │ ├── FFXIV_media_tour2019_19.png │ │ ├── FFXIV_media_tour2019_20.png │ │ ├── add-character.svg │ │ ├── angle-left-regular.svg │ │ ├── angle-right-regular.svg │ │ ├── background1.jpg │ │ ├── background2.jpg │ │ ├── background3.jpg │ │ ├── background4.jpg │ │ ├── background5.jpg │ │ ├── background6.jpg │ │ ├── background7.jpg │ │ ├── background8.jpg │ │ ├── bars-regular.68e0d910.svg │ │ ├── bars-regular.svg │ │ ├── buttons.png │ │ ├── expansion.png │ │ ├── faceless.png │ │ ├── letters.png │ │ ├── log-out.svg │ │ ├── logo_small.ico │ │ ├── logo_small.png │ │ ├── maximize.svg │ │ ├── minus.svg │ │ ├── more-vertical.svg │ │ ├── news-regular.svg │ │ ├── numbers.png │ │ ├── power.svg │ │ ├── settings.svg │ │ ├── times-regular.svg │ │ ├── user-plus.svg │ │ └── wrench-light.svg └── index.html ├── src ├── css │ ├── CharacterAddForm.scss │ ├── CharacterList.scss │ ├── CharacterView.scss │ ├── LodestoneNews.scss │ ├── Reset.scss │ ├── SettingsForm.scss │ ├── UI.scss │ ├── _mq.scss │ ├── app.scss │ └── debug.scss └── js │ ├── app.js │ └── xiv │ ├── ButtonActions.js │ ├── Characters.js │ ├── GameFiles.js │ ├── GameLauncher.js │ ├── GameLauncherWindow.js │ ├── LodestoneNews.js │ ├── Login.js │ ├── Notice.js │ ├── RaelysAPI.js │ ├── Settings.js │ ├── SettingsManager.js │ ├── XIVAPI.js │ └── XIVRequest.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | /data/* 5 | !/data/.gitkeep 6 | yarn-error.log 7 | .vs 8 | public/assets/*.js 9 | public/assets/*.json 10 | public/assets/css 11 | public/assets/js 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | # FINAL FANTASY XIV: STORMBLOOD - CUSTOM LAUNCHER 4 | 5 | A custom built launcher for FFXIV written in Javascript on Nodejs via Electron. Needs a lot of love to make it look good and be useful 6 | 7 | > This is a Javascript implementation of: https://github.com/goaaats/FFXIVQuickLauncher 8 | 9 | > Note: DO NOT run npm commands in this project, it will install different versions of packages and break stuff. 10 | 11 | ## Source files 12 | 13 | - Styles: `src/css/**.scss` 14 | - Javascript: `src/js/**.js` 15 | - HTML: `public/index.html` 16 | 17 | For images, you must host them somewhere, eg Github, Imgur or I can put them on XIVAPI.com 18 | 19 | ## Getting started 20 | 21 | ```sh 22 | git clone 'git@github.com:xivapi/ffxiv-launcher.git' # Clone the repo! 23 | cd ffxiv-launcher 24 | yarn install # Snag the dependencies! 25 | yarn start # Build and run it locally in dev mode! 26 | ``` 27 | 28 | Electron is basically a chrome browser without a head (no address bar, buttons etc). 29 | 30 | You can do modifications to the html, css or js and reload the app via: `CTRL+SHIFT+R`. 31 | 32 | ## Compiling the JS and SCSS 33 | 34 | Make sure your IDE is set to: EMCAScript 6 35 | 36 | (If you need SASS installed: `yarn add sass-loader node-sass --dev`) 37 | 38 | - `yarn build` or `yarn build-dev` or `yarn build-dev --watch` 39 | 40 | ## Compile 41 | 42 | - `yarn dist` - This also compiles the CSS and builds the electron app installer. 43 | 44 | This will build the installer to: `/dist/FFXIV-Launcher Setup .exe` which you can share and install. 45 | 46 | ## Dev Tools 47 | 48 | Open with `CTRL+Shift+I` 49 | 50 | # Preview 51 | 52 | ![preview](./github/preview.png) 53 | 54 | # Todo 55 | 56 | - Save expansion per character rather than app wide 57 | - Add an "Auto-Login" option, if character has OTP it will just auto-prompt, if game is already running it won't auto-login (assumes you're selecting another character) 58 | - Re-write the settings system to split Default Settings and User Settings, simplify it all 59 | - Add maintenance check (parse: http://frontier.ffxiv.com/worldStatus/gate_status.json) 60 | - Allow non-lodestone characters (eg accounts with many characters), right now it spams XIVAPI 61 | - Allow "Named" entries rather than just character/server 62 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Compile styles in prod mode 4 | echo "Compiling styles in prod mode" 5 | yarn build 6 | 7 | # Compile the full app 8 | echo "Compiling the full app to the dist folder" 9 | yarn dist 10 | -------------------------------------------------------------------------------- /build/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/build/background.jpg -------------------------------------------------------------------------------- /build/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/build/logo.icns -------------------------------------------------------------------------------- /build/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/build/logo.ico -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/data/.gitkeep -------------------------------------------------------------------------------- /github/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/github/preview.png -------------------------------------------------------------------------------- /logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/logo.ico -------------------------------------------------------------------------------- /logo_small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/logo_small.ico -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | 5 | // Keep a global reference of the window object, if you don't, the window will 6 | // be closed automatically when the JavaScript object is garbage collected. 7 | let win; 8 | 9 | function createWindow () { 10 | // Create the browser window. 11 | win = new BrowserWindow({ 12 | width: 1400, 13 | height: 900, 14 | icon: path.join(__dirname, 'logo_small.ico'), 15 | frame: false, 16 | webPreferences: { 17 | nodeIntegration: true 18 | } 19 | }); 20 | 21 | // and load the index.html of the app. 22 | win.loadURL(url.format({ 23 | pathname: path.join(__dirname, 'public/index.html'), 24 | protocol: 'file:', 25 | slashes: true 26 | })); 27 | 28 | // Open the DevTools. 29 | //win.webContents.openDevTools(); 30 | 31 | // Emitted when the window is closed. 32 | win.on('closed', () => { 33 | // Dereference the window object, usually you would store windows 34 | // in an array if your app supports multi windows, this is the time 35 | // when you should delete the corresponding element. 36 | win = null 37 | }); 38 | } 39 | 40 | // This method will be called when Electron has finished 41 | // initialization and is ready to create browser windows. 42 | // Some APIs can only be used after this event occurs. 43 | app.on('ready', createWindow); 44 | 45 | // Quit when all windows are closed. 46 | app.on('window-all-closed', () => { 47 | // On macOS it is common for applications and their menu bar 48 | // to stay active until the user quits explicitly with Cmd + Q 49 | if (process.platform !== 'darwin') { 50 | app.quit() 51 | } 52 | }); 53 | 54 | app.on('activate', () => { 55 | // On macOS it's common to re-create a window in the app when the 56 | // dock icon is clicked and there are no other windows open. 57 | if (win === null) { 58 | createWindow() 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FFXIV-Launcher", 3 | "description": "A custom FFXIV launcher", 4 | "author": "Josh Freeman ", 5 | "version": "0.3.6", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@symfony/webpack-encore": "^0.27.0", 9 | "concurrently": "^4.1.0", 10 | "electron": "^5.0.1", 11 | "electron-builder": "^20.38.4", 12 | "electron-packager": "^13.0.1", 13 | "node-sass": "^4.12.0", 14 | "sass-loader": "^7.1.0" 15 | }, 16 | "main": "main.js", 17 | "build": { 18 | "appId": "ffxiv.custom.launcher", 19 | "win": { 20 | "icon": "build/logo.ico" 21 | }, 22 | "mac": { 23 | "category": "gaming" 24 | } 25 | }, 26 | "scripts": { 27 | "watch": "encore dev --watch", 28 | "electron-dev": "electron . --dev", 29 | "start": "concurrently -k -c \"green,blue\" \"yarn:watch\" \"yarn:electron-dev\"", 30 | "prestart-prod": "encore prod", 31 | "start-prod": "electron . --dev", 32 | "build-dev": "encore dev", 33 | "build": "encore prod", 34 | "prepack": "yarn run build", 35 | "pack": "electron-builder --dir", 36 | "predist": "yarn run build", 37 | "dist": "electron-builder" 38 | }, 39 | "dependencies": { 40 | "moment": "^2.22.2", 41 | "sha1-file": "^1.0.0", 42 | "uuid": "^3.3.2", 43 | "webpack-zepto": "^0.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; } 24 | 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; } 29 | 30 | body { 31 | line-height: 1; } 32 | 33 | ol, ul { 34 | list-style: none; } 35 | 36 | blockquote, q { 37 | quotes: none; } 38 | 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; } 43 | 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; } 47 | 48 | * { 49 | box-sizing: border-box; 50 | text-shadow: 0 1px 2px #000; 51 | -webkit-touch-callout: none; 52 | /* iOS Safari */ 53 | -webkit-user-select: none; 54 | /* Safari */ 55 | -khtml-user-select: none; 56 | /* Konqueror HTML */ 57 | -moz-user-select: none; 58 | /* Firefox */ 59 | -ms-user-select: none; 60 | /* Internet Explorer/Edge */ 61 | user-select: none; 62 | /* Non-prefixed version, currently 63 | supported by Chrome and Opera */ } 64 | 65 | ::-webkit-scrollbar { 66 | width: 8px; } 67 | 68 | ::-webkit-scrollbar-button { 69 | display: none; } 70 | 71 | ::-webkit-scrollbar-track { 72 | background-color: rgba(0, 0, 0, 0.5); } 73 | 74 | ::-webkit-scrollbar-track-piece { 75 | background-color: rgba(0, 0, 0, 0); } 76 | 77 | ::-webkit-scrollbar-thumb { 78 | height: 50px; 79 | background-color: #666; 80 | border-radius: 3px; } 81 | 82 | ::-webkit-resizer { 83 | background-color: rgba(0, 0, 0, 0); } 84 | 85 | html, body { 86 | background-color: #111; 87 | margin: 0; 88 | min-height: 100%; 89 | color: #fff; 90 | font-family: 'Open Sans', sans-serif; 91 | font-weight: 400; 92 | /* This sets the base UI scaling. Everything else should be in ems. */ 93 | font-size: 16px; 94 | overflow: hidden; } 95 | 96 | main { 97 | position: relative; 98 | background-color: #000; 99 | background-size: cover; } 100 | 101 | body, main { 102 | position: absolute; 103 | top: 0; 104 | bottom: 0; 105 | width: 100%; } 106 | 107 | header { 108 | -webkit-app-region: drag; 109 | height: 3em; 110 | line-height: 3em; 111 | padding: 0 0 0 1.5em; 112 | overflow: hidden; 113 | background-color: #222; 114 | box-shadow: 0 0 30px 5px rgba(0, 0, 0, 0.8); 115 | border-bottom: solid 1px #000; } 116 | header > div { 117 | flex: 0 1 50%; } 118 | header > div:first-of-type { 119 | font-size: .8em; 120 | letter-spacing: .01em; 121 | opacity: 0.4; 122 | font-weight: 600; 123 | float: left; } 124 | header > div:last-of-type { 125 | text-align: right; 126 | float: right; } 127 | header > div:last-of-type > button { 128 | position: relative; 129 | background-color: transparent; 130 | border: none; 131 | width: 4em; 132 | padding: 1em; 133 | transition: .2s; 134 | opacity: 0.6; 135 | cursor: pointer; } 136 | header > div:last-of-type > button:hover { 137 | opacity: 1; 138 | background-color: #000; } 139 | header > div:last-of-type > button img { 140 | height: 18px; } 141 | 142 | a, button, input { 143 | outline: none; 144 | -webkit-app-region: no-drag; } 145 | 146 | form label { 147 | display: block; 148 | margin-bottom: 5px; } 149 | 150 | form input[type="text"], 151 | form input[type="password"], 152 | form select { 153 | outline: none; 154 | background-color: #fff; 155 | padding: 10px 12px; 156 | border: none; 157 | border-radius: 3px; 158 | width: 100%; 159 | font-size: 16px; 160 | box-shadow: 0 1px 3px #000; 161 | text-shadow: none; } 162 | 163 | form button { 164 | outline: none; 165 | background-color: #555; 166 | color: #fff; 167 | text-align: center; 168 | padding: 10px 15px; 169 | border: none; 170 | border-radius: 3px; 171 | font-size: 16px; 172 | text-shadow: 0 1px 2px #000; 173 | cursor: pointer; 174 | box-shadow: 0 1px 3px #000; 175 | transition: .2s; 176 | font-weight: 400; } 177 | form button:hover { 178 | background-color: #3c3c3c; } 179 | 180 | button.btn { 181 | background-color: #111; 182 | border: none; 183 | padding: 10px 15px; 184 | box-shadow: 0 1px 3px #000; 185 | border-radius: 5px; 186 | font-size: 16px; 187 | color: #fff; 188 | transition: .2s; 189 | cursor: pointer; } 190 | button.btn:hover { 191 | background-color: #333; } 192 | 193 | nav { 194 | float: left; 195 | width: 65%; 196 | padding: 1em 2em; 197 | background: -moz-linear-gradient(left, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); 198 | background: -webkit-linear-gradient(left, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); 199 | background: linear-gradient(to right, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); } 200 | nav > button { 201 | color: #fff; 202 | padding: .5em 1em; 203 | font-size: 1.25em; 204 | background-color: transparent; 205 | border: solid 1px rgba(80, 80, 80, 0.8); 206 | box-shadow: 0 0 5px rgba(150, 150, 150, 0.3); 207 | border-radius: 6px; 208 | cursor: pointer; 209 | transition: 100ms; 210 | margin-right: 20px; } 211 | nav > button:hover { 212 | border: solid 1px #4498ff; 213 | background-color: rgba(20, 20, 20, 0.2); } 214 | nav > button > svg { 215 | height: 32px; 216 | display: inline-block; 217 | fill: #fff; 218 | vertical-align: middle; 219 | margin: -4px 6px 0 0; } 220 | 221 | .fr { 222 | float: right; } 223 | 224 | .notice-fade { 225 | position: absolute; 226 | top: 48px; 227 | left: 0; 228 | right: 0; 229 | bottom: 0; 230 | width: 100%; 231 | height: 100%; 232 | background-color: rgba(0, 0, 0, 0.75); 233 | display: none; 234 | z-index: 500; } 235 | .notice-fade.open { 236 | display: block; } 237 | 238 | .notice { 239 | position: absolute; 240 | top: 50%; 241 | left: 50%; 242 | transform: translate(-50%, -50%); 243 | background-color: #111; 244 | box-shadow: 0 2px 20px #000; 245 | z-index: 1000; 246 | border: solid 3px #000; 247 | padding: 50px; 248 | width: 500px; 249 | border-radius: 3px; 250 | transition: 500ms; 251 | display: none; } 252 | .notice h1 { 253 | font-size: 32px; 254 | margin-bottom: 20px; } 255 | .notice p { 256 | line-height: 26px; 257 | font-size: 16px; 258 | margin-bottom: 15px; } 259 | .notice p:last-of-type { 260 | margin-bottom: 0; } 261 | .notice.open { 262 | display: block; 263 | animation-duration: 250ms; 264 | animation-name: alert; } 265 | 266 | @keyframes alert { 267 | from { 268 | transform: translate(-50%, -50%) scale(0.5); } 269 | to { 270 | transform: translate(-50%, -50%) scale(1); } } 271 | 272 | .settings-form { 273 | position: absolute; 274 | top: 60px; 275 | left: 10px; 276 | width: 500px; 277 | padding: 20px; 278 | background-color: #333; 279 | border-radius: 3px; 280 | box-shadow: 0 0 3px #000; 281 | z-index: 50; 282 | display: none; } 283 | .settings-form.open { 284 | display: block; } 285 | .settings-form label { 286 | display: block; 287 | color: #888; 288 | font-size: 14px; 289 | text-transform: uppercase; 290 | font-weight: 400; 291 | margin-top: 15px; } 292 | .settings-form p { 293 | font-size: 13px; 294 | color: #aaa; 295 | margin-bottom: 20px; 296 | line-height: 20px; } 297 | .settings-form h1 { 298 | font-size: 32px; 299 | font-weight: 300; 300 | margin-bottom: 15px; } 301 | .settings-form h2 { 302 | font-size: 20px; 303 | margin-bottom: 10px; } 304 | .settings-form strong { 305 | font-weight: 600; } 306 | .settings-form .ac-close { 307 | float: right; 308 | padding: 5px 10px; 309 | border-radius: 50px; } 310 | .settings-form .ac-close img { 311 | vertical-align: middle; } 312 | .settings-form .ac-row-flex2 { 313 | display: flex; } 314 | .settings-form .ac-row-flex2 > div { 315 | flex: 0 1 50%; } 316 | .settings-form .ac-row-flex2 > div:first-of-type { 317 | margin-right: 5px; } 318 | .settings-form .ac-row-flex2 > div:first-of-type > input { 319 | width: 160%; } 320 | .settings-form .ac-row-flex2 > div:last-of-type { 321 | display: flex; 322 | justify-content: flex-end; 323 | margin-left: 5px; } 324 | .settings-form .ac-row-small input { 325 | width: 30%; } 326 | 327 | .lodestone-news { 328 | width: 65%; 329 | overflow-y: auto; 330 | position: fixed; 331 | left: 0; 332 | top: 8.15em; 333 | bottom: 0; 334 | display: none; 335 | padding-left: 2em; 336 | direction: rtl; 337 | background: -moz-linear-gradient(left, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); 338 | background: -webkit-linear-gradient(left, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); 339 | background: linear-gradient(to right, #111111 0%, #111111 5%, rgba(17, 17, 17, 0.7) 60%, rgba(17, 17, 17, 0) 100%); } 340 | .lodestone-news.open { 341 | display: block; } 342 | .lodestone-news > div { 343 | direction: ltr; 344 | display: flex; 345 | margin-bottom: 20px; } 346 | .lodestone-news > div > div:first-of-type { 347 | flex: 0 1 200px; } 348 | .lodestone-news > div > div:last-of-type { 349 | flex: 0 1 calc(100% - 200px); } 350 | .lodestone-news > div img { 351 | width: 180px; 352 | border-radius: 5px; } 353 | .lodestone-news > div h2 { 354 | font-size: 20px; 355 | font-weight: 300; 356 | margin-bottom: 5px; } 357 | .lodestone-news > div h2 a { 358 | color: #faff8f; 359 | text-decoration: none; } 360 | .lodestone-news > div h2 a:hover { 361 | text-decoration: underline; } 362 | .lodestone-news > div small { 363 | font-size: 13px; 364 | text-transform: uppercase; 365 | opacity: 0.3; } 366 | 367 | .add-character-form { 368 | position: absolute; 369 | top: 50%; 370 | left: 50%; 371 | transform: translate(-50%, -50%); 372 | box-shadow: 0 0 250px 40px #000; 373 | right: 410px; 374 | width: 500px; 375 | padding: 40px; 376 | background-color: #333; 377 | border-radius: 5px; 378 | border: solid 1px #000; 379 | z-index: 50; 380 | display: none; } 381 | .add-character-form.open { 382 | display: block; } 383 | .add-character-form label { 384 | display: block; 385 | color: #888; 386 | font-size: 14px; 387 | text-transform: uppercase; 388 | font-weight: 400; 389 | margin-top: 15px; } 390 | .add-character-form p { 391 | font-size: 13px; 392 | color: #aaa; 393 | margin-bottom: 20px; 394 | line-height: 20px; } 395 | .add-character-form h1 { 396 | font-size: 32px; 397 | font-weight: 300; 398 | margin-bottom: 15px; } 399 | .add-character-form h2 { 400 | font-size: 20px; 401 | margin-bottom: 10px; } 402 | .add-character-form div { 403 | margin-bottom: 10px; } 404 | .add-character-form .btn-save { 405 | font-weight: 600; 406 | font-size: 20px; } 407 | .add-character-form .ac-close { 408 | float: right; 409 | padding: 5px 10px; 410 | border-radius: 50px; } 411 | .add-character-form .ac-close img { 412 | vertical-align: middle; } 413 | .add-character-form .ac-row-flex2 { 414 | display: flex; } 415 | .add-character-form .ac-row-flex2 > div { 416 | flex: 0 1 50%; } 417 | .add-character-form .ac-row-flex2 > div:first-of-type { 418 | margin-right: 5px; } 419 | .add-character-form .ac-row-flex2 > div:last-of-type { 420 | margin-left: 5px; } 421 | .add-character-form .ac-row-small input { 422 | width: 30%; } 423 | 424 | .character-list { 425 | width: 35%; 426 | position: fixed; 427 | top: 3em; 428 | bottom: 0; 429 | right: 0; 430 | background-color: transparent; 431 | background: -moz-linear-gradient(left, rgba(0, 0, 0, 0) 10%, black 95%); 432 | background: -webkit-linear-gradient(left, rgba(0, 0, 0, 0) 10%, black 95%); 433 | background: linear-gradient(to right, rgba(0, 0, 0, 0) 10%, black 95%); } 434 | .character-list .cl-add { 435 | padding: 1em 2em; 436 | text-align: right; } 437 | .character-list .cl-add button { 438 | color: #fff; 439 | padding: 0 15px; 440 | height: 50px; 441 | line-height: 50px; 442 | font-size: 20px; 443 | background-color: transparent; 444 | border: solid 1px rgba(80, 80, 80, 0.8); 445 | box-shadow: 0 0 5px rgba(150, 150, 150, 0.3); 446 | border-radius: 6px; 447 | cursor: pointer; 448 | transition: 100ms; } 449 | .character-list .cl-add button:hover { 450 | border: solid 1px #faff8f; 451 | background-color: rgba(255, 250, 137, 0.2); } 452 | .character-list .cl-add button svg { 453 | height: 32px; 454 | display: inline-block; 455 | fill: #fff; 456 | vertical-align: middle; 457 | margin: -4px 6px 0 0; } 458 | .character-list .cl-list { 459 | text-align: right; 460 | padding: 0 2em 0 .5em; 461 | position: fixed; 462 | top: 8.25em; 463 | bottom: 0; 464 | right: 0; 465 | width: 35%; 466 | overflow-y: auto; } 467 | .character-list .cl-list > button { 468 | width: 100%; 469 | display: flex; 470 | margin-bottom: 1.5em; 471 | flex-direction: row; 472 | justify-content: flex-end; 473 | background-color: transparent; 474 | border: none; 475 | height: 5em; 476 | line-height: 5em; 477 | color: #fff; 478 | border-radius: 5em; 479 | padding: 0; 480 | transition: 100ms; } 481 | .character-list .cl-list > button > img { 482 | height: 5em; 483 | border-radius: 5em; 484 | box-shadow: 0 1px 6px #000; 485 | content: url("https://xivapi.com/launcher/faceless.png"); } 486 | .character-list .cl-list > button > div { 487 | margin: 0 1em 0 0; 488 | display: flex; } 489 | .character-list .cl-list > button > div h4 { 490 | font-size: 1.5em; 491 | font-weight: 300; 492 | white-space: nowrap; 493 | text-overflow: ellipsis; 494 | color: transparent; 495 | text-shadow: 0 0 0.8em rgba(255, 255, 255, 0.8); } 496 | .character-list .cl-list > button > div svg { 497 | fill: #fff; 498 | height: 1.7em; 499 | margin: 1.5em .5em 0 0; 500 | opacity: 0.2; } 501 | .character-list .cl-list > button:hover { 502 | cursor: pointer; 503 | background-color: rgba(67, 72, 87, 0.3); } 504 | 505 | .character-view { 506 | position: absolute; 507 | top: 50%; 508 | left: 50%; 509 | transform: translate(-50%, -50%); 510 | padding: 50px; 511 | width: 500px; 512 | z-index: 500; 513 | display: none; 514 | border-radius: 8px; 515 | box-shadow: 0 1px 20px #000; 516 | border: solid 1px #000; 517 | background-color: rgba(25, 25, 25, 0.85); } 518 | .character-view.open { 519 | display: block; } 520 | .character-view > div:nth-of-type(1) { 521 | text-align: center; } 522 | .character-view > div:nth-of-type(2) { 523 | text-align: center; } 524 | .character-view > div:nth-of-type(3) { 525 | text-align: center; } 526 | .character-view .avatar { 527 | height: 96px; 528 | border-radius: 96px; 529 | box-shadow: 0 1px 8px #000; 530 | margin-bottom: 15px; } 531 | .character-view h1 { 532 | font-size: 26px; 533 | font-weight: 300; 534 | margin-bottom: 5px; } 535 | .character-view small { 536 | text-transform: uppercase; 537 | color: #666; 538 | font-size: 14px; } 539 | .character-view hr { 540 | border: none; 541 | height: 2px; 542 | background-color: #333; 543 | margin: 40px 20px; } 544 | .character-view .btn-start-game { 545 | background-color: #009e2a; 546 | color: #fff; 547 | height: 50px; 548 | line-height: 50px; 549 | padding: 0 20px; 550 | font-size: 19px; 551 | font-weight: 400; 552 | border: none; 553 | border-radius: 8px; 554 | box-shadow: 0 1px 5px #000; 555 | cursor: pointer; 556 | transition: 100ms; } 557 | .character-view .btn-start-game:hover { 558 | background-color: #008727; } 559 | .character-view .btn-delete-character { 560 | color: #e53939; 561 | background-color: transparent; 562 | border: none; 563 | text-transform: uppercase; 564 | font-weight: 500; 565 | font-size: 14px; 566 | cursor: pointer; } 567 | .character-view .btn-delete-character:hover { 568 | text-decoration: underline; } 569 | .character-view .otp2 { 570 | line-height: 50px; 571 | padding: 0 20px; 572 | margin-bottom: 15px; 573 | background-color: #333; 574 | border: solid 1px #000; 575 | font-size: 23px; 576 | font-weight: 600; 577 | letter-spacing: 1px; 578 | color: #aaa; 579 | width: 165px; 580 | text-align: center; 581 | border-radius: 8px; } 582 | 583 | .character-fade { 584 | position: absolute; 585 | top: 48px; 586 | left: 0; 587 | right: 0; 588 | bottom: 0; 589 | width: 100%; 590 | height: 100%; 591 | z-index: 495; 592 | background-color: rgba(0, 0, 0, 0.75); 593 | display: none; } 594 | .character-fade.open { 595 | display: block; } 596 | 597 | 598 | /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vUmVzZXQuc2NzcyIsIndlYnBhY2s6Ly8vZGVidWcuc2NzcyIsIndlYnBhY2s6Ly8vVUkuc2NzcyIsIndlYnBhY2s6Ly8vU2V0dGluZ3NGb3JtLnNjc3MiLCJ3ZWJwYWNrOi8vL0xvZGVzdG9uZU5ld3Muc2NzcyIsIndlYnBhY2s6Ly8vQ2hhcmFjdGVyQWRkRm9ybS5zY3NzIiwid2VicGFjazovLy9DaGFyYWN0ZXJMaXN0LnNjc3MiLCJ3ZWJwYWNrOi8vL0NoYXJhY3RlclZpZXcuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0NDR0M7QURFRDs7Ozs7Ozs7Ozs7OztFQWFDLFNBQVM7RUFDVCxVQUFVO0VBQ1YsU0FBUztFQUNULGVBQWU7RUFDZixhQUFhO0VBQ2Isd0JBQXdCOztBQUV6QjtBQUNBOztFQUVDLGNBQWM7O0FBRWY7RUFDQyxjQUFjOztBQUVmO0VBQ0MsZ0JBQWdCOztBQUVqQjtFQUNDLFlBQVk7O0FBRWI7O0VBRUMsV0FBVztFQUNYLGFBQWE7O0FBRWQ7RUFDQyx5QkFBeUI7RUFDekIsaUJBQWlCOztBRWxDbEI7RUFDSSxzQkFBc0I7RUFDdEIsMkJBQTJCO0VBRTNCLDJCQUEyQjtFQUFFO0VBQzdCLHlCQUF5QjtFQUFFO0VBQzNCLHdCQUF3QjtFQUFFO0VBQzFCLHNCQUFzQjtFQUFFO0VBQ3hCLHFCQUFxQjtFQUFFO0VBQ3ZCLGlCQUFpQjtFQUFFO2lFRHlDMEMsRUN4Q0M7O0FBSWxFO0VBQ0ksVUFBVTs7QUFFZDtFQUNJLGFBQVk7O0FBRWhCO0VBQ0ksb0NBQWdDOztBQUVwQztFQUNJLGtDQUErQjs7QUFFbkM7RUFDSSxZQUFZO0VBQ1osc0JBQXNCO0VBQ3RCLGtCQUFrQjs7QUFFdEI7RUFDSSxrQ0FBK0I7O0FBR25DO0VBQ0ksc0JBQXNCO0VBQ3RCLFNBQVM7RUFDVCxnQkFBZ0I7RUFDaEIsV0FBVztFQUNYLG9DQUFvQztFQUNwQyxnQkFBZ0I7RUFDaEI7RUFDQSxlQUFlO0VBQ2YsZ0JBQWdCOztBQUdwQjtFQUNJLGtCQUFrQjtFQUNsQixzQkFBc0I7RUFDdEIsc0JBQXNCOztBQUUxQjtFQUNJLGtCQUFrQjtFQUNsQixNQUFLO0VBQ0wsU0FBUTtFQUNSLFdBQVU7O0FBR2Q7RUFDSSx3QkFBd0I7RUFDeEIsV0FBVztFQUNYLGdCQUFlO0VBQ2Ysb0JBQW9CO0VBQ3BCLGdCQUFnQjtFQUNoQixzQkFBc0I7RUFDdEIsMkNBQXdDO0VBQ3hDLDZCQUE2QjtFQVJqQztJQVdRLGFBQWE7SUFYckI7TUFjWSxlQUFlO01BQ2YscUJBQXFCO01BQ3JCLFlBQVk7TUFDWixnQkFBZ0I7TUFDaEIsV0FBVTtJQWxCdEI7TUFzQlksaUJBQWlCO01BQ2pCLFlBQVc7TUF2QnZCO1FBMEJnQixrQkFBa0I7UUFDbEIsNkJBQTZCO1FBQzdCLFlBQVk7UUFDWixVQUFVO1FBQ1YsWUFBWTtRQUNaLGVBQWU7UUFDZixZQUFZO1FBQ1osZUFBZTtRQWpDL0I7VUFvQ29CLFVBQVU7VUFDVixzQkFBc0I7UUFyQzFDO1VBeUNvQixZQUFZOztBQVFoQztFQUNJLGFBQWE7RUFDYiwyQkFBMkI7O0FBRy9CO0VBRVEsY0FBYztFQUNkLGtCQUFrQjs7QUFIMUI7OztFQVNRLGFBQWE7RUFDYixzQkFBc0I7RUFDdEIsa0JBQWtCO0VBQ2xCLFlBQVk7RUFDWixrQkFBa0I7RUFDbEIsV0FBVztFQUNYLGVBQWU7RUFDZiwwQkFBMEI7RUFDMUIsaUJBQWlCOztBQWpCekI7RUFxQlEsYUFBYTtFQUNiLHNCQUFzQjtFQUN0QixXQUFXO0VBQ1gsa0JBQWtCO0VBQ2xCLGtCQUFrQjtFQUNsQixZQUFZO0VBQ1osa0JBQWtCO0VBQ2xCLGVBQWU7RUFDZiwyQkFBMkI7RUFDM0IsZUFBZTtFQUNmLDBCQUEwQjtFQUMxQixlQUFlO0VBQ2YsZ0JBQWdCO0VBakN4QjtJQW9DWSx5QkFBbUM7O0FBSy9DO0VBQ0ksc0JBQXNCO0VBQ3RCLFlBQVk7RUFDWixrQkFBa0I7RUFDbEIsMEJBQTBCO0VBQzFCLGtCQUFrQjtFQUNsQixlQUFlO0VBQ2YsV0FBVztFQUNYLGVBQWU7RUFDZixlQUFlO0VBVG5CO0lBWVEsc0JBQXNCOztBQUk5QjtFQUNJLFdBQVU7RUFDVixVQUFTO0VBQ1QsZ0JBQWdCO0VBQ2hCLG1IQUErSDtFQUMvSCxzSEFBK0g7RUFDL0gsa0hBQTJIO0VBTi9IO0lBU1EsV0FBVztJQUNYLGlCQUFpQjtJQUNqQixpQkFBaUI7SUFDakIsNkJBQTZCO0lBQzdCLHVDQUF1QztJQUN2Qyw0Q0FBNEM7SUFDNUMsa0JBQWtCO0lBQ2xCLGVBQWU7SUFDZixpQkFBaUI7SUFDakIsa0JBQWtCO0lBbEIxQjtNQXFCWSx5QkFBbUM7TUFDbkMsdUNBQXVDO0lBdEJuRDtNQTBCWSxZQUFZO01BQ1oscUJBQXFCO01BQ3JCLFVBQVU7TUFDVixzQkFBc0I7TUFDdEIsb0JBQW9COztBQUtoQztFQUNJLFlBQVk7O0FBR2hCO0VBQ0ksa0JBQWtCO0VBQ2xCLFNBQVM7RUFDVCxPQUFPO0VBQ1AsUUFBUTtFQUNSLFNBQVM7RUFDVCxXQUFXO0VBQ1gsWUFBWTtFQUNaLHFDQUFrQztFQUNsQyxhQUFhO0VBQ2IsWUFBWTtFQVZoQjtJQWFRLGNBQWM7O0FBR3RCO0VBQ0ksa0JBQWtCO0VBQ2xCLFFBQVE7RUFDUixTQUFTO0VBQ1QsZ0NBQWdDO0VBQ2hDLHNCQUFzQjtFQUN0QiwyQkFBMkI7RUFDM0IsYUFBYTtFQUNiLHNCQUFzQjtFQUN0QixhQUFhO0VBQ2IsWUFBWTtFQUNaLGtCQUFrQjtFQUNsQixpQkFBaUI7RUFFakIsYUFBYTtFQWRqQjtJQWlCUSxlQUFlO0lBQ2YsbUJBQW1CO0VBbEIzQjtJQXNCUSxpQkFBaUI7SUFDakIsZUFBZTtJQUNmLG1CQUFtQjtJQXhCM0I7TUEyQlksZ0JBQWdCO0VBM0I1QjtJQWdDUSxjQUFjO0lBQ2QseUJBQXlCO0lBQ3pCLHFCQUFxQjs7QUFFckI7RUFDSTtJQUFPLDJDQUEyQztFQUNsRDtJQUFLLHlDQUEyQzs7QUNuUjVEO0VBQ0ksa0JBQWtCO0VBQ2xCLFNBQVM7RUFDVCxVQUFVO0VBQ1YsWUFBWTtFQUNaLGFBQWE7RUFDYixzQkFBc0I7RUFDdEIsa0JBQWtCO0VBQ2xCLHdCQUF3QjtFQUN4QixXQUFXO0VBRVgsYUFBYTtFQVhqQjtJQWFRLGNBQWM7RUFidEI7SUFpQlEsY0FBYztJQUNkLFdBQVc7SUFDWCxlQUFlO0lBQ2YseUJBQXlCO0lBQ3pCLGdCQUFnQjtJQUNoQixnQkFBZ0I7RUF0QnhCO0lBMEJRLGVBQWU7SUFDZixXQUFXO0lBRVgsbUJBQW1CO0lBQ25CLGlCQUFpQjtFQTlCekI7SUFtQ1EsZUFBZTtJQUNmLGdCQUFnQjtJQUNoQixtQkFBbUI7RUFyQzNCO0lBeUNRLGVBQWU7SUFDZixtQkFBbUI7RUExQzNCO0lBOENRLGdCQUFnQjtFQTlDeEI7SUFrRFEsWUFBWTtJQUNaLGlCQUFpQjtJQUNqQixtQkFBbUI7SUFwRDNCO01BdURZLHNCQUFzQjtFQXZEbEM7SUE0RFEsYUFBYTtJQTVEckI7TUErRFksYUFBYTtNQS9EekI7UUFrRWdCLGlCQUFpQjtRQWxFakM7VUFvRW9CLFdBQVc7TUFwRS9CO1FBeUVnQixhQUFhO1FBQ2IseUJBQXlCO1FBQ3pCLGdCQUFnQjtFQTNFaEM7SUFrRlksVUFBVTs7QUM3RXRCO0VBQ0ksVUFBVTtFQUNWLGdCQUFnQjtFQUNoQixlQUFlO0VBQ2YsT0FBTztFQUNQLFdBQVc7RUFDWCxTQUFTO0VBQ1QsYUFBYTtFQUNiLGlCQUFpQjtFQUNqQixjQUFjO0VBRWQsbUhBQStIO0VBQy9ILHNIQUErSDtFQUMvSCxrSEFBMkg7RUFiL0g7SUFnQlEsY0FBYztFQWhCdEI7SUFvQlEsY0FBYztJQUNkLGFBQWE7SUFDYixtQkFBbUI7SUF0QjNCO01BeUJZLGVBQWU7SUF6QjNCO01BNkJZLDRCQUE0QjtJQTdCeEM7TUFpQ1ksWUFBWTtNQUNaLGtCQUFrQjtJQWxDOUI7TUFzQ1ksZUFBZTtNQUNmLGdCQUFnQjtNQUNoQixrQkFBa0I7TUF4QzlCO1FBMkNnQixjQUFjO1FBQ2QscUJBQXFCO1FBNUNyQztVQStDb0IsMEJBQTBCO0lBL0M5QztNQXFEWSxlQUFlO01BQ2YseUJBQXlCO01BQ3pCLFlBQVk7O0FDNUR4QjtFQUNJLGtCQUFrQjtFQUNsQixRQUFRO0VBQ1IsU0FBUztFQUNULGdDQUFnQztFQUNoQywrQkFBK0I7RUFFL0IsWUFBWTtFQUNaLFlBQVk7RUFDWixhQUFhO0VBQ2Isc0JBQXNCO0VBQ3RCLGtCQUFrQjtFQUNsQixzQkFBc0I7RUFDdEIsV0FBVztFQUNYLGFBQWE7RUFkakI7SUFpQlEsY0FBYztFQWpCdEI7SUFxQlEsY0FBYztJQUNkLFdBQVc7SUFDWCxlQUFlO0lBQ2YseUJBQXlCO0lBQ3pCLGdCQUFnQjtJQUNoQixnQkFBZ0I7RUExQnhCO0lBOEJRLGVBQWU7SUFDZixXQUFXO0lBQ1gsbUJBQW1CO0lBQ25CLGlCQUFpQjtFQWpDekI7SUFxQ1EsZUFBZTtJQUNmLGdCQUFnQjtJQUNoQixtQkFBbUI7RUF2QzNCO0lBMkNRLGVBQWU7SUFDZixtQkFBbUI7RUE1QzNCO0lBZ0RRLG1CQUFtQjtFQWhEM0I7SUFvRFEsZ0JBQWdCO0lBQ2hCLGVBQWU7RUFyRHZCO0lBeURRLFlBQVk7SUFDWixpQkFBaUI7SUFDakIsbUJBQW1CO0lBM0QzQjtNQThEWSxzQkFBc0I7RUE5RGxDO0lBbUVRLGFBQWE7SUFuRXJCO01Bc0VZLGFBQWE7TUF0RXpCO1FBeUVnQixpQkFBaUI7TUF6RWpDO1FBNkVnQixnQkFBZ0I7RUE3RWhDO0lBb0ZZLFVBQVU7O0FDL0V0QjtFQUNJLFVBQVU7RUFDVixlQUFlO0VBQ2YsUUFBUTtFQUNSLFNBQVE7RUFDUixRQUFPO0VBRVAsNkJBQTZCO0VBQzdCLHVFQUE0RTtFQUM1RSwwRUFBOEU7RUFDOUUsc0VBQTBFO0VBVjlFO0lBYVEsZ0JBQWdCO0lBQ2hCLGlCQUFpQjtJQWR6QjtNQWlCWSxXQUFXO01BQ1gsZUFBZTtNQUNmLFlBQVk7TUFDWixpQkFBaUI7TUFDakIsZUFBZTtNQUNmLDZCQUE2QjtNQUM3Qix1Q0FBdUM7TUFDdkMsNENBQTRDO01BQzVDLGtCQUFrQjtNQUNsQixlQUFlO01BQ2YsaUJBQWlCO01BM0I3QjtRQThCZ0IseUJBQXdDO1FBQ3hDLDBDQUEwQztNQS9CMUQ7UUFtQ2dCLFlBQVk7UUFDWixxQkFBcUI7UUFDckIsVUFBVTtRQUNWLHNCQUFzQjtRQUN0QixvQkFBb0I7RUF2Q3BDO0lBNkNRLGlCQUFpQjtJQUNqQixxQkFBcUI7SUFDckIsZUFBZTtJQUNmLFdBQVU7SUFDVixTQUFRO0lBQ1IsUUFBTztJQUNQLFVBQVM7SUFDVCxnQkFBZ0I7SUFwRHhCO01BdURZLFdBQVc7TUFDWCxhQUFhO01BQ2Isb0JBQW9CO01BQ3BCLG1CQUFtQjtNQUNuQix5QkFBeUI7TUFDekIsNkJBQTZCO01BQzdCLFlBQVk7TUFDWixXQUFXO01BQ1gsZ0JBQWdCO01BQ2hCLFdBQVc7TUFDWCxrQkFBa0I7TUFDbEIsVUFBVTtNQUNWLGlCQUFpQjtNQW5FN0I7UUFzRWdCLFdBQVc7UUFDWCxrQkFBa0I7UUFDbEIsMEJBQTBCO1FBS3RCLHdEQUF3RDtNQTdFNUU7UUFrRmdCLGlCQUFpQjtRQUNqQixhQUFhO1FBbkY3QjtVQXNGb0IsZ0JBQWdCO1VBQ2hCLGdCQUFnQjtVQUNoQixtQkFBa0I7VUFDbEIsdUJBQXVCO1VBR3JCLGtCQUFrQjtVQUNsQiwrQ0FBMEM7UUE3RmhFO1VBa0dvQixVQUFVO1VBQ1YsYUFBYTtVQUNiLHNCQUFzQjtVQUN0QixZQUFZO01BckdoQztRQTBHZ0IsZUFBZTtRQUNmLHVDQUF1Qzs7QUM3R3ZEO0VBQ0ksa0JBQWtCO0VBQ2xCLFFBQVE7RUFBRSxTQUFTO0VBQ25CLGdDQUFnQztFQUVoQyxhQUFhO0VBQ2IsWUFBWTtFQUNaLFlBQVk7RUFDWixhQUFhO0VBQ2Isa0JBQWtCO0VBQ2xCLDJCQUEyQjtFQUMzQixzQkFBc0I7RUFDdEIsd0NBQXdDO0VBWjVDO0lBZVEsY0FBYztFQWZ0QjtJQW1CUSxrQkFBa0I7RUFuQjFCO0lBdUJRLGtCQUFrQjtFQXZCMUI7SUEyQlEsa0JBQWtCO0VBM0IxQjtJQStCUSxZQUFZO0lBQ1osbUJBQW1CO0lBQ25CLDBCQUEwQjtJQUMxQixtQkFBbUI7RUFsQzNCO0lBc0NRLGVBQWU7SUFDZixnQkFBZ0I7SUFDaEIsa0JBQWtCO0VBeEMxQjtJQTRDUSx5QkFBeUI7SUFDekIsV0FBVztJQUNYLGVBQWU7RUE5Q3ZCO0lBa0RRLFlBQVk7SUFDWixXQUFXO0lBQ1gsc0JBQXNCO0lBQ3RCLGlCQUFpQjtFQXJEekI7SUF5RFEseUJBQXlCO0lBQ3pCLFdBQVc7SUFDWCxZQUFZO0lBQ1osaUJBQWlCO0lBQ2pCLGVBQWU7SUFDZixlQUFlO0lBQ2YsZ0JBQWdCO0lBQ2hCLFlBQVk7SUFDWixrQkFBa0I7SUFDbEIsMEJBQTBCO0lBQzFCLGVBQWU7SUFDZixpQkFBaUI7SUFwRXpCO01BdUVZLHlCQUF5QjtFQXZFckM7SUE0RVEsY0FBYztJQUNkLDZCQUE2QjtJQUM3QixZQUFZO0lBQ1oseUJBQXlCO0lBQ3pCLGdCQUFnQjtJQUNoQixlQUFlO0lBQ2YsZUFBZTtJQWxGdkI7TUFxRlksMEJBQTBCO0VBckZ0QztJQTBGUSxpQkFBaUI7SUFDakIsZUFBZTtJQUNmLG1CQUFtQjtJQUNuQixzQkFBc0I7SUFDdEIsc0JBQXNCO0lBQ3RCLGVBQWU7SUFDZixnQkFBZ0I7SUFDaEIsbUJBQW1CO0lBQ25CLFdBQVc7SUFDWCxZQUFZO0lBQ1osa0JBQWtCO0lBQ2xCLGtCQUFrQjs7QUFHMUI7RUFDSSxrQkFBa0I7RUFDbEIsU0FBUztFQUNULE9BQU87RUFDUCxRQUFRO0VBQ1IsU0FBUztFQUNULFdBQVc7RUFDWCxZQUFZO0VBQ1osWUFBWTtFQUNaLHFDQUFrQztFQUNsQyxhQUFhO0VBVmpCO0lBYVEsY0FBYyIsImZpbGUiOiJjc3MvYXBwLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIi8qIGh0dHA6Ly9tZXllcndlYi5jb20vZXJpYy90b29scy9jc3MvcmVzZXQvXG4gICB2Mi4wIHwgMjAxMTAxMjZcbiAgIExpY2Vuc2U6IG5vbmUgKHB1YmxpYyBkb21haW4pXG4qL1xuXG5odG1sLCBib2R5LCBkaXYsIHNwYW4sIGFwcGxldCwgb2JqZWN0LCBpZnJhbWUsXG5oMSwgaDIsIGgzLCBoNCwgaDUsIGg2LCBwLCBibG9ja3F1b3RlLCBwcmUsXG5hLCBhYmJyLCBhY3JvbnltLCBhZGRyZXNzLCBiaWcsIGNpdGUsIGNvZGUsXG5kZWwsIGRmbiwgZW0sIGltZywgaW5zLCBrYmQsIHEsIHMsIHNhbXAsXG5zbWFsbCwgc3RyaWtlLCBzdHJvbmcsIHN1Yiwgc3VwLCB0dCwgdmFyLFxuYiwgdSwgaSwgY2VudGVyLFxuZGwsIGR0LCBkZCwgb2wsIHVsLCBsaSxcbmZpZWxkc2V0LCBmb3JtLCBsYWJlbCwgbGVnZW5kLFxudGFibGUsIGNhcHRpb24sIHRib2R5LCB0Zm9vdCwgdGhlYWQsIHRyLCB0aCwgdGQsXG5hcnRpY2xlLCBhc2lkZSwgY2FudmFzLCBkZXRhaWxzLCBlbWJlZCxcbmZpZ3VyZSwgZmlnY2FwdGlvbiwgZm9vdGVyLCBoZWFkZXIsIGhncm91cCxcbm1lbnUsIG5hdiwgb3V0cHV0LCBydWJ5LCBzZWN0aW9uLCBzdW1tYXJ5LFxudGltZSwgbWFyaywgYXVkaW8sIHZpZGVvIHtcblx0bWFyZ2luOiAwO1xuXHRwYWRkaW5nOiAwO1xuXHRib3JkZXI6IDA7XG5cdGZvbnQtc2l6ZTogMTAwJTtcblx0Zm9udDogaW5oZXJpdDtcblx0dmVydGljYWwtYWxpZ246IGJhc2VsaW5lO1xufVxuLyogSFRNTDUgZGlzcGxheS1yb2xlIHJlc2V0IGZvciBvbGRlciBicm93c2VycyAqL1xuYXJ0aWNsZSwgYXNpZGUsIGRldGFpbHMsIGZpZ2NhcHRpb24sIGZpZ3VyZSxcbmZvb3RlciwgaGVhZGVyLCBoZ3JvdXAsIG1lbnUsIG5hdiwgc2VjdGlvbiB7XG5cdGRpc3BsYXk6IGJsb2NrO1xufVxuYm9keSB7XG5cdGxpbmUtaGVpZ2h0OiAxO1xufVxub2wsIHVsIHtcblx0bGlzdC1zdHlsZTogbm9uZTtcbn1cbmJsb2NrcXVvdGUsIHEge1xuXHRxdW90ZXM6IG5vbmU7XG59XG5ibG9ja3F1b3RlOmJlZm9yZSwgYmxvY2txdW90ZTphZnRlcixcbnE6YmVmb3JlLCBxOmFmdGVyIHtcblx0Y29udGVudDogJyc7XG5cdGNvbnRlbnQ6IG5vbmU7XG59XG50YWJsZSB7XG5cdGJvcmRlci1jb2xsYXBzZTogY29sbGFwc2U7XG5cdGJvcmRlci1zcGFjaW5nOiAwO1xufSIsIi8qIGh0dHA6Ly9tZXllcndlYi5jb20vZXJpYy90b29scy9jc3MvcmVzZXQvXG4gICB2Mi4wIHwgMjAxMTAxMjZcbiAgIExpY2Vuc2U6IG5vbmUgKHB1YmxpYyBkb21haW4pXG4qL1xuaHRtbCwgYm9keSwgZGl2LCBzcGFuLCBhcHBsZXQsIG9iamVjdCwgaWZyYW1lLFxuaDEsIGgyLCBoMywgaDQsIGg1LCBoNiwgcCwgYmxvY2txdW90ZSwgcHJlLFxuYSwgYWJiciwgYWNyb255bSwgYWRkcmVzcywgYmlnLCBjaXRlLCBjb2RlLFxuZGVsLCBkZm4sIGVtLCBpbWcsIGlucywga2JkLCBxLCBzLCBzYW1wLFxuc21hbGwsIHN0cmlrZSwgc3Ryb25nLCBzdWIsIHN1cCwgdHQsIHZhcixcbmIsIHUsIGksIGNlbnRlcixcbmRsLCBkdCwgZGQsIG9sLCB1bCwgbGksXG5maWVsZHNldCwgZm9ybSwgbGFiZWwsIGxlZ2VuZCxcbnRhYmxlLCBjYXB0aW9uLCB0Ym9keSwgdGZvb3QsIHRoZWFkLCB0ciwgdGgsIHRkLFxuYXJ0aWNsZSwgYXNpZGUsIGNhbnZhcywgZGV0YWlscywgZW1iZWQsXG5maWd1cmUsIGZpZ2NhcHRpb24sIGZvb3RlciwgaGVhZGVyLCBoZ3JvdXAsXG5tZW51LCBuYXYsIG91dHB1dCwgcnVieSwgc2VjdGlvbiwgc3VtbWFyeSxcbnRpbWUsIG1hcmssIGF1ZGlvLCB2aWRlbyB7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMDtcbiAgYm9yZGVyOiAwO1xuICBmb250LXNpemU6IDEwMCU7XG4gIGZvbnQ6IGluaGVyaXQ7XG4gIHZlcnRpY2FsLWFsaWduOiBiYXNlbGluZTsgfVxuXG4vKiBIVE1MNSBkaXNwbGF5LXJvbGUgcmVzZXQgZm9yIG9sZGVyIGJyb3dzZXJzICovXG5hcnRpY2xlLCBhc2lkZSwgZGV0YWlscywgZmlnY2FwdGlvbiwgZmlndXJlLFxuZm9vdGVyLCBoZWFkZXIsIGhncm91cCwgbWVudSwgbmF2LCBzZWN0aW9uIHtcbiAgZGlzcGxheTogYmxvY2s7IH1cblxuYm9keSB7XG4gIGxpbmUtaGVpZ2h0OiAxOyB9XG5cbm9sLCB1bCB7XG4gIGxpc3Qtc3R5bGU6IG5vbmU7IH1cblxuYmxvY2txdW90ZSwgcSB7XG4gIHF1b3Rlczogbm9uZTsgfVxuXG5ibG9ja3F1b3RlOmJlZm9yZSwgYmxvY2txdW90ZTphZnRlcixcbnE6YmVmb3JlLCBxOmFmdGVyIHtcbiAgY29udGVudDogJyc7XG4gIGNvbnRlbnQ6IG5vbmU7IH1cblxudGFibGUge1xuICBib3JkZXItY29sbGFwc2U6IGNvbGxhcHNlO1xuICBib3JkZXItc3BhY2luZzogMDsgfVxuXG4qIHtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbiAgdGV4dC1zaGFkb3c6IDAgMXB4IDJweCAjMDAwO1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC8qIGlPUyBTYWZhcmkgKi9cbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLyogU2FmYXJpICovXG4gIC1raHRtbC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLyogS29ucXVlcm9yIEhUTUwgKi9cbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLyogRmlyZWZveCAqL1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC8qIEludGVybmV0IEV4cGxvcmVyL0VkZ2UgKi9cbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC8qIE5vbi1wcmVmaXhlZCB2ZXJzaW9uLCBjdXJyZW50bHlcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdXBwb3J0ZWQgYnkgQ2hyb21lIGFuZCBPcGVyYSAqLyB9XG5cbjo6LXdlYmtpdC1zY3JvbGxiYXIge1xuICB3aWR0aDogOHB4OyB9XG5cbjo6LXdlYmtpdC1zY3JvbGxiYXItYnV0dG9uIHtcbiAgZGlzcGxheTogbm9uZTsgfVxuXG46Oi13ZWJraXQtc2Nyb2xsYmFyLXRyYWNrIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgwLCAwLCAwLCAwLjUpOyB9XG5cbjo6LXdlYmtpdC1zY3JvbGxiYXItdHJhY2stcGllY2Uge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDApOyB9XG5cbjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWIge1xuICBoZWlnaHQ6IDUwcHg7XG4gIGJhY2tncm91bmQtY29sb3I6ICM2NjY7XG4gIGJvcmRlci1yYWRpdXM6IDNweDsgfVxuXG46Oi13ZWJraXQtcmVzaXplciB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMCk7IH1cblxuaHRtbCwgYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6ICMxMTE7XG4gIG1hcmdpbjogMDtcbiAgbWluLWhlaWdodDogMTAwJTtcbiAgY29sb3I6ICNmZmY7XG4gIGZvbnQtZmFtaWx5OiAnT3BlbiBTYW5zJywgc2Fucy1zZXJpZjtcbiAgZm9udC13ZWlnaHQ6IDQwMDtcbiAgLyogVGhpcyBzZXRzIHRoZSBiYXNlIFVJIHNjYWxpbmcuIEV2ZXJ5dGhpbmcgZWxzZSBzaG91bGQgYmUgaW4gZW1zLiAqL1xuICBmb250LXNpemU6IDE2cHg7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cblxubWFpbiB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgYmFja2dyb3VuZC1jb2xvcjogIzAwMDtcbiAgYmFja2dyb3VuZC1zaXplOiBjb3ZlcjsgfVxuXG5ib2R5LCBtYWluIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IDEwMCU7IH1cblxuaGVhZGVyIHtcbiAgLXdlYmtpdC1hcHAtcmVnaW9uOiBkcmFnO1xuICBoZWlnaHQ6IDNlbTtcbiAgbGluZS1oZWlnaHQ6IDNlbTtcbiAgcGFkZGluZzogMCAwIDAgMS41ZW07XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIGJhY2tncm91bmQtY29sb3I6ICMyMjI7XG4gIGJveC1zaGFkb3c6IDAgMCAzMHB4IDVweCByZ2JhKDAsIDAsIDAsIDAuOCk7XG4gIGJvcmRlci1ib3R0b206IHNvbGlkIDFweCAjMDAwOyB9XG4gIGhlYWRlciA+IGRpdiB7XG4gICAgZmxleDogMCAxIDUwJTsgfVxuICAgIGhlYWRlciA+IGRpdjpmaXJzdC1vZi10eXBlIHtcbiAgICAgIGZvbnQtc2l6ZTogLjhlbTtcbiAgICAgIGxldHRlci1zcGFjaW5nOiAuMDFlbTtcbiAgICAgIG9wYWNpdHk6IDAuNDtcbiAgICAgIGZvbnQtd2VpZ2h0OiA2MDA7XG4gICAgICBmbG9hdDogbGVmdDsgfVxuICAgIGhlYWRlciA+IGRpdjpsYXN0LW9mLXR5cGUge1xuICAgICAgdGV4dC1hbGlnbjogcmlnaHQ7XG4gICAgICBmbG9hdDogcmlnaHQ7IH1cbiAgICAgIGhlYWRlciA+IGRpdjpsYXN0LW9mLXR5cGUgPiBidXR0b24ge1xuICAgICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xuICAgICAgICBib3JkZXI6IG5vbmU7XG4gICAgICAgIHdpZHRoOiA0ZW07XG4gICAgICAgIHBhZGRpbmc6IDFlbTtcbiAgICAgICAgdHJhbnNpdGlvbjogLjJzO1xuICAgICAgICBvcGFjaXR5OiAwLjY7XG4gICAgICAgIGN1cnNvcjogcG9pbnRlcjsgfVxuICAgICAgICBoZWFkZXIgPiBkaXY6bGFzdC1vZi10eXBlID4gYnV0dG9uOmhvdmVyIHtcbiAgICAgICAgICBvcGFjaXR5OiAxO1xuICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6ICMwMDA7IH1cbiAgICAgICAgaGVhZGVyID4gZGl2Omxhc3Qtb2YtdHlwZSA+IGJ1dHRvbiBpbWcge1xuICAgICAgICAgIGhlaWdodDogMThweDsgfVxuXG5hLCBidXR0b24sIGlucHV0IHtcbiAgb3V0bGluZTogbm9uZTtcbiAgLXdlYmtpdC1hcHAtcmVnaW9uOiBuby1kcmFnOyB9XG5cbmZvcm0gbGFiZWwge1xuICBkaXNwbGF5OiBibG9jaztcbiAgbWFyZ2luLWJvdHRvbTogNXB4OyB9XG5cbmZvcm0gaW5wdXRbdHlwZT1cInRleHRcIl0sXG5mb3JtIGlucHV0W3R5cGU9XCJwYXNzd29yZFwiXSxcbmZvcm0gc2VsZWN0IHtcbiAgb3V0bGluZTogbm9uZTtcbiAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZjtcbiAgcGFkZGluZzogMTBweCAxMnB4O1xuICBib3JkZXI6IG5vbmU7XG4gIGJvcmRlci1yYWRpdXM6IDNweDtcbiAgd2lkdGg6IDEwMCU7XG4gIGZvbnQtc2l6ZTogMTZweDtcbiAgYm94LXNoYWRvdzogMCAxcHggM3B4ICMwMDA7XG4gIHRleHQtc2hhZG93OiBub25lOyB9XG5cbmZvcm0gYnV0dG9uIHtcbiAgb3V0bGluZTogbm9uZTtcbiAgYmFja2dyb3VuZC1jb2xvcjogIzU1NTtcbiAgY29sb3I6ICNmZmY7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbiAgcGFkZGluZzogMTBweCAxNXB4O1xuICBib3JkZXI6IG5vbmU7XG4gIGJvcmRlci1yYWRpdXM6IDNweDtcbiAgZm9udC1zaXplOiAxNnB4O1xuICB0ZXh0LXNoYWRvdzogMCAxcHggMnB4ICMwMDA7XG4gIGN1cnNvcjogcG9pbnRlcjtcbiAgYm94LXNoYWRvdzogMCAxcHggM3B4ICMwMDA7XG4gIHRyYW5zaXRpb246IC4ycztcbiAgZm9udC13ZWlnaHQ6IDQwMDsgfVxuICBmb3JtIGJ1dHRvbjpob3ZlciB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzNjM2MzYzsgfVxuXG5idXR0b24uYnRuIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogIzExMTtcbiAgYm9yZGVyOiBub25lO1xuICBwYWRkaW5nOiAxMHB4IDE1cHg7XG4gIGJveC1zaGFkb3c6IDAgMXB4IDNweCAjMDAwO1xuICBib3JkZXItcmFkaXVzOiA1cHg7XG4gIGZvbnQtc2l6ZTogMTZweDtcbiAgY29sb3I6ICNmZmY7XG4gIHRyYW5zaXRpb246IC4ycztcbiAgY3Vyc29yOiBwb2ludGVyOyB9XG4gIGJ1dHRvbi5idG46aG92ZXIge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICMzMzM7IH1cblxubmF2IHtcbiAgZmxvYXQ6IGxlZnQ7XG4gIHdpZHRoOiA2NSU7XG4gIHBhZGRpbmc6IDFlbSAyZW07XG4gIGJhY2tncm91bmQ6IC1tb3otbGluZWFyLWdyYWRpZW50KGxlZnQsICMxMTExMTEgMCUsICMxMTExMTEgNSUsIHJnYmEoMTcsIDE3LCAxNywgMC43KSA2MCUsIHJnYmEoMTcsIDE3LCAxNywgMCkgMTAwJSk7XG4gIGJhY2tncm91bmQ6IC13ZWJraXQtbGluZWFyLWdyYWRpZW50KGxlZnQsICMxMTExMTEgMCUsICMxMTExMTEgNSUsIHJnYmEoMTcsIDE3LCAxNywgMC43KSA2MCUsIHJnYmEoMTcsIDE3LCAxNywgMCkgMTAwJSk7XG4gIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCh0byByaWdodCwgIzExMTExMSAwJSwgIzExMTExMSA1JSwgcmdiYSgxNywgMTcsIDE3LCAwLjcpIDYwJSwgcmdiYSgxNywgMTcsIDE3LCAwKSAxMDAlKTsgfVxuICBuYXYgPiBidXR0b24ge1xuICAgIGNvbG9yOiAjZmZmO1xuICAgIHBhZGRpbmc6IC41ZW0gMWVtO1xuICAgIGZvbnQtc2l6ZTogMS4yNWVtO1xuICAgIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xuICAgIGJvcmRlcjogc29saWQgMXB4IHJnYmEoODAsIDgwLCA4MCwgMC44KTtcbiAgICBib3gtc2hhZG93OiAwIDAgNXB4IHJnYmEoMTUwLCAxNTAsIDE1MCwgMC4zKTtcbiAgICBib3JkZXItcmFkaXVzOiA2cHg7XG4gICAgY3Vyc29yOiBwb2ludGVyO1xuICAgIHRyYW5zaXRpb246IDEwMG1zO1xuICAgIG1hcmdpbi1yaWdodDogMjBweDsgfVxuICAgIG5hdiA+IGJ1dHRvbjpob3ZlciB7XG4gICAgICBib3JkZXI6IHNvbGlkIDFweCAjNDQ5OGZmO1xuICAgICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyMCwgMjAsIDIwLCAwLjIpOyB9XG4gICAgbmF2ID4gYnV0dG9uID4gc3ZnIHtcbiAgICAgIGhlaWdodDogMzJweDtcbiAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgIGZpbGw6ICNmZmY7XG4gICAgICB2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO1xuICAgICAgbWFyZ2luOiAtNHB4IDZweCAwIDA7IH1cblxuLmZyIHtcbiAgZmxvYXQ6IHJpZ2h0OyB9XG5cbi5ub3RpY2UtZmFkZSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiA0OHB4O1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNzUpO1xuICBkaXNwbGF5OiBub25lO1xuICB6LWluZGV4OiA1MDA7IH1cbiAgLm5vdGljZS1mYWRlLm9wZW4ge1xuICAgIGRpc3BsYXk6IGJsb2NrOyB9XG5cbi5ub3RpY2Uge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogNTAlO1xuICBsZWZ0OiA1MCU7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTExO1xuICBib3gtc2hhZG93OiAwIDJweCAyMHB4ICMwMDA7XG4gIHotaW5kZXg6IDEwMDA7XG4gIGJvcmRlcjogc29saWQgM3B4ICMwMDA7XG4gIHBhZGRpbmc6IDUwcHg7XG4gIHdpZHRoOiA1MDBweDtcbiAgYm9yZGVyLXJhZGl1czogM3B4O1xuICB0cmFuc2l0aW9uOiA1MDBtcztcbiAgZGlzcGxheTogbm9uZTsgfVxuICAubm90aWNlIGgxIHtcbiAgICBmb250LXNpemU6IDMycHg7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDsgfVxuICAubm90aWNlIHAge1xuICAgIGxpbmUtaGVpZ2h0OiAyNnB4O1xuICAgIGZvbnQtc2l6ZTogMTZweDtcbiAgICBtYXJnaW4tYm90dG9tOiAxNXB4OyB9XG4gICAgLm5vdGljZSBwOmxhc3Qtb2YtdHlwZSB7XG4gICAgICBtYXJnaW4tYm90dG9tOiAwOyB9XG4gIC5ub3RpY2Uub3BlbiB7XG4gICAgZGlzcGxheTogYmxvY2s7XG4gICAgYW5pbWF0aW9uLWR1cmF0aW9uOiAyNTBtcztcbiAgICBhbmltYXRpb24tbmFtZTogYWxlcnQ7IH1cblxuQGtleWZyYW1lcyBhbGVydCB7XG4gIGZyb20ge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpIHNjYWxlKDAuNSk7IH1cbiAgdG8ge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpIHNjYWxlKDEpOyB9IH1cblxuLnNldHRpbmdzLWZvcm0ge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogNjBweDtcbiAgbGVmdDogMTBweDtcbiAgd2lkdGg6IDUwMHB4O1xuICBwYWRkaW5nOiAyMHB4O1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzO1xuICBib3JkZXItcmFkaXVzOiAzcHg7XG4gIGJveC1zaGFkb3c6IDAgMCAzcHggIzAwMDtcbiAgei1pbmRleDogNTA7XG4gIGRpc3BsYXk6IG5vbmU7IH1cbiAgLnNldHRpbmdzLWZvcm0ub3BlbiB7XG4gICAgZGlzcGxheTogYmxvY2s7IH1cbiAgLnNldHRpbmdzLWZvcm0gbGFiZWwge1xuICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgIGNvbG9yOiAjODg4O1xuICAgIGZvbnQtc2l6ZTogMTRweDtcbiAgICB0ZXh0LXRyYW5zZm9ybTogdXBwZXJjYXNlO1xuICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgbWFyZ2luLXRvcDogMTVweDsgfVxuICAuc2V0dGluZ3MtZm9ybSBwIHtcbiAgICBmb250LXNpemU6IDEzcHg7XG4gICAgY29sb3I6ICNhYWE7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDtcbiAgICBsaW5lLWhlaWdodDogMjBweDsgfVxuICAuc2V0dGluZ3MtZm9ybSBoMSB7XG4gICAgZm9udC1zaXplOiAzMnB4O1xuICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgbWFyZ2luLWJvdHRvbTogMTVweDsgfVxuICAuc2V0dGluZ3MtZm9ybSBoMiB7XG4gICAgZm9udC1zaXplOiAyMHB4O1xuICAgIG1hcmdpbi1ib3R0b206IDEwcHg7IH1cbiAgLnNldHRpbmdzLWZvcm0gc3Ryb25nIHtcbiAgICBmb250LXdlaWdodDogNjAwOyB9XG4gIC5zZXR0aW5ncy1mb3JtIC5hYy1jbG9zZSB7XG4gICAgZmxvYXQ6IHJpZ2h0O1xuICAgIHBhZGRpbmc6IDVweCAxMHB4O1xuICAgIGJvcmRlci1yYWRpdXM6IDUwcHg7IH1cbiAgICAuc2V0dGluZ3MtZm9ybSAuYWMtY2xvc2UgaW1nIHtcbiAgICAgIHZlcnRpY2FsLWFsaWduOiBtaWRkbGU7IH1cbiAgLnNldHRpbmdzLWZvcm0gLmFjLXJvdy1mbGV4MiB7XG4gICAgZGlzcGxheTogZmxleDsgfVxuICAgIC5zZXR0aW5ncy1mb3JtIC5hYy1yb3ctZmxleDIgPiBkaXYge1xuICAgICAgZmxleDogMCAxIDUwJTsgfVxuICAgICAgLnNldHRpbmdzLWZvcm0gLmFjLXJvdy1mbGV4MiA+IGRpdjpmaXJzdC1vZi10eXBlIHtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiA1cHg7IH1cbiAgICAgICAgLnNldHRpbmdzLWZvcm0gLmFjLXJvdy1mbGV4MiA+IGRpdjpmaXJzdC1vZi10eXBlID4gaW5wdXQge1xuICAgICAgICAgIHdpZHRoOiAxNjAlOyB9XG4gICAgICAuc2V0dGluZ3MtZm9ybSAuYWMtcm93LWZsZXgyID4gZGl2Omxhc3Qtb2YtdHlwZSB7XG4gICAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICAgIGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7XG4gICAgICAgIG1hcmdpbi1sZWZ0OiA1cHg7IH1cbiAgLnNldHRpbmdzLWZvcm0gLmFjLXJvdy1zbWFsbCBpbnB1dCB7XG4gICAgd2lkdGg6IDMwJTsgfVxuXG4ubG9kZXN0b25lLW5ld3Mge1xuICB3aWR0aDogNjUlO1xuICBvdmVyZmxvdy15OiBhdXRvO1xuICBwb3NpdGlvbjogZml4ZWQ7XG4gIGxlZnQ6IDA7XG4gIHRvcDogOC4xNWVtO1xuICBib3R0b206IDA7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBhZGRpbmctbGVmdDogMmVtO1xuICBkaXJlY3Rpb246IHJ0bDtcbiAgYmFja2dyb3VuZDogLW1vei1saW5lYXItZ3JhZGllbnQobGVmdCwgIzExMTExMSAwJSwgIzExMTExMSA1JSwgcmdiYSgxNywgMTcsIDE3LCAwLjcpIDYwJSwgcmdiYSgxNywgMTcsIDE3LCAwKSAxMDAlKTtcbiAgYmFja2dyb3VuZDogLXdlYmtpdC1saW5lYXItZ3JhZGllbnQobGVmdCwgIzExMTExMSAwJSwgIzExMTExMSA1JSwgcmdiYSgxNywgMTcsIDE3LCAwLjcpIDYwJSwgcmdiYSgxNywgMTcsIDE3LCAwKSAxMDAlKTtcbiAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KHRvIHJpZ2h0LCAjMTExMTExIDAlLCAjMTExMTExIDUlLCByZ2JhKDE3LCAxNywgMTcsIDAuNykgNjAlLCByZ2JhKDE3LCAxNywgMTcsIDApIDEwMCUpOyB9XG4gIC5sb2Rlc3RvbmUtbmV3cy5vcGVuIHtcbiAgICBkaXNwbGF5OiBibG9jazsgfVxuICAubG9kZXN0b25lLW5ld3MgPiBkaXYge1xuICAgIGRpcmVjdGlvbjogbHRyO1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDsgfVxuICAgIC5sb2Rlc3RvbmUtbmV3cyA+IGRpdiA+IGRpdjpmaXJzdC1vZi10eXBlIHtcbiAgICAgIGZsZXg6IDAgMSAyMDBweDsgfVxuICAgIC5sb2Rlc3RvbmUtbmV3cyA+IGRpdiA+IGRpdjpsYXN0LW9mLXR5cGUge1xuICAgICAgZmxleDogMCAxIGNhbGMoMTAwJSAtIDIwMHB4KTsgfVxuICAgIC5sb2Rlc3RvbmUtbmV3cyA+IGRpdiBpbWcge1xuICAgICAgd2lkdGg6IDE4MHB4O1xuICAgICAgYm9yZGVyLXJhZGl1czogNXB4OyB9XG4gICAgLmxvZGVzdG9uZS1uZXdzID4gZGl2IGgyIHtcbiAgICAgIGZvbnQtc2l6ZTogMjBweDtcbiAgICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgICBtYXJnaW4tYm90dG9tOiA1cHg7IH1cbiAgICAgIC5sb2Rlc3RvbmUtbmV3cyA+IGRpdiBoMiBhIHtcbiAgICAgICAgY29sb3I6ICNmYWZmOGY7XG4gICAgICAgIHRleHQtZGVjb3JhdGlvbjogbm9uZTsgfVxuICAgICAgICAubG9kZXN0b25lLW5ld3MgPiBkaXYgaDIgYTpob3ZlciB7XG4gICAgICAgICAgdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7IH1cbiAgICAubG9kZXN0b25lLW5ld3MgPiBkaXYgc21hbGwge1xuICAgICAgZm9udC1zaXplOiAxM3B4O1xuICAgICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcbiAgICAgIG9wYWNpdHk6IDAuMzsgfVxuXG4uYWRkLWNoYXJhY3Rlci1mb3JtIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDUwJTtcbiAgbGVmdDogNTAlO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgYm94LXNoYWRvdzogMCAwIDI1MHB4IDQwcHggIzAwMDtcbiAgcmlnaHQ6IDQxMHB4O1xuICB3aWR0aDogNTAwcHg7XG4gIHBhZGRpbmc6IDQwcHg7XG4gIGJhY2tncm91bmQtY29sb3I6ICMzMzM7XG4gIGJvcmRlci1yYWRpdXM6IDVweDtcbiAgYm9yZGVyOiBzb2xpZCAxcHggIzAwMDtcbiAgei1pbmRleDogNTA7XG4gIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmFkZC1jaGFyYWN0ZXItZm9ybS5vcGVuIHtcbiAgICBkaXNwbGF5OiBibG9jazsgfVxuICAuYWRkLWNoYXJhY3Rlci1mb3JtIGxhYmVsIHtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICBjb2xvcjogIzg4ODtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcbiAgICBmb250LXdlaWdodDogNDAwO1xuICAgIG1hcmdpbi10b3A6IDE1cHg7IH1cbiAgLmFkZC1jaGFyYWN0ZXItZm9ybSBwIHtcbiAgICBmb250LXNpemU6IDEzcHg7XG4gICAgY29sb3I6ICNhYWE7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDtcbiAgICBsaW5lLWhlaWdodDogMjBweDsgfVxuICAuYWRkLWNoYXJhY3Rlci1mb3JtIGgxIHtcbiAgICBmb250LXNpemU6IDMycHg7XG4gICAgZm9udC13ZWlnaHQ6IDMwMDtcbiAgICBtYXJnaW4tYm90dG9tOiAxNXB4OyB9XG4gIC5hZGQtY2hhcmFjdGVyLWZvcm0gaDIge1xuICAgIGZvbnQtc2l6ZTogMjBweDtcbiAgICBtYXJnaW4tYm90dG9tOiAxMHB4OyB9XG4gIC5hZGQtY2hhcmFjdGVyLWZvcm0gZGl2IHtcbiAgICBtYXJnaW4tYm90dG9tOiAxMHB4OyB9XG4gIC5hZGQtY2hhcmFjdGVyLWZvcm0gLmJ0bi1zYXZlIHtcbiAgICBmb250LXdlaWdodDogNjAwO1xuICAgIGZvbnQtc2l6ZTogMjBweDsgfVxuICAuYWRkLWNoYXJhY3Rlci1mb3JtIC5hYy1jbG9zZSB7XG4gICAgZmxvYXQ6IHJpZ2h0O1xuICAgIHBhZGRpbmc6IDVweCAxMHB4O1xuICAgIGJvcmRlci1yYWRpdXM6IDUwcHg7IH1cbiAgICAuYWRkLWNoYXJhY3Rlci1mb3JtIC5hYy1jbG9zZSBpbWcge1xuICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTsgfVxuICAuYWRkLWNoYXJhY3Rlci1mb3JtIC5hYy1yb3ctZmxleDIge1xuICAgIGRpc3BsYXk6IGZsZXg7IH1cbiAgICAuYWRkLWNoYXJhY3Rlci1mb3JtIC5hYy1yb3ctZmxleDIgPiBkaXYge1xuICAgICAgZmxleDogMCAxIDUwJTsgfVxuICAgICAgLmFkZC1jaGFyYWN0ZXItZm9ybSAuYWMtcm93LWZsZXgyID4gZGl2OmZpcnN0LW9mLXR5cGUge1xuICAgICAgICBtYXJnaW4tcmlnaHQ6IDVweDsgfVxuICAgICAgLmFkZC1jaGFyYWN0ZXItZm9ybSAuYWMtcm93LWZsZXgyID4gZGl2Omxhc3Qtb2YtdHlwZSB7XG4gICAgICAgIG1hcmdpbi1sZWZ0OiA1cHg7IH1cbiAgLmFkZC1jaGFyYWN0ZXItZm9ybSAuYWMtcm93LXNtYWxsIGlucHV0IHtcbiAgICB3aWR0aDogMzAlOyB9XG5cbi5jaGFyYWN0ZXItbGlzdCB7XG4gIHdpZHRoOiAzNSU7XG4gIHBvc2l0aW9uOiBmaXhlZDtcbiAgdG9wOiAzZW07XG4gIGJvdHRvbTogMDtcbiAgcmlnaHQ6IDA7XG4gIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xuICBiYWNrZ3JvdW5kOiAtbW96LWxpbmVhci1ncmFkaWVudChsZWZ0LCByZ2JhKDAsIDAsIDAsIDApIDEwJSwgYmxhY2sgOTUlKTtcbiAgYmFja2dyb3VuZDogLXdlYmtpdC1saW5lYXItZ3JhZGllbnQobGVmdCwgcmdiYSgwLCAwLCAwLCAwKSAxMCUsIGJsYWNrIDk1JSk7XG4gIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCh0byByaWdodCwgcmdiYSgwLCAwLCAwLCAwKSAxMCUsIGJsYWNrIDk1JSk7IH1cbiAgLmNoYXJhY3Rlci1saXN0IC5jbC1hZGQge1xuICAgIHBhZGRpbmc6IDFlbSAyZW07XG4gICAgdGV4dC1hbGlnbjogcmlnaHQ7IH1cbiAgICAuY2hhcmFjdGVyLWxpc3QgLmNsLWFkZCBidXR0b24ge1xuICAgICAgY29sb3I6ICNmZmY7XG4gICAgICBwYWRkaW5nOiAwIDE1cHg7XG4gICAgICBoZWlnaHQ6IDUwcHg7XG4gICAgICBsaW5lLWhlaWdodDogNTBweDtcbiAgICAgIGZvbnQtc2l6ZTogMjBweDtcbiAgICAgIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xuICAgICAgYm9yZGVyOiBzb2xpZCAxcHggcmdiYSg4MCwgODAsIDgwLCAwLjgpO1xuICAgICAgYm94LXNoYWRvdzogMCAwIDVweCByZ2JhKDE1MCwgMTUwLCAxNTAsIDAuMyk7XG4gICAgICBib3JkZXItcmFkaXVzOiA2cHg7XG4gICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICB0cmFuc2l0aW9uOiAxMDBtczsgfVxuICAgICAgLmNoYXJhY3Rlci1saXN0IC5jbC1hZGQgYnV0dG9uOmhvdmVyIHtcbiAgICAgICAgYm9yZGVyOiBzb2xpZCAxcHggI2ZhZmY4ZjtcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyNTUsIDI1MCwgMTM3LCAwLjIpOyB9XG4gICAgICAuY2hhcmFjdGVyLWxpc3QgLmNsLWFkZCBidXR0b24gc3ZnIHtcbiAgICAgICAgaGVpZ2h0OiAzMnB4O1xuICAgICAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gICAgICAgIGZpbGw6ICNmZmY7XG4gICAgICAgIHZlcnRpY2FsLWFsaWduOiBtaWRkbGU7XG4gICAgICAgIG1hcmdpbjogLTRweCA2cHggMCAwOyB9XG4gIC5jaGFyYWN0ZXItbGlzdCAuY2wtbGlzdCB7XG4gICAgdGV4dC1hbGlnbjogcmlnaHQ7XG4gICAgcGFkZGluZzogMCAyZW0gMCAuNWVtO1xuICAgIHBvc2l0aW9uOiBmaXhlZDtcbiAgICB0b3A6IDguMjVlbTtcbiAgICBib3R0b206IDA7XG4gICAgcmlnaHQ6IDA7XG4gICAgd2lkdGg6IDM1JTtcbiAgICBvdmVyZmxvdy15OiBhdXRvOyB9XG4gICAgLmNoYXJhY3Rlci1saXN0IC5jbC1saXN0ID4gYnV0dG9uIHtcbiAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIG1hcmdpbi1ib3R0b206IDEuNWVtO1xuICAgICAgZmxleC1kaXJlY3Rpb246IHJvdztcbiAgICAgIGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7XG4gICAgICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDtcbiAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgIGhlaWdodDogNWVtO1xuICAgICAgbGluZS1oZWlnaHQ6IDVlbTtcbiAgICAgIGNvbG9yOiAjZmZmO1xuICAgICAgYm9yZGVyLXJhZGl1czogNWVtO1xuICAgICAgcGFkZGluZzogMDtcbiAgICAgIHRyYW5zaXRpb246IDEwMG1zOyB9XG4gICAgICAuY2hhcmFjdGVyLWxpc3QgLmNsLWxpc3QgPiBidXR0b24gPiBpbWcge1xuICAgICAgICBoZWlnaHQ6IDVlbTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNWVtO1xuICAgICAgICBib3gtc2hhZG93OiAwIDFweCA2cHggIzAwMDtcbiAgICAgICAgY29udGVudDogdXJsKFwiaHR0cHM6Ly94aXZhcGkuY29tL2xhdW5jaGVyL2ZhY2VsZXNzLnBuZ1wiKTsgfVxuICAgICAgLmNoYXJhY3Rlci1saXN0IC5jbC1saXN0ID4gYnV0dG9uID4gZGl2IHtcbiAgICAgICAgbWFyZ2luOiAwIDFlbSAwIDA7XG4gICAgICAgIGRpc3BsYXk6IGZsZXg7IH1cbiAgICAgICAgLmNoYXJhY3Rlci1saXN0IC5jbC1saXN0ID4gYnV0dG9uID4gZGl2IGg0IHtcbiAgICAgICAgICBmb250LXNpemU6IDEuNWVtO1xuICAgICAgICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgICAgICAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgICAgICAgICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbiAgICAgICAgICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gICAgICAgICAgdGV4dC1zaGFkb3c6IDAgMCAwLjhlbSByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuOCk7IH1cbiAgICAgICAgLmNoYXJhY3Rlci1saXN0IC5jbC1saXN0ID4gYnV0dG9uID4gZGl2IHN2ZyB7XG4gICAgICAgICAgZmlsbDogI2ZmZjtcbiAgICAgICAgICBoZWlnaHQ6IDEuN2VtO1xuICAgICAgICAgIG1hcmdpbjogMS41ZW0gLjVlbSAwIDA7XG4gICAgICAgICAgb3BhY2l0eTogMC4yOyB9XG4gICAgICAuY2hhcmFjdGVyLWxpc3QgLmNsLWxpc3QgPiBidXR0b246aG92ZXIge1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoNjcsIDcyLCA4NywgMC4zKTsgfVxuXG4uY2hhcmFjdGVyLXZpZXcge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogNTAlO1xuICBsZWZ0OiA1MCU7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpO1xuICBwYWRkaW5nOiA1MHB4O1xuICB3aWR0aDogNTAwcHg7XG4gIHotaW5kZXg6IDUwMDtcbiAgZGlzcGxheTogbm9uZTtcbiAgYm9yZGVyLXJhZGl1czogOHB4O1xuICBib3gtc2hhZG93OiAwIDFweCAyMHB4ICMwMDA7XG4gIGJvcmRlcjogc29saWQgMXB4ICMwMDA7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjUsIDI1LCAyNSwgMC44NSk7IH1cbiAgLmNoYXJhY3Rlci12aWV3Lm9wZW4ge1xuICAgIGRpc3BsYXk6IGJsb2NrOyB9XG4gIC5jaGFyYWN0ZXItdmlldyA+IGRpdjpudGgtb2YtdHlwZSgxKSB7XG4gICAgdGV4dC1hbGlnbjogY2VudGVyOyB9XG4gIC5jaGFyYWN0ZXItdmlldyA+IGRpdjpudGgtb2YtdHlwZSgyKSB7XG4gICAgdGV4dC1hbGlnbjogY2VudGVyOyB9XG4gIC5jaGFyYWN0ZXItdmlldyA+IGRpdjpudGgtb2YtdHlwZSgzKSB7XG4gICAgdGV4dC1hbGlnbjogY2VudGVyOyB9XG4gIC5jaGFyYWN0ZXItdmlldyAuYXZhdGFyIHtcbiAgICBoZWlnaHQ6IDk2cHg7XG4gICAgYm9yZGVyLXJhZGl1czogOTZweDtcbiAgICBib3gtc2hhZG93OiAwIDFweCA4cHggIzAwMDtcbiAgICBtYXJnaW4tYm90dG9tOiAxNXB4OyB9XG4gIC5jaGFyYWN0ZXItdmlldyBoMSB7XG4gICAgZm9udC1zaXplOiAyNnB4O1xuICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgbWFyZ2luLWJvdHRvbTogNXB4OyB9XG4gIC5jaGFyYWN0ZXItdmlldyBzbWFsbCB7XG4gICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcbiAgICBjb2xvcjogIzY2NjtcbiAgICBmb250LXNpemU6IDE0cHg7IH1cbiAgLmNoYXJhY3Rlci12aWV3IGhyIHtcbiAgICBib3JkZXI6IG5vbmU7XG4gICAgaGVpZ2h0OiAycHg7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzMzMztcbiAgICBtYXJnaW46IDQwcHggMjBweDsgfVxuICAuY2hhcmFjdGVyLXZpZXcgLmJ0bi1zdGFydC1nYW1lIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDA5ZTJhO1xuICAgIGNvbG9yOiAjZmZmO1xuICAgIGhlaWdodDogNTBweDtcbiAgICBsaW5lLWhlaWdodDogNTBweDtcbiAgICBwYWRkaW5nOiAwIDIwcHg7XG4gICAgZm9udC1zaXplOiAxOXB4O1xuICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgYm9yZGVyOiBub25lO1xuICAgIGJvcmRlci1yYWRpdXM6IDhweDtcbiAgICBib3gtc2hhZG93OiAwIDFweCA1cHggIzAwMDtcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgdHJhbnNpdGlvbjogMTAwbXM7IH1cbiAgICAuY2hhcmFjdGVyLXZpZXcgLmJ0bi1zdGFydC1nYW1lOmhvdmVyIHtcbiAgICAgIGJhY2tncm91bmQtY29sb3I6ICMwMDg3Mjc7IH1cbiAgLmNoYXJhY3Rlci12aWV3IC5idG4tZGVsZXRlLWNoYXJhY3RlciB7XG4gICAgY29sb3I6ICNlNTM5Mzk7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogdHJhbnNwYXJlbnQ7XG4gICAgYm9yZGVyOiBub25lO1xuICAgIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7XG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgY3Vyc29yOiBwb2ludGVyOyB9XG4gICAgLmNoYXJhY3Rlci12aWV3IC5idG4tZGVsZXRlLWNoYXJhY3Rlcjpob3ZlciB7XG4gICAgICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsgfVxuICAuY2hhcmFjdGVyLXZpZXcgLm90cDIge1xuICAgIGxpbmUtaGVpZ2h0OiA1MHB4O1xuICAgIHBhZGRpbmc6IDAgMjBweDtcbiAgICBtYXJnaW4tYm90dG9tOiAxNXB4O1xuICAgIGJhY2tncm91bmQtY29sb3I6ICMzMzM7XG4gICAgYm9yZGVyOiBzb2xpZCAxcHggIzAwMDtcbiAgICBmb250LXNpemU6IDIzcHg7XG4gICAgZm9udC13ZWlnaHQ6IDYwMDtcbiAgICBsZXR0ZXItc3BhY2luZzogMXB4O1xuICAgIGNvbG9yOiAjYWFhO1xuICAgIHdpZHRoOiAxNjVweDtcbiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgYm9yZGVyLXJhZGl1czogOHB4OyB9XG5cbi5jaGFyYWN0ZXItZmFkZSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgdG9wOiA0OHB4O1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICB6LWluZGV4OiA0OTU7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC43NSk7XG4gIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmNoYXJhY3Rlci1mYWRlLm9wZW4ge1xuICAgIGRpc3BsYXk6IGJsb2NrOyB9XG4iLCIvL1xuLy8gVUlcbi8vXG4vLyBDb250YWlucyBhbGwgdGhlIGNvcmUgc3R5bGVzIG9mIHRoZSB3aW5kb3dcbi8vIGFuZCB0aGUgbG9vayAmIGZlZWwgb2YgdGhlIGxhdW5jaGVyLlxuLy9cbi8vIFRoaXMgaXMgaW50ZW5kZWQgZm9yIGdlbmVyYWwgc3R5bGVzIGFuZCBub3Rcbi8vIGluZGl2aWR1YWwgbW9kdWxlcyB3aGljaCBzaG91bGQgaGF2ZSB0aGVpclxuLy8gb3duIGZpbGVzLiBWaWV3IFwiQWRkQ2hhcmFjdGVyRm9ybVwiIGFzIGFuXG4vLyBleGFtcGxlLlxuLy9cblxuKiB7XG4gICAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbiAgICB0ZXh0LXNoYWRvdzogMCAxcHggMnB4ICMwMDA7XG5cbiAgICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7IC8qIGlPUyBTYWZhcmkgKi9cbiAgICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lOyAvKiBTYWZhcmkgKi9cbiAgICAta2h0bWwtdXNlci1zZWxlY3Q6IG5vbmU7IC8qIEtvbnF1ZXJvciBIVE1MICovXG4gICAgLW1vei11c2VyLXNlbGVjdDogbm9uZTsgLyogRmlyZWZveCAqL1xuICAgIC1tcy11c2VyLXNlbGVjdDogbm9uZTsgLyogSW50ZXJuZXQgRXhwbG9yZXIvRWRnZSAqL1xuICAgIHVzZXItc2VsZWN0OiBub25lOyAvKiBOb24tcHJlZml4ZWQgdmVyc2lvbiwgY3VycmVudGx5XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VwcG9ydGVkIGJ5IENocm9tZSBhbmQgT3BlcmEgKi9cblxufVxuXG46Oi13ZWJraXQtc2Nyb2xsYmFyIHtcbiAgICB3aWR0aDogOHB4O1xufVxuOjotd2Via2l0LXNjcm9sbGJhci1idXR0b24ge1xuICAgIGRpc3BsYXk6bm9uZTtcbn1cbjo6LXdlYmtpdC1zY3JvbGxiYXItdHJhY2sge1xuICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwwLDAsLjUpO1xufVxuOjotd2Via2l0LXNjcm9sbGJhci10cmFjay1waWVjZSB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgwLDAsMCwwKTtcbn1cbjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWIge1xuICAgIGhlaWdodDogNTBweDtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNjY2O1xuICAgIGJvcmRlci1yYWRpdXM6IDNweDtcbn1cbjo6LXdlYmtpdC1yZXNpemVyIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsMCwwLDApO1xufVxuXG5odG1sLCBib2R5IHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTExO1xuICAgIG1hcmdpbjogMDtcbiAgICBtaW4taGVpZ2h0OiAxMDAlO1xuICAgIGNvbG9yOiAjZmZmO1xuICAgIGZvbnQtZmFtaWx5OiAnT3BlbiBTYW5zJywgc2Fucy1zZXJpZjtcbiAgICBmb250LXdlaWdodDogNDAwO1xuICAgIC8qIFRoaXMgc2V0cyB0aGUgYmFzZSBVSSBzY2FsaW5nLiBFdmVyeXRoaW5nIGVsc2Ugc2hvdWxkIGJlIGluIGVtcy4gKi9cbiAgICBmb250LXNpemU6IDE2cHg7XG4gICAgb3ZlcmZsb3c6IGhpZGRlbjtcbn1cblxubWFpbiB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgIGJhY2tncm91bmQtY29sb3I6ICMwMDA7XG4gICAgYmFja2dyb3VuZC1zaXplOiBjb3Zlcjtcbn1cbmJvZHksIG1haW4ge1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICB0b3A6MDtcbiAgICBib3R0b206MDtcbiAgICB3aWR0aDoxMDAlO1xufVxuXG5oZWFkZXIge1xuICAgIC13ZWJraXQtYXBwLXJlZ2lvbjogZHJhZztcbiAgICBoZWlnaHQ6IDNlbTtcbiAgICBsaW5lLWhlaWdodDozZW07XG4gICAgcGFkZGluZzogMCAwIDAgMS41ZW07XG4gICAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMjIyO1xuICAgIGJveC1zaGFkb3c6IDAgMCAzMHB4IDVweCByZ2JhKDAsMCwwLDAuOCk7XG4gICAgYm9yZGVyLWJvdHRvbTogc29saWQgMXB4ICMwMDA7XG5cbiAgICA+IGRpdiB7XG4gICAgICAgIGZsZXg6IDAgMSA1MCU7XG5cbiAgICAgICAgJjpmaXJzdC1vZi10eXBlIHtcbiAgICAgICAgICAgIGZvbnQtc2l6ZTogLjhlbTtcbiAgICAgICAgICAgIGxldHRlci1zcGFjaW5nOiAuMDFlbTtcbiAgICAgICAgICAgIG9wYWNpdHk6IDAuNDtcbiAgICAgICAgICAgIGZvbnQtd2VpZ2h0OiA2MDA7XG4gICAgICAgICAgICBmbG9hdDpsZWZ0O1xuICAgICAgICB9XG5cbiAgICAgICAgJjpsYXN0LW9mLXR5cGUge1xuICAgICAgICAgICAgdGV4dC1hbGlnbjogcmlnaHQ7XG4gICAgICAgICAgICBmbG9hdDpyaWdodDtcblxuICAgICAgICAgICAgPiBidXR0b24ge1xuICAgICAgICAgICAgICAgIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDtcbiAgICAgICAgICAgICAgICBib3JkZXI6IG5vbmU7XG4gICAgICAgICAgICAgICAgd2lkdGg6IDRlbTtcbiAgICAgICAgICAgICAgICBwYWRkaW5nOiAxZW07XG4gICAgICAgICAgICAgICAgdHJhbnNpdGlvbjogLjJzO1xuICAgICAgICAgICAgICAgIG9wYWNpdHk6IDAuNjtcbiAgICAgICAgICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG5cbiAgICAgICAgICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICAgICAgICAgICAgb3BhY2l0eTogMTtcbiAgICAgICAgICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogIzAwMDtcbiAgICAgICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICAgICBpbWcge1xuICAgICAgICAgICAgICAgICAgICBoZWlnaHQ6IDE4cHg7XG4gICAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59XG5cbmEsIGJ1dHRvbiwgaW5wdXQge1xuICAgIG91dGxpbmU6IG5vbmU7XG4gICAgLXdlYmtpdC1hcHAtcmVnaW9uOiBuby1kcmFnO1xufVxuXG5mb3JtIHtcbiAgICBsYWJlbCB7XG4gICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgICAgICBtYXJnaW4tYm90dG9tOiA1cHg7XG4gICAgfVxuXG4gICAgaW5wdXRbdHlwZT1cInRleHRcIl0sXG4gICAgaW5wdXRbdHlwZT1cInBhc3N3b3JkXCJdLFxuICAgIHNlbGVjdCB7XG4gICAgICAgIG91dGxpbmU6IG5vbmU7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6ICNmZmY7XG4gICAgICAgIHBhZGRpbmc6IDEwcHggMTJweDtcbiAgICAgICAgYm9yZGVyOiBub25lO1xuICAgICAgICBib3JkZXItcmFkaXVzOiAzcHg7XG4gICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICBmb250LXNpemU6IDE2cHg7XG4gICAgICAgIGJveC1zaGFkb3c6IDAgMXB4IDNweCAjMDAwO1xuICAgICAgICB0ZXh0LXNoYWRvdzogbm9uZTtcbiAgICB9XG5cbiAgICBidXR0b24ge1xuICAgICAgICBvdXRsaW5lOiBub25lO1xuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNTU1O1xuICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgICAgICBwYWRkaW5nOiAxMHB4IDE1cHg7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogM3B4O1xuICAgICAgICBmb250LXNpemU6IDE2cHg7XG4gICAgICAgIHRleHQtc2hhZG93OiAwIDFweCAycHggIzAwMDtcbiAgICAgICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICAgICBib3gtc2hhZG93OiAwIDFweCAzcHggIzAwMDtcbiAgICAgICAgdHJhbnNpdGlvbjogLjJzO1xuICAgICAgICBmb250LXdlaWdodDogNDAwO1xuXG4gICAgICAgICY6aG92ZXIge1xuICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogZGFya2VuKCM1NTUsIDEwJSk7XG4gICAgICAgIH1cbiAgICB9XG59XG5cbmJ1dHRvbi5idG4ge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICMxMTE7XG4gICAgYm9yZGVyOiBub25lO1xuICAgIHBhZGRpbmc6IDEwcHggMTVweDtcbiAgICBib3gtc2hhZG93OiAwIDFweCAzcHggIzAwMDtcbiAgICBib3JkZXItcmFkaXVzOiA1cHg7XG4gICAgZm9udC1zaXplOiAxNnB4O1xuICAgIGNvbG9yOiAjZmZmO1xuICAgIHRyYW5zaXRpb246IC4ycztcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG5cbiAgICAmOmhvdmVyIHtcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogIzMzMztcbiAgICB9XG59XG5cbm5hdiB7XG4gICAgZmxvYXQ6bGVmdDtcbiAgICB3aWR0aDo2NSU7XG4gICAgcGFkZGluZzogMWVtIDJlbTtcbiAgICBiYWNrZ3JvdW5kOiAtbW96LWxpbmVhci1ncmFkaWVudChsZWZ0LCByZ2JhKDE3LDE3LDE3LDEpIDAlLCByZ2JhKDE3LDE3LDE3LDEpIDUlLCByZ2JhKDE3LDE3LDE3LDAuNykgNjAlLCByZ2JhKDE3LDE3LDE3LDApIDEwMCUpO1xuICAgIGJhY2tncm91bmQ6IC13ZWJraXQtbGluZWFyLWdyYWRpZW50KGxlZnQsIHJnYmEoMTcsMTcsMTcsMSkgMCUscmdiYSgxNywxNywxNywxKSA1JSxyZ2JhKDE3LDE3LDE3LDAuNykgNjAlLHJnYmEoMTcsMTcsMTcsMCkgMTAwJSk7XG4gICAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KHRvIHJpZ2h0LCByZ2JhKDE3LDE3LDE3LDEpIDAlLHJnYmEoMTcsMTcsMTcsMSkgNSUscmdiYSgxNywxNywxNywwLjcpIDYwJSxyZ2JhKDE3LDE3LDE3LDApIDEwMCUpO1xuXG4gICAgPiBidXR0b24ge1xuICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgcGFkZGluZzogLjVlbSAxZW07XG4gICAgICAgIGZvbnQtc2l6ZTogMS4yNWVtO1xuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDtcbiAgICAgICAgYm9yZGVyOiBzb2xpZCAxcHggcmdiYSg4MCwgODAsIDgwLCAwLjgpO1xuICAgICAgICBib3gtc2hhZG93OiAwIDAgNXB4IHJnYmEoMTUwLCAxNTAsIDE1MCwgMC4zKTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNnB4O1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgIHRyYW5zaXRpb246IDEwMG1zO1xuICAgICAgICBtYXJnaW4tcmlnaHQ6IDIwcHg7XG5cbiAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICBib3JkZXI6IHNvbGlkIDFweCByZ2IoNjgsIDE1MiwgMjU1KTtcbiAgICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjAsIDIwLCAyMCwgMC4yKTtcbiAgICAgICAgfVxuXG4gICAgICAgID4gc3ZnIHtcbiAgICAgICAgICAgIGhlaWdodDogMzJweDtcbiAgICAgICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgICAgICAgIGZpbGw6ICNmZmY7XG4gICAgICAgICAgICB2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO1xuICAgICAgICAgICAgbWFyZ2luOiAtNHB4IDZweCAwIDA7XG4gICAgICAgIH1cbiAgICB9XG59XG5cbi5mciB7XG4gICAgZmxvYXQ6IHJpZ2h0O1xufVxuXG4ubm90aWNlLWZhZGUge1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICB0b3A6IDQ4cHg7XG4gICAgbGVmdDogMDtcbiAgICByaWdodDogMDtcbiAgICBib3R0b206IDA7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgaGVpZ2h0OiAxMDAlO1xuICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwwLDAsMC43NSk7XG4gICAgZGlzcGxheTogbm9uZTtcbiAgICB6LWluZGV4OiA1MDA7XG5cbiAgICAmLm9wZW4ge1xuICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICB9XG59XG4ubm90aWNlIHtcbiAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgdG9wOiA1MCU7XG4gICAgbGVmdDogNTAlO1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpO1xuICAgIGJhY2tncm91bmQtY29sb3I6ICMxMTE7XG4gICAgYm94LXNoYWRvdzogMCAycHggMjBweCAjMDAwO1xuICAgIHotaW5kZXg6IDEwMDA7XG4gICAgYm9yZGVyOiBzb2xpZCAzcHggIzAwMDtcbiAgICBwYWRkaW5nOiA1MHB4O1xuICAgIHdpZHRoOiA1MDBweDtcbiAgICBib3JkZXItcmFkaXVzOiAzcHg7XG4gICAgdHJhbnNpdGlvbjogNTAwbXM7XG5cbiAgICBkaXNwbGF5OiBub25lO1xuXG4gICAgaDEge1xuICAgICAgICBmb250LXNpemU6IDMycHg7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDIwcHg7XG4gICAgfVxuXG4gICAgcCB7XG4gICAgICAgIGxpbmUtaGVpZ2h0OiAyNnB4O1xuICAgICAgICBmb250LXNpemU6IDE2cHg7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDE1cHg7XG5cbiAgICAgICAgJjpsYXN0LW9mLXR5cGUge1xuICAgICAgICAgICAgbWFyZ2luLWJvdHRvbTogMDtcbiAgICAgICAgfVxuICAgIH1cblxuICAgICYub3BlbiB7XG4gICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgICAgICBhbmltYXRpb24tZHVyYXRpb246IDI1MG1zO1xuICAgICAgICBhbmltYXRpb24tbmFtZTogYWxlcnQ7XG5cbiAgICAgICAgQGtleWZyYW1lcyBhbGVydCB7XG4gICAgICAgICAgICBmcm9tIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGUoLTUwJSwgLTUwJSkgc2NhbGUoMC41KSB9XG4gICAgICAgICAgICB0byB7IHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpIHNjYWxlKDEuMCkgfVxuICAgICAgICB9XG4gICAgfVxufVxuIiwiLnNldHRpbmdzLWZvcm0ge1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICB0b3A6IDYwcHg7XG4gICAgbGVmdDogMTBweDtcbiAgICB3aWR0aDogNTAwcHg7XG4gICAgcGFkZGluZzogMjBweDtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzO1xuICAgIGJvcmRlci1yYWRpdXM6IDNweDtcbiAgICBib3gtc2hhZG93OiAwIDAgM3B4ICMwMDA7XG4gICAgei1pbmRleDogNTA7XG5cbiAgICBkaXNwbGF5OiBub25lO1xuICAgICYub3BlbiB7XG4gICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgIH1cblxuICAgIGxhYmVsIHtcbiAgICAgICAgZGlzcGxheTogYmxvY2s7XG4gICAgICAgIGNvbG9yOiAjODg4O1xuICAgICAgICBmb250LXNpemU6IDE0cHg7XG4gICAgICAgIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7XG4gICAgICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgICAgIG1hcmdpbi10b3A6IDE1cHg7XG4gICAgfVxuXG4gICAgcCB7XG4gICAgICAgIGZvbnQtc2l6ZTogMTNweDtcbiAgICAgICAgY29sb3I6ICNhYWE7XG5cbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMjBweDtcbiAgICAgICAgbGluZS1oZWlnaHQ6IDIwcHg7XG5cbiAgICB9XG5cbiAgICBoMSB7XG4gICAgICAgIGZvbnQtc2l6ZTogMzJweDtcbiAgICAgICAgZm9udC13ZWlnaHQ6IDMwMDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMTVweDtcbiAgICB9XG5cbiAgICBoMiB7XG4gICAgICAgIGZvbnQtc2l6ZTogMjBweDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMTBweDtcbiAgICB9XG5cbiAgICBzdHJvbmcge1xuICAgICAgICBmb250LXdlaWdodDogNjAwO1xuICAgIH1cblxuICAgIC5hYy1jbG9zZSB7XG4gICAgICAgIGZsb2F0OiByaWdodDtcbiAgICAgICAgcGFkZGluZzogNXB4IDEwcHg7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDUwcHg7XG5cbiAgICAgICAgaW1nIHtcbiAgICAgICAgICAgIHZlcnRpY2FsLWFsaWduOiBtaWRkbGU7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICAuYWMtcm93LWZsZXgyIHtcbiAgICAgICAgZGlzcGxheTogZmxleDtcblxuICAgICAgICA+IGRpdiB7XG4gICAgICAgICAgICBmbGV4OiAwIDEgNTAlO1xuXG4gICAgICAgICAgICAmOmZpcnN0LW9mLXR5cGUge1xuICAgICAgICAgICAgICAgIG1hcmdpbi1yaWdodDogNXB4O1xuICAgICAgICAgICAgICAgID4gaW5wdXQge1xuICAgICAgICAgICAgICAgICAgICB3aWR0aDogMTYwJTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICY6bGFzdC1vZi10eXBlIHtcbiAgICAgICAgICAgICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7XG4gICAgICAgICAgICAgICAgbWFyZ2luLWxlZnQ6IDVweDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cblxuICAgIC5hYy1yb3ctc21hbGwge1xuICAgICAgICBpbnB1dCB7XG4gICAgICAgICAgICB3aWR0aDogMzAlO1xuICAgICAgICB9XG4gICAgfVxufVxuIiwi77u/Ly9cbi8vIExvZGVzdG9uZSBOZXdzIFN0eWxpbmdcbi8vXG4vLyBUaGlzIGNvbnRhaW5zIHRoZSBzdHlsaW5nIHJlcXVpcmVkIGZvciB0aGUgbmV3cyBzaG93biBvbiB0aGUgbGF1bmNoZXJcbi8vXG4ubG9kZXN0b25lLW5ld3Mge1xuICAgIHdpZHRoOiA2NSU7XG4gICAgb3ZlcmZsb3cteTogYXV0bztcbiAgICBwb3NpdGlvbjogZml4ZWQ7XG4gICAgbGVmdDogMDtcbiAgICB0b3A6IDguMTVlbTtcbiAgICBib3R0b206IDA7XG4gICAgZGlzcGxheTogbm9uZTtcbiAgICBwYWRkaW5nLWxlZnQ6IDJlbTtcbiAgICBkaXJlY3Rpb246IHJ0bDsgLy8gcGxhY2UgdGhlIHNjcm9sbGJhciBvbiB0aGUgbGVmdCBzaWRlLlxuXG4gICAgYmFja2dyb3VuZDogLW1vei1saW5lYXItZ3JhZGllbnQobGVmdCwgcmdiYSgxNywxNywxNywxKSAwJSwgcmdiYSgxNywxNywxNywxKSA1JSwgcmdiYSgxNywxNywxNywwLjcpIDYwJSwgcmdiYSgxNywxNywxNywwKSAxMDAlKTtcbiAgICBiYWNrZ3JvdW5kOiAtd2Via2l0LWxpbmVhci1ncmFkaWVudChsZWZ0LCByZ2JhKDE3LDE3LDE3LDEpIDAlLHJnYmEoMTcsMTcsMTcsMSkgNSUscmdiYSgxNywxNywxNywwLjcpIDYwJSxyZ2JhKDE3LDE3LDE3LDApIDEwMCUpO1xuICAgIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCh0byByaWdodCwgcmdiYSgxNywxNywxNywxKSAwJSxyZ2JhKDE3LDE3LDE3LDEpIDUlLHJnYmEoMTcsMTcsMTcsMC43KSA2MCUscmdiYSgxNywxNywxNywwKSAxMDAlKTtcblxuICAgICYub3BlbiB7XG4gICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgIH1cblxuICAgID4gZGl2IHtcbiAgICAgICAgZGlyZWN0aW9uOiBsdHI7IC8vIEVuc3VyZSBvdXIgdGV4dCBpcyBzdGlsbCBkaXNwbGF5ZWQgY29ycmVjdGx5LlxuICAgICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgICBtYXJnaW4tYm90dG9tOiAyMHB4O1xuXG4gICAgICAgID4gZGl2OmZpcnN0LW9mLXR5cGUge1xuICAgICAgICAgICAgZmxleDogMCAxIDIwMHB4O1xuICAgICAgICB9XG5cbiAgICAgICAgPiBkaXY6bGFzdC1vZi10eXBlIHtcbiAgICAgICAgICAgIGZsZXg6IDAgMSBjYWxjKDEwMCUgLSAyMDBweCk7XG4gICAgICAgIH1cblxuICAgICAgICBpbWcge1xuICAgICAgICAgICAgd2lkdGg6IDE4MHB4O1xuICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogNXB4O1xuICAgICAgICB9XG5cbiAgICAgICAgaDIge1xuICAgICAgICAgICAgZm9udC1zaXplOiAyMHB4O1xuICAgICAgICAgICAgZm9udC13ZWlnaHQ6IDMwMDtcbiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDVweDtcblxuICAgICAgICAgICAgYSB7XG4gICAgICAgICAgICAgICAgY29sb3I6ICNmYWZmOGY7XG4gICAgICAgICAgICAgICAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xuXG4gICAgICAgICAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICAgICAgICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuXG4gICAgICAgIHNtYWxsIHtcbiAgICAgICAgICAgIGZvbnQtc2l6ZTogMTNweDtcbiAgICAgICAgICAgIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7XG4gICAgICAgICAgICBvcGFjaXR5OiAwLjM7XG4gICAgICAgIH1cbiAgICB9XG59XG4iLCIuYWRkLWNoYXJhY3Rlci1mb3JtIHtcbiAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgdG9wOiA1MCU7XG4gICAgbGVmdDogNTAlO1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpO1xuICAgIGJveC1zaGFkb3c6IDAgMCAyNTBweCA0MHB4ICMwMDA7XG5cbiAgICByaWdodDogNDEwcHg7XG4gICAgd2lkdGg6IDUwMHB4O1xuICAgIHBhZGRpbmc6IDQwcHg7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzMzMztcbiAgICBib3JkZXItcmFkaXVzOiA1cHg7XG4gICAgYm9yZGVyOiBzb2xpZCAxcHggIzAwMDtcbiAgICB6LWluZGV4OiA1MDtcbiAgICBkaXNwbGF5OiBub25lO1xuXG4gICAgJi5vcGVuIHtcbiAgICAgICAgZGlzcGxheTogYmxvY2s7XG4gICAgfVxuXG4gICAgbGFiZWwge1xuICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICAgICAgY29sb3I6ICM4ODg7XG4gICAgICAgIGZvbnQtc2l6ZTogMTRweDtcbiAgICAgICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcbiAgICAgICAgZm9udC13ZWlnaHQ6IDQwMDtcbiAgICAgICAgbWFyZ2luLXRvcDogMTVweDtcbiAgICB9XG5cbiAgICBwIHtcbiAgICAgICAgZm9udC1zaXplOiAxM3B4O1xuICAgICAgICBjb2xvcjogI2FhYTtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMjBweDtcbiAgICAgICAgbGluZS1oZWlnaHQ6IDIwcHg7XG4gICAgfVxuXG4gICAgaDEge1xuICAgICAgICBmb250LXNpemU6IDMycHg7XG4gICAgICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDE1cHg7XG4gICAgfVxuXG4gICAgaDIge1xuICAgICAgICBmb250LXNpemU6IDIwcHg7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDEwcHg7XG4gICAgfVxuXG4gICAgZGl2IHtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMTBweDtcbiAgICB9XG5cbiAgICAuYnRuLXNhdmUge1xuICAgICAgICBmb250LXdlaWdodDogNjAwO1xuICAgICAgICBmb250LXNpemU6IDIwcHg7XG4gICAgfVxuXG4gICAgLmFjLWNsb3NlIHtcbiAgICAgICAgZmxvYXQ6IHJpZ2h0O1xuICAgICAgICBwYWRkaW5nOiA1cHggMTBweDtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogNTBweDtcblxuICAgICAgICBpbWcge1xuICAgICAgICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIC5hYy1yb3ctZmxleDIge1xuICAgICAgICBkaXNwbGF5OiBmbGV4O1xuXG4gICAgICAgID4gZGl2IHtcbiAgICAgICAgICAgIGZsZXg6IDAgMSA1MCU7XG5cbiAgICAgICAgICAgICY6Zmlyc3Qtb2YtdHlwZSB7XG4gICAgICAgICAgICAgICAgbWFyZ2luLXJpZ2h0OiA1cHg7XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICY6bGFzdC1vZi10eXBlIHtcbiAgICAgICAgICAgICAgICBtYXJnaW4tbGVmdDogNXB4O1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxuXG4gICAgLmFjLXJvdy1zbWFsbCB7XG4gICAgICAgIGlucHV0IHtcbiAgICAgICAgICAgIHdpZHRoOiAzMCU7XG4gICAgICAgIH1cbiAgICB9XG59XG4iLCIvL1xuLy8gQ2hhcmFjdGVyTGlzdFxuLy9cbi8vIExpc3Qgb2YgY2hhcmFjdGVycyB5b3Ugc2VlIG9uIHRoZSByaWdodCBzaWRlLlxuLy9cbi5jaGFyYWN0ZXItbGlzdCB7XG4gICAgd2lkdGg6IDM1JTtcbiAgICBwb3NpdGlvbjogZml4ZWQ7XG4gICAgdG9wOiAzZW07XG4gICAgYm90dG9tOjA7XG4gICAgcmlnaHQ6MDtcblxuICAgIGJhY2tncm91bmQtY29sb3I6IHRyYW5zcGFyZW50O1xuICAgIGJhY2tncm91bmQ6IC1tb3otbGluZWFyLWdyYWRpZW50KGxlZnQsIHJnYmEoMCwwLDAsMCkgMTAlLCByZ2JhKDAsMCwwLDEpIDk1JSk7XG4gICAgYmFja2dyb3VuZDogLXdlYmtpdC1saW5lYXItZ3JhZGllbnQobGVmdCwgcmdiYSgwLDAsMCwwKSAxMCUscmdiYSgwLDAsMCwxKSA5NSUpO1xuICAgIGJhY2tncm91bmQ6IGxpbmVhci1ncmFkaWVudCh0byByaWdodCwgcmdiYSgwLDAsMCwwKSAxMCUscmdiYSgwLDAsMCwxKSA5NSUpO1xuXG4gICAgLmNsLWFkZCB7XG4gICAgICAgIHBhZGRpbmc6IDFlbSAyZW07XG4gICAgICAgIHRleHQtYWxpZ246IHJpZ2h0O1xuXG4gICAgICAgIGJ1dHRvbiB7XG4gICAgICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgICAgIHBhZGRpbmc6IDAgMTVweDtcbiAgICAgICAgICAgIGhlaWdodDogNTBweDtcbiAgICAgICAgICAgIGxpbmUtaGVpZ2h0OiA1MHB4O1xuICAgICAgICAgICAgZm9udC1zaXplOiAyMHB4O1xuICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogdHJhbnNwYXJlbnQ7XG4gICAgICAgICAgICBib3JkZXI6IHNvbGlkIDFweCByZ2JhKDgwLCA4MCwgODAsIDAuOCk7XG4gICAgICAgICAgICBib3gtc2hhZG93OiAwIDAgNXB4IHJnYmEoMTUwLCAxNTAsIDE1MCwgMC4zKTtcbiAgICAgICAgICAgIGJvcmRlci1yYWRpdXM6IDZweDtcbiAgICAgICAgICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICAgICAgICAgIHRyYW5zaXRpb246IDEwMG1zO1xuXG4gICAgICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICAgICAgICBib3JkZXI6IHNvbGlkIDFweCByZ2JhKDI1MCwgMjU1LCAxNDMsIDEpO1xuICAgICAgICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjU1LCAyNTAsIDEzNywgMC4yKTtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgc3ZnIHtcbiAgICAgICAgICAgICAgICBoZWlnaHQ6IDMycHg7XG4gICAgICAgICAgICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgICAgICAgICAgIGZpbGw6ICNmZmY7XG4gICAgICAgICAgICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcbiAgICAgICAgICAgICAgICBtYXJnaW46IC00cHggNnB4IDAgMDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cblxuICAgIC5jbC1saXN0IHtcbiAgICAgICAgdGV4dC1hbGlnbjogcmlnaHQ7XG4gICAgICAgIHBhZGRpbmc6IDAgMmVtIDAgLjVlbTtcbiAgICAgICAgcG9zaXRpb246IGZpeGVkO1xuICAgICAgICB0b3A6OC4yNWVtO1xuICAgICAgICBib3R0b206MDtcbiAgICAgICAgcmlnaHQ6MDtcbiAgICAgICAgd2lkdGg6MzUlO1xuICAgICAgICBvdmVyZmxvdy15OiBhdXRvO1xuXG4gICAgICAgID4gYnV0dG9uIHtcbiAgICAgICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgICAgICAgIG1hcmdpbi1ib3R0b206IDEuNWVtO1xuICAgICAgICAgICAgZmxleC1kaXJlY3Rpb246IHJvdztcbiAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7XG4gICAgICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDtcbiAgICAgICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgICAgIGhlaWdodDogNWVtO1xuICAgICAgICAgICAgbGluZS1oZWlnaHQ6IDVlbTtcbiAgICAgICAgICAgIGNvbG9yOiAjZmZmO1xuICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogNWVtO1xuICAgICAgICAgICAgcGFkZGluZzogMDtcbiAgICAgICAgICAgIHRyYW5zaXRpb246IDEwMG1zO1xuXG4gICAgICAgICAgICA+IGltZyB7XG4gICAgICAgICAgICAgICAgaGVpZ2h0OiA1ZW07XG4gICAgICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogNWVtO1xuICAgICAgICAgICAgICAgIGJveC1zaGFkb3c6IDAgMXB4IDZweCAjMDAwO1xuXG4gICAgICAgICAgICAgICAgQGlmIHZhcmlhYmxlLWV4aXN0cyhkZWJ1Zykge1xuICAgICAgICAgICAgICAgICAgICAvLyBEZWJ1ZyBtb2RlIHdpbGwgcmVwbGFjZSB5b3VyIGNoYXJhY3RlcnMgaW4gdGhlIGxpc3QgYW5kXG4gICAgICAgICAgICAgICAgICAgIC8vIGJsdXIgdGhlaXIgbmFtZXMsIHNvIHlvdSBjYW4gdGFrZSBlYXNpZXIgc2NyZWVuc2hvdHMuXG4gICAgICAgICAgICAgICAgICAgIGNvbnRlbnQ6IHVybCgnaHR0cHM6Ly94aXZhcGkuY29tL2xhdW5jaGVyL2ZhY2VsZXNzLnBuZycpO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgPiBkaXYge1xuICAgICAgICAgICAgICAgIG1hcmdpbjogMCAxZW0gMCAwO1xuICAgICAgICAgICAgICAgIGRpc3BsYXk6IGZsZXg7XG5cbiAgICAgICAgICAgICAgICBoNCB7XG4gICAgICAgICAgICAgICAgICAgIGZvbnQtc2l6ZTogMS41ZW07XG4gICAgICAgICAgICAgICAgICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgICAgICAgICAgICAgICAgIHdoaXRlLXNwYWNlOm5vd3JhcDtcbiAgICAgICAgICAgICAgICAgICAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG5cbiAgICAgICAgICAgICAgICAgICAgQGlmIHZhcmlhYmxlLWV4aXN0cyhkZWJ1Zykge1xuICAgICAgICAgICAgICAgICAgICAgIGNvbG9yOiB0cmFuc3BhcmVudDtcbiAgICAgICAgICAgICAgICAgICAgICB0ZXh0LXNoYWRvdzogMCAwIC44ZW0gcmdiYSgyNTUsMjU1LDI1NSwuOCk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG5cbiAgICAgICAgICAgICAgICBzdmcge1xuICAgICAgICAgICAgICAgICAgICBmaWxsOiAjZmZmO1xuICAgICAgICAgICAgICAgICAgICBoZWlnaHQ6IDEuN2VtO1xuICAgICAgICAgICAgICAgICAgICBtYXJnaW46IDEuNWVtIC41ZW0gMCAwO1xuICAgICAgICAgICAgICAgICAgICBvcGFjaXR5OiAwLjI7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgICAgICAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSg2NywgNzIsIDg3LCAwLjMpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxufVxuIiwiLy9cbi8vIFZpZXcgd2hlbiB5b3UgY2xpY2sgYSBjaGFyYWN0ZXJcbi8vXG4uY2hhcmFjdGVyLXZpZXcge1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICB0b3A6IDUwJTsgbGVmdDogNTAlO1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpO1xuXG4gICAgcGFkZGluZzogNTBweDtcbiAgICB3aWR0aDogNTAwcHg7XG4gICAgei1pbmRleDogNTAwO1xuICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgYm9yZGVyLXJhZGl1czogOHB4O1xuICAgIGJveC1zaGFkb3c6IDAgMXB4IDIwcHggIzAwMDtcbiAgICBib3JkZXI6IHNvbGlkIDFweCAjMDAwO1xuICAgIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjUsIDI1LCAyNSwgMC44NSk7XG5cbiAgICAmLm9wZW4ge1xuICAgICAgICBkaXNwbGF5OiBibG9jazs7XG4gICAgfVxuXG4gICAgPiBkaXY6bnRoLW9mLXR5cGUoMSkge1xuICAgICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgfVxuXG4gICAgPiBkaXY6bnRoLW9mLXR5cGUoMikge1xuICAgICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgfVxuXG4gICAgPiBkaXY6bnRoLW9mLXR5cGUoMykge1xuICAgICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgfVxuXG4gICAgLmF2YXRhciB7XG4gICAgICAgIGhlaWdodDogOTZweDtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogOTZweDtcbiAgICAgICAgYm94LXNoYWRvdzogMCAxcHggOHB4ICMwMDA7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDE1cHg7XG4gICAgfVxuXG4gICAgaDEge1xuICAgICAgICBmb250LXNpemU6IDI2cHg7XG4gICAgICAgIGZvbnQtd2VpZ2h0OiAzMDA7XG4gICAgICAgIG1hcmdpbi1ib3R0b206IDVweDtcbiAgICB9XG5cbiAgICBzbWFsbCB7XG4gICAgICAgIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7XG4gICAgICAgIGNvbG9yOiAjNjY2O1xuICAgICAgICBmb250LXNpemU6IDE0cHg7XG4gICAgfVxuXG4gICAgaHIge1xuICAgICAgICBib3JkZXI6IG5vbmU7XG4gICAgICAgIGhlaWdodDogMnB4O1xuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMzMzO1xuICAgICAgICBtYXJnaW46IDQwcHggMjBweDtcbiAgICB9XG5cbiAgICAuYnRuLXN0YXJ0LWdhbWUge1xuICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDA5ZTJhO1xuICAgICAgICBjb2xvcjogI2ZmZjtcbiAgICAgICAgaGVpZ2h0OiA1MHB4O1xuICAgICAgICBsaW5lLWhlaWdodDogNTBweDtcbiAgICAgICAgcGFkZGluZzogMCAyMHB4O1xuICAgICAgICBmb250LXNpemU6IDE5cHg7XG4gICAgICAgIGZvbnQtd2VpZ2h0OiA0MDA7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgYm9yZGVyLXJhZGl1czogOHB4O1xuICAgICAgICBib3gtc2hhZG93OiAwIDFweCA1cHggIzAwMDtcbiAgICAgICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICAgICB0cmFuc2l0aW9uOiAxMDBtcztcblxuICAgICAgICAmOmhvdmVyIHtcbiAgICAgICAgICAgIGJhY2tncm91bmQtY29sb3I6ICMwMDg3Mjc7XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICAuYnRuLWRlbGV0ZS1jaGFyYWN0ZXIge1xuICAgICAgICBjb2xvcjogI2U1MzkzOTtcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogdHJhbnNwYXJlbnQ7XG4gICAgICAgIGJvcmRlcjogbm9uZTtcbiAgICAgICAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTtcbiAgICAgICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICAgICAgZm9udC1zaXplOiAxNHB4O1xuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XG5cbiAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTtcbiAgICAgICAgfVxuICAgIH1cblxuICAgIC5vdHAyIHtcbiAgICAgICAgbGluZS1oZWlnaHQ6IDUwcHg7XG4gICAgICAgIHBhZGRpbmc6IDAgMjBweDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMTVweDtcbiAgICAgICAgYmFja2dyb3VuZC1jb2xvcjogIzMzMztcbiAgICAgICAgYm9yZGVyOiBzb2xpZCAxcHggIzAwMDtcbiAgICAgICAgZm9udC1zaXplOiAyM3B4O1xuICAgICAgICBmb250LXdlaWdodDogNjAwO1xuICAgICAgICBsZXR0ZXItc3BhY2luZzogMXB4O1xuICAgICAgICBjb2xvcjogI2FhYTtcbiAgICAgICAgd2lkdGg6IDE2NXB4O1xuICAgICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDhweDtcbiAgICB9XG59XG4uY2hhcmFjdGVyLWZhZGUge1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICB0b3A6IDQ4cHg7XG4gICAgbGVmdDogMDtcbiAgICByaWdodDogMDtcbiAgICBib3R0b206IDA7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgaGVpZ2h0OiAxMDAlO1xuICAgIHotaW5kZXg6IDQ5NTtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsMCwwLDAuNzUpO1xuICAgIGRpc3BsYXk6IG5vbmU7XG5cbiAgICAmLm9wZW4ge1xuICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICB9XG59XG4iXSwic291cmNlUm9vdCI6IiJ9*/ -------------------------------------------------------------------------------- /public/assets/images/FFXIV_media_tour2019_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/FFXIV_media_tour2019_19.png -------------------------------------------------------------------------------- /public/assets/images/FFXIV_media_tour2019_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/FFXIV_media_tour2019_20.png -------------------------------------------------------------------------------- /public/assets/images/add-character.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 325 | 330 | 331 | -------------------------------------------------------------------------------- /public/assets/images/angle-left-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/angle-right-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/background1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background1.jpg -------------------------------------------------------------------------------- /public/assets/images/background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background2.jpg -------------------------------------------------------------------------------- /public/assets/images/background3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background3.jpg -------------------------------------------------------------------------------- /public/assets/images/background4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background4.jpg -------------------------------------------------------------------------------- /public/assets/images/background5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background5.jpg -------------------------------------------------------------------------------- /public/assets/images/background6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background6.jpg -------------------------------------------------------------------------------- /public/assets/images/background7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background7.jpg -------------------------------------------------------------------------------- /public/assets/images/background8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/background8.jpg -------------------------------------------------------------------------------- /public/assets/images/bars-regular.68e0d910.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/bars-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/buttons.png -------------------------------------------------------------------------------- /public/assets/images/expansion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/expansion.png -------------------------------------------------------------------------------- /public/assets/images/faceless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/faceless.png -------------------------------------------------------------------------------- /public/assets/images/letters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/letters.png -------------------------------------------------------------------------------- /public/assets/images/log-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/logo_small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/logo_small.ico -------------------------------------------------------------------------------- /public/assets/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/logo_small.png -------------------------------------------------------------------------------- /public/assets/images/maximize.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/more-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/news-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xivapi/ffxiv-launcher/1fc6702a2d74a5046a166e21e61183644fc0f149/public/assets/images/numbers.png -------------------------------------------------------------------------------- /public/assets/images/power.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/times-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/user-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/images/wrench-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 7 | FINAL FANTASY XIV: SHADOWBRINGERS 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 | FINAL FANTASY XIV: SHADOWBRINGERS 18 |
19 |
20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 44 |
45 |
46 |
47 | 48 | 49 |
50 |
51 |

52 | Add Character 53 | 56 |

57 |

58 |

Character

59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 |
68 |

Square-Enix Account

69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 | 80 |
81 |
82 |

83 | When you save your character, you are storing your username and 84 | password in plain text on this machine. It is your responsibility 85 | to protect this machine and your SE Account such as using 86 | a One Time Password token) 87 |

88 |
89 |
90 | 91 |
92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 |

103 | Launcher Settings 104 | 107 |

108 | 109 |
110 |
111 | 112 |
113 |
114 | 115 |
116 |
117 |

Browse and select the folder: FINAL FANTASY XIV - A Realm Reborn

118 | 119 |
120 | 126 |
127 | 128 |
129 | 135 |
136 | 137 |
138 | 143 |
144 | 145 |
146 | 153 |
154 | 155 |
156 | 160 |
161 |
162 | 163 |
164 |
165 | 166 | 167 |
168 |
169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/css/CharacterAddForm.scss: -------------------------------------------------------------------------------- 1 | .add-character-form { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | box-shadow: 0 0 250px 40px #000; 7 | 8 | right: 410px; 9 | width: 500px; 10 | padding: 40px; 11 | background-color: #333; 12 | border-radius: 5px; 13 | border: solid 1px #000; 14 | z-index: 50; 15 | display: none; 16 | 17 | &.open { 18 | display: block; 19 | } 20 | 21 | label { 22 | display: block; 23 | color: #888; 24 | font-size: 14px; 25 | text-transform: uppercase; 26 | font-weight: 400; 27 | margin-top: 15px; 28 | } 29 | 30 | p { 31 | font-size: 13px; 32 | color: #aaa; 33 | margin-bottom: 20px; 34 | line-height: 20px; 35 | } 36 | 37 | h1 { 38 | font-size: 32px; 39 | font-weight: 300; 40 | margin-bottom: 15px; 41 | } 42 | 43 | h2 { 44 | font-size: 20px; 45 | margin-bottom: 10px; 46 | } 47 | 48 | div { 49 | margin-bottom: 10px; 50 | } 51 | 52 | .btn-save { 53 | font-weight: 600; 54 | font-size: 20px; 55 | } 56 | 57 | .ac-close { 58 | float: right; 59 | padding: 5px 10px; 60 | border-radius: 50px; 61 | 62 | img { 63 | vertical-align: middle; 64 | } 65 | } 66 | 67 | .ac-row-flex2 { 68 | display: flex; 69 | 70 | > div { 71 | flex: 0 1 50%; 72 | 73 | &:first-of-type { 74 | margin-right: 5px; 75 | } 76 | 77 | &:last-of-type { 78 | margin-left: 5px; 79 | } 80 | } 81 | } 82 | 83 | .ac-row-small { 84 | input { 85 | width: 30%; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/css/CharacterList.scss: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterList 3 | // 4 | // List of characters you see on the right side. 5 | // 6 | .character-list { 7 | width: 35%; 8 | position: fixed; 9 | top: 3em; 10 | bottom:0; 11 | right:0; 12 | 13 | background-color: transparent; 14 | background: -moz-linear-gradient(left, rgba(0,0,0,0) 10%, rgba(0,0,0,1) 95%); 15 | background: -webkit-linear-gradient(left, rgba(0,0,0,0) 10%,rgba(0,0,0,1) 95%); 16 | background: linear-gradient(to right, rgba(0,0,0,0) 10%,rgba(0,0,0,1) 95%); 17 | 18 | .cl-add { 19 | padding: 1em 2em; 20 | text-align: right; 21 | 22 | button { 23 | color: #fff; 24 | padding: 0 15px; 25 | height: 50px; 26 | line-height: 50px; 27 | font-size: 20px; 28 | background-color: transparent; 29 | border: solid 1px rgba(80, 80, 80, 0.8); 30 | box-shadow: 0 0 5px rgba(150, 150, 150, 0.3); 31 | border-radius: 6px; 32 | cursor: pointer; 33 | transition: 100ms; 34 | 35 | &:hover { 36 | border: solid 1px rgba(250, 255, 143, 1); 37 | background-color: rgba(255, 250, 137, 0.2); 38 | } 39 | 40 | svg { 41 | height: 32px; 42 | display: inline-block; 43 | fill: #fff; 44 | vertical-align: middle; 45 | margin: -4px 6px 0 0; 46 | } 47 | } 48 | } 49 | 50 | .cl-list { 51 | text-align: right; 52 | padding: 0 2em 0 .5em; 53 | position: fixed; 54 | top:8.25em; 55 | bottom:0; 56 | right:0; 57 | width:35%; 58 | overflow-y: auto; 59 | 60 | > button { 61 | width: 100%; 62 | display: flex; 63 | margin-bottom: 1.5em; 64 | flex-direction: row; 65 | justify-content: flex-end; 66 | background-color: transparent; 67 | border: none; 68 | height: 5em; 69 | line-height: 5em; 70 | color: #fff; 71 | border-radius: 5em; 72 | padding: 0; 73 | transition: 100ms; 74 | 75 | > img { 76 | height: 5em; 77 | border-radius: 5em; 78 | box-shadow: 0 1px 6px #000; 79 | 80 | @if variable-exists(debug) { 81 | // Debug mode will replace your characters in the list and 82 | // blur their names, so you can take easier screenshots. 83 | content: url('https://xivapi.com/launcher/faceless.png'); 84 | } 85 | } 86 | 87 | > div { 88 | margin: 0 1em 0 0; 89 | display: flex; 90 | 91 | h4 { 92 | font-size: 1.5em; 93 | font-weight: 300; 94 | white-space:nowrap; 95 | text-overflow: ellipsis; 96 | 97 | @if variable-exists(debug) { 98 | color: transparent; 99 | text-shadow: 0 0 .8em rgba(255,255,255,.8); 100 | } 101 | } 102 | 103 | svg { 104 | fill: #fff; 105 | height: 1.7em; 106 | margin: 1.5em .5em 0 0; 107 | opacity: 0.2; 108 | } 109 | } 110 | 111 | &:hover { 112 | cursor: pointer; 113 | background-color: rgba(67, 72, 87, 0.3); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/css/CharacterView.scss: -------------------------------------------------------------------------------- 1 | // 2 | // View when you click a character 3 | // 4 | .character-view { 5 | position: absolute; 6 | top: 50%; left: 50%; 7 | transform: translate(-50%, -50%); 8 | 9 | padding: 50px; 10 | width: 500px; 11 | z-index: 500; 12 | display: none; 13 | border-radius: 8px; 14 | box-shadow: 0 1px 20px #000; 15 | border: solid 1px #000; 16 | background-color: rgba(25, 25, 25, 0.85); 17 | 18 | &.open { 19 | display: block;; 20 | } 21 | 22 | > div:nth-of-type(1) { 23 | text-align: center; 24 | } 25 | 26 | > div:nth-of-type(2) { 27 | text-align: center; 28 | } 29 | 30 | > div:nth-of-type(3) { 31 | text-align: center; 32 | } 33 | 34 | .avatar { 35 | height: 96px; 36 | border-radius: 96px; 37 | box-shadow: 0 1px 8px #000; 38 | margin-bottom: 15px; 39 | } 40 | 41 | h1 { 42 | font-size: 26px; 43 | font-weight: 300; 44 | margin-bottom: 5px; 45 | } 46 | 47 | small { 48 | text-transform: uppercase; 49 | color: #666; 50 | font-size: 14px; 51 | } 52 | 53 | hr { 54 | border: none; 55 | height: 2px; 56 | background-color: #333; 57 | margin: 40px 20px; 58 | } 59 | 60 | .btn-start-game { 61 | background-color: #009e2a; 62 | color: #fff; 63 | height: 50px; 64 | line-height: 50px; 65 | padding: 0 20px; 66 | font-size: 19px; 67 | font-weight: 400; 68 | border: none; 69 | border-radius: 8px; 70 | box-shadow: 0 1px 5px #000; 71 | cursor: pointer; 72 | transition: 100ms; 73 | 74 | &:hover { 75 | background-color: #008727; 76 | } 77 | } 78 | 79 | .btn-delete-character { 80 | color: #e53939; 81 | background-color: transparent; 82 | border: none; 83 | text-transform: uppercase; 84 | font-weight: 500; 85 | font-size: 14px; 86 | cursor: pointer; 87 | 88 | &:hover { 89 | text-decoration: underline; 90 | } 91 | } 92 | 93 | .otp2 { 94 | line-height: 50px; 95 | padding: 0 20px; 96 | margin-bottom: 15px; 97 | background-color: #333; 98 | border: solid 1px #000; 99 | font-size: 23px; 100 | font-weight: 600; 101 | letter-spacing: 1px; 102 | color: #aaa; 103 | width: 165px; 104 | text-align: center; 105 | border-radius: 8px; 106 | } 107 | } 108 | .character-fade { 109 | position: absolute; 110 | top: 48px; 111 | left: 0; 112 | right: 0; 113 | bottom: 0; 114 | width: 100%; 115 | height: 100%; 116 | z-index: 495; 117 | background-color: rgba(0,0,0,0.75); 118 | display: none; 119 | 120 | &.open { 121 | display: block; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/css/LodestoneNews.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Lodestone News Styling 3 | // 4 | // This contains the styling required for the news shown on the launcher 5 | // 6 | .lodestone-news { 7 | width: 65%; 8 | overflow-y: auto; 9 | position: fixed; 10 | left: 0; 11 | top: 8.15em; 12 | bottom: 0; 13 | display: none; 14 | padding-left: 2em; 15 | direction: rtl; // place the scrollbar on the left side. 16 | 17 | background: -moz-linear-gradient(left, rgba(17,17,17,1) 0%, rgba(17,17,17,1) 5%, rgba(17,17,17,0.7) 60%, rgba(17,17,17,0) 100%); 18 | background: -webkit-linear-gradient(left, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 5%,rgba(17,17,17,0.7) 60%,rgba(17,17,17,0) 100%); 19 | background: linear-gradient(to right, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 5%,rgba(17,17,17,0.7) 60%,rgba(17,17,17,0) 100%); 20 | 21 | &.open { 22 | display: block; 23 | } 24 | 25 | > div { 26 | direction: ltr; // Ensure our text is still displayed correctly. 27 | display: flex; 28 | margin-bottom: 20px; 29 | 30 | > div:first-of-type { 31 | flex: 0 1 200px; 32 | } 33 | 34 | > div:last-of-type { 35 | flex: 0 1 calc(100% - 200px); 36 | } 37 | 38 | img { 39 | width: 180px; 40 | border-radius: 5px; 41 | } 42 | 43 | h2 { 44 | font-size: 20px; 45 | font-weight: 300; 46 | margin-bottom: 5px; 47 | 48 | a { 49 | color: #faff8f; 50 | text-decoration: none; 51 | 52 | &:hover { 53 | text-decoration: underline; 54 | } 55 | } 56 | } 57 | 58 | small { 59 | font-size: 13px; 60 | text-transform: uppercase; 61 | opacity: 0.3; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/css/Reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /src/css/SettingsForm.scss: -------------------------------------------------------------------------------- 1 | .settings-form { 2 | position: absolute; 3 | top: 60px; 4 | left: 10px; 5 | width: 500px; 6 | padding: 20px; 7 | background-color: #333; 8 | border-radius: 3px; 9 | box-shadow: 0 0 3px #000; 10 | z-index: 50; 11 | 12 | display: none; 13 | &.open { 14 | display: block; 15 | } 16 | 17 | label { 18 | display: block; 19 | color: #888; 20 | font-size: 14px; 21 | text-transform: uppercase; 22 | font-weight: 400; 23 | margin-top: 15px; 24 | } 25 | 26 | p { 27 | font-size: 13px; 28 | color: #aaa; 29 | 30 | margin-bottom: 20px; 31 | line-height: 20px; 32 | 33 | } 34 | 35 | h1 { 36 | font-size: 32px; 37 | font-weight: 300; 38 | margin-bottom: 15px; 39 | } 40 | 41 | h2 { 42 | font-size: 20px; 43 | margin-bottom: 10px; 44 | } 45 | 46 | strong { 47 | font-weight: 600; 48 | } 49 | 50 | .ac-close { 51 | float: right; 52 | padding: 5px 10px; 53 | border-radius: 50px; 54 | 55 | img { 56 | vertical-align: middle; 57 | } 58 | } 59 | 60 | .ac-row-flex2 { 61 | display: flex; 62 | 63 | > div { 64 | flex: 0 1 50%; 65 | 66 | &:first-of-type { 67 | margin-right: 5px; 68 | > input { 69 | width: 160%; 70 | } 71 | } 72 | 73 | &:last-of-type { 74 | display: flex; 75 | justify-content: flex-end; 76 | margin-left: 5px; 77 | } 78 | } 79 | } 80 | 81 | .ac-row-small { 82 | input { 83 | width: 30%; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/css/UI.scss: -------------------------------------------------------------------------------- 1 | // 2 | // UI 3 | // 4 | // Contains all the core styles of the window 5 | // and the look & feel of the launcher. 6 | // 7 | // This is intended for general styles and not 8 | // individual modules which should have their 9 | // own files. View "AddCharacterForm" as an 10 | // example. 11 | // 12 | 13 | * { 14 | box-sizing: border-box; 15 | text-shadow: 0 1px 2px #000; 16 | 17 | -webkit-touch-callout: none; /* iOS Safari */ 18 | -webkit-user-select: none; /* Safari */ 19 | -khtml-user-select: none; /* Konqueror HTML */ 20 | -moz-user-select: none; /* Firefox */ 21 | -ms-user-select: none; /* Internet Explorer/Edge */ 22 | user-select: none; /* Non-prefixed version, currently 23 | supported by Chrome and Opera */ 24 | 25 | } 26 | 27 | ::-webkit-scrollbar { 28 | width: 8px; 29 | } 30 | ::-webkit-scrollbar-button { 31 | display:none; 32 | } 33 | ::-webkit-scrollbar-track { 34 | background-color: rgba(0,0,0,.5); 35 | } 36 | ::-webkit-scrollbar-track-piece { 37 | background-color: rgba(0,0,0,0); 38 | } 39 | ::-webkit-scrollbar-thumb { 40 | height: 50px; 41 | background-color: #666; 42 | border-radius: 3px; 43 | } 44 | ::-webkit-resizer { 45 | background-color: rgba(0,0,0,0); 46 | } 47 | 48 | html, body { 49 | background-color: #111; 50 | margin: 0; 51 | min-height: 100%; 52 | color: #fff; 53 | font-family: 'Open Sans', sans-serif; 54 | font-weight: 400; 55 | /* This sets the base UI scaling. Everything else should be in ems. */ 56 | font-size: 16px; 57 | overflow: hidden; 58 | } 59 | 60 | main { 61 | position: relative; 62 | background-color: #000; 63 | background-size: cover; 64 | } 65 | body, main { 66 | position: absolute; 67 | top:0; 68 | bottom:0; 69 | width:100%; 70 | } 71 | 72 | header { 73 | -webkit-app-region: drag; 74 | height: 3em; 75 | line-height:3em; 76 | padding: 0 0 0 1.5em; 77 | overflow: hidden; 78 | background-color: #222; 79 | box-shadow: 0 0 30px 5px rgba(0,0,0,0.8); 80 | border-bottom: solid 1px #000; 81 | 82 | > div { 83 | flex: 0 1 50%; 84 | 85 | &:first-of-type { 86 | font-size: .8em; 87 | letter-spacing: .01em; 88 | opacity: 0.4; 89 | font-weight: 600; 90 | float:left; 91 | } 92 | 93 | &:last-of-type { 94 | text-align: right; 95 | float:right; 96 | 97 | > button { 98 | position: relative; 99 | background-color: transparent; 100 | border: none; 101 | width: 4em; 102 | padding: 1em; 103 | transition: .2s; 104 | opacity: 0.6; 105 | cursor: pointer; 106 | 107 | &:hover { 108 | opacity: 1; 109 | background-color: #000; 110 | } 111 | 112 | img { 113 | height: 18px; 114 | } 115 | 116 | } 117 | } 118 | } 119 | } 120 | 121 | a, button, input { 122 | outline: none; 123 | -webkit-app-region: no-drag; 124 | } 125 | 126 | form { 127 | label { 128 | display: block; 129 | margin-bottom: 5px; 130 | } 131 | 132 | input[type="text"], 133 | input[type="password"], 134 | select { 135 | outline: none; 136 | background-color: #fff; 137 | padding: 10px 12px; 138 | border: none; 139 | border-radius: 3px; 140 | width: 100%; 141 | font-size: 16px; 142 | box-shadow: 0 1px 3px #000; 143 | text-shadow: none; 144 | } 145 | 146 | button { 147 | outline: none; 148 | background-color: #555; 149 | color: #fff; 150 | text-align: center; 151 | padding: 10px 15px; 152 | border: none; 153 | border-radius: 3px; 154 | font-size: 16px; 155 | text-shadow: 0 1px 2px #000; 156 | cursor: pointer; 157 | box-shadow: 0 1px 3px #000; 158 | transition: .2s; 159 | font-weight: 400; 160 | 161 | &:hover { 162 | background-color: darken(#555, 10%); 163 | } 164 | } 165 | } 166 | 167 | button.btn { 168 | background-color: #111; 169 | border: none; 170 | padding: 10px 15px; 171 | box-shadow: 0 1px 3px #000; 172 | border-radius: 5px; 173 | font-size: 16px; 174 | color: #fff; 175 | transition: .2s; 176 | cursor: pointer; 177 | 178 | &:hover { 179 | background-color: #333; 180 | } 181 | } 182 | 183 | nav { 184 | float:left; 185 | width:65%; 186 | padding: 1em 2em; 187 | background: -moz-linear-gradient(left, rgba(17,17,17,1) 0%, rgba(17,17,17,1) 5%, rgba(17,17,17,0.7) 60%, rgba(17,17,17,0) 100%); 188 | background: -webkit-linear-gradient(left, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 5%,rgba(17,17,17,0.7) 60%,rgba(17,17,17,0) 100%); 189 | background: linear-gradient(to right, rgba(17,17,17,1) 0%,rgba(17,17,17,1) 5%,rgba(17,17,17,0.7) 60%,rgba(17,17,17,0) 100%); 190 | 191 | > button { 192 | color: #fff; 193 | padding: .5em 1em; 194 | font-size: 1.25em; 195 | background-color: transparent; 196 | border: solid 1px rgba(80, 80, 80, 0.8); 197 | box-shadow: 0 0 5px rgba(150, 150, 150, 0.3); 198 | border-radius: 6px; 199 | cursor: pointer; 200 | transition: 100ms; 201 | margin-right: 20px; 202 | 203 | &:hover { 204 | border: solid 1px rgb(68, 152, 255); 205 | background-color: rgba(20, 20, 20, 0.2); 206 | } 207 | 208 | > svg { 209 | height: 32px; 210 | display: inline-block; 211 | fill: #fff; 212 | vertical-align: middle; 213 | margin: -4px 6px 0 0; 214 | } 215 | } 216 | } 217 | 218 | .fr { 219 | float: right; 220 | } 221 | 222 | .notice-fade { 223 | position: absolute; 224 | top: 48px; 225 | left: 0; 226 | right: 0; 227 | bottom: 0; 228 | width: 100%; 229 | height: 100%; 230 | background-color: rgba(0,0,0,0.75); 231 | display: none; 232 | z-index: 500; 233 | 234 | &.open { 235 | display: block; 236 | } 237 | } 238 | .notice { 239 | position: absolute; 240 | top: 50%; 241 | left: 50%; 242 | transform: translate(-50%, -50%); 243 | background-color: #111; 244 | box-shadow: 0 2px 20px #000; 245 | z-index: 1000; 246 | border: solid 3px #000; 247 | padding: 50px; 248 | width: 500px; 249 | border-radius: 3px; 250 | transition: 500ms; 251 | 252 | display: none; 253 | 254 | h1 { 255 | font-size: 32px; 256 | margin-bottom: 20px; 257 | } 258 | 259 | p { 260 | line-height: 26px; 261 | font-size: 16px; 262 | margin-bottom: 15px; 263 | 264 | &:last-of-type { 265 | margin-bottom: 0; 266 | } 267 | } 268 | 269 | &.open { 270 | display: block; 271 | animation-duration: 250ms; 272 | animation-name: alert; 273 | 274 | @keyframes alert { 275 | from { transform: translate(-50%, -50%) scale(0.5) } 276 | to { transform: translate(-50%, -50%) scale(1.0) } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/css/_mq.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; // Fixes an issue where Ruby locale is not set properly 2 | // See https://github.com/sass-mq/sass-mq/pull/10 3 | 4 | /// Base font size on the `` element 5 | /// @type Number (unit) 6 | $mq-base-font-size: 16px !default; 7 | 8 | /// Responsive mode 9 | /// 10 | /// Set to `false` to enable support for browsers that do not support @media queries, 11 | /// (IE <= 8, Firefox <= 3, Opera <= 9) 12 | /// 13 | /// You could create a stylesheet served exclusively to older browsers, 14 | /// where @media queries are rasterized 15 | /// 16 | /// @example scss 17 | /// // old-ie.scss 18 | /// $mq-responsive: false; 19 | /// @import 'main'; // @media queries in this file will be rasterized up to $mq-static-breakpoint 20 | /// // larger breakpoints will be ignored 21 | /// 22 | /// @type Boolean 23 | /// @link https://github.com/sass-mq/sass-mq#responsive-mode-off Disabled responsive mode documentation 24 | $mq-responsive: true !default; 25 | 26 | /// Breakpoint list 27 | /// 28 | /// Name your breakpoints in a way that creates a ubiquitous language 29 | /// across team members. It will improve communication between 30 | /// stakeholders, designers, developers, and testers. 31 | /// 32 | /// @type Map 33 | /// @link https://github.com/sass-mq/sass-mq#seeing-the-currently-active-breakpoint Full documentation and examples 34 | $mq-breakpoints: ( 35 | mobile: 320px, 36 | tablet: 740px, 37 | desktop: 980px, 38 | wide: 1300px 39 | ) !default; 40 | 41 | /// Static breakpoint (for fixed-width layouts) 42 | /// 43 | /// Define the breakpoint from $mq-breakpoints that should 44 | /// be used as the target width for the fixed-width layout 45 | /// (i.e. when $mq-responsive is set to 'false') in a old-ie.scss 46 | /// 47 | /// @example scss 48 | /// // tablet-only.scss 49 | /// // 50 | /// // Ignore all styles above tablet breakpoint, 51 | /// // and fix the styles (e.g. layout) at tablet width 52 | /// $mq-responsive: false; 53 | /// $mq-static-breakpoint: tablet; 54 | /// @import 'main'; // @media queries in this file will be rasterized up to tablet 55 | /// // larger breakpoints will be ignored 56 | /// 57 | /// @type String 58 | /// @link https://github.com/sass-mq/sass-mq#adding-custom-breakpoints Full documentation and examples 59 | $mq-static-breakpoint: desktop !default; 60 | 61 | /// Show breakpoints in the top right corner 62 | /// 63 | /// If you want to display the currently active breakpoint in the top 64 | /// right corner of your site during development, add the breakpoints 65 | /// to this list, ordered by width, e.g. (mobile, tablet, desktop). 66 | /// 67 | /// @example scss 68 | /// $mq-show-breakpoints: (mobile, tablet, desktop); 69 | /// @import 'path/to/mq'; 70 | /// 71 | /// @type map 72 | $mq-show-breakpoints: () !default; 73 | 74 | /// Customize the media type (e.g. `@media screen` or `@media print`) 75 | /// By default sass-mq uses an "all" media type (`@media all and …`) 76 | /// 77 | /// @type String 78 | /// @link https://github.com/sass-mq/sass-mq#changing-media-type Full documentation and examples 79 | $mq-media-type: all !default; 80 | 81 | /// Convert pixels to ems 82 | /// 83 | /// @param {Number} $px - value to convert 84 | /// @param {Number} $base-font-size ($mq-base-font-size) - `` font size 85 | /// 86 | /// @example scss 87 | /// $font-size-in-ems: mq-px2em(16px); 88 | /// p { font-size: mq-px2em(16px); } 89 | /// 90 | /// @requires $mq-base-font-size 91 | /// @returns {Number} 92 | @function mq-px2em($px, $base-font-size: $mq-base-font-size) { 93 | @if unitless($px) { 94 | @warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels."; 95 | @return mq-px2em($px * 1px, $base-font-size); 96 | } @else if unit($px) == em { 97 | @return $px; 98 | } 99 | @return ($px / $base-font-size) * 1em; 100 | } 101 | 102 | /// Get a breakpoint's width 103 | /// 104 | /// @param {String} $name - Name of the breakpoint. One of $mq-breakpoints 105 | /// 106 | /// @example scss 107 | /// $tablet-width: mq-get-breakpoint-width(tablet); 108 | /// @media (min-width: mq-get-breakpoint-width(desktop)) {} 109 | /// 110 | /// @requires {Variable} $mq-breakpoints 111 | /// 112 | /// @returns {Number} Value in pixels 113 | @function mq-get-breakpoint-width($name, $breakpoints: $mq-breakpoints) { 114 | @if map-has-key($breakpoints, $name) { 115 | @return map-get($breakpoints, $name); 116 | } @else { 117 | @warn "Breakpoint #{$name} wasn't found in $breakpoints."; 118 | } 119 | } 120 | 121 | /// Media Query mixin 122 | /// 123 | /// @param {String | Boolean} $from (false) - One of $mq-breakpoints 124 | /// @param {String | Boolean} $until (false) - One of $mq-breakpoints 125 | /// @param {String | Boolean} $and (false) - Additional media query parameters 126 | /// @param {String} $media-type ($mq-media-type) - Media type: screen, print… 127 | /// 128 | /// @ignore Undocumented API, for advanced use only: 129 | /// @ignore @param {Map} $breakpoints ($mq-breakpoints) 130 | /// @ignore @param {String} $static-breakpoint ($mq-static-breakpoint) 131 | /// 132 | /// @content styling rules, wrapped into a @media query when $responsive is true 133 | /// 134 | /// @requires {Variable} $mq-media-type 135 | /// @requires {Variable} $mq-breakpoints 136 | /// @requires {Variable} $mq-static-breakpoint 137 | /// @requires {function} mq-px2em 138 | /// @requires {function} mq-get-breakpoint-width 139 | /// 140 | /// @link https://github.com/sass-mq/sass-mq#responsive-mode-on-default Full documentation and examples 141 | /// 142 | /// @example scss 143 | /// .element { 144 | /// @include mq($from: mobile) { 145 | /// color: red; 146 | /// } 147 | /// @include mq($until: tablet) { 148 | /// color: blue; 149 | /// } 150 | /// @include mq(mobile, tablet) { 151 | /// color: green; 152 | /// } 153 | /// @include mq($from: tablet, $and: '(orientation: landscape)') { 154 | /// color: teal; 155 | /// } 156 | /// @include mq(950px) { 157 | /// color: hotpink; 158 | /// } 159 | /// @include mq(tablet, $media-type: screen) { 160 | /// color: hotpink; 161 | /// } 162 | /// // Advanced use: 163 | /// $my-breakpoints: (L: 900px, XL: 1200px); 164 | /// @include mq(L, $breakpoints: $my-breakpoints, $static-breakpoint: L) { 165 | /// color: hotpink; 166 | /// } 167 | /// } 168 | @mixin mq( 169 | $from: false, 170 | $until: false, 171 | $and: false, 172 | $media-type: $mq-media-type, 173 | $breakpoints: $mq-breakpoints, 174 | $responsive: $mq-responsive, 175 | $static-breakpoint: $mq-static-breakpoint 176 | ) { 177 | $min-width: 0; 178 | $max-width: 0; 179 | $media-query: ''; 180 | 181 | // From: this breakpoint (inclusive) 182 | @if $from { 183 | @if type-of($from) == number { 184 | $min-width: mq-px2em($from); 185 | } @else { 186 | $min-width: mq-px2em(mq-get-breakpoint-width($from, $breakpoints)); 187 | } 188 | } 189 | 190 | // Until: that breakpoint (exclusive) 191 | @if $until { 192 | @if type-of($until) == number { 193 | $max-width: mq-px2em($until); 194 | } @else { 195 | $max-width: mq-px2em(mq-get-breakpoint-width($until, $breakpoints)) - .01em; 196 | } 197 | } 198 | 199 | // Responsive support is disabled, rasterize the output outside @media blocks 200 | // The browser will rely on the cascade itself. 201 | @if $responsive == false { 202 | $static-breakpoint-width: mq-get-breakpoint-width($static-breakpoint, $breakpoints); 203 | $target-width: mq-px2em($static-breakpoint-width); 204 | 205 | // Output only rules that start at or span our target width 206 | @if ( 207 | $and == false 208 | and $min-width <= $target-width 209 | and ( 210 | $until == false or $max-width >= $target-width 211 | ) 212 | and $media-type != 'print' 213 | ) { 214 | @content; 215 | } 216 | } 217 | 218 | // Responsive support is enabled, output rules inside @media queries 219 | @else { 220 | @if $min-width != 0 { $media-query: '#{$media-query} and (min-width: #{$min-width})'; } 221 | @if $max-width != 0 { $media-query: '#{$media-query} and (max-width: #{$max-width})'; } 222 | @if $and { $media-query: '#{$media-query} and #{$and}'; } 223 | 224 | // Remove unnecessary media query prefix 'all and ' 225 | @if ($media-type == 'all' and $media-query != '') { 226 | $media-type: ''; 227 | $media-query: str-slice(unquote($media-query), 6); 228 | } 229 | 230 | @media #{$media-type + $media-query} { 231 | @content; 232 | } 233 | } 234 | } 235 | 236 | /// Quick sort 237 | /// 238 | /// @author Sam Richards 239 | /// @access private 240 | /// @param {List} $list - List to sort 241 | /// @returns {List} Sorted List 242 | @function _mq-quick-sort($list) { 243 | $less: (); 244 | $equal: (); 245 | $large: (); 246 | 247 | @if length($list) > 1 { 248 | $seed: nth($list, ceil(length($list) / 2)); 249 | 250 | @each $item in $list { 251 | @if ($item == $seed) { 252 | $equal: append($equal, $item); 253 | } @else if ($item < $seed) { 254 | $less: append($less, $item); 255 | } @else if ($item > $seed) { 256 | $large: append($large, $item); 257 | } 258 | } 259 | 260 | @return join(join(_mq-quick-sort($less), $equal), _mq-quick-sort($large)); 261 | } 262 | 263 | @return $list; 264 | } 265 | 266 | /// Sort a map by values (works with numbers only) 267 | /// 268 | /// @access private 269 | /// @param {Map} $map - Map to sort 270 | /// @returns {Map} Map sorted by value 271 | @function _mq-map-sort-by-value($map) { 272 | $map-sorted: (); 273 | $map-keys: map-keys($map); 274 | $map-values: map-values($map); 275 | $map-values-sorted: _mq-quick-sort($map-values); 276 | 277 | // Reorder key/value pairs based on key values 278 | @each $value in $map-values-sorted { 279 | $index: index($map-values, $value); 280 | $key: nth($map-keys, $index); 281 | $map-sorted: map-merge($map-sorted, ($key: $value)); 282 | 283 | // Unset the value in $map-values to prevent the loop 284 | // from finding the same index twice 285 | $map-values: set-nth($map-values, $index, 0); 286 | } 287 | 288 | @return $map-sorted; 289 | } 290 | 291 | /// Add a breakpoint 292 | /// 293 | /// @param {String} $name - Name of the breakpoint 294 | /// @param {Number} $width - Width of the breakpoint 295 | /// 296 | /// @requires {Variable} $mq-breakpoints 297 | /// 298 | /// @example scss 299 | /// @include mq-add-breakpoint(tvscreen, 1920px); 300 | /// @include mq(tvscreen) {} 301 | @mixin mq-add-breakpoint($name, $width) { 302 | $new-breakpoint: ($name: $width); 303 | $mq-breakpoints: map-merge($mq-breakpoints, $new-breakpoint) !global; 304 | $mq-breakpoints: _mq-map-sort-by-value($mq-breakpoints) !global; 305 | } 306 | 307 | /// Show the active breakpoint in the top right corner of the viewport 308 | /// @link https://github.com/sass-mq/sass-mq#seeing-the-currently-active-breakpoint 309 | /// 310 | /// @param {List} $show-breakpoints ($mq-show-breakpoints) - List of breakpoints to show in the top right corner 311 | /// @param {Map} $breakpoints ($mq-breakpoints) - Breakpoint names and sizes 312 | /// 313 | /// @requires {Variable} $mq-breakpoints 314 | /// @requires {Variable} $mq-show-breakpoints 315 | /// 316 | /// @example scss 317 | /// // Show breakpoints using global settings 318 | /// @include mq-show-breakpoints; 319 | /// 320 | /// // Show breakpoints using custom settings 321 | /// @include mq-show-breakpoints((L, XL), (S: 300px, L: 800px, XL: 1200px)); 322 | @mixin mq-show-breakpoints($show-breakpoints: $mq-show-breakpoints, $breakpoints: $mq-breakpoints) { 323 | body:before { 324 | background-color: #FCF8E3; 325 | border-bottom: 1px solid #FBEED5; 326 | border-left: 1px solid #FBEED5; 327 | color: #C09853; 328 | font: small-caption; 329 | padding: 3px 6px; 330 | pointer-events: none; 331 | position: fixed; 332 | right: 0; 333 | top: 0; 334 | z-index: 100; 335 | 336 | // Loop through the breakpoints that should be shown 337 | @each $show-breakpoint in $show-breakpoints { 338 | $width: mq-get-breakpoint-width($show-breakpoint, $breakpoints); 339 | @include mq($show-breakpoint, $breakpoints: $breakpoints) { 340 | content: "#{$show-breakpoint} ≥ #{$width} (#{mq-px2em($width)})"; 341 | } 342 | } 343 | } 344 | } 345 | 346 | @if length($mq-show-breakpoints) > 0 { 347 | @include mq-show-breakpoints; 348 | } 349 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Core styles 3 | // 4 | @import 'Reset'; 5 | @import 'UI'; 6 | @import '_mq.scss'; 7 | 8 | $mq-breakpoints: ( 9 | mobile: 320px, 10 | tablet: 740px, 11 | desktop: 980px, 12 | wide: 1300px 13 | ); 14 | 15 | // 16 | // Custom Elements 17 | // 18 | @import 'SettingsForm'; 19 | @import 'LodestoneNews'; 20 | 21 | @import 'CharacterAddForm'; 22 | @import 'CharacterList'; 23 | @import 'CharacterView'; 24 | -------------------------------------------------------------------------------- /src/css/debug.scss: -------------------------------------------------------------------------------- 1 | $debug: 1; 2 | 3 | @import 'app'; 4 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | // Initialize Game Launcher logic 2 | import GameLauncher from './xiv/GameLauncher'; 3 | import GameLauncherWindow from './xiv/GameLauncherWindow'; 4 | 5 | GameLauncher.init(); 6 | GameLauncherWindow.init(); 7 | -------------------------------------------------------------------------------- /src/js/xiv/ButtonActions.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | import GameLauncher from "./GameLauncher"; 3 | import Characters from "./Characters"; 4 | import SettingsManager from "./SettingsManager"; 5 | import LodestoneNews from "./LodestoneNews"; 6 | const dialog = require('electron').remote.dialog; 7 | const { shell } = require('electron'); 8 | 9 | 10 | /** 11 | * Watches all the buttons 12 | */ 13 | class ButtonActions 14 | { 15 | watch() 16 | { 17 | const $html = $('html'); 18 | const $addCharacterForm = $('.add-character-form'); 19 | const $btnAddCharacter = $('#AddCharacter'); 20 | const $windowSettingsForm = $('.settings-form'); 21 | 22 | $html.on('click', '#AddCharacter', event => { 23 | GameLauncher.requestLogin(); 24 | }); 25 | 26 | $html.on('click', '#OpenAddCharacterWindow', event => { 27 | $addCharacterForm.addClass('open'); 28 | $btnAddCharacter.prop('disabled', false); 29 | }); 30 | 31 | $html.on('click', '#CloseAddCharacterWindow', event => { 32 | $addCharacterForm.removeClass('open'); 33 | $btnAddCharacter.prop('disabled', false); 34 | }); 35 | 36 | $html.on('click', '#FindGamePath', event => { 37 | const path = dialog.showOpenDialog({ 38 | properties: ['openDirectory'] 39 | }); 40 | 41 | if (path) { 42 | document.getElementById('gamePath').value = path[0].trim(); 43 | } 44 | }); 45 | 46 | $html.on('click', '#SaveSettings', event => { 47 | const gamePath = document.getElementById('gamePath').value.trim(); 48 | const expansion = document.getElementById('expansion').value.trim(); 49 | const language = document.getElementById('language').value.trim(); 50 | const region = document.getElementById('region').value.trim(); 51 | const raelysLanguage = document.getElementById('raelysLanguage').value.trim(); 52 | const closeAppOnGameStart = document.getElementById('closeAppOnGameStart').value.trim(); 53 | 54 | SettingsManager.saveSettings({ 55 | gamePath: gamePath, 56 | expansion: expansion, 57 | language: language, 58 | region: region, 59 | raelysLanguage: raelysLanguage, 60 | closeAppOnGameStart: (closeAppOnGameStart === "true") 61 | }); 62 | 63 | $('.settings-form').removeClass('open'); 64 | }); 65 | 66 | $html.on('click', '#OpenLauncherSettingsWindow', event => { 67 | $windowSettingsForm.addClass('open'); 68 | }); 69 | 70 | $html.on('click', '#CloseLauncherSettingsWindow', event => { 71 | $windowSettingsForm.removeClass('open'); 72 | }); 73 | 74 | $html.on('click', '#ShowCharacter', event => { 75 | const id = $(event.currentTarget).data('id'); 76 | Characters.showCharacter(id); 77 | }); 78 | 79 | $html.on('click', '#StartGame', event => { 80 | const id = $(event.currentTarget).data('id'); 81 | Characters.bootCharacter(id); 82 | }); 83 | 84 | $html.on('click', '#DeleteCharacter', event => { 85 | const id = $(event.currentTarget).data('id'); 86 | Characters.deleteCharacter(id); 87 | }); 88 | 89 | $html.on('click', '#ShowLodestoneNews', event => { 90 | LodestoneNews.open ? LodestoneNews.hideNews() : LodestoneNews.showNews(); 91 | }); 92 | 93 | $html.on('click', '#ShowMogStation', event => { 94 | shell.openExternal("https://secure.square-enix.com/account/app/svc/mogstation/"); 95 | }); 96 | 97 | $html.on('keyup', '#otp2', event => { 98 | const key = event.which || event.charCode || event.keyCode; 99 | 100 | if (key === 13) { 101 | $('.btn-start-game').trigger('click'); 102 | } 103 | }) 104 | } 105 | } 106 | 107 | export default new ButtonActions(); 108 | -------------------------------------------------------------------------------- /src/js/xiv/Characters.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | import Settings from './Settings'; 3 | import SettingsManager from './SettingsManager'; 4 | import GameLauncher from "./GameLauncher"; 5 | import Notice from './Notice'; 6 | import XIVAPI from "./XIVAPI"; 7 | import Login from "./Login"; 8 | 9 | const fs = require("fs"); 10 | const app = require('electron').remote.app; 11 | 12 | /** 13 | * Handle managing saved characters 14 | */ 15 | class Characters 16 | { 17 | constructor() 18 | { 19 | this.list = {}; 20 | this.directory = app.getPath('userData') + '/data/'; 21 | this.filename = 'characters.json'; 22 | 23 | // https://xivapi.com/servers/dc 24 | this.servers = { 25 | "Aether": ["Adamantoise", "Cactuar", "Faerie", "Gilgamesh", "Jenova", "Midgardsormr", "Sargatanas", "Siren"], 26 | "Chaos": ["Cerberus", "Louisoix", "Moogle", "Omega", "Ragnarok", "Spriggan"], 27 | "Crystal": ["Balmung", "Brynhildr", "Coeurl", "Diabolos", "Goblin", "Malboro", "Mateus", "Zalera"], 28 | "Elemental": ["Aegis", "Atomos", "Carbuncle", "Garuda", "Gungnir", "Kujata", "Ramuh", "Tonberry", "Typhon", "Unicorn"], 29 | "Gaia": ["Alexander", "Bahamut", "Durandal", "Fenrir", "Ifrit", "Ridill", "Tiamat", "Ultima", "Valefor", "Yojimbo", "Zeromus"], 30 | "Light": ["Lich", "Odin", "Phoenix", "Shiva", "Zodiark", "Twintania"], 31 | "Mana": ["Anima", "Asura", "Belias", "Chocobo", "Hades", "Ixion", "Mandragora", "Masamune", "Pandaemonium", "Shinryu", "Titan"], 32 | "Primal": ["Behemoth", "Excalibur", "Exodus", "Famfrit", "Hyperion", "Lamia", "Leviathan", "Ultros"] 33 | }; 34 | 35 | $('.character-fade').on('click', event => { 36 | this.hideCharacterView(); 37 | }); 38 | } 39 | 40 | /** 41 | * Load the game servers into the "Add Character" server selection. 42 | */ 43 | loadGameServers() 44 | { 45 | const $select = $('#characterServer'); 46 | 47 | for(let dc in this.servers) { 48 | let servers = this.servers[dc]; 49 | const $ele = $(``); 50 | 51 | for(let i in servers) { 52 | let server = servers[i]; 53 | $ele.append(``); 54 | } 55 | 56 | $select.append($ele); 57 | } 58 | } 59 | 60 | /** 61 | * Populates some data from XIVAPI 62 | */ 63 | populateCharacterData() 64 | { 65 | // do nothing if no characters 66 | if (Object.keys(this.list).length === 0) { 67 | return; 68 | } 69 | 70 | for(let i in this.list) { 71 | let character = this.list[i]; 72 | 73 | // only process null characters 74 | if (character.lsid === null) { 75 | // search for character 76 | // the timeout here is a bit of a hack to prevent getting rate limited on XIVAPI if you have 10+ characters 77 | setTimeout(() => { 78 | XIVAPI.searchCharacter(character.name, character.server, response => { 79 | if (response.Pagination.ResultsTotal > 0) { 80 | for(let i in response.Results) { 81 | let result = response.Results[i]; 82 | 83 | // if found, update character 84 | if (result.Name === character.name && result.Selection === character.Server) { 85 | // update character and request to save it 86 | character.avatar = result.Avatar; 87 | character.lsid = result.ID; 88 | this.saveCharacter(character); 89 | break; 90 | } 91 | } 92 | } 93 | }); 94 | }, Math.floor((Math.random() * 5000) + 1)); 95 | } 96 | 97 | // process adding to XIVAPI 98 | if (character.lsid !== null && character.api === false) { 99 | // the timeout here is a bit of a hack to prevent getting rate limited on XIVAPI if you have 10+ characters 100 | setTimeout(() => { 101 | XIVAPI.getCharacter(character.lsid, response => { 102 | if (response.Info.Character.State === 2) { 103 | character.api = true; 104 | this.saveCharacter(character); 105 | } 106 | }); 107 | }, Math.floor((Math.random() * 5000) + 1)); 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Update a character in our list 114 | */ 115 | saveCharacter(character) 116 | { 117 | this.list[character.id] = character; 118 | this.saveCharacterList(true); 119 | } 120 | 121 | /** 122 | * Save the character list and reload the visual list 123 | */ 124 | saveCharacterList(populate) 125 | { 126 | // create directory if it does not exist 127 | if (!fs.existsSync(this.directory)){ 128 | fs.mkdirSync(this.directory); 129 | } 130 | 131 | const json = JSON.stringify(this.list, null, 2); 132 | fs.writeFileSync(`${this.directory}${this.filename}`, json, "utf-8"); 133 | 134 | // cheap way to initialize all ui :D 135 | this.loadCharacters(populate); 136 | } 137 | 138 | /** 139 | * Load characters 140 | * @param populate 141 | */ 142 | loadCharacters(populate) 143 | { 144 | // if directory does not exist, create it + the characters file 145 | if (!fs.existsSync(this.directory)){ 146 | this.saveCharacterList(false); 147 | return; 148 | } 149 | 150 | // if file does not exist 151 | if (!fs.existsSync(`${this.directory}${this.filename}`)) { 152 | console.log('No character.json file'); 153 | return; 154 | } 155 | 156 | // load character list 157 | let list = fs.readFileSync(`${this.directory}${this.filename}`, 'utf8'); 158 | 159 | // do nothing if we have no saved characters 160 | if (list.length === 0) { 161 | return; 162 | } 163 | 164 | // character list 165 | this.list = JSON.parse(list); 166 | 167 | // dom element 168 | const $list = $('.cl-list'); 169 | $list.html(''); 170 | 171 | for(let i in this.list) { 172 | let character = this.list[i]; 173 | 174 | const otp = character.otp ? '' : ''; 175 | 176 | $list.append(` 177 | 184 | `); 185 | } 186 | 187 | if (populate) { 188 | // populate character info 189 | this.populateCharacterData(); 190 | } 191 | } 192 | 193 | /** 194 | * Request a character to be deleted 195 | */ 196 | deleteCharacter(id) 197 | { 198 | // do nothing if no characters 199 | if (Object.keys(this.list).length === 0) { 200 | return; 201 | } 202 | 203 | delete this.list[id]; 204 | this.saveCharacterList(true); 205 | this.hideCharacterView(); 206 | } 207 | 208 | /** 209 | * Show a specific characters information and "START GAME" action 210 | */ 211 | showCharacter(id) 212 | { 213 | const $view = $('.character-view'); 214 | const character = this.list[id]; 215 | $('.character-fade').addClass('open'); 216 | 217 | let otp = ''; 218 | if (character.otp) { 219 | otp = `
` 220 | } 221 | 222 | $view.html(` 223 |
224 | 225 |

${character.name}

226 | ${character.server} - ${character.username} 227 |
228 |
229 |
230 | ${otp} 231 | 232 |
233 |
234 |
235 | 236 |
237 | `); 238 | 239 | $view.addClass('open'); 240 | 241 | // auto focus 242 | if (character.otp) { 243 | $('.otp2').focus(); 244 | } 245 | } 246 | 247 | /** 248 | * Hide the characters information view 249 | */ 250 | hideCharacterView() 251 | { 252 | $('.character-view').removeClass('open'); 253 | $('.character-fade').removeClass('open'); 254 | } 255 | 256 | /** 257 | * Boot up a specific character, will auto-login and start the game. 258 | */ 259 | bootCharacter(id) 260 | { 261 | // grab character 262 | const character = this.list[id]; 263 | if (typeof character === 'undefined') { 264 | // this IN THEORY should never happen ... 265 | Notice.show('

Character save invalid

could not find character in load list

'); 266 | } 267 | 268 | // get otp if its needed 269 | const $otp = $('#otp2'); 270 | const otpCode = $otp.val(); 271 | const otpNumeric = !isNaN(parseFloat(otpCode)) && isFinite(otpCode); 272 | 273 | if (otpCode && (otpCode.length !== 6 || otpNumeric === false)) { 274 | Notice.show('

Your OTP is invalid!

One Time Passwords are six digit numbers provided by the SQEX Token mobile app.

'); 275 | return; 276 | } 277 | 278 | // notify user 279 | Notice.show('

Starting Game

Logging into character...

'); 280 | this.hideCharacterView(); 281 | 282 | // load custom settings 283 | SettingsManager.loadSettings(); 284 | 285 | // login to character 286 | Login.login(character.username, character.password, otpCode, response => { 287 | // launch game! 288 | Notice.show('

Starting Game

Launching game!

'); 289 | GameLauncher.launchGame(response.userRealSid); 290 | 291 | // hide notice after 5 seconds 292 | setTimeout(() => { 293 | // if close app on game start is set 294 | if (Settings.closeAppOnGameStart) { 295 | require('electron').remote.getCurrentWindow().close(); 296 | return; 297 | } 298 | 299 | Notice.hide(); 300 | }, 3000); 301 | }); 302 | } 303 | } 304 | 305 | export default new Characters(); 306 | -------------------------------------------------------------------------------- /src/js/xiv/GameFiles.js: -------------------------------------------------------------------------------- 1 | import Settings from './Settings'; 2 | const sha1File = require('sha1-file'); 3 | const fs = require('fs'); 4 | 5 | class GameFiles 6 | { 7 | hash() 8 | { 9 | const files = [ 10 | 'ffxivboot.exe', 11 | 'ffxivboot64.exe', 12 | 'ffxivlauncher.exe', 13 | 'ffxivlauncher64.exe', 14 | 'ffxivupdater.exe', 15 | 'ffxivupdater64.exe' 16 | ]; 17 | 18 | for(let i in files) { 19 | const sizeAndHash = this.getSizeAndHash(`/boot/${files[i]}`); 20 | 21 | if (!sizeAndHash) { 22 | return false; 23 | } 24 | 25 | files[i] = `${files[i]}/${sizeAndHash}`; 26 | } 27 | 28 | return files.join(','); 29 | } 30 | 31 | version() 32 | { 33 | let filename = Settings.se.GamePath + '/game/ffxivgame.ver'; 34 | if (!fs.existsSync(filename)) { 35 | return false; 36 | } 37 | 38 | let buffer = fs.readFileSync(filename); 39 | 40 | return buffer.toString(); 41 | } 42 | 43 | getSizeAndHash(filename) 44 | { 45 | filename = Settings.se.GamePath + filename; 46 | 47 | if (!fs.existsSync(filename)) { 48 | alert("Your game path could not be found, please update it via the settings."); 49 | return false; 50 | } 51 | 52 | let hash = sha1File(filename), 53 | length = fs.statSync(filename).size; 54 | return length + '/' + hash; 55 | } 56 | } 57 | 58 | export default new GameFiles(); 59 | -------------------------------------------------------------------------------- /src/js/xiv/GameLauncher.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | import Settings from './Settings'; 3 | import SettingsManager from './SettingsManager'; 4 | import Login from './Login'; 5 | import ButtonActions from './ButtonActions'; 6 | import Characters from './Characters'; 7 | import Lodestone from './LodestoneNews'; 8 | import GameFiles from './GameFiles'; 9 | import Notice from './Notice'; 10 | const fs = require('fs'); 11 | //const keytar = require('keytar'); 12 | 13 | class GameLauncher 14 | { 15 | init() 16 | { 17 | // load custom settings 18 | SettingsManager.loadSettings(); 19 | 20 | // load characters 21 | Characters.loadCharacters(true); 22 | Characters.loadGameServers(); 23 | 24 | // watch button interactions 25 | ButtonActions.watch(); 26 | 27 | // show lodestone news 28 | Lodestone.showNews(); 29 | 30 | // this is on a loop so that if a character does not exist on the API, 31 | // then hopefully in the next few minutes it will. Current settings 32 | // is every 1 minute, if a character is on the API then it is not 33 | // queried anymore times. 34 | Characters.populateCharacterData(); 35 | setInterval(() => { 36 | Characters.populateCharacterData(); 37 | }, Settings.custom.xivapiPollDelay); 38 | } 39 | 40 | /** 41 | * Request a login to Square-Enix account system 42 | */ 43 | requestLogin() 44 | { 45 | /* 46 | todo - fix, this is trying to valid otp, but not everyone has one... 47 | 48 | const invalid = []; 49 | $('#AddCharacterForm').find('input:invalid').each((i, o)=>invalid.push(o.placeholder)); 50 | 51 | if (invalid) { 52 | Notice.show(`

Some inputs were invalid!

Please check the following: ${invalid.join(", ")}

`); 53 | return; 54 | } 55 | 56 | */ 57 | 58 | const name = $('#characterName').val().trim(); 59 | const server = $('#characterServer').val().trim(); 60 | const username = $('#username').val().trim(); 61 | const password = $('#password').val().trim(); 62 | const otp = $('#otp').val().trim(); 63 | 64 | // check we filled in form 65 | if (name.length === 0 || server.length === 0 || username.length === 0 || password.length === 0) { 66 | Notice.show('Please fill in the form correctly!'); 67 | return; 68 | } 69 | 70 | // disable save button and inform user what we're doing 71 | $('#AddCharacter').prop('disable', true); 72 | Notice.show('

Checking account details

Using the details provided, the launcher is attempting to login so it can confirm and save this character.

This will not start the game.

'); 73 | 74 | // process login 75 | Login.login(username, password, otp, response => { 76 | console.log('LOGIN COMPLETE'); 77 | console.log('USER SID == '+ response.userRealSid); 78 | console.log('LIVE GAME VERSION == '+ response.latestGameVersion); 79 | 80 | if (response.userRealSid) { 81 | const id = require('uuid/v4')(); 82 | 83 | // store password in users OS vault 84 | //keytar.setPassword('ffxiv-launcher', id, password); 85 | 86 | // save character 87 | Characters.saveCharacter({ 88 | id: id, 89 | api: false, 90 | lsid: null, 91 | name: name, 92 | server: server, 93 | avatar: 'https://xivapi.com/launcher/faceless.png', 94 | username: username, 95 | password: password, 96 | otp: otp.length > 1, 97 | added: (new Date).getTime() 98 | }); 99 | 100 | // hide add character view 101 | $('.add-character-form').removeClass('open'); 102 | Notice.show('

Saved character!

Click on the character on the right to start the game.

'); 103 | 104 | // auto close notice after 5 seconds 105 | setTimeout(() => { 106 | Notice.hide(); 107 | }, 5000); 108 | } else { 109 | Notice.show('

Login failed

Either your Username/Password/OTP is wrong or the game is down for maintenance right now.

'); 110 | } 111 | }); 112 | } 113 | 114 | /** 115 | * Launch the game using the session id 116 | */ 117 | launchGame(userSid) 118 | { 119 | const gameFilename = Settings.se.GamePath + Settings.se.Dx11Path; 120 | if (!fs.existsSync(gameFilename)) { 121 | Notice.show("Your game path could not be found, please update it via the settings."); 122 | return false; 123 | } 124 | 125 | let expansion = 2; 126 | if (Settings.expansion in Settings.expansions){ 127 | expansion = Settings.expansion 128 | } 129 | 130 | let language = 1; 131 | if (Settings.language in Settings.languages){ 132 | language = Settings.language 133 | } 134 | 135 | let region = 3; 136 | if (Settings.region in Settings.regions){ 137 | region = Settings.region 138 | } 139 | 140 | const gameArguments = [ 141 | 'DEV.UseSqPack=' + Settings.se.UseSqPack, 142 | 'DEV.DataPathType=' + Settings.se.DataPathType, 143 | 'DEV.TestSID=' + userSid, 144 | 'DEV.MaxEntitledExpansionID=' + expansion, 145 | 'language=' + language, 146 | 'SYS.Region=' + region, 147 | 'ver=' + GameFiles.version() 148 | ]; 149 | 150 | const options = { 151 | detached: true, 152 | stdio: 'ignore' 153 | }; 154 | 155 | require('child_process').spawn(gameFilename, gameArguments, options); 156 | } 157 | } 158 | 159 | export default new GameLauncher(); 160 | -------------------------------------------------------------------------------- /src/js/xiv/GameLauncherWindow.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | const { remote } = require('electron'); 3 | const shell = require('electron').shell; 4 | 5 | class GameLauncherWindow 6 | { 7 | constructor() 8 | { 9 | this.maximizeState = false; 10 | } 11 | 12 | init() 13 | { 14 | document.getElementById("Launcher.Window.Min").addEventListener("click", () => { 15 | remote.BrowserWindow.getFocusedWindow().minimize(); 16 | }); 17 | 18 | document.getElementById("Launcher.Window.Max").addEventListener("click", () => { 19 | let window = remote.BrowserWindow.getFocusedWindow(); 20 | 21 | if (this.maximizeState) { 22 | window.unmaximize(); 23 | this.maximizeState = false; 24 | return; 25 | } 26 | 27 | window.maximize(); 28 | this.maximizeState = true; 29 | }); 30 | 31 | document.getElementById("Launcher.Window.Close").addEventListener("click", () => { 32 | remote.BrowserWindow.getFocusedWindow().close(); 33 | }); 34 | 35 | /** 36 | * Ensures links open externally and not within the launcher 37 | */ 38 | $(document).on('click', 'a[href^="http"]', (event) => { 39 | event.preventDefault(); 40 | shell.openExternal(event.target.href); 41 | }); 42 | 43 | /** 44 | * Set a random background 45 | */ 46 | const bgNumber = Math.floor(Math.random() * (8 - 1 + 1)) + 1; 47 | const $main = $('main'); 48 | 49 | $main.css('background', `url('https://xivapi.com/launcher/background${bgNumber}.jpg')`); 50 | $main.css('background-size', 'cover'); 51 | } 52 | } 53 | 54 | export default new GameLauncherWindow(); 55 | 56 | -------------------------------------------------------------------------------- /src/js/xiv/LodestoneNews.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | import XIVAPI from "./XIVAPI"; 3 | const moment = require("moment"); 4 | 5 | /** 6 | * Lodestone News 7 | */ 8 | class LodestoneNews 9 | { 10 | constructor() 11 | { 12 | this.news = null; 13 | this.$view = $('.lodestone-news'); 14 | this.open = true; 15 | } 16 | 17 | showNews() 18 | { 19 | this.open = true; 20 | this.$view.html(''); 21 | 22 | if (this.news) { 23 | this.showNewsRender(this.news); 24 | return; 25 | } 26 | 27 | XIVAPI.getLodestoneData(response => { 28 | this.news = response; 29 | this.showNewsRender(response); 30 | }); 31 | } 32 | 33 | /** 34 | * todo - the url for opening links should be a config setting. 35 | * @param response 36 | */ 37 | showNewsRender(response) 38 | { 39 | this.$view.addClass('open'); 40 | 41 | for(let i in response.News) { 42 | const news = response.News[i]; 43 | const time = moment(news.Time * 1000).format("dddd, MMMM Do YYYY"); 44 | 45 | this.$view.append(` 46 |
47 |
48 |
49 |

${news.Title}

50 | ${time} 51 |
52 |
53 | `); 54 | 55 | if (i > 8) { 56 | break; 57 | } 58 | } 59 | } 60 | 61 | hideNews() 62 | { 63 | this.open = false; 64 | this.$view.removeClass('open'); 65 | } 66 | } 67 | 68 | export default new LodestoneNews(); 69 | -------------------------------------------------------------------------------- /src/js/xiv/Login.js: -------------------------------------------------------------------------------- 1 | import GameFiles from './GameFiles'; 2 | import XIVRequest from './XIVRequest'; 3 | import Notice from './Notice'; 4 | 5 | class Login 6 | { 7 | constructor() 8 | { 9 | this.username = false; 10 | this.password = false; 11 | this.otp = false; 12 | 13 | } 14 | 15 | login(username, password, otp, callback) 16 | { 17 | this.username = username; 18 | this.password = password; 19 | this.otp = otp; 20 | 21 | // ask for the real USER_SID 22 | this.getRealUserSid(callback); 23 | } 24 | 25 | getRealUserSid(callback) 26 | { 27 | this.getSudoUserSid(SUDO_USER_ID => { 28 | let localGameVersion = GameFiles.version(); 29 | if (!localGameVersion) { 30 | Notice.show("Your game version could not be found, please check the game path via the settings."); 31 | return; 32 | } 33 | 34 | let localGameHash = GameFiles.hash(); 35 | 36 | XIVRequest.getRealUserSid( 37 | SUDO_USER_ID, 38 | localGameVersion, 39 | localGameHash, 40 | callback 41 | ) 42 | }); 43 | } 44 | 45 | getSudoUserSid(callback) 46 | { 47 | // get temp id for form 48 | this.getTempUserSid(TEMP_USER_ID => { 49 | // login to get fake user id 50 | XIVRequest.getFakeUserSid( 51 | TEMP_USER_ID, 52 | this.username, 53 | this.password, 54 | this.otp, 55 | callback 56 | ) 57 | }); 58 | } 59 | 60 | getTempUserSid(callback) 61 | { 62 | XIVRequest.getTempUserSid(callback); 63 | } 64 | } 65 | 66 | export default new Login(); 67 | -------------------------------------------------------------------------------- /src/js/xiv/Notice.js: -------------------------------------------------------------------------------- 1 | import $ from 'webpack-zepto'; 2 | 3 | class Notice 4 | { 5 | constructor() 6 | { 7 | this.$notice = $('.notice'); 8 | this.$noticeFade = $('.notice-fade'); 9 | 10 | // hide the notice on click (anywhere) 11 | this.$noticeFade.on('click', event => { 12 | this.hide(); 13 | }); 14 | 15 | this.$notice.on('click', event => { 16 | this.hide(); 17 | }); 18 | } 19 | 20 | show(message) 21 | { 22 | this.$notice.html(message); 23 | this.$notice.addClass('open'); 24 | this.$noticeFade.addClass('open'); 25 | } 26 | 27 | hide() 28 | { 29 | this.$notice.removeClass('open'); 30 | this.$noticeFade.removeClass('open'); 31 | } 32 | } 33 | 34 | export default new Notice(); 35 | -------------------------------------------------------------------------------- /src/js/xiv/RaelysAPI.js: -------------------------------------------------------------------------------- 1 | import Settings from "./Settings"; 2 | 3 | /** 4 | * Interact with XIVAPI 5 | */ 6 | class RaelysAPI 7 | { 8 | 9 | getLodestoneData(callback) 10 | { 11 | this.request('/news/feed', [], callback); 12 | } 13 | 14 | /** 15 | * Send request to XIVAPI 16 | */ 17 | request(url, params, callback) 18 | { 19 | if (Settings.RaelysAPILanguage in Settings.custom.RaelysAPILanguages){ 20 | Settings.custom.RaelysAPIEndpoint = Settings.custom.RaelysAPIProtocol + Settings.RaelysAPILanguage + "." + Settings.custom.RaelysAPIURL; 21 | } 22 | 23 | url = `${Settings.custom.RaelysAPIEndpoint}${url}`; 24 | 25 | console.log(url); 26 | 27 | let xhr = new XMLHttpRequest(); 28 | xhr.open("GET", url, true); 29 | xhr.onreadystatechange = () => { 30 | if (xhr.readyState === 4 && xhr.status === 200) { 31 | callback( 32 | JSON.parse(xhr.responseText) 33 | ); 34 | } else if (xhr.readyState === 4) { 35 | console.error('RaelysAPI ERROR', xhr.status); 36 | } 37 | }; 38 | 39 | xhr.send( 40 | //JSON.stringify(payload) 41 | ); 42 | } 43 | } 44 | 45 | export default new RaelysAPI(); -------------------------------------------------------------------------------- /src/js/xiv/Settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FFXIV Custom Launcher Settings 3 | */ 4 | module.exports = { 5 | custom: { 6 | // XIVAPI 7 | xivapiEndpoint: "https://xivapi.com", 8 | xivapiPollDelay: 60 * 1000, 9 | RaelysAPIProtocol: "http://", 10 | RaelysAPIURL: "lodestone.raelys.com", 11 | RaelysAPIEndpoint: "http://na.lodestone.raelys.com", 12 | RaelysAPILanguages: { 13 | "na": 'North America', 14 | "de": 'Germany', 15 | "eu": 'Europe', 16 | "fr": 'France', 17 | "jp": 'Japan', 18 | }, 19 | // Characters 20 | checkCharacterExpiryDelay: 60000, 21 | closeAppOnGameStart: false, 22 | }, 23 | // Square-Enix specific options 24 | se: { 25 | GamePath: 'C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn', 26 | Dx9Path: '\\game\\ffxiv.exe', 27 | Dx11Path: '\\game\\ffxiv_dx11.exe', 28 | UserAgent: 'SQEXAuthor/2.0.0(Windows 6.2; ja-jp; -id-)', 29 | UseSqPack: 1, 30 | DataPathType: 1, 31 | LoginGameVersionRequest: { 32 | Host: 'patch-gamever.ffxiv.com', 33 | Port: 443, 34 | Path: '/http/win32/ffxivneo_release_game/{GAMEVER}/{USER_SID}', 35 | ContentType: 'application/x-www-form-urlencoded', 36 | Referer: 'https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top?lng=en&rgn=3' 37 | }, 38 | LoginOAuthFormRequest: { 39 | Host: 'ffxiv-login.square-enix.com', 40 | Port: 443, 41 | Path: '/oauth/ffxivarr/login/top?lng=en&rgn=3&isft=0&issteam=0', 42 | Method: 'POST', 43 | }, 44 | LoginOAuthActionRequest: { 45 | Host: 'ffxiv-login.square-enix.com', 46 | Port: 443, 47 | Path: '/oauth/ffxivarr/login/login.send', 48 | Method: 'POST', 49 | ContentType: 'application/x-www-form-urlencoded', 50 | Referer: 'https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top?lng=en&rgn=3&isft=0&issteam=0' 51 | } 52 | }, 53 | // the numbers of these are important 54 | languages: { 55 | 0: 'Japanese', 56 | 1: 'English', 57 | 2: 'German', 58 | 3: 'French', 59 | }, 60 | // the numbers of these are important 61 | expansions: { 62 | 0: 'A Realm Reborn', 63 | 1: 'Heavensward', 64 | 2: 'Stormblood', 65 | 3: 'ShadowBringers' 66 | }, 67 | // the numbers of these are important 68 | regions: { 69 | 0: 'No Message', //Data Center not working 70 | 1: 'Japanese Message', 71 | 2: 'ESRB Rating Message', 72 | 3: 'No Message' //Data Center working 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/js/xiv/SettingsManager.js: -------------------------------------------------------------------------------- 1 | import Settings from "./Settings"; 2 | const fs = require("fs"); 3 | const app = require('electron').remote.app; 4 | 5 | class SettingsManager 6 | { 7 | constructor() 8 | { 9 | // List of custom settings 10 | this.custom = { 11 | gamePath: null, 12 | }; 13 | 14 | this.directory = app.getPath('userData') + '/data/'; 15 | this.filename = 'settings.json'; 16 | } 17 | 18 | setOption(option, value) 19 | { 20 | this.custom[option] = value; 21 | } 22 | 23 | loadSettings() 24 | { 25 | // create directory if it does not exist 26 | if (!fs.existsSync(this.directory)){ 27 | fs.mkdirSync(this.directory); 28 | } 29 | 30 | // if file does not exist, create it 31 | if (!fs.existsSync(`${this.directory}${this.filename}`)) { 32 | this.saveSettings(); 33 | } 34 | 35 | // load character list 36 | const localSettings = fs.readFileSync(`${this.directory}${this.filename}`, 'utf8'); 37 | if (localSettings !== null && localSettings.length > 3) { 38 | this.custom = JSON.parse(localSettings); 39 | } 40 | 41 | // special ones that are required for the launcher 42 | Settings.se.GamePath = this.custom.gamePath; 43 | Settings.language = this.custom.language; //Game Language 44 | Settings.region = this.custom.region; //Game Language 45 | Settings.RaelysAPILanguage = this.custom.raelysLanguage; 46 | Settings.expansion = this.custom.expansion; 47 | Settings.closeAppOnGameStart = this.custom.closeAppOnGameStart; 48 | 49 | // populate forms 50 | for (let option in this.custom) { 51 | let value = this.custom[option]; 52 | document.getElementById(option).value = value; 53 | } 54 | } 55 | 56 | saveSettings(settings) 57 | { 58 | for (let option in settings) { 59 | let value = settings[option]; 60 | this.setOption(option, value); 61 | } 62 | 63 | const json = JSON.stringify(this.custom, null, 2); 64 | fs.writeFileSync(`${this.directory}${this.filename}`, json, "utf-8"); 65 | 66 | // reload settings as it does some critical checks 67 | this.loadSettings(); 68 | } 69 | } 70 | 71 | export default new SettingsManager(); 72 | -------------------------------------------------------------------------------- /src/js/xiv/XIVAPI.js: -------------------------------------------------------------------------------- 1 | import Settings from "./Settings"; 2 | 3 | /** 4 | * Interact with XIVAPI 5 | */ 6 | class XIVAPI 7 | { 8 | getCharacter(id, callback) 9 | { 10 | this.request(`/character/${id}`, [], callback); 11 | } 12 | 13 | searchCharacter(name, server, callback) 14 | { 15 | if (name.split(' ').length != 2) { 16 | return; 17 | } 18 | 19 | name = name.replace(' ', '+'); 20 | const params = [ 21 | `name=${name}`, 22 | `server=${server}` 23 | ]; 24 | 25 | this.request('/character/search', params, callback); 26 | } 27 | 28 | getLodestoneData(callback) 29 | { 30 | this.request('/lodestone', [], callback); 31 | } 32 | 33 | /** 34 | * Send request to XIVAPI 35 | */ 36 | request(url, params, callback) 37 | { 38 | const timestamp = +new Date; 39 | 40 | url = `${Settings.custom.xivapiEndpoint}${url}`; 41 | params.push(`t=${timestamp}`); 42 | url = `${url}?${params.join('&')}`; 43 | 44 | let xhr = new XMLHttpRequest(); 45 | xhr.open("POST", url, true); 46 | xhr.onreadystatechange = () => { 47 | if (xhr.readyState === 4 && xhr.status === 200) { 48 | callback( 49 | JSON.parse(xhr.responseText) 50 | ); 51 | } else if (xhr.readyState === 4) { 52 | console.error('XIVAPI ERROR', xhr.status); 53 | } 54 | }; 55 | 56 | xhr.send( 57 | //JSON.stringify(payload) 58 | ); 59 | } 60 | } 61 | 62 | 63 | export default new XIVAPI(); 64 | -------------------------------------------------------------------------------- /src/js/xiv/XIVRequest.js: -------------------------------------------------------------------------------- 1 | import Settings from './Settings'; 2 | import os from 'os'; 3 | import crypto from 'crypto'; 4 | 5 | /** 6 | * This handles all auto-login logic 7 | */ 8 | class XIVRequest 9 | { 10 | constructor() { 11 | this.machineId = this.generateMachineId(); 12 | console.log("XIVRequest:constructor -> Generated MachineID: " + this.machineId); 13 | } 14 | 15 | printHex(char) { 16 | return ("0" + char.valueOf().toString(16)).substr(-2); 17 | }; 18 | 19 | generateMachineId() { 20 | // Uses the same method as goaaats/FFXIVQuickLauncher. 21 | // See: https://github.com/goaaats/FFXIVQuickLauncher/blob/master/XIVLauncher/XIVGame.cs#L322 22 | const idstring = [ os.hostname().toUpperCase(), os.userInfo().username, `${os.type()} ${os.release()}`, os.cpus().length ].join(''); 23 | var bytes = []; 24 | 25 | for (var i = 0; i < idstring.length; ++i) { 26 | bytes.concat([idstring.charCodeAt(i)]); 27 | } 28 | 29 | // Hash this data, yank the first 4 bytes and grab a checksum. 30 | var shasum = crypto.createHash('SHA1').update(Int8Array.from(bytes)).digest().slice(0, 4); 31 | var checksum = Math.abs(256 - (shasum.reduce((a, b) => a + b) % 256)); 32 | 33 | // Put it together as a hex byte string. 34 | return [ this.printHex(checksum), ... shasum ].reduce((s, b) => s + this.printHex(b)); 35 | } 36 | 37 | /** 38 | * Perform a request action 39 | */ 40 | action(options, postdata, callback) 41 | { 42 | // request object 43 | let req = require("https").request(options, function (response) { 44 | let body = ''; 45 | response.on('data', function (chunk) { 46 | body += chunk; 47 | }); 48 | response.on('end', function () { 49 | callback({ 50 | headers: response.headers, 51 | body: body, 52 | }) 53 | }); 54 | response.on('error', function (error) { 55 | console.log('RESPONSE_ERROR', error); 56 | }) 57 | }); 58 | 59 | req.on('error', function (error) { 60 | console.log('REQUEST_ERROR', error); 61 | }); 62 | 63 | // if any post data, attach it 64 | if (postdata) { 65 | req.write(postdata); 66 | } 67 | 68 | req.end(); 69 | } 70 | 71 | /** 72 | * Get the users temp session id for the login form. 73 | */ 74 | getTempUserSid(callback) 75 | { 76 | console.log('XIVRequest --> getTempUserSid'); 77 | 78 | // options 79 | let options = { 80 | host: Settings.se.LoginOAuthFormRequest.Host, 81 | port: Settings.se.LoginOAuthFormRequest.Port, 82 | path: Settings.se.LoginOAuthFormRequest.Path, 83 | method: Settings.se.LoginOAuthFormRequest.Method, 84 | rejectUnauthorized: false, 85 | requestCert: true, 86 | agent: false, 87 | headers: { 88 | 'User-Agent': Settings.se.UserAgent.replace('-id-', this.machineId), 89 | }, 90 | }; 91 | 92 | this.action(options, false, response => { 93 | callback( 94 | this.findDataInDom(response.body, '_STORED_') 95 | ); 96 | }); 97 | } 98 | 99 | /** 100 | * Get the users fake session id for a game-version check 101 | */ 102 | getFakeUserSid(tempUserId, username, password, otp, callback) 103 | { 104 | console.log('XIVRequest --> getFakeUserSid: ' + tempUserId); 105 | 106 | const postdata = require('querystring').stringify({ 107 | '_STORED_': tempUserId, 108 | 'sqexid': username, 109 | 'password': password, 110 | 'otppw': otp 111 | }); 112 | 113 | // options 114 | let options = { 115 | host: Settings.se.LoginOAuthActionRequest.Host, 116 | port: Settings.se.LoginOAuthActionRequest.Port, 117 | path: Settings.se.LoginOAuthActionRequest.Path, 118 | method: Settings.se.LoginOAuthActionRequest.Method, 119 | rejectUnauthorized: false, 120 | requestCert: true, 121 | agent: false, 122 | headers: { 123 | 'User-Agent': Settings.se.UserAgent.replace('-id-', this.machineId), 124 | 'Content-Type': Settings.se.LoginOAuthActionRequest.ContentType, 125 | 'Content-Length': postdata.length, 126 | 'Referer': Settings.se.LoginOAuthActionRequest.Referer 127 | }, 128 | }; 129 | 130 | this.action(options, postdata, response => { 131 | callback( 132 | this.findDataInDom(response.body, 'login=auth,ok,sid') 133 | ); 134 | }); 135 | } 136 | 137 | /** 138 | * Get the users real session id! 139 | */ 140 | getRealUserSid(tempUserId, localGameVersion, localGameHash, callback) 141 | { 142 | console.log('XIVRequest --> getRealUserSid: '+ tempUserId); 143 | 144 | let path = Settings.se.LoginGameVersionRequest.Path 145 | .replace('{GAMEVER}', localGameVersion) 146 | .replace('{USER_SID}', tempUserId); 147 | 148 | // options 149 | let options = { 150 | host: Settings.se.LoginGameVersionRequest.Host, 151 | port: Settings.se.LoginGameVersionRequest.Port, 152 | path: path, 153 | method: Settings.se.LoginGameVersionRequest.Method, 154 | rejectUnauthorized: false, 155 | requestCert: true, 156 | agent: false, 157 | headers: { 158 | 'X-Hash-Check': 'X-Hash-Check', 159 | 'User-Agent': Settings.se.UserAgent.replace('-id-', this.machineId), 160 | 'Content-Type': Settings.se.LoginGameVersionRequest.ContentType, 161 | 'Content-Length': localGameHash.length, 162 | 'Referer': Settings.se.LoginGameVersionRequest.Referer 163 | }, 164 | }; 165 | 166 | this.action(options, localGameHash, response => { 167 | callback({ 168 | latestGameVersion: response.headers['x-latest-version'], 169 | userRealSid: response.headers['x-patch-unique-id'] 170 | }); 171 | }); 172 | } 173 | 174 | findDataInDom(body, data) 175 | { 176 | let line = body.split("\n").filter(line => line.indexOf(data) > -1)[0]; 177 | 178 | if (!line) { 179 | return false; 180 | } 181 | 182 | return line 183 | .replace('', '') 194 | .trim(); 195 | } 196 | } 197 | 198 | export default new XIVRequest(); 199 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let Encore = require('@symfony/webpack-encore'); 2 | 3 | Encore 4 | .setOutputPath('public/assets/') 5 | .setPublicPath('/assets') 6 | .enableSourceMaps(!Encore.isProduction()) 7 | .addEntry('js/app', './src/js/app.js') 8 | .addStyleEntry('css/app', Encore.isProduction() ? './src/css/app.scss' : './src/css/debug.scss') 9 | .enableSingleRuntimeChunk() 10 | .enableSassLoader() 11 | ; 12 | // enaboe node environment 13 | let config = Encore.getWebpackConfig(); 14 | config.target = 'electron-renderer'; 15 | module.exports = config; 16 | --------------------------------------------------------------------------------