├── .gitattributes ├── COPYING ├── README.md ├── app ├── blockly │ ├── LICENSE │ ├── blockly.js │ ├── en.js │ ├── media │ │ ├── 1x1.gif │ │ ├── click.mp3 │ │ ├── click.ogg │ │ ├── click.wav │ │ ├── delete.mp3 │ │ ├── delete.ogg │ │ ├── delete.wav │ │ ├── disconnect.mp3 │ │ ├── disconnect.ogg │ │ ├── disconnect.wav │ │ ├── handclosed.cur │ │ ├── handdelete.cur │ │ ├── handopen.cur │ │ ├── quote0.png │ │ ├── quote1.png │ │ ├── sprites.png │ │ └── sprites.svg │ └── pibakery.js ├── blocks.js ├── img │ ├── export.png │ ├── icon.icns │ ├── icon.ico │ ├── import.png │ ├── load-sd.png │ ├── settings.png │ ├── success.png │ ├── update-sd.png │ ├── updating.gif │ ├── write-to-sd.png │ └── writing.gif ├── index.html ├── index.js ├── open-sans │ ├── open-sans-bold-ext.woff2 │ ├── open-sans-bold.woff2 │ ├── open-sans-light-ext.woff2 │ ├── open-sans-light.woff2 │ ├── open-sans-normal-ext.woff2 │ ├── open-sans-normal.woff2 │ └── open-sans.css └── styles.css ├── lib ├── blockloader.js ├── blocks.js ├── blocktransform.js ├── blockupdates.js ├── cmdline-parser.js ├── elevate.js ├── imgmanager.js ├── kpartx.js ├── multifs.js ├── rootwriter.js ├── sdcard.js ├── settings.js ├── ui.js └── updates.js ├── main.js ├── package.json ├── pibakery-blocks ├── LICENSE ├── README.md ├── authorizekey │ ├── authorizekey.json │ ├── authorizekey.py │ └── authorizekey.sh ├── bootbehaviour │ ├── bootbehaviour.json │ └── bootbehaviour.sh ├── categories.json ├── changepass │ ├── changepass.json │ └── changepass.py ├── changesshkeys │ ├── changesshkeys.json │ └── changesshkeys.sh ├── deletefile │ ├── deleteFile.sh │ └── deletefile.json ├── downloadfile │ ├── downloadFile.sh │ └── downloadfile.json ├── info.json ├── iqaudio │ ├── iqaudio.json │ └── iqaudio.sh ├── lampinstall │ ├── lampInstall.sh │ └── lampinstall.json ├── memsplit │ ├── memsplit.json │ └── memsplit.sh ├── newcronjob │ ├── createjob.sh │ ├── newcronjob.json │ └── newcronjob.sh ├── otgether │ ├── otgEther.sh │ └── otgether.json ├── otgmassstorage │ ├── otgMassStorage.sh │ └── otgmassstorage.json ├── otgmidi │ ├── otgMidi.sh │ └── otgmidi.json ├── otgremove │ ├── otgRemove.sh │ └── otgremove.json ├── otgserial │ ├── otgSerial.sh │ └── otgserial.json ├── packageinstall │ ├── packageInstall.sh │ └── packageinstall.json ├── reboot │ ├── reboot.json │ └── reboot.sh ├── retropiesetup │ ├── RetroPieInstall.sh │ └── retropiesetup.json ├── runcommand │ ├── runCommand.sh │ └── runcommand.json ├── runpython │ ├── runPython.sh │ └── runpython.json ├── sethostname │ ├── sethostname.json │ └── sethostname.sh ├── seti2c │ ├── seti2c.json │ └── seti2c.sh ├── setspi │ ├── setspi.json │ └── setspi.sh ├── shutdown │ ├── shutdown.json │ └── shutdown.sh ├── uartconsole │ ├── uartconsole.json │ └── uartconsole.sh ├── updateupgrade │ ├── updateupgrade.json │ └── updateupgrade.sh ├── vncenable │ ├── vncenable.json │ └── vncenable.sh ├── waitfornetwork │ ├── waitfornetwork.json │ └── waitfornetwork.sh └── wifisetup │ ├── waitForNetwork.sh │ ├── wifiConnect.py │ └── wifisetup.json ├── pibakery-raspbian ├── README.md ├── etc │ └── systemd │ │ └── system │ │ └── pibakery.service ├── images.json ├── info.json ├── lib │ └── systemd │ │ └── system │ │ └── lightdm.service └── opt │ └── PiBakery │ ├── console-lite.sh │ ├── console.sh │ ├── display.sh │ ├── removeFirst.py │ ├── removeNext.py │ ├── runscripts.sh │ ├── startup.sh │ └── waitForNetwork.sh ├── resources ├── busybox ├── pibakery-install.sh └── pibakery-mount.sh ├── settings.json └── start.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # make sure shell script line endings are set to lf. 2 | # if set to crlf for resources/pibakery-mount.sh bad things happen (see #164) 3 | 4 | *.sh text eol=lf 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PiBakery 2 | 3 | [![Dependency Status](https://img.shields.io/david/davidferguson/pibakery.svg?maxAge=2592000)](https://david-dm.org/davidferguson/pibakery) 4 | [![devDependency Status](https://img.shields.io/david/dev/davidferguson/pibakery.svg?maxAge=2592000)](https://david-dm.org/davidferguson/pibakery) 5 | 6 | The blocks based, easy to use setup tool for Raspberry Pi 7 | 8 | ![PiBakery demo screen](http://pibakery.org/img/blocks-on-workspace.png#2) 9 | 10 | PiBakery is a a blocks based drag and drop tool that allows you to customise and edit your Raspberry Pi without powering the Pi on. Simply insert your SD card into your computer, choose which features you want on your Pi, and hit **Write**. PiBakery will write the latest version of Raspbian to your SD card, with your customisations added too. 11 | 12 | For more information see www.PiBakery.org or follow [@PiBakery](http://twitter.com/PiBakery) on Twitter. 13 | 14 |
15 | 16 | # Installing from source 17 | 18 | While it is recommended to install PiBakery from one of the downloads on www.PiBakery.org/download.html, you can also install PiBakery from source if you want to see how it works, or edit PiBakery in any way. 19 | 20 | To install PiBakery from source, you'll need NodeJS and npm installed. Once you have them installed, clone the GitHub repository with 21 |
22 | `git clone https://github.com/davidferguson/pibakery.git` 23 | 24 | Change into the newly downloaded directory with 25 |
26 | `cd pibakery` 27 | 28 | And install the required node modules using 29 |
30 | `npm install` 31 |
32 | This will take a few minutes to complete. 33 | 34 | You can then run PiBakery using 35 |
36 | `npm start` 37 | 38 | # PiBakery on Linux 39 | PiBakery should run on Linux if you build from source, however you will need to have `kpartx` installed. Most distributions have these in package repositories, and in Debian/Ubuntu can be installed with 40 | `sudo apt-get install kpartx` 41 | 42 | ---- 43 | 44 | # PiBakery v2 45 | The latest version of PiBakery, *PiBakery v2*, is a complete re-write of the original application, with many additional features, including: 46 | 47 | - PiBakery no longer bundles .img files in the installer/program. Instead, the user must supply their own Raspbian .img file. This means that any Raspbian-based .img can be used, with the possibility of other distros in the future 48 | - Ability to edit **any** Raspbian SD card, not just ones that have been written with PiBakery 49 | - More robust Linux support 50 | - Ability to add multiple block sources, so the user can maintain their own block repo with their own custom blocks 51 | - Importing of recipes (.xml files) created with older versions of PiBakery no longer fail, instead they are converted automatically into the new format 52 | - The entire program no longer runs as root/admin. Instead, just the writer process is elevated when needed to be 53 | - Modularised code to increase readability, and add option for command line mode in the future 54 | 55 | # Legal 56 | 57 | PiBakery is Copyright (c) 2018 David Ferguson, and is licenced under the GNU General Public License version 3 or later; please see the file `COPYING` for details. 58 | 59 | This project uses and distributes a binary of [Busybox](https://busybox.net) version 1.27.2 taken from Raspbian Stretch's [`busybox-static`](https://archive.raspbian.org/raspbian/pool/main/b/busybox/busybox-static_1.27.2-3_armhf.deb) package, the source code of which can be found [here](https://archive.raspbian.org/raspbian/pool/main/b/busybox/). 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/blockly/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2011 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /app/blockly/media/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/1x1.gif -------------------------------------------------------------------------------- /app/blockly/media/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/click.mp3 -------------------------------------------------------------------------------- /app/blockly/media/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/click.ogg -------------------------------------------------------------------------------- /app/blockly/media/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/click.wav -------------------------------------------------------------------------------- /app/blockly/media/delete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/delete.mp3 -------------------------------------------------------------------------------- /app/blockly/media/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/delete.ogg -------------------------------------------------------------------------------- /app/blockly/media/delete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/delete.wav -------------------------------------------------------------------------------- /app/blockly/media/disconnect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/disconnect.mp3 -------------------------------------------------------------------------------- /app/blockly/media/disconnect.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/disconnect.ogg -------------------------------------------------------------------------------- /app/blockly/media/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/disconnect.wav -------------------------------------------------------------------------------- /app/blockly/media/handclosed.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/handclosed.cur -------------------------------------------------------------------------------- /app/blockly/media/handdelete.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/handdelete.cur -------------------------------------------------------------------------------- /app/blockly/media/handopen.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/handopen.cur -------------------------------------------------------------------------------- /app/blockly/media/quote0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/quote0.png -------------------------------------------------------------------------------- /app/blockly/media/quote1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/quote1.png -------------------------------------------------------------------------------- /app/blockly/media/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/blockly/media/sprites.png -------------------------------------------------------------------------------- /app/blockly/media/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/blockly/pibakery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.provide('Blockly.PiBakery'); 4 | goog.require('Blockly.Generator'); 5 | Blockly.PiBakery = new Blockly.Generator('PiBakery'); 6 | 7 | 8 | Blockly.PiBakery.addReservedWords(); 9 | 10 | Blockly.PiBakery.init = function(workspace) {}; 11 | 12 | Blockly.PiBakery.finish = function(code) { 13 | return code; 14 | }; 15 | 16 | 17 | Blockly.PiBakery.scrubNakedValue = function(line) { 18 | return line; 19 | }; 20 | 21 | 22 | Blockly.PiBakery.quote_ = function(string) { 23 | return string; 24 | }; 25 | 26 | Blockly.PiBakery.scrub_ = function(block, code) { 27 | var nextBlock = block.nextConnection && block.nextConnection.targetBlock(); 28 | var nextCode = Blockly.PiBakery.blockToCode(nextBlock); 29 | return code + nextCode; 30 | }; -------------------------------------------------------------------------------- /app/blocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | Blockly.Blocks['onboot'] = { 20 | init: function() { 21 | this.appendDummyInput() 22 | .appendField("On Every Boot"); 23 | this.setNextStatement(true); 24 | this.setColour(20); 25 | this.setTooltip(''); 26 | //this.setHelpUrl('http://www.example.com/'); 27 | this.setHelpUrl('This is a very long description of how this block works, which has been placed into a space which is only meant for a URL. Hopefully Blockly allows this to be placed here, and doesn\'t check for a URL with regex or such. Anyway, onto the description of this block. This is the block that is used to select what other blocks run when the Raspberry Pi is first booted up. Lets see if we can fit some more words.'); 28 | } 29 | }; 30 | 31 | Blockly.Blocks['onfirstboot'] = { 32 | init: function() { 33 | this.appendDummyInput() 34 | .appendField("On First Boot"); 35 | this.setNextStatement(true); 36 | this.setColour(20); 37 | this.setTooltip(''); 38 | this.setHelpUrl('http://www.example.com/'); 39 | } 40 | }; 41 | 42 | Blockly.Blocks['onnextboot'] = { 43 | init: function() { 44 | this.appendDummyInput() 45 | .appendField("On Next Boot"); 46 | this.setNextStatement(true); 47 | this.setColour(20); 48 | this.setTooltip(''); 49 | this.setHelpUrl('http://www.example.com/'); 50 | } 51 | }; 52 | 53 | Blockly.PiBakery['onboot'] = function(block) { 54 | var code = '_pibakery-oneveryboot'; 55 | return code; 56 | }; 57 | 58 | Blockly.PiBakery['onfirstboot'] = function(block) { 59 | var code = '_pibakery-onfirstboot'; 60 | return code; 61 | }; 62 | 63 | Blockly.PiBakery['onnextboot'] = function(block) { 64 | var code = '_pibakery-onnextboot'; 65 | return code; 66 | }; 67 | -------------------------------------------------------------------------------- /app/img/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/export.png -------------------------------------------------------------------------------- /app/img/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/icon.icns -------------------------------------------------------------------------------- /app/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/icon.ico -------------------------------------------------------------------------------- /app/img/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/import.png -------------------------------------------------------------------------------- /app/img/load-sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/load-sd.png -------------------------------------------------------------------------------- /app/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/settings.png -------------------------------------------------------------------------------- /app/img/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/success.png -------------------------------------------------------------------------------- /app/img/update-sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/update-sd.png -------------------------------------------------------------------------------- /app/img/updating.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/updating.gif -------------------------------------------------------------------------------- /app/img/write-to-sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/write-to-sd.png -------------------------------------------------------------------------------- /app/img/writing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/img/writing.gif -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PiBakery 4 | 5 | 6 | 7 | 8 | 37 | 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |

www.PiBakery.org

46 |

@PiBakery

47 |

Version

48 |
49 | 50 |
51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | var fs = require('fs-extra') 22 | var webFrame = require('electron').webFrame 23 | var dialog = require('electron').remote.dialog 24 | var path = require('path') 25 | var sdcard = require('../lib/sdcard.js') 26 | var ui = require('../lib/ui.js') 27 | var imgmanager = require('../lib/imgmanager.js') 28 | var blocks = require('../lib/blocks.js') 29 | var multifs = require('../lib/multifs.js') 30 | 31 | // disable zooming 32 | webFrame.setVisualZoomLevelLimits(1, 1) 33 | webFrame.setLayoutZoomLevelLimits(0, 0) 34 | 35 | 36 | /* 37 | * -------------------------------CODE FOR WRITING------------------------------ 38 | */ 39 | function writeClicked () { 40 | // show the chooser window, but don't populate it yet 41 | ui.showSdChooser(beginImageWrite) 42 | 43 | // keep updating the sd chooser while the sd chooser is still open 44 | updateSdChooser() 45 | } 46 | 47 | 48 | function updateSdChooser () { 49 | // do nothing if the SD chooser is closed 50 | if (!ui.sdChooserOpen) { 51 | return 52 | } 53 | 54 | // get available images 55 | imgmanager.getImages(function (error, images) { 56 | if (error) { 57 | console.error(error) 58 | return 59 | } 60 | 61 | if (ui.sdChooserOpen) { 62 | // update the select element 63 | ui.updateSdChooser(null, images) 64 | 65 | // get available sd cards 66 | updateSDChooserSDs() 67 | } 68 | }) 69 | } 70 | 71 | 72 | function updateSDChooserSDs() { 73 | // get available sd cards 74 | sdcard.getSDs(function sdCallback (error, cards) { 75 | if (error) { 76 | console.error(error) 77 | return 78 | } 79 | 80 | if (!ui.sdChooserOpen) { 81 | return 82 | } 83 | 84 | // update the chooser and run this function again if the chooser is open 85 | ui.updateSdChooser(cards, null) 86 | setTimeout(updateSDChooserSDs, 5000) 87 | }) 88 | } 89 | 90 | 91 | // callback when a new image is chosen by the user 92 | ui.imageChosenCallback = function (newImage) { 93 | // add this to the imgmanager list of images 94 | imgmanager.addImage(newImage) 95 | } 96 | 97 | 98 | function beginImageWrite (drive, os) { 99 | // show the writer window 100 | ui.showSdWriter('Writing to SD') 101 | 102 | // start the write 103 | sdcard.write(os.path, drive, { 104 | progress: writeImageProgress, 105 | error: function (error) { 106 | writeImageError(error, 'Write Error') 107 | }, 108 | done: writeImageDone 109 | }) 110 | 111 | ui.updateProgress(false, false, 'Waiting for Authentication...') 112 | } 113 | 114 | 115 | function writeImageDone (state, drive) { 116 | writeImageProgress({text:'Searching for remount path...'}) 117 | // when the write is done, get the mountpoint of the /boot partition 118 | sdcard.getRemountPath(drive, function mountpointCallback (error, mountpoint) { 119 | if (error) { 120 | writeImageError(error, 'Write Error') 121 | return 122 | } 123 | 124 | installPiBakery(mountpoint, function () { 125 | // perform the cleanup 126 | writeImageProgress({text:'Cleaning up...'}) 127 | sdcard.cleanup(drive, mountpoint, writeComplete) 128 | }) 129 | }) 130 | } 131 | 132 | 133 | function writeComplete (error, drive) { 134 | if (error) { 135 | writeImageError(error, 'Write Error') 136 | return 137 | } 138 | 139 | // sd image has been written, /boot has been mounted, script have been written, kpartx has been removed (if it was used) 140 | ui.showSdComplete('Write Complete') 141 | } 142 | 143 | /* 144 | * -----------------------------END CODE FOR WRITING---------------------------- 145 | */ 146 | 147 | 148 | 149 | 150 | /* 151 | * ------------------------------CODE FOR EDITING------------------------------ 152 | */ 153 | 154 | function updateClicked () { 155 | ui.showDriveChooser(beginImageUpdate, 'Update Existing SD Card', 'Update') 156 | 157 | // update the update chooser 158 | updateUpdateChooser() 159 | } 160 | 161 | 162 | function updateUpdateChooser () { 163 | // get available sd cards 164 | sdcard.getRaspbianSDs(function sdCallback (error, cards) { 165 | if (error) { 166 | console.error(error) 167 | return 168 | } 169 | 170 | // exit if the choose has been closed 171 | if (!ui.updateChooserOpen) { 172 | return 173 | } 174 | 175 | // update the chooser and run this function again if the chooser is open 176 | ui.updateUpdateChooser(cards) 177 | setTimeout(updateUpdateChooser, 5000) 178 | }) 179 | } 180 | 181 | function beginImageUpdate (drive) { 182 | // show the updater window 183 | ui.showSdWriter('Updating SD') 184 | 185 | // get mountpoints 186 | var mountpoints = [] 187 | for (var i = 0; i < drive.mountpoints.length; i++) { 188 | mountpoints.push(path.join(drive.mountpoints[i].path, 'bootcode.bin')) 189 | } 190 | 191 | // see which one(s) exist 192 | multifs.exists(mountpoints, function (error, exists) { 193 | if (error) { 194 | writeImageError(error, 'Update Error') 195 | return 196 | } 197 | 198 | if (exists.length === 0) { 199 | writeImageError(null, 'Update Error') 200 | return 201 | } 202 | 203 | // get the mountpoint, and write PiBakery to it 204 | var mountpoint = path.join(mountpoints[0], '../') // remove the 'bootcode.bin' part 205 | installPiBakery(mountpoint, updateComplete) 206 | }) 207 | } 208 | 209 | 210 | function updateComplete (error) { 211 | if (error) { 212 | writeImageError(error, 'Update Error') 213 | return 214 | } 215 | 216 | // pibakery has been added to this sd card, scripts have veen written 217 | ui.showSdComplete('Update Complete') 218 | } 219 | 220 | /* 221 | * ----------------------------END CODE FOR EDITING---------------------------- 222 | */ 223 | 224 | 225 | /* 226 | * ------------------------------CODE FOR LOADING------------------------------ 227 | */ 228 | 229 | function loadClicked () { 230 | ui.showDriveChooser(loadFromSD, 'Load From SD Card', 'Load') 231 | 232 | // update the update chooser 233 | updateLoadChooser() 234 | } 235 | 236 | 237 | function updateLoadChooser () { 238 | // get available sd cards 239 | sdcard.getPiBakerySDs(function sdCallback (error, cards) { 240 | if (error) { 241 | console.error(error) 242 | return 243 | } 244 | 245 | // exit if the chooser has been closed 246 | if (!ui.updateChooserOpen) { 247 | return 248 | } 249 | 250 | // update the chooser and run this function again if the chooser is open 251 | ui.updateUpdateChooser(cards) 252 | setTimeout(updateLoadChooser, 5000) 253 | }) 254 | } 255 | 256 | function loadFromSD (drive) { 257 | // show the updater window 258 | 259 | // get mountpoints 260 | var mountpoints = [] 261 | for (var i = 0; i < drive.mountpoints.length; i++) { 262 | mountpoints.push(path.join(drive.mountpoints[i].path, 'PiBakery/blocks.xml')) 263 | } 264 | 265 | // see which one(s) exist 266 | multifs.exists(mountpoints, function (error, exists) { 267 | if (error) { 268 | ui.showSdWriter('Load Error') 269 | writeImageError(error, 'Load Error') 270 | throw error 271 | return 272 | } 273 | 274 | if (exists.length === 0) { 275 | ui.showSdWriter('Load Error') 276 | writeImageError(null, 'Load Error') 277 | return 278 | } 279 | 280 | // get the mountpoint, and write PiBakery to it 281 | var blockFile = exists[0] 282 | 283 | fs.readFile(blockFile, 'utf8', function (error, data) { 284 | if (error) { 285 | ui.showSdWriter('Load Error') 286 | writeImageError(null, 'Load Error') 287 | throw error 288 | } 289 | 290 | // load the blocks 291 | blocks.loadXml(data) 292 | 293 | // close the dialog 294 | ui.closeDialog() 295 | }) 296 | }) 297 | } 298 | 299 | /* 300 | * ----------------------------END CODE FOR LOADING---------------------------- 301 | */ 302 | 303 | 304 | function installPiBakery (mountpoint, cb) { 305 | // we have the mountpoint, now write the scripts and blocks 306 | writeImageProgress({text:'Generating scripts...'}) 307 | 308 | // get scripts 309 | var data = blocks.generateScript() 310 | 311 | // get blocks.xml 312 | data.xml = blocks.getXml() 313 | 314 | // write this data to the sd card 315 | sdcard.installPiBakery(mountpoint, data, { 316 | error: function (error) { 317 | writeImageError(error, 'Write Error') 318 | }, 319 | progress: writeImageProgress, 320 | done: cb 321 | }) 322 | } 323 | 324 | function writeImageProgress (state) { 325 | // when the write progress changes, update the UI 326 | var value = false 327 | var max = false 328 | var text = false 329 | 330 | if ('transferred' in state && 'length' in state) { 331 | value = state.transferred 332 | max = state.length 333 | } 334 | 335 | if ('text' in state) { 336 | text = state.text 337 | } 338 | 339 | ui.updateProgress(value, max, text) 340 | } 341 | 342 | 343 | function writeImageError (error, title) { 344 | // if we get an error, show the error UI 345 | var msg = "Can't write to SD card, error code " 346 | msg = msg + error.name + ' :: ' + error.message 347 | 348 | ui.showSdError(title, msg) 349 | 350 | console.error(error) 351 | } 352 | 353 | 354 | 355 | 356 | 357 | function importRecipe () { 358 | var options = { 359 | title: 'Choose XML Recipe to Import', 360 | filters: [ 361 | {name: 'Recipe Files (*.xml)', extensions: ['xml']} 362 | ], 363 | properties: [ 364 | 'openFile' 365 | ] 366 | } 367 | var path = dialog.showOpenDialog(options)[0] 368 | 369 | fs.readFile(path, 'utf8', function (error, data) { 370 | if (error) { 371 | dialog.showErrorBox('XML Import Error', error.message) 372 | throw error 373 | } 374 | 375 | blocks.loadXml(data) 376 | }) 377 | } 378 | 379 | 380 | function exportRecipe () { 381 | var options = { 382 | title: 'Export to XML Recipe', 383 | filters: [ 384 | {name: 'Recipe Files (*.xml)', extensions: ['xml']} 385 | ] 386 | } 387 | var path = dialog.showSaveDialog(options) 388 | var xml = blocks.getXml() 389 | 390 | fs.writeFile(path, xml, function (error) { 391 | if (error) { 392 | dialog.showErrorBox('XML Export Error', error.message) 393 | throw error 394 | } 395 | }) 396 | } 397 | 398 | 399 | 400 | 401 | 402 | 403 | /* BEGIN IMPORTING CODE HERE */ 404 | 405 | // setup blockly and import the blocks 406 | blocks.inject(ui.elements.editor, ui.elements.toolbox) 407 | blocks.loadBlocks(ui.elements.toolbox, finishLoad) 408 | 409 | // called when blocks have finished loading, removes intro and shows workspace 410 | function finishLoad (error) { 411 | if (error) { 412 | console.error(error) 413 | return 414 | } 415 | 416 | // add handlers to buttons 417 | ui.elements.writeBtn.addEventListener('click', writeClicked) 418 | ui.elements.updateBtn.addEventListener('click', updateClicked) 419 | ui.elements.loadBtn.addEventListener('click', loadClicked) 420 | ui.elements.importBtn.addEventListener('click', importRecipe) 421 | ui.elements.exportBtn.addEventListener('click', exportRecipe) 422 | // TODO - add import and export buttons 423 | 424 | // remove intro message and show blockly workspace 425 | ui.removeIntro() 426 | ui.showWorkspace() 427 | } 428 | -------------------------------------------------------------------------------- /app/open-sans/open-sans-bold-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-bold-ext.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-bold.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans-light-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-light-ext.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-light.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans-normal-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-normal-ext.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidferguson/pibakery/db16e37719dc854be2d8820fe68db8e0d3589fcf/app/open-sans/open-sans-normal.woff2 -------------------------------------------------------------------------------- /app/open-sans/open-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Open Sans Light'), local('OpenSans-Light'), url(./open-sans-light-ext.woff) format('woff2'); 6 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 7 | } 8 | 9 | 10 | @font-face { 11 | font-family: 'Open Sans'; 12 | font-style: normal; 13 | font-weight: 300; 14 | src: local('Open Sans Light'), local('OpenSans-Light'), url(./open-sans-light.woff) format('woff2'); 15 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 16 | } 17 | 18 | @font-face { 19 | font-family: 'Open Sans'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Open Sans Regular'), local('OpenSans-Regular'), url(./open-sans-normal-ext.woff) format('woff2'); 23 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 24 | } 25 | 26 | 27 | @font-face { 28 | font-family: 'Open Sans'; 29 | font-style: normal; 30 | font-weight: 400; 31 | src: local('Open Sans Regular'), local('OpenSans-Regular'), url(./open-sans-normal.woff) format('woff2'); 32 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 33 | } 34 | 35 | @font-face { 36 | font-family: 'Open Sans'; 37 | font-style: normal; 38 | font-weight: 700; 39 | src: local('Open Sans Bold'), local('OpenSans-Bold'), url(./open-sans-bold-ext.woff) format('woff2'); 40 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 41 | } 42 | 43 | @font-face { 44 | font-family: 'Open Sans'; 45 | font-style: normal; 46 | font-weight: 700; 47 | src: local('Open Sans Bold'), local('OpenSans-Bold'), url(./open-sans-bold.woff) format('woff2'); 48 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 49 | } 50 | -------------------------------------------------------------------------------- /app/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: 'Open Sans'; 8 | font-size: 16pt; 9 | background: white; 10 | margin: 0px; 11 | height: 100%; 12 | overflow: auto; 13 | } 14 | 15 | #header { 16 | background-color: #c6c6c6; 17 | width: 100%; 18 | height: 70px; 19 | } 20 | 21 | #credits { 22 | text-align: center; 23 | width: 100%; 24 | margin-top: 90px; 25 | } 26 | 27 | #blockly { 28 | border:0px solid #CCC; 29 | padding-left:0; 30 | height: 100%; 31 | } 32 | 33 | #blockly_editor { 34 | width:100%; 35 | height: calc(100% - 70px); 36 | display: none; 37 | } 38 | 39 | .blocklyToolboxDiv, #header { 40 | -webkit-user-select: none; 41 | } 42 | 43 | #description { 44 | width:49%; 45 | float:right; 46 | margin-top:0; 47 | } 48 | 49 | #button_run { 50 | margin-left:2%; 51 | } 52 | 53 | #logo { 54 | font-size: 48px; 55 | padding-left: 10px; 56 | padding-right: 10px; 57 | float:left; 58 | height:calc(100% - 3px); 59 | } 60 | 61 | .headerBtn { 62 | width: 40px; 63 | margin-left: 5px; 64 | margin-right: 5px; 65 | text-align: center; 66 | cursor: pointer; 67 | font-size: 13px; 68 | /*padding-right: 10px; 69 | padding-left: 10px;*/ 70 | padding-top: 7px; 71 | float:right; 72 | height:calc(100% - 7px); 73 | -webkit-touch-callout: none; 74 | -webkit-user-select: none; 75 | -khtml-user-select: none; 76 | -moz-user-select: none; 77 | -ms-user-select: none; 78 | user-select: none; 79 | } 80 | 81 | .headerBtn.wide { 82 | width: 80px; 83 | } 84 | 85 | .wideBtn p { 86 | float: right; 87 | } 88 | 89 | .headerBtn p { 90 | margin-top: 4px; 91 | font-size: 11px; 92 | } 93 | 94 | .headerBtn img { 95 | margin-top:2px; 96 | } 97 | 98 | .btnDisabled { 99 | opacity: 0.4; 100 | -webkit-filter: grayscale(100%); 101 | -moz-filter: grayscale(100%); 102 | -o-filter: grayscale(100%); 103 | -ms-filter: grayscale(100%); 104 | filter: grayscale(100%); 105 | } 106 | 107 | #loadSD, #import { 108 | margin-left: 40px; 109 | } 110 | 111 | .sdImage { 112 | height:35px; 113 | cursor:pointer; 114 | } 115 | 116 | .slimImage { 117 | height:17px; 118 | cursor:pointer; 119 | } 120 | 121 | /* css styles for ui module begin here */ 122 | #hider { 123 | width: 100%; 124 | height: 100%; 125 | position: absolute; 126 | top: 0px; 127 | left: 0px; 128 | background-color: rgba(100,100,100,0.5); 129 | } 130 | 131 | /*#sdSelector*/#dialog { 132 | width: 350px; 133 | height: 100px; 134 | background-color: white; 135 | position: absolute; 136 | top: 50%; 137 | left: 50%; 138 | margin-top: -40px; 139 | margin-left: -126px; 140 | border-radius: 7px; 141 | text-align: center; 142 | } 143 | 144 | /*#selectionTable*/#dialogTable { 145 | font-size: 13px; 146 | margin-top: 10px; 147 | margin-left: auto; 148 | margin-right: auto; 149 | } 150 | 151 | /*#sdChoice*/.dialogChoice { 152 | width: 215px; 153 | float: right; 154 | } 155 | 156 | /*#osChoice { 157 | width: 110px; 158 | float: right; 159 | }*/ 160 | 161 | /*#writeButton*/.dialogButton { 162 | margin-top: 12px; 163 | margin-left: 5px; 164 | margin-right: 5px; 165 | } 166 | 167 | /*#writingMessage 168 | { 169 | width: 250px; 170 | height: 200px; 171 | background-color: white; 172 | position: absolute; 173 | top: 50%; 174 | left: 50%; 175 | margin-top: -100px; 176 | margin-left: -126px; 177 | border-radius: 7px; 178 | text-align: center; 179 | }*/ 180 | 181 | /*#writeProgress { 182 | margin-top: 120px; 183 | }*/ 184 | 185 | /*#writeProgressbar*/#dialogProgress { 186 | margin-top: 8px; 187 | } 188 | 189 | #dialogProgressText { 190 | margin-top: -1px; 191 | font-size: 11px; 192 | } 193 | 194 | /*#writeDetailedProgress 195 | { 196 | margin-top: 9px; 197 | font-size: 11px; 198 | }*/ 199 | 200 | /*#writeAnimation*/#dialogImage { 201 | max-width: 160px; 202 | max-height: 90px; 203 | } 204 | /*.updateAnimation 205 | { 206 | margin-top: 10px; 207 | margin-bottom: 10px; 208 | }*/ 209 | /*#closeBtn 210 | { 211 | margin-top: -7px; 212 | }*/ 213 | /*.infoParagraph { 214 | font-size: 12px; 215 | margin-left: 6px; 216 | margin-right: 6px; 217 | }*/ 218 | /*.blockInfo { 219 | font-size: 10px; 220 | padding-left: 10px; 221 | padding-right: 10px; 222 | }*/ 223 | 224 | #dialogInfo { 225 | font-size: 10px; 226 | padding-left: 10px; 227 | padding-right: 10px; 228 | } 229 | 230 | .spinner { 231 | margin: 100px auto 0; 232 | width: 70px; 233 | text-align: center; 234 | } 235 | .spinner > div { 236 | width: 18px; 237 | height: 18px; 238 | background-color: black; 239 | 240 | border-radius: 100%; 241 | display: inline-block; 242 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 243 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 244 | } 245 | 246 | .spinner .bounce1 { 247 | -webkit-animation-delay: -0.32s; 248 | animation-delay: -0.32s; 249 | } 250 | 251 | .spinner .bounce2 { 252 | -webkit-animation-delay: -0.16s; 253 | animation-delay: -0.16s; 254 | } 255 | 256 | @-webkit-keyframes sk-bouncedelay { 257 | 0%, 80%, 100% { -webkit-transform: scale(0) } 258 | 40% { -webkit-transform: scale(1.0) } 259 | } 260 | 261 | @keyframes sk-bouncedelay { 262 | 0%, 80%, 100% { 263 | -webkit-transform: scale(0); 264 | transform: scale(0); 265 | } 40% { 266 | -webkit-transform: scale(1.0); 267 | transform: scale(1.0); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /lib/blockloader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | load: load, 23 | validation: validation, 24 | blockMapper: blockMapper 25 | } 26 | 27 | 28 | var app = require('electron').app || require('electron').remote.app 29 | var fs = require('fs-extra') 30 | var path = require('path') 31 | 32 | var blockDirectory = path.join(app.getPath('appData'), 'PiBakery/blocks') 33 | var backupBlocks = path.join(__dirname, '../pibakery-blocks') 34 | 35 | var workspace = {} 36 | var blockMapper = {} 37 | var validation = [] 38 | module.exports.blockMapper = blockMapper 39 | module.exports.validation = validation 40 | 41 | 42 | function load (workspace, toolbox, cb) { 43 | fs.stat(blockDirectory, function (error, stats) { 44 | // block directory doesn't exist - revert to backup blocks 45 | if (error) { 46 | loadFromDirectories([backupBlocks], workspace, toolbox, cb) 47 | return 48 | } 49 | 50 | // read the block directories and load them 51 | fs.readdir(blockDirectory, function (error, files) { 52 | if (error || files.length == 0) { 53 | // error or no directories - revert back to backup blocks 54 | loadFromDirectories([backupBlocks], workspace, toolbox, cb) 55 | return 56 | } 57 | 58 | for (var i = 0; i < files.length; i++) { 59 | files[i] = path.join(blockDirectory, files[i]) 60 | } 61 | 62 | // load blocks from those directories 63 | loadFromDirectories(files, workspace, toolbox, function (error) { 64 | // either an error, or no blocks added 65 | if (error || Object.keys(blockMapper).length === 0) { 66 | loadFromDirectories([backupBlocks], workspace, toolbox, cb) 67 | return 68 | } 69 | cb(null) 70 | }) 71 | }) 72 | }) 73 | } 74 | 75 | 76 | function loadFromDirectories (directories, workspace, toolbox, cb, count) { 77 | // first time calling? set count to 0 78 | if (typeof count === 'undefined') { 79 | count = 0 80 | } 81 | 82 | // done all directories? run callback 83 | if (count >= directories.length) { 84 | cb(null) 85 | return 86 | } 87 | 88 | var directory = directories[count] 89 | loadFromDirectory(directory, workspace, toolbox, function (error) { 90 | if (error) { 91 | // go onto next block 92 | loadFromDirectories(directories, workspace, toolbox, cb, (count + 1)) 93 | return 94 | } 95 | 96 | loadFromDirectories(directories, workspace, toolbox, cb, (count + 1)) 97 | }) 98 | } 99 | 100 | 101 | function loadFromDirectory (directory, workspace, toolbox, cb) { 102 | // read the .json files from this directory 103 | readBlockFiles(directory, function (error, categoriesJSON, blocksJSON) { 104 | if (error) { 105 | cb(error) 106 | return 107 | } 108 | 109 | // load the categories from this directory 110 | loadCategories(categoriesJSON, toolbox) 111 | 112 | // load the blocks (async) from this directory 113 | loadBlocksAsync(blocksJSON, directory, toolbox, function (error) { 114 | if (error) { 115 | cb(error) 116 | return 117 | } 118 | 119 | // update the toolbox 120 | workspace.updateToolbox(toolbox) 121 | cb(null) 122 | }) 123 | }) 124 | } 125 | 126 | 127 | function readBlockFiles (directory, cb) { 128 | var blockFile = path.join(directory, 'info.json') 129 | var categoryFile = path.join(directory, 'categories.json') 130 | 131 | // read the block file 132 | fs.readFile(blockFile, 'utf8', function (error, blockInfo) { 133 | if (error) { 134 | cb(error) 135 | return 136 | } 137 | 138 | // also read the categories file 139 | fs.readFile(categoryFile, 'utf8', function (error, categoryInfo) { 140 | if (error) { 141 | cb(error) 142 | return 143 | } 144 | 145 | // get them both from JSON into javascript object 146 | try { 147 | var categoriesJSON = JSON.parse(categoryInfo).categories 148 | var blocksJSON = JSON.parse(blockInfo).loadOrder 149 | } catch (error) { 150 | cb(error) 151 | return 152 | } 153 | 154 | // return the JSON data 155 | cb(null, categoriesJSON, blocksJSON) 156 | }) 157 | }) 158 | } 159 | 160 | 161 | function loadCategories (categoriesJSON, toolbox) { 162 | // add in the custom categories 163 | for (var i = 0; i < categoriesJSON.length; i++) { 164 | categoriesJSON[categoriesJSON[i].name] = categoriesJSON[i].colour 165 | var newCategory = document.createElement('category') 166 | newCategory.setAttribute('name', categoriesJSON[i].display) 167 | newCategory.setAttribute('colour', categoriesJSON[i].colour) 168 | newCategory.setAttribute('id', categoriesJSON[i].name) 169 | toolbox.appendChild(newCategory) 170 | } 171 | } 172 | 173 | 174 | function loadBlocksAsync (blocks, directory, toolbox, cb, count) { 175 | if (typeof count === 'undefined') { 176 | count = 0 177 | } 178 | 179 | if (count >= blocks.length) { 180 | cb(null) 181 | return 182 | } 183 | 184 | var blockName = blocks[count] 185 | var jsonPath = path.join(directory, blockName, (blockName + '.json')) 186 | 187 | fs.readFile(jsonPath, 'utf8', function (error, data) { 188 | if (error) { 189 | cb(error) 190 | return 191 | } 192 | 193 | // parse the json 194 | try { 195 | var blockJSON = JSON.parse(data) 196 | } catch (error) { 197 | cb(error) 198 | return 199 | } 200 | 201 | // import this block 202 | importBlock(blockJSON, toolbox) 203 | 204 | // add this block to the path mapper 205 | var blockPath = path.join(directory, blockName) 206 | blockMapper[blockName] = blockPath 207 | 208 | // go load the next block 209 | loadBlocksAsync(blocks, directory, toolbox, cb, (count + 1)) 210 | }) 211 | } 212 | 213 | 214 | function importBlock (blockJSON, toolbox) { 215 | blockJSON.type = blockJSON.category 216 | 217 | // get the block properties 218 | var blockName = blockJSON.name 219 | var blockText = blockJSON.text 220 | var blockArgs = blockJSON.args 221 | var numArgs = blockArgs.length 222 | var supportedOperatingSystems = blockJSON.supportedOperatingSystems 223 | 224 | // get the block colour 225 | var blockColour = 0 226 | for (var i = 0; i < toolbox.children.length; i++) { 227 | if (toolbox.children[i].id === blockJSON.type) { 228 | blockColour = toolbox.children[i].getAttribute('colour') 229 | } 230 | } 231 | 232 | // create the blockly JSON object 233 | var blocklyBlock = {} 234 | blocklyBlock.id = blockName 235 | blocklyBlock.colour = blockColour 236 | blocklyBlock.helpUrl = blockJSON.longDescription 237 | blocklyBlock.tooltip = blockJSON.shortDescription 238 | 239 | // if the block has multiple lines, implement that in the blockly standard 240 | var currentCount = 0 241 | while (blockText.indexOf('\\n') !== -1) { 242 | blockText = blockText.replace('\\n', ('%' + (numArgs + 1 + currentCount))) 243 | currentCount++ 244 | } 245 | blocklyBlock.message0 = blockText 246 | 247 | // loop through the arguments adding them to the blockly object 248 | blocklyBlock.args0 = [] 249 | for (var i = 0; i < blockJSON.args.length; i++) { 250 | var newArg = {} 251 | var currentArg = blockJSON.args[i] 252 | if (currentArg.type === 'number' || currentArg.type === 'text') { 253 | newArg.type = 'field_input' 254 | newArg.name = i + 1 255 | newArg.text = currentArg.default 256 | validation.push({ 257 | block: blockName, 258 | field: i + 1, 259 | max: currentArg.maxLength, 260 | type: currentArg.type 261 | }) 262 | } else if (currentArg.type === 'menu') { 263 | newArg.type = 'field_dropdown' 264 | newArg.name = i + 1 265 | newArg.options = [] 266 | for (var j = 0; j < currentArg.options.length; j++) { 267 | var currentOption = currentArg.options[j] 268 | var newOption = [currentOption, currentOption] 269 | newArg.options.push(newOption) 270 | } 271 | } else if (currentArg.type === 'check') { 272 | newArg.type = 'field_checkbox' 273 | newArg.name = i + 1 274 | newArg.checked = currentArg.default 275 | } 276 | blocklyBlock.args0.push(newArg) 277 | } 278 | for (var i = 0; i < currentCount; i++) { 279 | blocklyBlock.args0.push({type: 'input_dummy'}) 280 | } 281 | blocklyBlock.previousStatement = true 282 | blocklyBlock.nextStatement = true 283 | 284 | // blocklyBlock has been created - now we need to add it into PiBakery 285 | Blockly.Blocks[blockName] = { 286 | init: function () { 287 | this.jsonInit(blocklyBlock) 288 | this.setNextStatement(blockJSON.continue) 289 | } 290 | } 291 | 292 | // That's us finished with blocklyBlock - now go and creat the code generator 293 | Blockly.PiBakery[blockName] = function (block) { 294 | var code = '\n\tchmod 755 /boot/PiBakery/blocks/' + blockName + '/' + blockJSON.script 295 | code = code + '\n\t/boot/PiBakery/blocks/' + blockName + '/' + blockJSON.script + ' ' 296 | for (var i = 0; i < blockJSON.args.length; i++) { 297 | var currentArg = bashEscape(block.getFieldValue(i + 1)) 298 | if (currentArg === '') { 299 | currentArg = '""' 300 | } 301 | code = code + currentArg + ' ' 302 | } 303 | code = code.slice(0, -1) 304 | if (blockJSON.network) { 305 | code = code + '\n\tNETWORK=True' 306 | } 307 | return code 308 | } 309 | 310 | // And the last thing to to is to add that block to the toolbox 311 | var newBlock = document.createElement('block') 312 | newBlock.setAttribute('type', blockName) 313 | document.getElementById(blockJSON.type).appendChild(newBlock) 314 | } 315 | 316 | 317 | function bashEscape (arg) { 318 | // Thanks to creationix on GitHub 319 | var safePattern = /^[a-z0-9_\/\-.,?:@#%^+=\[\]]*$/i 320 | var safeishPattern = /^[a-z0-9_\/\-.,?:@#%^+=\[\]{}|&()<>; *']*$/i 321 | if (safePattern.test(arg)) { 322 | return arg 323 | } 324 | if (safeishPattern.test(arg)) { 325 | return '"' + arg + '"' 326 | } 327 | return "'" + arg.replace(/'+/g, function (val) { 328 | if (val.length < 3) { 329 | return "'" + val.replace(/'/g, "\\'") + "'" 330 | } 331 | return '\'"' + val + '"\'' 332 | }) + "'" 333 | } 334 | -------------------------------------------------------------------------------- /lib/blocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | inject: inject, 23 | loadBlocks: loadBlocks, 24 | validateField: validateField, 25 | generateScript: generateScript, 26 | getXml: getXml, 27 | loadXml: loadXml 28 | } 29 | 30 | 31 | var pd = require('pretty-data').pd 32 | var fs = require('fs-extra') 33 | var path = require('path') 34 | var blockloader = require('./blockloader.js') 35 | var blockupdates = require('./blockupdates.js') 36 | var blocktransform = require('./blocktransform.js') 37 | 38 | 39 | var workspace = {} 40 | var categories = {} 41 | 42 | var validation = blockloader.validation 43 | var blockMapper = blockloader.blockMapper 44 | 45 | 46 | function loadBlocks (toolbox, cb) { 47 | // check and perform update if necessary 48 | blockupdates.update(function (error) { 49 | if (error) { 50 | console.error(error) 51 | } 52 | 53 | // load the blocks 54 | blockloader.load(workspace, toolbox, cb) 55 | }) 56 | } 57 | 58 | 59 | function inject (editor, toolbox) { 60 | //module.exports.workspace = Blockly.inject(editor, {toolbox: toolbox}) 61 | workspace = Blockly.inject(editor, {toolbox: toolbox}) 62 | //module.exports.workspace.addChangeListener(validateBlocks) 63 | workspace.addChangeListener(validateBlocks) 64 | } 65 | 66 | 67 | function bashEscape (arg) { 68 | // Thanks to creationix on GitHub 69 | var safePattern = /^[a-z0-9_\/\-.,?:@#%^+=\[\]]*$/i 70 | var safeishPattern = /^[a-z0-9_\/\-.,?:@#%^+=\[\]{}|&()<>; *']*$/i 71 | if (safePattern.test(arg)) { 72 | return arg 73 | } 74 | if (safeishPattern.test(arg)) { 75 | return '"' + arg + '"' 76 | } 77 | return "'" + arg.replace(/'+/g, function (val) { 78 | if (val.length < 3) { 79 | return "'" + val.replace(/'/g, "\\'") + "'" 80 | } 81 | return '\'"' + val + '"\'' 82 | }) + "'" 83 | } 84 | 85 | 86 | /** 87 | * @desc called every time a block field is edited, checks to see if the input 88 | * to the block is valid for that block 89 | * @param object block - the block object that contains all the info about the 90 | * block that has been edited, including info about the block's fields 91 | * @param string value - the current value of the field being edited 92 | * @return null 93 | */ 94 | function validateField (block, value) { 95 | for (var i = 0; i < block.inputList.length; i++) { 96 | for (var j = 0; j < block.inputList[i].fieldRow.length; j++) { 97 | if (block.inputList[i].fieldRow[j].text_ === value) { 98 | for (var k = 0; k < validation.length; k++) { 99 | if (validation[k].block === block.type && validation[k].field === block.inputList[i].fieldRow[j].name) { 100 | if (validation[k].type === 'number') { 101 | value = value.replace(/[^0-9]/g, '') 102 | } 103 | if (validation[k].max !== 0) { 104 | if (value.length > validation[k].max) { 105 | return value.substring(0, validation[k].max) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | return value 114 | } 115 | 116 | 117 | /** 118 | * @desc called whenever blocks are moved around, checks to make sure the user 119 | * hasn't put the shutdown or reboot block with the oneveryboot startup 120 | * @param object event - passed from blockly 121 | * @return null 122 | */ 123 | function validateBlocks (event) { 124 | // generate the code for the blocks 125 | //var code = window.Blockly.PiBakery.workspaceToCode(module.exports.workspace) 126 | var code = window.Blockly.PiBakery.workspaceToCode(workspace) 127 | code = code.split('\n') 128 | 129 | var everyBootCode = '' 130 | var codeType = '' 131 | var expectHat = true 132 | 133 | // move the code sections into their variables (firstboot, everyboot, etc.) 134 | for (var i = 0; i < code.length; i++) { 135 | var currentLine = code[i] 136 | if (currentLine.indexOf('\t') === 0 && expectHat === false) { 137 | if (codeType === 'everyBoot') { 138 | if (everyBootCode === '') { 139 | everyBootCode = currentLine.replace('\t', '') 140 | } else { 141 | everyBootCode = everyBootCode + '\n' + currentLine.replace(' ', '') 142 | } 143 | } 144 | } else if (currentLine === '_pibakery-oneveryboot') { 145 | codeType = 'everyBoot' 146 | expectHat = false 147 | } else if (currentLine === '_pibakery-onfirstboot') { 148 | codeType = 'firstBoot' 149 | expectHat = false 150 | } else if (currentLine === '_pibakery-onnextboot') { 151 | codeType = 'nextBoot' 152 | expectHat = false 153 | } else if (currentLine === '') { 154 | expectHat = true 155 | } 156 | } 157 | 158 | // check to see if the shutdown or reboot block has been used with everyboot 159 | if ((everyBootCode.split('\n')[everyBootCode.split('\n').length - 1] === '/boot/PiBakery/blocks/shutdown/shutdown.sh') || 160 | (everyBootCode.split('\n')[everyBootCode.split('\n').length - 1] === '/boot/PiBakery/blocks/reboot/reboot.sh')) { 161 | //module.exports.workspace.getBlockById(event.blockId).unplug() 162 | workspace.getBlockById(event.blockId).unplug() 163 | //module.exports.workspace.getBlockById(event.blockId).bumpNeighbours_() 164 | workspace.getBlockById(event.blockId).bumpNeighbours_() 165 | // alert("You can't put that block there.") 166 | } 167 | } 168 | 169 | 170 | // generates the script of the blocks 171 | // returns array [everyBootCode, firstBootCode, nextBootCode, neededBlocks, 172 | // waitForNetwork] - the every boot script, the first boot script, the next 173 | // boot script, the list of blocks needed to be copied, and whether PiBakery 174 | // should wait for a network connection before running the scripts 175 | function generateScript () { 176 | //var code = window.Blockly.PiBakery.workspaceToCode(module.exports.workspace) 177 | var code = window.Blockly.PiBakery.workspaceToCode(workspace) 178 | code = code.split('\n') 179 | 180 | var firstBootCode = '' 181 | var everyBootCode = '' 182 | var nextBootCode = '' 183 | 184 | var firstBootCount = 0 185 | var everyBootCount = 0 186 | var nextBootCount = 0 187 | 188 | var codeType = '' 189 | var neededBlocks = [] 190 | var expectHat = true 191 | 192 | var networkRequiredPosition = [-1, -1, -1] 193 | var wifiPosition = [-1, -1, -1] 194 | var waitForNetwork = [false, false, false] 195 | 196 | for ( var x = 0; x < code.length; x++) { 197 | var currentLine = code[x] 198 | 199 | if (currentLine.indexOf('\t') == 0 && expectHat == false) { 200 | if (currentLine != '\tNETWORK=True') { 201 | // add the blockname to our list 202 | var blockName = currentLine.split('/boot/PiBakery/blocks/')[1].split('/')[0] 203 | if (neededBlocks.indexOf(blockName) == -1) { 204 | neededBlocks.push(blockName) 205 | } 206 | 207 | // actually generate the code (with whiptail dialogs as well) 208 | if (codeType == 'everyBoot') { 209 | everyBootCount++ 210 | everyBootCode = everyBootCode + '\n' + currentLine.replace('\t', '') + ' >>/boot/PiBakery/everyboot.log 2>&1 || true' 211 | // everyBootCode = everyBootCode + "\necho $(expr $PERCENTAGE \\* " + everyBootCount + " )" 212 | everyBootCode = everyBootCode + '\necho XXX\necho $(expr $PERCENTAGE \\* ' + everyBootCount + ' )\necho "\\nProcessing Every Boot Script\\n\\nRunning Block: ' + currentLine.split('/boot/PiBakery/blocks/')[1].split('/')[0] + '"\necho XXX' 213 | } 214 | else if (codeType == 'firstBoot') { 215 | firstBootCount++ 216 | firstBootCode = firstBootCode + '\n' + currentLine.replace('\t', '') + ' >>/boot/PiBakery/firstboot.log 2>&1 || true' 217 | // firstBootCode = firstBootCode + "\necho $(expr $PERCENTAGE \\* " + firstBootCount + " )" 218 | firstBootCode = firstBootCode + '\necho XXX\necho $(expr $PERCENTAGE \\* ' + firstBootCount + ' )\necho "\\nProcessing First Boot Script\\n\\nRunning Block: ' + currentLine.split('/boot/PiBakery/blocks/')[1].split('/')[0] + '"\necho XXX' 219 | } 220 | else if (codeType == 'nextBoot') { 221 | nextBootCount++ 222 | nextBootCode = nextBootCode + '\n' + currentLine.replace('\t', '') + ' >>/boot/PiBakery/nextboot.log 2>&1 || true' 223 | // nextBootCode = nextBootCode + "\necho $(expr $PERCENTAGE \\* " + nextBootCount + " )" 224 | nextBootCode = nextBootCode + '\necho XXX\necho $(expr $PERCENTAGE \\* ' + nextBootCount + ' )\necho "\\nProcessing Next Boot Script\\n\\nRunning Block: ' + currentLine.split('/boot/PiBakery/blocks/')[1].split('/')[0] + '"\necho XXX' 225 | } 226 | 227 | // handle the waitForNetwork stuff 228 | if (blockName == 'wifisetup') { 229 | if (codeType == 'everyBoot') { 230 | wifiPosition[0] = everyBootCount 231 | } 232 | else if (codeType == 'firstBoot') { 233 | wifiPosition[1] = firstBootCount 234 | } 235 | else if (codeType == 'nextBoot') { 236 | wifiPosition[2] = nextBootCount 237 | } 238 | } 239 | } 240 | else if (currentLine == '\tNETWORK=True') { 241 | if (codeType == 'everyBoot') { 242 | networkRequiredPosition[0] = everyBootCount 243 | } 244 | else if (codeType == 'firstBoot') { 245 | networkRequiredPosition[1] = firstBootCount 246 | } 247 | else if (codeType == 'nextBoot') { 248 | networkRequiredPosition[2] = nextBootCount 249 | } 250 | } 251 | } 252 | else if (currentLine == '_pibakery-oneveryboot') { 253 | codeType = 'everyBoot' 254 | expectHat = false 255 | } 256 | else if (currentLine == '_pibakery-onfirstboot') { 257 | codeType = 'firstBoot' 258 | expectHat = false 259 | } 260 | else if (currentLine == '_pibakery-onnextboot') { 261 | codeType = 'nextBoot' 262 | expectHat = false 263 | } 264 | else if (currentLine == '') { 265 | expectHat = true 266 | } 267 | } 268 | 269 | if (firstBootCode != '') { 270 | firstBootCode = '#!/bin/bash\n\nPERCENTAGE=' + Math.floor(100 / firstBootCount) + '\n\n{' + firstBootCode + '\necho 100\n} | whiptail --title "PiBakery" --gauge "\\nProcessing First Boot Script\\n\\n\\n" 11 40 0' 271 | }else { 272 | firstBootCode = '#!/bin/bash' 273 | } 274 | 275 | if (everyBootCode != '') { 276 | everyBootCode = '#!/bin/bash\n\nPERCENTAGE=' + Math.floor(100 / everyBootCount) + '\n\n{' + everyBootCode + '\necho 100\n} | whiptail --title "PiBakery" --gauge "\\nProcessing Every Boot Script\\n\\n\\n" 11 40 0' 277 | }else { 278 | everyBootCode = '#!/bin/bash' 279 | } 280 | 281 | if (nextBootCode != '') { 282 | nextBootCode = '#!/bin/bash\n\nPERCENTAGE=' + Math.floor(100 / nextBootCount) + '\n\n{' + nextBootCode + '\necho 100\n} | whiptail --title "PiBakery" --gauge "\\nProcessing Next Boot Script\\n\\n\\n" 11 40 0' 283 | }else { 284 | nextBootCode = '#!/bin/bash' 285 | } 286 | 287 | // if we do need a network connection, and (there is not wifi) or (there is wifi but it's after we need network) 288 | if ((networkRequiredPosition[0] != -1) && (wifiPosition[0] == -1 || (wifiPosition[0] != -1 && networkRequiredPosition[0] < wifiPosition[0]))) { 289 | waitForNetwork[0] = true 290 | } 291 | 292 | if ((networkRequiredPosition[1] != -1) && (wifiPosition[1] == -1 || (wifiPosition[1] != -1 && networkRequiredPosition[1] < wifiPosition[1]))) { 293 | waitForNetwork[1] = true 294 | } 295 | 296 | if ((networkRequiredPosition[2] != -1) && (wifiPosition[2] == -1 || (wifiPosition[2] != -1 && networkRequiredPosition[2] < wifiPosition[2]))) { 297 | waitForNetwork[2] = true 298 | } 299 | 300 | // create the proper paths for the needed blocks 301 | var blockPaths = [] 302 | for (var i = 0; i < neededBlocks.length; i++) { 303 | blockPaths.push(blockMapper[neededBlocks[i]]) 304 | } 305 | 306 | return { 307 | everyBoot: everyBootCode, 308 | firstBoot: firstBootCode, 309 | nextBoot: nextBootCode, 310 | blocks: neededBlocks, 311 | blockPaths: blockPaths, 312 | waitForNetwork: waitForNetwork 313 | } 314 | //return [everyBootCode, firstBootCode, nextBootCode, neededBlocks, waitForNetwork] 315 | } 316 | 317 | 318 | function getXml () { 319 | // get the XML text version of the boot type 320 | 321 | // convert the blocks to xml 322 | var blocksXml = Blockly.Xml.domToPrettyText(Blockly.Xml.workspaceToDom(workspace)) 323 | 324 | // add in the xml element representing the firstboot status - legacy reasons 325 | var xmlElement = (new window.DOMParser()).parseFromString(blocksXml, 'text/xml') 326 | var firstboot = xmlElement.createElement('firstboot') 327 | firstboot.appendChild(xmlElement.createTextNode('0')) 328 | xmlElement.getElementsByTagName('xml')[0].appendChild(firstboot) 329 | 330 | // serialise the xml element to string 331 | blocksXml = new XMLSerializer().serializeToString(xmlElement) 332 | var prettyXml = pd.xml(blocksXml) 333 | return prettyXml 334 | } 335 | 336 | 337 | function loadXml (data) { 338 | // perform transformations on the blocks 339 | var xml = blocktransform.transform(data) 340 | 341 | // load it into blockly 342 | Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xml), workspace) 343 | } 344 | -------------------------------------------------------------------------------- /lib/blocktransform.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | transform: transform 23 | } 24 | 25 | 26 | function transform (xml) { 27 | var parser = new DOMParser() 28 | xml = parser.parseFromString(xml, 'application/xml') 29 | var blocks = xml.getElementsByTagName('block') 30 | 31 | while (true) { 32 | var blocks = xml.getElementsByTagName('block') 33 | 34 | var madeTransformation = false 35 | for (var i = 0; i < blocks.length; i++) { 36 | var block = blocks[i] 37 | 38 | var transformed = transformBlock(block, xml) 39 | if (transformed) { 40 | madeTransformation = true 41 | break 42 | } 43 | } 44 | 45 | if (!madeTransformation) { 46 | break 47 | } 48 | } 49 | 50 | var string = new XMLSerializer().serializeToString(xml); 51 | return string 52 | } 53 | 54 | 55 | function transformBlock (block, xml) { 56 | if (isOldWiFi(block)) { 57 | transformOldWiFi(block) 58 | return true 59 | } 60 | 61 | if (isOldVncInstall(block)) { 62 | transformOldVncInstall(block) 63 | return true 64 | } 65 | 66 | if (isOldVncStart(block)) { 67 | transformOldVncStart(block) 68 | return true 69 | } 70 | 71 | return false 72 | } 73 | 74 | 75 | function isOldWiFi (block) { 76 | if (block.getAttribute('type') === 'wifisetup') { 77 | var fields = block.getElementsByTagName('field') 78 | for (var i = 0; i < fields.length; i++) { 79 | var field = fields[i] 80 | if (field.getAttribute('name') === '4') { 81 | return false 82 | } 83 | } 84 | 85 | return true 86 | } 87 | 88 | return false 89 | } 90 | 91 | 92 | function transformOldWiFi (block) { 93 | var countryCode = document.createElement('field') 94 | countryCode.setAttribute('name', '4') 95 | countryCode.innerText = 'GB' 96 | block.appendChild(countryCode) 97 | //return block 98 | return 99 | } 100 | 101 | 102 | function isOldVncInstall (block) { 103 | return (block.getAttribute('type') === 'vncserver') 104 | } 105 | 106 | 107 | function transformOldVncInstall (block) { 108 | // basically remove this block - vnc is already installed now 109 | var nexts = block.getElementsByTagName('next') 110 | var next = false 111 | for (var i = 0; i < nexts.length; i++) { 112 | if (nexts[i].parentNode === block) { 113 | next = nexts[i] 114 | break 115 | } 116 | } 117 | 118 | if (next === false) { 119 | // there is no block after this one, just remove it 120 | block.parentNode.parentNode.removeChild(block.parentNode) 121 | //return block 122 | return 123 | } 124 | 125 | // there is another block after this, replace this block with that one 126 | //block.parentNode.innerHTML = next.innerHTML 127 | block.parentNode.parentNode.appendChild(next) 128 | block.parentNode.parentNode.removeChild(block.parentNode) 129 | } 130 | 131 | 132 | function isOldVncStart (block) { 133 | return (block.getAttribute('type') === 'vncstart') 134 | } 135 | 136 | 137 | function transformOldVncStart (block) { 138 | block.setAttribute('type', 'vncenable') 139 | var field = block.getElementsByTagName('field')[0] 140 | field.innerText = 'Enable' 141 | 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /lib/blockupdates.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | update: update 23 | } 24 | 25 | 26 | var app = require('electron').app || require('electron').remote.app 27 | var fs = require('fs-extra') 28 | var path = require('path') 29 | var request = require('request') 30 | var unzipper = require('unzipper') 31 | var digestStream = require('digest-stream') 32 | var settings = require('./settings.js') 33 | 34 | var downloadCorrect = undefined 35 | var blockDirectory = path.join(app.getPath('appData'), 'PiBakery/blocks') 36 | 37 | 38 | function update (cb) { 39 | fs.mkdir(blockDirectory, function (error) { 40 | if (error && error.code !== 'EEXIST') { 41 | cb(error) 42 | return 43 | } 44 | 45 | settings.get('blocksources', function (error, sources) { 46 | if (error || sources.length === 0) { 47 | cb(error) 48 | return 49 | } 50 | 51 | updateBlockSources(sources, cb) 52 | }) 53 | }) 54 | } 55 | 56 | 57 | function updateBlockSources (sources, cb, count) { 58 | if (typeof count === 'undefined') { 59 | count = 0 60 | } 61 | 62 | if (count >= sources.length) { 63 | cb(null) 64 | return 65 | } 66 | 67 | var source = sources[count] 68 | var updateUrl = source.url + '?' + Math.random().toString(36).substr(2, 5) 69 | var infoFile = path.join(blockDirectory, source.name, 'info.json') 70 | 71 | // get the update json file 72 | request(updateUrl, {json: true}, function (error, response, body) { 73 | if (error) { 74 | updateBlockSources(sources, cb, (count + 1)) 75 | return 76 | } 77 | 78 | // read te local json file 79 | fs.readFile(infoFile, 'utf8', function (error, data) { 80 | if (error && error.code !== 'ENOENT') { 81 | updateBlockSources(sources, cb, (count + 1)) 82 | return 83 | } 84 | 85 | if (error && error.code === 'ENOENT') { 86 | // if no such local file exists, set the data to version 0 to force an update 87 | data = { version: 0.0 } 88 | } 89 | 90 | // see if there's an update 91 | var updateVersion = body.version 92 | var currentVersion = data.version 93 | if (updateVersion <= currentVersion) { 94 | // no update available, go to next block 95 | updateBlockSources(sources, cb, (count + 1)) 96 | return 97 | } 98 | 99 | // there is an update available, perform it 100 | updateSource(body, source, function (error) { 101 | updateBlockSources(sources, cb, (count + 1)) 102 | return 103 | }) 104 | }) 105 | }) 106 | } 107 | 108 | 109 | function updateSource (updateJson, settingJson, cb) { 110 | var savename = path.join(blockDirectory, settingJson.name) 111 | var url = updateJson.downloadUrl + '?' + Math.random().toString(36).substr(2, 5) 112 | 113 | // first delete the current directory 114 | fs.remove(savename, function (error) { 115 | if (error && error.code !== 'ENOENT') { 116 | cb() 117 | return 118 | } 119 | 120 | // now perform the download 121 | request.get(url) 122 | 123 | // check the hash of the tar file 124 | .pipe(digestStream('md5', 'hex', function (hash, length) { 125 | downloadCorrect = (hash === updateJson.compressedMD5) 126 | })) 127 | 128 | // extract the file 129 | .pipe(unzipper.Extract({ path: blockDirectory })) 130 | 131 | // call downloadComplete when it's done 132 | .promise() 133 | .then(function () { 134 | downloadComplete(updateJson, settingJson, cb) 135 | }, cb) 136 | }) 137 | } 138 | 139 | 140 | // called when download is complete 141 | function downloadComplete (updateJson, settingJson, cb) { 142 | // if not calculated yet, re-run in 1 second 143 | if (downloadCorrect === 'undefined') { 144 | setTimeout(function () { 145 | downloadComplete(updateJson, settingJson, cb) 146 | }, 1000) 147 | return 148 | } 149 | 150 | // check download hash matched correctly 151 | if (!downloadCorrect) { 152 | cb() 153 | return 154 | } 155 | 156 | // get the name the zip extracted to 157 | var extractName = 'pibakery-blocks-new' 158 | if ('extract' in updateJson) { 159 | extractName = updateJson.extract 160 | } 161 | 162 | // where we are moving from and to 163 | var source = path.join(blockDirectory, extractName) 164 | var target = path.join(blockDirectory, settingJson.name) 165 | 166 | // move the downloaded directory to the target directory 167 | fs.move(source, target, {overwrite: true}, function (error) { 168 | if (error) { 169 | cb() 170 | return 171 | } 172 | 173 | cb() 174 | }) 175 | } 176 | -------------------------------------------------------------------------------- /lib/cmdline-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | parse: parse, 23 | write, write, 24 | get, get, 25 | set, set 26 | } 27 | 28 | var fs = require('fs-extra') 29 | var childProcess = require('child_process') 30 | 31 | var parsed = [] 32 | 33 | 34 | // parse a given cmdline.txt file 35 | function parse (filename, cb) { 36 | // reset the variables 37 | parsed = [] 38 | 39 | // read the cmdline.txt file 40 | fs.readFile(filename, 'utf8', function readComplete (error, contents) { 41 | if (error) { 42 | cb(error) 43 | return 44 | } 45 | 46 | // now parse the file into paramaters and attributes 47 | contents = contents.trim() 48 | contents = contents.split(' ') 49 | for (var i = 0; i < contents.length; i++) { 50 | 51 | // special case for non key-values, just keys 52 | if (contents[i].indexOf('=') == -1) { 53 | var obj = { 54 | key: contents[i], 55 | value: null 56 | } 57 | parsed.push(obj) 58 | continue 59 | } 60 | 61 | // loop through all key-value pairs 62 | var pair = contents[i].split(/=/) 63 | var key = pair.shift() 64 | var value = pair.join('=') 65 | 66 | var obj = { 67 | key: key, 68 | value: value 69 | } 70 | 71 | parsed.push(obj) 72 | } 73 | 74 | cb(null) 75 | }) 76 | } 77 | 78 | 79 | // write out the (probably modified) contents of parsed to new cmdline.txt file 80 | function write (filename, cb) { 81 | var pairs = [] 82 | 83 | // loop through all pairs, combining them into a string 84 | for (var i = 0; i < parsed.length; i++) { 85 | var key = parsed[i].key 86 | var value = parsed[i].value 87 | 88 | var pair = key 89 | if (value) { 90 | pair = key + '=' + value 91 | } 92 | pairs.push(pair) 93 | } 94 | 95 | // join all pairs together as string, and write them to file 96 | var contents = pairs.join(' ').trim() 97 | fs.writeFile(filename, contents, 'utf8', function writeComplete (error) { 98 | if (error) { 99 | cb(error) 100 | return 101 | } 102 | 103 | // file was written successfully 104 | cb(null) 105 | }) 106 | } 107 | 108 | 109 | // retrieve a paramater 110 | function get (paramater) { 111 | for (var i = 0; i < parsed.length; i++) { 112 | if (parsed[i].key == paramater) { 113 | return parsed[i].value 114 | } 115 | } 116 | 117 | return '' 118 | } 119 | 120 | 121 | // set a paramater 122 | function set (paramater, value) { 123 | for (var i = 0; i < parsed.length; i++) { 124 | if (parsed[i].key == paramater) { 125 | parsed[i].value = value 126 | return 127 | } 128 | } 129 | 130 | var obj = { 131 | key: paramater, 132 | value: value 133 | } 134 | parsed.push(obj) 135 | } 136 | -------------------------------------------------------------------------------- /lib/elevate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Resin.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict' 18 | 19 | var electron = require('electron') 20 | var isElevated = require('is-elevated') 21 | var sudoPrompt = require('sudo-prompt') 22 | var os = require('os') 23 | var platform = os.platform() 24 | var packageJSON = require('../package.json') 25 | var path = require('path') 26 | 27 | exports.require = function (additionalArguments, callback) { 28 | isElevated(function (error, elevated) { 29 | if (error) { 30 | return callback(error) 31 | } 32 | 33 | if (elevated) { 34 | return callback() 35 | } 36 | 37 | // copy the args and add in the additional arguments 38 | var args = process.argv.slice() 39 | for (var i = 0; i < additionalArguments.length; i++) { 40 | args.push(additionalArguments[i]) 41 | } 42 | 43 | // case split for unix and windows 44 | if (platform === 'darwin' || platform === 'linux') { 45 | 46 | sudoPrompt.exec(args.join(' '), { 47 | name: packageJSON.displayName, 48 | icns: path.join(__dirname, '../app/img/icon.icns') 49 | }, callback) 50 | 51 | } else if (platform === 'win32') { 52 | 53 | const elevator = require('elevator') 54 | elevator.execute(args, {}, callback) 55 | 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /lib/imgmanager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | getImages: getImages, 23 | addImage: addImage 24 | } 25 | 26 | var path = require('path') 27 | var fs = require('fs-extra') 28 | var multifs = require('./multifs.js') 29 | var settings = require('./settings.js') 30 | 31 | 32 | function getImages (cb) { 33 | // get previously used images 34 | settings.get('previousImages', function (error, previousImages) { 35 | if (error) { 36 | cb(error) 37 | return 38 | } 39 | 40 | // see how many of the previousImages still exist 41 | multifs.exists(previousImages, function (error, existingImages) { 42 | if (error) { 43 | cb(error) 44 | return 45 | } 46 | 47 | // if the array is longer than 6 elements, only give the first 6 48 | if (existingImages.length > 6) { 49 | existingImages = array.slice(0, 6); 50 | } 51 | 52 | // convert list of operatingSystems to array of objects 53 | var existingImagesObjects = [] 54 | for (var i = 0; i < existingImages.length; i++) { 55 | var existingImage = { 56 | display: path.basename(existingImages[i]), 57 | value: existingImages[i] 58 | } 59 | existingImagesObjects.push(existingImage) 60 | } 61 | 62 | // return all the previously used images that still exist 63 | cb(null, existingImagesObjects) 64 | 65 | // save the possibly truncated esults back to settings, do nothing with callback 66 | settings.set('previousImages', existingImages, function () {}) 67 | }) 68 | }) 69 | } 70 | 71 | 72 | function addImage (imagePath) { 73 | settings.get('previousImages', function (error, currentImages) { 74 | if (error) { 75 | cb(error) 76 | return 77 | } 78 | 79 | // append the new image 80 | currentImages.push(imagePath) 81 | 82 | // save the results back to settings, do nothing with callback 83 | settings.set('previousImages', currentImages, function () {}) 84 | }) 85 | } 86 | 87 | 88 | // OLD CODE. 89 | /*function getOsPath () { 90 | if (process.platform === 'darwin') { 91 | // Mac stores the OS in Application Support 92 | return path.normalize('/Library/Application Support/PiBakery/os/') 93 | } else if (process.platform === 'win32' || process.platform === 'linux') { 94 | // Windows and Linux store the OS in install directory 95 | return path.join(__dirname, '/../os/') 96 | } else { 97 | return null 98 | } 99 | } 100 | 101 | function getImages (cb) { 102 | // get the directory that the operating systems (and images.json) are stored 103 | var operatingSystemDirectory = getOsPath() 104 | 105 | // read the images configuration file 106 | fs.readFile(path.join(operatingSystemDirectory, 'images.json'), 'utf8', function (error, data) { 107 | if (error) { 108 | cb(error, false) 109 | return 110 | } 111 | 112 | // load the json file into javascript object 113 | try { 114 | var localImagesJson = JSON.parse(data) 115 | } catch (error) { 116 | cb(error, false) 117 | return 118 | } 119 | 120 | // use function for loop 121 | loopThroughImages([], localImagesJson, 0, function installedImagesResult(error, installedImages) { 122 | cb(false, installedImages) 123 | }) 124 | }) 125 | } 126 | 127 | // extra function needed because we can't have callbacks in loops 128 | function loopThroughImages(installedImages, localImagesJson, counter, cb) { 129 | if (counter == localImagesJson.length) { 130 | // end of the loop, return the result 131 | cb(null, installedImages) 132 | return 133 | } 134 | 135 | var operatingSystemDirectory = getOsPath() 136 | 137 | // see if a specific image is installed (index counter) 138 | localImagesJson[counter].path = path.join(operatingSystemDirectory, localImagesJson[counter].filename) 139 | osIsInstalled(localImagesJson[counter].path, function(error, installed) { 140 | // if the os is installed, push it to the installedImages array 141 | if (installed) { 142 | installedImages.push(localImagesJson[counter]) 143 | } 144 | 145 | // run on the next image by calling this function again, with the counter+1 146 | loopThroughImages(installedImages, localImagesJson, counter+1, cb) 147 | }) 148 | } 149 | 150 | function osIsInstalled (filepath, cb) { 151 | // try to access the file, return whether we can or not 152 | fs.stat(filepath, function (error, stat) { 153 | if (!error) { 154 | cb(null, true) 155 | return 156 | } 157 | cb(error) 158 | }) 159 | }*/ 160 | -------------------------------------------------------------------------------- /lib/kpartx.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | isInstalled: isInstalled, 23 | addPartitionMappings, addPartitionMappings, 24 | deletePartitionMappings, deletePartitionMappings 25 | } 26 | 27 | var fs = require('fs-extra') 28 | var childProcess = require('child_process') 29 | 30 | 31 | // cb(true|false) 32 | function isInstalled (cb) { 33 | // run command to determine if kpartx is installed 34 | childProcess.execFile('which', ['kpartx'], function (error, stdout, stderr) { 35 | if (error) { 36 | // if error, return false (system doesn't have kpartx) 37 | cb(false) 38 | return 39 | } 40 | 41 | // return true if there is a path to kpartx 42 | cb(stdout != '') 43 | }) 44 | } 45 | 46 | 47 | function addPartitionMappings (device, cb) { 48 | // command to add mappers, with verbose output 49 | var cmd = 'kpartx -asv ' + device 50 | childProcess.execFile('kpartx', ['-asv', device], function (error, stdout, stderr) { 51 | if (error) { 52 | // if error, return error 53 | cb(error) 54 | return 55 | } 56 | 57 | // if there is content in stderr, then 58 | if (stderr.trim() != '') { 59 | var error = new Error(stderr) 60 | error.name = 'KPARTX_ADD_ERROR' 61 | cb(error) 62 | return 63 | } 64 | 65 | // check the stdout is as expected 66 | if (stdout.trim().indexOf('add map ') != 0) { 67 | var error = new Error('Unable to add mappings, output:' + stdout) 68 | error.name = 'KPARTX_ADD_ERROR' 69 | cb(error) 70 | return 71 | } 72 | 73 | // parse stdout to get the mapping paths and sizes 74 | var lines = stdout.split('\n') 75 | var maps = [] 76 | for (var i = 0; i < lines.length; i++) { 77 | // loop through all the mounts 78 | var map = lines[i].split(' ')[2] 79 | var size = lines[i].split(' ')[5] 80 | 81 | if (map && size) { 82 | maps.push({ 83 | map: map, 84 | size: size 85 | }) 86 | } 87 | } 88 | 89 | cb(null, maps) 90 | }) 91 | } 92 | 93 | 94 | function deletePartitionMappings (device, cb) { 95 | // command to delete mappers 96 | var cmd = 'kpartx -d ' + device 97 | childProcess.execFile('kpartx', ['-d', device], function (error, stdout, stderr) { 98 | if (error) { 99 | // if error, return error 100 | cb(error) 101 | return 102 | } 103 | 104 | // if there is content in stderr, then 105 | if (stderr.trim() != '') { 106 | var error = new Error(stderr) 107 | error.name = 'KPARTX_DELETE_ERROR' 108 | cb(error) 109 | return 110 | } 111 | 112 | // check stdout is as expected 113 | /*if (stdout.trim().indexOf('loop deleted : ') != 0) { 114 | var error = new Error('Unable to delete mappings, output:' + stdout) 115 | error.name = 'KPARTX_DELETE_ERROR' 116 | cb(error) 117 | return 118 | }*/ 119 | 120 | cb(null) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /lib/multifs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | // Used to make copying of multiple files to multiple destinations easy 22 | 23 | module.exports = { 24 | copy: copy, 25 | write: write, 26 | exists: exists 27 | } 28 | 29 | var fs = require('fs-extra') 30 | 31 | 32 | function copy (files, cb, count) { 33 | if (typeof count === 'undefined') { 34 | count = 0 35 | } 36 | 37 | if (count == files.length) { 38 | cb() 39 | return 40 | } 41 | 42 | var source = files[count].source 43 | var destination = files[count].destination 44 | fs.copy(source, destination, function (error) { 45 | if (error) { 46 | cb(error) 47 | return 48 | } 49 | 50 | // do we need to set file permissions for this file? 51 | if ('chmod' in files[count]) { 52 | fs.chmod(destination, files[count].chmod, function (error) { 53 | if (error) { 54 | cb(error) 55 | return 56 | } 57 | 58 | // permissions set fine, copy next file 59 | copy(files, cb, count + 1) 60 | }) 61 | } else { 62 | // that file copied fine, go and copy the next one 63 | copy(files, cb, count + 1) 64 | } 65 | }) 66 | } 67 | 68 | 69 | function write (files, cb, count) { 70 | if (typeof count === 'undefined') { 71 | count = 0 72 | } 73 | 74 | if (count == files.length) { 75 | cb() 76 | return 77 | } 78 | 79 | var file = files[count].file 80 | var contents = files[count].contents 81 | 82 | // delete the file if it exists 83 | fs.remove(file, function (error) { 84 | if (error) { 85 | // for some reason we don't get an error if the file doesn't exist, so we 86 | // don't need to check for that condition 87 | cb(error) 88 | return 89 | } 90 | 91 | // now write the file 92 | fs.writeFile(file, contents, function (error) { 93 | if (error) { 94 | cb(error) 95 | return 96 | } 97 | 98 | // do we need to set file permissions for this file? 99 | if ('chmod' in files[count]) { 100 | fs.chmod(file, files[count].chmod, function (error) { 101 | if (error) { 102 | cb(error) 103 | return 104 | } 105 | 106 | // permissions set fine, write next file 107 | write(files, cb, count + 1) 108 | }) 109 | } else { 110 | // that file wrote fine, go and write the next one 111 | write(files, cb, count + 1) 112 | } 113 | }) 114 | }) 115 | } 116 | 117 | 118 | function exists (files, cb, count, result) { 119 | if (typeof count === 'undefined') { 120 | count = 0 121 | } 122 | 123 | if (typeof result === 'undefined') { 124 | result = [] 125 | } 126 | 127 | if (count == files.length) { 128 | cb(null, result) 129 | return 130 | } 131 | 132 | var file = files[count] 133 | fs.stat(file, function (error) { 134 | if (error && error.code != 'ENOENT') { 135 | cb(error) 136 | return 137 | } 138 | 139 | if (!error) { 140 | // file exists 141 | result.push(file) 142 | } 143 | 144 | // process next file 145 | exists(files, cb, count + 1, result) 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /lib/rootwriter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | // runs as root to write the sd cards 22 | 23 | module.exports = { 24 | connect: connect, 25 | write: write, 26 | doWrite: doWrite 27 | } 28 | 29 | var electron = require('electron') 30 | var imageWrite = require('etcher-image-write') 31 | var fs = require('fs-extra') 32 | var childProcess = require('child_process') 33 | var ipc = require("crocket") 34 | var client = new ipc() 35 | 36 | 37 | function connect (socketPath, cb) { 38 | client.connect({ 39 | path: socketPath 40 | }, function (error) { 41 | if (error) { 42 | throw error 43 | } 44 | 45 | // connected successfully 46 | cb() 47 | }) 48 | } 49 | 50 | 51 | function write (imagePath, drive, cb) { 52 | doWrite(imagePath, drive, { 53 | validate: false 54 | }, { 55 | progress: writeImageProgress, 56 | error: function (error) { 57 | writeImageError(error) 58 | client.close() 59 | cb() 60 | }, 61 | done: function (state, drive) { 62 | writeImageDone(state, drive) 63 | client.close() 64 | cb() 65 | } 66 | }) 67 | } 68 | 69 | 70 | function writeImageProgress (state) { 71 | // send message to the client 72 | client.emit('write-progress', JSON.stringify(state)) 73 | } 74 | 75 | function writeImageError (error) { 76 | client.emit('write-error', JSON.stringify(error)) 77 | } 78 | 79 | function writeImageDone (state, drive) { 80 | client.emit('write-done', JSON.stringify(state)) 81 | } 82 | 83 | 84 | function doWrite (imagePath, drive, options, cb) { 85 | cb.progress({text:'Unmounting SD card...'}) 86 | unmount(drive, function (error) { 87 | if (error) { 88 | error.guimsg = 'Error writing to SD card' 89 | cb.error(error) 90 | return 91 | } 92 | beginWrite(imagePath, drive, options, cb.progress, cb.error, cb.done) 93 | }) 94 | } 95 | 96 | function beginWrite (imagePath, drive, options, onProgress, onError, onDone) { 97 | onProgress({text:'Preparing for write...'}) 98 | fs.open(drive.raw, 'rs+', function (error, driveFileDescriptor) { 99 | if (error) { 100 | error.guimsg = 'Error opening .img for reading' 101 | onError(error) 102 | return 103 | } 104 | 105 | fs.stat(imagePath, function (error, stats) { 106 | if (error) { 107 | error.guimsg = 'Error reading stats of .img file' 108 | onError(error) 109 | return 110 | } 111 | 112 | // check the sd card is large enough for the chosen image 113 | if (drive.size < stats.size) { 114 | 115 | var msg = 'The chosen device is not large enough for the chosen image. ' 116 | msg = msg + 'Device: ' + drive.device 117 | msg = msg + '. Image: ' + imagePath 118 | 119 | var error = new Error('drive too small') 120 | error.name = 'SD_TOO_SMALL' 121 | error.guimsg = msg 122 | onError(error) 123 | return 124 | } 125 | 126 | var writer = imageWrite.write({ 127 | fd: driveFileDescriptor, 128 | device: drive.raw, 129 | size: drive.size 130 | }, { 131 | stream: fs.createReadStream(imagePath), 132 | size: stats.size 133 | }, { 134 | check: options.check 135 | }) 136 | 137 | onProgress({text:'Writing IMG to SD card...'}) 138 | 139 | writer.on('progress', onProgress) 140 | writer.on('error', function (error) { 141 | error.guimsg = 'Error during SD write' 142 | onError(error) 143 | }) 144 | 145 | writer.on('done', function (state) { 146 | fs.close(driveFileDescriptor, function (error) { 147 | if (error) { 148 | error.guimsg = 'Error closing drive file descriptor' 149 | onError(error) 150 | return 151 | } 152 | onDone(state, drive) 153 | }) 154 | }) 155 | }) 156 | }) 157 | } 158 | 159 | function unmount (drive, cb) { 160 | var unmountCommand = '' 161 | if (process.platform === 'darwin') { 162 | unmountCommand = '/usr/sbin/diskutil unmountDisk force ' + drive.device 163 | } else if (process.platform === 'linux') { 164 | unmountCommand = 'umount ' + drive.device + '?* 2>/dev/null || /bin/true' 165 | } else { 166 | cb(false) 167 | return 168 | } 169 | 170 | childProcess.exec(unmountCommand, function (error, stdout, stderr) { 171 | if (error) { 172 | error.guimsg = 'Error unmounting drive' 173 | cb(error) 174 | return 175 | } 176 | cb(false) 177 | }) 178 | } 179 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | get: get, 23 | set: set 24 | } 25 | 26 | var app = require('electron').app || require('electron').remote.app 27 | var path = require('path') 28 | var fs = require('fs-extra') 29 | 30 | 31 | var settings = {} 32 | 33 | 34 | // get the proper settings file 35 | var settingsFile = path.join(app.getPath('appData'), 'PiBakery/settings.json') 36 | var hardSettingsFile = path.join(__dirname, '../settings.json') 37 | var exists = fs.existsSync(settingsFile) 38 | 39 | // if settings file doesn't exist, try and create it 40 | if (!exists) { 41 | try { 42 | fs.copySync(hardSettingsFile, settingsFile) 43 | } catch (e) { 44 | // if error on copying, revert back to hard settings file 45 | settingsFile = hardSettingsFile 46 | } 47 | } 48 | 49 | 50 | function load (cb) { 51 | fs.readJson(settingsFile, function (error, data) { 52 | if (error) { 53 | cb(error) 54 | return 55 | } 56 | 57 | settings = data 58 | cb(null) 59 | }) 60 | } 61 | 62 | 63 | function save (cb) { 64 | // don't modify the in-app settings file 65 | if (settingsFile === hardSettingsFile) { 66 | return 67 | } 68 | 69 | try { 70 | var data = JSON.stringify(settings, null, 2) // save with pretty printing 71 | } catch (e) { 72 | cb(e) 73 | return 74 | } 75 | 76 | fs.writeFile(settingsFile, data, function (error) { 77 | if (error) { 78 | cb(error) 79 | return 80 | } 81 | 82 | cb(null) 83 | }) 84 | } 85 | 86 | 87 | function get (setting, cb) { 88 | load(function (error) { 89 | if (error) { 90 | cb(error) 91 | return 92 | } 93 | 94 | if (! settings.hasOwnProperty(setting)) { 95 | cb(null, undefined) 96 | return 97 | } 98 | 99 | cb(null, settings[setting]) 100 | }) 101 | } 102 | 103 | 104 | function set (setting, value, cb) { 105 | load(function (error) { 106 | if (error) { 107 | cb(error) 108 | return 109 | } 110 | 111 | settings[setting] = value 112 | 113 | save(cb) 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PiBakery 2.0.0 - The easy to use setup tool for Raspberry Pi 3 | * Copyright (C) 2018 David Ferguson 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = { 22 | imageChosenCallback: null, 23 | 24 | sdChooserOpen: false, 25 | updateChooserOpen: false, 26 | loadChooserOpen: false, 27 | 28 | showSdChooser: showSdChooser, 29 | updateSdChooser: updateSdChooser, 30 | showSdWriter: showSdWriter, 31 | updateProgress: updateProgress, 32 | showSdError: showSdError, 33 | showSdComplete: showSdComplete, 34 | 35 | showDriveChooser: showDriveChooser, 36 | updateUpdateChooser: updateUpdateChooser, 37 | 38 | closeDialog: dialogCloseClicked, 39 | 40 | showBlockInfo: showBlockInfo, 41 | removeIntro: removeIntro, 42 | showWorkspace: showWorkspace, 43 | elements: {} 44 | } 45 | 46 | module.exports.elements = { 47 | editor: document.getElementById('blockly_editor'), 48 | toolbox: document.getElementById('toolbox'), 49 | writeBtn: document.getElementById('flashSD'), 50 | updateBtn: document.getElementById('updateSD'), 51 | loadBtn: document.getElementById('loadSD'), 52 | importBtn: document.getElementById('import'), 53 | exportBtn: document.getElementById('export'), 54 | settingsBtn: document.getElementById('settings') 55 | } 56 | 57 | var path = require('path') 58 | var dialog = require('electron').remote.dialog 59 | 60 | 61 | function showChooserDialog (title, btnText, sd, os, modifications, dialogWriteClicked) { 62 | var hiderElement = document.createElement('div') 63 | hiderElement.setAttribute('id', 'hider') 64 | 65 | var dialogElement = document.createElement('div') 66 | dialogElement.setAttribute('id', 'dialog') 67 | 68 | var titleElement = document.createElement('p') 69 | titleElement.setAttribute('id', 'dialogTitle') 70 | titleElement.innerText = title 71 | dialogElement.appendChild(titleElement) 72 | 73 | var tableElement = document.createElement('table') 74 | tableElement.setAttribute('id', 'dialogTable') 75 | dialogElement.appendChild(tableElement) 76 | 77 | if (sd) { 78 | var sdRowElement = document.createElement('tr') 79 | var sdTextColumnElement = document.createElement('td') 80 | var sdTextElement = document.createElement('span') 81 | sdTextElement.innerText = 'SD Card:' 82 | sdTextColumnElement.appendChild(sdTextElement) 83 | var sdChoiceColumnElement = document.createElement('td') 84 | var sdChoiceElement = document.createElement('select') 85 | sdChoiceElement.setAttribute('id', 'dialogSdChoice') 86 | sdChoiceElement.setAttribute('class', 'dialogChoice') 87 | var sdChoicePlaceholderElement = document.createElement('option') 88 | sdChoicePlaceholderElement.innerText = 'loading...' 89 | sdChoicePlaceholderElement.disabled = true 90 | sdChoicePlaceholderElement.selected = true 91 | sdChoiceElement.appendChild(sdChoicePlaceholderElement) 92 | sdChoiceColumnElement.appendChild(sdChoiceElement) 93 | sdRowElement.appendChild(sdTextColumnElement) 94 | sdRowElement.appendChild(sdChoiceColumnElement) 95 | tableElement.appendChild(sdRowElement) 96 | } 97 | 98 | if (os) { 99 | var osRowElement = document.createElement('tr') 100 | var osTextColumnElement = document.createElement('td') 101 | var osTextElement = document.createElement('span') 102 | osTextElement.innerText = 'Operating System:' 103 | osTextColumnElement.appendChild(osTextElement) 104 | var osChoiceColumnElement = document.createElement('td') 105 | var osChoiceElement = document.createElement('select') 106 | osChoiceElement.setAttribute('id', 'dialogOsChoice') 107 | osChoiceElement.setAttribute('class', 'dialogChoice') 108 | var osChoicePlaceholderElement = document.createElement('option') 109 | osChoicePlaceholderElement.innerText = 'loading...' 110 | osChoicePlaceholderElement.disabled = true 111 | osChoicePlaceholderElement.selected = true 112 | osChoiceElement.appendChild(osChoicePlaceholderElement) 113 | osChoiceColumnElement.appendChild(osChoiceElement) 114 | osRowElement.appendChild(osTextColumnElement) 115 | osRowElement.appendChild(osChoiceColumnElement) 116 | tableElement.appendChild(osRowElement) 117 | } 118 | 119 | var writeButtonElement = document.createElement('button') 120 | writeButtonElement.setAttribute('id', 'dialogWriteButton') 121 | writeButtonElement.setAttribute('class', 'dialogButton') 122 | writeButtonElement.disabled = true 123 | writeButtonElement.innerText = btnText 124 | writeButtonElement.addEventListener('click', dialogWriteClicked) 125 | dialogElement.appendChild(writeButtonElement) 126 | 127 | var closeButtonElement = document.createElement('button') 128 | closeButtonElement.setAttribute('id', 'dialogCloseButton') 129 | closeButtonElement.setAttribute('class', 'dialogButton') 130 | closeButtonElement.innerText = 'Close' 131 | closeButtonElement.addEventListener('click', dialogCloseClicked) 132 | dialogElement.appendChild(closeButtonElement) 133 | 134 | if (typeof modifications === 'function') { 135 | modifications(dialogElement) 136 | } 137 | 138 | document.body.appendChild(hiderElement) 139 | document.body.appendChild(dialogElement) 140 | } 141 | 142 | 143 | function showSdChooser (writeCallback) { 144 | module.exports.sdChooserOpen = true 145 | 146 | showChooserDialog('Write New SD Card', 'Write', true, true, function (dialog) { 147 | // any modifications go here 148 | dialog.style.height = '165px' 149 | dialog.style.marginTop = '-70px' 150 | }, function () { 151 | // write button pushed callback 152 | module.exports.sdChooserOpen = false 153 | 154 | var sdChoiceElement = document.getElementById('dialogSdChoice') 155 | var chosenDriveElement = sdChoiceElement.options[sdChoiceElement.selectedIndex] 156 | var chosenDrive = JSON.parse(chosenDriveElement.value) 157 | 158 | var osChoiceElement = document.getElementById('dialogOsChoice') 159 | var chosenOperatingSystemElement = osChoiceElement.options[osChoiceElement.selectedIndex] 160 | var chosenOs = { 161 | name: chosenOperatingSystemElement.innerHTML, 162 | path: chosenOperatingSystemElement.value 163 | } 164 | 165 | writeCallback(chosenDrive, chosenOs) 166 | }) 167 | } 168 | 169 | 170 | function showDriveChooser (updateCallback, title, btn) { 171 | module.exports.updateChooserOpen = true 172 | 173 | showChooserDialog(title, btn, true, false, function (dialog) { 174 | // any modifications go here 175 | dialog.style.height = '150px' 176 | dialog.style.marginTop = '-65px' 177 | }, function () { 178 | // write button pushed callback 179 | module.exports.updateChooserOpen = false 180 | 181 | var sdChoiceElement = document.getElementById('dialogSdChoice') 182 | var chosenDriveElement = sdChoiceElement.options[sdChoiceElement.selectedIndex] 183 | var chosenDrive = JSON.parse(chosenDriveElement.value) 184 | 185 | updateCallback(chosenDrive) 186 | }) 187 | } 188 | 189 | 190 | function updateUpdateChooser (drives) { 191 | if (drives) { 192 | var sdChoiceElement = document.getElementById('dialogSdChoice') 193 | 194 | // build up array of new drives 195 | var newDrives = [] 196 | for (var i = 0; i < drives.length; i++) { 197 | var currentDrive = drives[i] 198 | var driveOptionElement = document.createElement('option') 199 | driveOptionElement.setAttribute('value', JSON.stringify(currentDrive)) 200 | driveOptionElement.innerText = currentDrive.name 201 | newDrives.push(driveOptionElement) 202 | } 203 | 204 | updateSelectElement(sdChoiceElement, newDrives) 205 | } 206 | 207 | enableWriteButton(true, false) 208 | } 209 | 210 | 211 | function dialogCloseClicked () { 212 | if (module.exports.sdChooserOpen) { 213 | module.exports.sdChooserOpen = false 214 | } else if (module.exports.updateChooserOpen) { 215 | module.exports.updateChooserOpen = false 216 | } 217 | 218 | var hiderElement = document.getElementById('hider') 219 | var dialogElement = document.getElementById('dialog') 220 | 221 | hiderElement.parentNode.removeChild(hiderElement) 222 | dialogElement.parentNode.removeChild(dialogElement) 223 | } 224 | 225 | 226 | function optionsIndexOf (array, option) { 227 | // a form of indexOf for an array of