├── .gitignore ├── Makefile ├── README.md ├── license.txt ├── package.json ├── pages ├── publish.md └── screencasts.md ├── public ├── codemirror.css ├── dialog.css ├── erlang-dark.css ├── img │ ├── appleIIe.jpg │ ├── computers-in-our-lives.jpg │ └── youtube.png ├── index.html ├── logo.png ├── logo.svg └── style.css ├── shadow-cljs.edn └── src ├── default-apps ├── 8bit-interface.zip ├── banana-dungeon-game.zip ├── chromium-dinosaur-game.zip ├── hello-world.zip ├── jquery-ui-demo.zip ├── leaflet-map.zip ├── mithril-todomvc.zip ├── party-like-its-98.zip ├── preact-demo.zip ├── savings-calculator.zip ├── text-log.zip ├── tiny-spreadsheet.zip └── widgets-order-form.zip ├── resourceupdater.js ├── site ├── ENV ├── Procfile └── nginx.conf ├── slingcode-bootleg.clj ├── slingcode-embed.html ├── slingcode-site-bootleg.clj ├── slingcode-social.html ├── slingcode-static.html └── slingcode ├── boilerplate.html ├── icons.cljs ├── logo.svg ├── main.cljs ├── not-found.html └── zxingwrap.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | .shadow-cljs 3 | node_modules 4 | public/js 5 | build 6 | bin/bootleg-* 7 | src/slingcode/revision.txt 8 | slingcode.net 9 | src/default-apps.zip* 10 | src/default-apps/* 11 | !src/default-apps/*.zip 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | STATIC=index.html style.css logo.png 2 | BUILD=build/js/main.js $(foreach S, $(STATIC), build/$(S)) 3 | 4 | SITEFILES=public/style.css public/img/computers-in-our-lives.jpg public/img/appleIIe.jpg public/img/youtube.png public/logo.svg public/logo.png 5 | SITEFILES_DEST=$(foreach S, $(SITEFILES), slingcode.net/$(S)) 6 | DISTFILES=index.html publish.html screencasts.html slingcode.html license.txt revision.txt ENV Procfile Makefile nginx.conf 7 | DEFAULTAPPS=hello-world chromium-dinosaur-game preact-demo mithril-todomvc savings-calculator widgets-order-form leaflet-map banana-dungeon-game jquery-ui-demo party-like-its-98 text-log 8bit-interface 8 | # DEBUGFLAG=$(if $(DEBUG), --debug,) 9 | DEBUGFLAG=--debug 10 | 11 | # bootleg stuff 12 | BOOTLEGVERSION=0.1.7 13 | BOOTLEG=./bin/bootleg-$(BOOTLEGVERSION) 14 | 15 | slingcode.net: $(foreach D, $(DISTFILES), slingcode.net/$(D)) 16 | 17 | slingcode.net/slingcode.html: $(BOOTLEG) $(BUILD) build/logo-b64-href.txt build/style.min.css src/slingcode/revision.txt build/index.html 18 | $(BOOTLEG) src/slingcode-bootleg.clj > build/slingcode-compiled.html 19 | npx minify build/slingcode-compiled.html > $@ 20 | 21 | slingcode.net/index.html: $(BOOTLEG) src/slingcode-site-bootleg.clj README.md src/slingcode-static.html build/index.html $(SITEFILES_DEST) 22 | $(BOOTLEG) src/slingcode-site-bootleg.clj README.md > $@ 23 | 24 | slingcode.net/publish.html: $(BOOTLEG) src/slingcode-site-bootleg.clj pages/publish.md src/slingcode-static.html build/index.html $(SITEFILES_DEST) 25 | $(BOOTLEG) src/slingcode-site-bootleg.clj pages/publish.md > $@ 26 | 27 | slingcode.net/screencasts.html: $(BOOTLEG) src/slingcode-site-bootleg.clj pages/screencasts.md src/slingcode-static.html build/index.html $(SITEFILES_DEST) 28 | $(BOOTLEG) src/slingcode-site-bootleg.clj pages/screencasts.md > $@ 29 | 30 | slingcode.net/public/%: public/% 31 | @mkdir -p `dirname $@` 32 | cp $< $@ 33 | 34 | slingcode.net/ENV: src/site/ENV 35 | cp $< $@ 36 | 37 | slingcode.net/Procfile: src/site/Procfile 38 | cp $< $@ 39 | 40 | slingcode.net/Makefile: src/site/Makefile 41 | cp $< $@ 42 | 43 | slingcode.net/nginx.conf: src/site/nginx.conf 44 | cp $< $@ 45 | 46 | slingcode.net/license.txt: license.txt 47 | cp $< $@ 48 | 49 | build/logo-b64-href.txt: build/logo.png 50 | echo "data:image/png;base64,"`base64 $< -w0` > $@ 51 | 52 | build/style.min.css: build/style.css 53 | npx minify $< > $@ 54 | 55 | build/js/main.js: $(shell find src) node_modules shadow-cljs.edn src/default-apps.zip.b64 56 | npx shadow-cljs release app $(DEBUGFLAG) 57 | 58 | build/style.css: public/*.css 59 | cat public/codemirror.css public/erlang-dark.css public/dialog.css public/style.css > $@ 60 | 61 | build/%: public/% 62 | @mkdir -p `dirname $@` 63 | cp $< $@ 64 | 65 | src/default-apps.zip.b64: src/default-apps.zip 66 | base64 $< > $@ 67 | 68 | src/default-apps.zip: $(foreach Z, $(DEFAULTAPPS), src/default-apps/$(Z).zip ) 69 | rm -f $@ 70 | cd src/default-apps && zipmerge ../default-apps.zip $(foreach Z, $(DEFAULTAPPS), $(Z).zip ) 71 | 72 | slingcode.net/revision.txt: src/slingcode/revision.txt 73 | cp $< $@ 74 | 75 | src/slingcode/revision.txt: .git/index 76 | git rev-parse HEAD | cut -b -16 > $@ 77 | 78 | node_modules: package.json 79 | npm i 80 | touch node_modules 81 | 82 | BOOTLEGTAR=bootleg-$(BOOTLEGVERSION)-linux-amd64.tgz 83 | BOOTLEGURL=https://github.com/retrogradeorbit/bootleg/releases/download/v${BOOTLEGVERSION}/${BOOTLEGTAR} 84 | 85 | $(BOOTLEG): 86 | @echo "Installing bootleg." 87 | mkdir -p bin 88 | wget $(BOOTLEGURL) -O $(BOOTLEGTAR) 89 | tar -zxvf $(BOOTLEGTAR) && mv bootleg $(BOOTLEG) 90 | rm $(BOOTLEGTAR) 91 | 92 | # dev targets 93 | 94 | .PHONY: watch clean 95 | 96 | watch: src/slingcode/revision.txt src/default-apps.zip.b64 node_modules 97 | npx shadow-cljs watch app 98 | 99 | watch-site: slingcode.net/index.html slingcode.net/publish.html $(SITEFILES_DEST) 100 | cd slingcode.net && live-server --no-browser --port=8000 --wait=500 & 101 | while true; do $(MAKE) -q || $(MAKE); sleep 0.5; done 102 | 103 | repl: 104 | npx shadow-cljs cljs-repl app 105 | 106 | clean: 107 | rm -rf build/* 108 | 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Slingcode is a **personal computing platform** in a [single html file](https://slingcode.net/slingcode.html). 4 | 5 | * You can **make, run, and share web apps** with it. 6 | * You don't need any complicated tools to use it, **just a web browser**. 7 | * You don't need a server, hosting, or an SSL certificate to run the web apps. 8 | * You can put Slingcode on a web site, run it from a USB stick, laptop, or phone, and it doesn't need an internet connection to work. 9 | * You can "**add to home screen**" in your phone's browser to easily access your library of programs on the go. 10 | * You can **share apps peer-to-peer** over WebTorrent. 11 | * **It's private**. You only share what you choose. 12 | 13 | ## Try it: [slingcode.html](https://slingcode.net/slingcode.html) 14 | 15 | Or right-click on [`slingcode.html`](https://slingcode.net/slingcode.html) and "save link as" to download the HTML file onto your own computer. 16 | 17 | ### Video 18 | 19 |

Slingcode video

20 | 21 | You can find more [screencasts here](https://slingcode.net/screencasts.html). 22 | 23 | ### About 24 | 25 | There is no server component for Slingcode. The apps are stored in the web browser's local storage, completely offline. You can move apps between browsers by using the peer-to-peer send and receive feature. You can also export an app as a zip file and import it into another Slingcode instance, or upload your app onto regular web hosting to deploy it online. 26 | 27 | > Your computer. Your software. Your data. 28 | 29 | ### Nostalgia 30 | 31 | Remember when computers were fun? When a kid could type out a BASIC listing from a magazine and make magic with light and sound. When your computer belonged to you and you could understand the software running on it. 32 | 33 | ![Apple IIe](./public/img/appleIIe.jpg) 34 | 35 | I'm trying to recreate that magic with Slingcode. To get rid of all the tooling and dependencies and hosting problems, and make coding fun again. To help you bridge the gap between your idea and running code. 36 | 37 | I built it so I can teach my kids to code without all the complex setup you need these days. I'm trying to re-create the simple environment I had when I learned to code with my Mum on an Apple IIe back in the 80s. 38 | 39 | ### Who 40 | 41 | Hi, 👋 I'm Chris and I made this. 42 | 43 | You can find me online here: 44 | 45 | * [@mccrmx](https://twitter.com/mccrmx) 46 | * [mccormick.cx](https://mccormick.cx/) 47 | 48 | ### Hack & contribute 49 | 50 | Slingcode is built with [ClojureScript](https://clojurescript.org/). To get started contributing to Slingcode itself, [check out the codebase](https://github.com/chr15m/slingcode) and run `make`. You'll need Node and Java installed. 51 | 52 | ### Inspiration 53 | 54 | > ...situated software. This is software designed in and for a particular social situation or context. ...a "small pieces, loosely joined" way of making software... Situated software isn't a technological strategy so much as an attitude about closeness of fit between software and its group of users, and a refusal to embrace scale, generality or completeness as unqualified virtues. 55 | 56 | -- Clay Shirky, [Situated Software](https://web.archive.org/web/20040411202042/http://www.shirky.com/writings/situated_software.html) 57 | 58 | > ...in the original visions of many personal computing pioneers... the PC was intended as personal property – the owner would have total control (and understanding) of the software running on the PC, including the ability to copy bits on the PC at will. Software complexity, Internet connectivity, and unresolved incentive mismatches between software publishers and users (PC owners) have substantially eroded the reality of the personal computer as personal property. 59 | 60 | -- Nick Szabo, [Trusted Third Parties are Security Holes](https://nakamotoinstitute.org/trusted-third-parties/) 61 | 62 | > The trick is to fix the problem you have, rather than the problem you want. 63 | 64 | -- Bram Cohen 65 | 66 | > Kakehashi had no musical training, and wanted musical instruments to be accessible for professionals as well as amateurs like himself. He also wanted them to be inexpensive, intuitive, small, and simple. 67 | 68 | -- [Wikipedia, Ikutaro Kakehashi](https://en.wikipedia.org/wiki/Ikutaro_Kakehashi) 69 | 70 | > ...alternative to the MVP: Simple, Lovable and Complete (SLC)... A skateboard is a SLC product. It’s faster than walking, it’s simple, many people love it, and it’s a complete product that doesn’t need additions to be fun or practical. 71 | 72 | -- Jason Cohen, [Make it SLC instead](https://blog.asmartbear.com/slc.html) 73 | 74 | ### Credits 75 | 76 | Thanks to [Crispin](https://twitter.com/epic_castle) and Joel for testing and giving brilliant feedback on early versions. 77 | 78 | Some technology and libraries that Slingcode uses: 79 | 80 | * ClojureScript and Clojure. 81 | * CodeMirror for the web based code editor component. 82 | * React + Reagent for the rendering of the user interface. 83 | * Jszip for wrangling zip files. 84 | * WebTorrent for peer-to-peer file transfer. 85 | * Niceware for turning hex into phrases. 86 | * bugout for peer-to-peer communication (I wrote this library). 87 | * Tweetnacl.js for cryptography. 88 | * bs58 for managing base 58 addresses. 89 | * localforage for browser side storage. 90 | * zxing for QR code scanning. 91 | * mime-types for managing content types. 92 | * url-search-params for managing query strings. 93 | * shadow-cljs for making the compilation phase simple. 94 | * npm for managing dependencies. 95 | * fontawesome for icons. 96 | 97 | Thanks! 98 | 99 | ### Copyright 100 | 101 | Slingcode is Copyright Chris McCormick, 2020. 102 | 103 | Distributed under the MIT software license. See [license.txt](./license.txt) for details. 104 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Chris McCormick 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@ungap/url-search-params": "0.1.4", 4 | "codemirror": "5.51.0", 5 | "create-react-class": "15.6.3", 6 | "jszip": "3.3.0", 7 | "localforage": "1.7.3", 8 | "mime-types": "2.1.26", 9 | "minify": "5.1.1", 10 | "niceware": "2.0.0", 11 | "qrcode-svg": "1.1.0", 12 | "readable-stream": "2.3.7", 13 | "end-of-stream": "1.4.1", 14 | "react": "16.13.0", 15 | "react-dom": "16.13.0", 16 | "sass": "1.26.2", 17 | "shadow-cljs": "2.8.98", 18 | "tweetnacl-auth": "1.0.1", 19 | "@zxing/library": "0.17.0", 20 | "bugout": "0.0.9" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/publish.md: -------------------------------------------------------------------------------- 1 | **Slingcode Publish** is a service I'm working on to help you get your web apps online easily. You'll be able to choose "publish" from the App menu inside Slingcode and have your site appear online instantly with its own URL that you can share. 2 | -------------------------------------------------------------------------------- /pages/screencasts.md: -------------------------------------------------------------------------------- 1 | ## Screencasts 2 | 3 | ### Introduction 4 | 5 |
6 | 7 | This video runs through the major features of Slingcode and how to use it. 8 | 9 | --- 10 | 11 | ### Get Started With React 12 | 13 |
14 | 15 | React is a front end framework for making user interfaces. In this video I'll show you how to use React to build web applications in the Slingcode editor. 16 | 17 | --- 18 | 19 | ### Live-code SVG Motion Graphics! 20 | 21 |
22 | 23 | In this screencast I'll show you how to live-code SVG motion graphics from scratch right in your browser using the Slingcode editor. We'll do this without using any frameworks and without installing anything. 24 | 25 | --- 26 | 27 | ### Up and Running With Vue.js 28 | 29 |
30 | 31 | Vue.js is a front end framework which is perfect for building "single page" web applications. In this video I'll show you how to get up and running with Vue.js in the Slingcode online code editor. You don't need to install anything and you don't need to use the command line to create your first Vue.js app using Slingcode. 32 | 33 | --- 34 | 35 | ### Make apps with Hyperapp 36 | 37 |
38 | 39 | Hyperapp is a minimal user interface library for make web applications. In this screencast I show you how to get started making apps with Hyperapp in the Slingcode editor. 40 | 41 | --- 42 | -------------------------------------------------------------------------------- /public/codemirror.css: -------------------------------------------------------------------------------- 1 | ../node_modules/codemirror/lib/codemirror.css -------------------------------------------------------------------------------- /public/dialog.css: -------------------------------------------------------------------------------- 1 | ../node_modules/codemirror/addon/dialog/dialog.css -------------------------------------------------------------------------------- /public/erlang-dark.css: -------------------------------------------------------------------------------- 1 | ../node_modules/codemirror/theme/erlang-dark.css -------------------------------------------------------------------------------- /public/img/appleIIe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/public/img/appleIIe.jpg -------------------------------------------------------------------------------- /public/img/computers-in-our-lives.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/public/img/computers-in-our-lives.jpg -------------------------------------------------------------------------------- /public/img/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/public/img/youtube.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Slingcode 14 | 15 | 16 | 17 |
Loading...
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 29 | 35 | 41 | 47 | 53 | 54 | 55 | 75 | 83 | 84 | 86 | 87 | 89 | image/svg+xml 90 | 92 | 93 | 94 | 95 | 96 | 101 | 103 | 112 | 114 | 120 | 126 | 132 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* #31D584 */ 2 | /* #E5B413 */ 3 | /* #50A1EA */ 4 | /* #C64438 */ 5 | 6 | body, html { 7 | margin: 0px; 8 | padding: 0px; 9 | background-color: #232a25; 10 | color: #999; 11 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 12 | font-size: 24px; 13 | font-weight: bold; 14 | } 15 | 16 | body, html, #app, #app > div { 17 | min-height: 100vh; 18 | overflow-x: hidden; 19 | } 20 | 21 | #dedication { 22 | text-align: center; 23 | margin-top: 4em; 24 | color: #555; 25 | } 26 | 27 | #loading { 28 | text-align: center; 29 | margin-top: 5em; 30 | margin-bottom: 5em; 31 | animation: blinker 1s linear infinite; 32 | } 33 | 34 | @keyframes blinker { 50% { opacity: 0; } } 35 | 36 | h3 { 37 | font-size: 32px; 38 | } 39 | 40 | h2 { 41 | font-size 48px; 42 | } 43 | 44 | h1 { 45 | font-size: 96px; 46 | } 47 | 48 | * { 49 | box-sizing: border-box; 50 | } 51 | 52 | a { 53 | text-decoration: none; 54 | color: #31D584; 55 | } 56 | 57 | a:hover { 58 | color: #8FF8C4; 59 | } 60 | 61 | .highlight { 62 | color: #50A1EA 63 | } 64 | 65 | button { 66 | cursor: pointer; 67 | background-color: #E5B413; 68 | border: 0px; 69 | border-radius: 3px; 70 | width: 128px; 71 | font-size: 24px; 72 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 73 | font-weight: bold; 74 | color: #232323; 75 | height: 48px; 76 | } 77 | 78 | button.warning { 79 | background-color: #C64438; 80 | } 81 | 82 | button.success { 83 | background-color: #31D584; 84 | } 85 | 86 | blockquote { 87 | font-style: italic; 88 | color: #666; 89 | } 90 | 91 | strong { 92 | color: #eee; 93 | } 94 | 95 | button:hover { 96 | background-color: #eee; 97 | } 98 | 99 | .light { 100 | color: #666; 101 | } 102 | 103 | input[type=file] { 104 | opacity: 0; 105 | float: right; 106 | position: absolute; 107 | max-width: 90%; 108 | font-size: 0.75em; 109 | cursor: pointer; 110 | } 111 | 112 | input { 113 | font-size: 24px; 114 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 115 | font-weight: bold; 116 | padding: 0.25em; 117 | border-radius: 5px; 118 | border: 2px solid #E5B413; 119 | background: none; 120 | color: #999; 121 | max-width: 100%; 122 | } 123 | 124 | section#header p { 125 | margin-left: 1em; 126 | } 127 | 128 | div#logo img { 129 | position: absolute; 130 | top: 24px; 131 | left: 22px; 132 | } 133 | 134 | div#logo span { 135 | position: absolute; 136 | top: 22px; 137 | left: 160px; 138 | } 139 | 140 | section#header svg.icon { 141 | fill: #E5B413; 142 | } 143 | 144 | #burger-menu { 145 | position: absolute; 146 | top: 64px; 147 | right: 32px; 148 | border-radius: 5px; 149 | border: 2px solid #E5B413; 150 | background-color: #232a25; 151 | padding: 1em; 152 | list-style-type: none; 153 | } 154 | 155 | #burger-menu li + li { 156 | margin-top: 0.5em; 157 | } 158 | 159 | #burger-menu a { 160 | color: #E5B413; 161 | } 162 | 163 | #burger-menu a:hover { 164 | color: #eee; 165 | } 166 | 167 | svg#lines { 168 | stroke: #777; 169 | position: absolute; 170 | top: 60px; 171 | } 172 | 173 | nav { 174 | position: absolute; 175 | top: 1em; 176 | right: 1em; 177 | } 178 | 179 | @media (max-width: 600px) { 180 | nav { 181 | top: 3.5em; 182 | z-index: 100; 183 | } 184 | } 185 | 186 | .title { 187 | color: #eee; 188 | } 189 | 190 | .message-wrapper { 191 | width: 100%; 192 | position: fixed; 193 | top: 2em; 194 | } 195 | 196 | .message-wrapper .message { 197 | background-color: #232a25; 198 | border-radius: 5px; 199 | width: 800px; 200 | max-width: 100%; 201 | margin: auto; 202 | padding: 1em; 203 | } 204 | 205 | .message-wrapper .message svg { 206 | float: right; 207 | cursor: pointer; 208 | } 209 | 210 | .warning .message { 211 | color: #C64438; 212 | border: 2px solid #C64438; 213 | } 214 | 215 | .warning .message svg { 216 | fill: #C64438; 217 | } 218 | 219 | .success .message { 220 | color: #31D584; 221 | border: 2px solid #31D584; 222 | } 223 | 224 | .success .message svg { 225 | fill: #31D584; 226 | } 227 | 228 | .input-group { 229 | margin-top: 3em; 230 | } 231 | 232 | .input-group button + button { 233 | margin-left: 1em; 234 | } 235 | 236 | /*** website markdown ***/ 237 | 238 | #logo a span { 239 | color: #999; 240 | } 241 | 242 | section.website li { 243 | margin-bottom: 0.5em; 244 | } 245 | 246 | section.website code { 247 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 248 | font-size: 24px; 249 | font-weight: bold; 250 | color: #eee; 251 | } 252 | 253 | section.website a code { 254 | color: #31D584; 255 | } 256 | 257 | section.website img { 258 | max-width: 100%; 259 | margin: auto; 260 | display: block; 261 | border: 2px solid #eee; 262 | border-radius: 5px; 263 | margin-top: 3em; 264 | margin-bottom: 3em; 265 | } 266 | 267 | section.website h3 { 268 | margin-top: 3em; 269 | } 270 | 271 | section.website blockquote { 272 | color: #50A1EA; 273 | margin: 2em; 274 | } 275 | 276 | section.website blockquote + p { 277 | margin-bottom: 4em; 278 | } 279 | 280 | section.website .screencast { 281 | position: relative; 282 | width: 100%; 283 | height: 0; 284 | padding-bottom: 56.25%; 285 | } 286 | 287 | section.website .video { 288 | position: absolute; 289 | top: 0; 290 | left: 0; 291 | width: 100%; 292 | height: 100%; 293 | } 294 | 295 | #news-signup { 296 | display: block; 297 | margin-top: 7em; 298 | margin-bottom: 7em; 299 | } 300 | 301 | .tinysignup { 302 | text-align: center; 303 | margin-top: 3em; 304 | margin-bottom: 3em; 305 | } 306 | 307 | .tinysignup p { 308 | color: #31D584; 309 | } 310 | 311 | .tinysignup input { 312 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 313 | font-size: 24px; 314 | font-weight: bold; 315 | border: 2px solid #555; 316 | height: 48px; 317 | border-radius: 5px; 318 | max-width: 90%; 319 | margin: 0.5em; 320 | padding: 0.25em; 321 | } 322 | 323 | .tinysignup button { 324 | margin-top: 1em; 325 | } 326 | 327 | .tinysignup .message { 328 | border: 2px solid #31D584; 329 | padding: 1em 1.5em; 330 | font-size: 1em; 331 | border-radius: 5px; 332 | color: #31D584; 333 | max-width: 800px; 334 | margin: auto; 335 | width: 90%; 336 | } 337 | 338 | .tinysignup .spinner { 339 | width: 40px; 340 | height: 40px; 341 | background-color: #31D584; 342 | border-radius: 5px; 343 | margin: 40px auto; 344 | animation: tinysignup-rotateplane 1.2s infinite ease-in-out; 345 | } 346 | 347 | @keyframes tinysignup-rotateplane { 348 | 0% { 349 | transform: perspective(120px) rotateX(0deg) rotateY(0deg); 350 | } 50% { 351 | transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); 352 | } 100% { 353 | transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); 354 | } 355 | } 356 | 357 | /*** apps ***/ 358 | 359 | #search svg { 360 | width: 32px; 361 | } 362 | 363 | #search span { 364 | position: relative; 365 | top: -1.9em; 366 | } 367 | 368 | #search .icon-search { 369 | fill: #555; 370 | float: left; 371 | } 372 | 373 | #search .icon-times { 374 | fill: #999; 375 | float: right; 376 | cursor: pointer; 377 | } 378 | 379 | #search input { 380 | padding-left: 48px; 381 | width: 100%; 382 | margin-bottom: 0.5em; 383 | border: none; 384 | background-color: transparent; 385 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 386 | font-size: 24px; 387 | font-weight: bold; 388 | color: #999; 389 | } 390 | 391 | #search input::placeholder{ 392 | color: #999; 393 | } 394 | 395 | /* tags */ 396 | 397 | #tags { 398 | border-bottom: 2px solid #444; 399 | margin-bottom: 1em; 400 | } 401 | 402 | #tags ul { 403 | display: inline-block; 404 | overflow: auto; 405 | margin: 0px; 406 | padding: 0.25em; 407 | } 408 | 409 | #tags ul li { 410 | display: inline-block; 411 | margin-right: 1.5em; 412 | } 413 | 414 | #tags ul li a { 415 | cursor: pointer; 416 | color: #999; 417 | text-decoration: none; 418 | } 419 | 420 | #tags ul li a:hover { 421 | color: #bbb; 422 | } 423 | 424 | #tags ul li.active a { 425 | color: #eee; 426 | } 427 | 428 | section.screen { 429 | margin-left: auto; 430 | margin-right: auto; 431 | margin-top: 7em; 432 | } 433 | 434 | section#apps { 435 | width: 100%; 436 | max-width: 800px; 437 | padding: 1em; 438 | } 439 | 440 | section#about, section#settings, section#send { 441 | width: 100%; 442 | max-width: 800px; 443 | padding: 1em; 444 | padding-top: 0px; 445 | } 446 | 447 | section#send ul li.completed { 448 | list-style-type: "✔"; 449 | color: #31D584; 450 | padding-left: 0.5em; 451 | } 452 | 453 | section#send blockquote { 454 | color: #50A1EA; 455 | } 456 | 457 | section#send button#copy { 458 | float: right; 459 | margin-bottom: 2em; 460 | } 461 | 462 | section#send #send-spinner { 463 | animation: blinker 1s linear infinite; 464 | } 465 | 466 | section#send .secret-container { 467 | margin-bottom: 3em; 468 | } 469 | 470 | section#send #qrcode { 471 | background-color: white; 472 | } 473 | 474 | section#send .qr { 475 | margin-top: 4em; 476 | text-align: center; 477 | margin-bottom: 1em; 478 | } 479 | 480 | section#send .qr button { 481 | max-width: 80%; 482 | width: 30ch; 483 | } 484 | 485 | section#send video { 486 | display: block; 487 | margin: auto; 488 | max-width: 75%; 489 | } 490 | 491 | section#send input#send-secret { 492 | font-size: 32px; 493 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 494 | font-weight: bold; 495 | padding: 0.25em; 496 | border-radius: 5px; 497 | border: 2px solid #E5B413; 498 | background: none; 499 | color: #31D584; 500 | width: 100%; 501 | } 502 | 503 | section#settings ul { 504 | list-style-type: none; 505 | padding-left: 0px; 506 | } 507 | 508 | section#settings li { 509 | margin-bottom: 1em; 510 | } 511 | 512 | section#settings ul button { 513 | width: 20ch; 514 | max-width: 98%; 515 | margin: auto; 516 | display: block; 517 | } 518 | 519 | .input-group #signaling-servers button { 520 | float: right; 521 | width: 3ch; 522 | min-width: 3ch; 523 | } 524 | 525 | .input-group #signaling-servers li input { 526 | max-width: calc(100% - 4ch); 527 | width: 100ch; 528 | } 529 | 530 | .input-group #signaling-servers { 531 | margin-bottom: 4em; 532 | } 533 | 534 | /*** individual app ***/ 535 | 536 | div.app { 537 | border-bottom: 2px solid #444; 538 | padding: 1.5em 0px; 539 | } 540 | 541 | div.app .app-icon { 542 | cursor: pointer; 543 | float: left; 544 | margin-right: 1em !important; 545 | } 546 | 547 | div.app .app-icon img { 548 | width: 64px; 549 | height: 64px; 550 | } 551 | 552 | div.app button svg { 553 | fill: #232323; 554 | margin-top: 6px; 555 | } 556 | 557 | div.app button { 558 | fill: #eee; 559 | } 560 | 561 | div.app button + button { 562 | margin-left: 4px; 563 | } 564 | 565 | div.app .link-out svg { 566 | fill: #bbb; 567 | margin-bottom: -0.5em; 568 | width: 24px; 569 | margin-left: 0.5em; 570 | } 571 | 572 | div.app .app-actions-menu { 573 | border-radius: 5px; 574 | border: 2px solid #E5B413; 575 | width: 160px; 576 | text-align: center; 577 | background-color: #232A25; 578 | margin-left: 120px; 579 | position: absolute; 580 | } 581 | 582 | div.app .app-actions-menu > div { 583 | padding: 0px; 584 | margin: 0.5em 0px; 585 | } 586 | 587 | @media (max-width: 600px) { 588 | div.app .app-actions-menu { 589 | position: absolute; 590 | margin-left: 0px; 591 | float: left; 592 | transform: translateY(2em); 593 | } 594 | } 595 | 596 | #apps .columns { 597 | display: flex; 598 | } 599 | 600 | @media (max-width: 600px) { 601 | #apps .columns { 602 | flex-direction: column-reverse; 603 | } 604 | 605 | #apps .column.action-buttons { 606 | margin-top: 1em; 607 | } 608 | 609 | #apps .column > div { 610 | display: inline-block; 611 | margin-right: 0.25em; 612 | } 613 | } 614 | 615 | #apps .column { 616 | display: inline-block; 617 | padding: 1em; 618 | vertical-align: top; 619 | min-width: 25%; 620 | } 621 | 622 | @media (max-width: 600px) { 623 | #apps .column { 624 | padding: 0px; 625 | } 626 | } 627 | 628 | #apps .column > * { 629 | padding-top: 0px; 630 | margin-top: 0px; 631 | margin-bottom: 0.5em; 632 | } 633 | 634 | #apps .column > p { 635 | display: block; 636 | clear: both; 637 | } 638 | 639 | #apps .actions { 640 | text-align: right; 641 | margin-bottom: 0.5em; 642 | float: right; 643 | margin-top: -1em; 644 | } 645 | 646 | #apps a.title { 647 | color: #eee; 648 | text-decoration: none; 649 | } 650 | 651 | #apps p.title { 652 | color: #50A1EA; 653 | } 654 | 655 | #apps a svg { 656 | fill: #50A1EA; 657 | } 658 | 659 | #apps p { 660 | color: #666; 661 | } 662 | 663 | #apps .tags span { 664 | background-color: #666; 665 | padding: 0.125em 0.5em; 666 | border-radius: 5px; 667 | color: #232323; 668 | } 669 | 670 | #apps .tags span + span { 671 | margin-left: 0.25em; 672 | } 673 | 674 | /*** add button ***/ 675 | 676 | button#add-app { 677 | position: fixed; 678 | bottom: 32px; 679 | right: 32px; 680 | font-size: 32px; 681 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 682 | font-weight: bold; 683 | border-radius: 2em; 684 | width: 64px; 685 | height: 64px; 686 | text-align: center; 687 | color: #232a25; 688 | border: 4px solid #232a25; 689 | margin: 0px; 690 | padding: 0px; 691 | } 692 | 693 | #add-app:hover { 694 | background-color: #eee; 695 | } 696 | 697 | #add-menu { 698 | position: fixed; 699 | bottom: 64px; 700 | right: 48px; 701 | border-radius: 5px; 702 | border: 2px solid #E5B413; 703 | background-color: #232a25; 704 | padding: 0.25em; 705 | } 706 | 707 | #add-menu ul { 708 | list-style-type: none; 709 | padding: 0px; 710 | margin: 0px; 711 | padding-bottom: 24px; 712 | } 713 | 714 | #add-menu ul li { 715 | margin-left: 0px; 716 | display: block; 717 | margin: 0px 0.25em; 718 | background-color: #e5b413; 719 | padding: 0.25em 0.75em; 720 | border-radius: 5px; 721 | margin: 0.25em; 722 | cursor: pointer; 723 | } 724 | 725 | #add-menu ul li a { 726 | color: #232a25; 727 | border-radius: 5px; 728 | } 729 | 730 | #add-menu ul li:hover { 731 | background-color: #eee; 732 | } 733 | 734 | #add-menu label { 735 | color: #232a25; 736 | } 737 | 738 | /*** editor ***/ 739 | 740 | section#editor { 741 | margin-top: 0px; 742 | padding-top: 7em; 743 | height: 100vh; 744 | } 745 | 746 | section#editor #file-menu { 747 | padding: 0px; 748 | margin-top: -3.5em; 749 | margin-left: 136px; 750 | margin-bottom: -0.5em; 751 | position: relative; 752 | } 753 | 754 | @media (max-width: 600px) { 755 | section#editor #file-menu { 756 | margin-top: 0px; 757 | margin-left: 0px; 758 | } 759 | } 760 | 761 | section#editor #file-menu > li { 762 | margin-left: 1em; 763 | color: #e5b413; 764 | } 765 | 766 | section#editor li.topmenu { 767 | display: inline-block; 768 | vertical-align: top; 769 | cursor: pointer; 770 | } 771 | 772 | section#editor li.topmenu input, section#editor li.topmenu label { 773 | cursor: pointer; 774 | } 775 | 776 | section#editor li.topmenu.button { 777 | border: 2px solid #e5b413; 778 | padding: 0px 0.5em; 779 | padding-top: 0px; 780 | padding-top: 0.25em; 781 | margin-top: -0.25em; 782 | border-radius: 3px; 783 | } 784 | 785 | section#editor li.topmenu svg { 786 | width: 1.5ch; 787 | padding: 0px; 788 | float: left; 789 | fill: #e5b413; 790 | margin-top: -0.125em; 791 | margin-right: 0.25em; 792 | } 793 | 794 | section#editor li.topmenu > ul { 795 | display: none; 796 | z-index: 1000; 797 | } 798 | 799 | section#editor li.topmenu > ul li { 800 | margin: 0.25em 0.25em; 801 | padding: 0em; 802 | } 803 | 804 | section#editor li.topmenu.open > ul { 805 | display: block; 806 | position: absolute; 807 | background-color: #232a25; 808 | border-bottom-left-radius: 5px; 809 | border-bottom-right-radius: 5px; 810 | border: 2px solid #999; 811 | padding: 0.5em; 812 | list-style-type: none; 813 | } 814 | 815 | section#editor .topmenu ul li { 816 | color: #aaa; 817 | cursor: pointer; 818 | } 819 | 820 | section#editor #file-menu li a { 821 | color: #aaa; 822 | } 823 | 824 | section#editor #file-menu a svg { 825 | fill: #e5b413; 826 | margin-right: -1em; 827 | } 828 | 829 | section#editor .topmenu ul li a:hover { 830 | color: #e5b413 !important; 831 | } 832 | 833 | section#editor .topmenu ul li:hover { 834 | color: #e5b413; 835 | } 836 | 837 | section#editor ul#files { 838 | padding-bottom: 0px; 839 | margin-bottom: 0px; 840 | list-style-type: none; 841 | border-bottom: 2px solid #999; 842 | white-space: nowrap; 843 | overflow-x: auto; 844 | overflow-y: hidden; 845 | scrollbar-color: #666 #999; 846 | } 847 | 848 | section#editor ul#files.out { 849 | width: 60%; 850 | } 851 | 852 | section#editor ul#files::-webkit-scrollbar { 853 | width: 11px; 854 | } 855 | 856 | section#editor ul#files::-webkit-scrollbar-track { 857 | background: #999; 858 | } 859 | 860 | section#editor ul#files::-webkit-scrollbar-thumb { 861 | background-color: #666; 862 | border-radius: 0px; 863 | border: none; 864 | } 865 | 866 | section#editor ul#files > li { 867 | border: 2px solid #999; 868 | border-bottom: none; 869 | padding: 0.25em 1em; 870 | border-top-left-radius: 5px; 871 | border-top-right-radius: 5px; 872 | display: inline-block; 873 | margin-bottom: -2px; 874 | cursor: pointer; 875 | } 876 | 877 | section#editor ul#files > li.active { 878 | background-color: #232a25; 879 | } 880 | 881 | section#editor ul#files > li.active input { 882 | background: none !important; 883 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 884 | font-size: 24px; 885 | font-weight: bold; 886 | color: white; 887 | border: none; 888 | padding: 0px; 889 | overflow: hidden; 890 | text-overflow: ellipsis; 891 | } 892 | 893 | section#editor { 894 | display: flex; 895 | flex-flow: column; 896 | } 897 | 898 | div#panes, div#panes > div, div#panes > div > .editor, .editor > .CodeMirror { 899 | flex: 1; 900 | display: flex; 901 | flex-flow: column; 902 | min-height: 0; 903 | } 904 | 905 | div.editor > div { 906 | } 907 | 908 | section#editor div#panes { 909 | width: 100%; 910 | } 911 | 912 | section#editor div#panes > div { 913 | width: 100%; 914 | } 915 | 916 | section#editor div#panes.out > div { 917 | width: 60%; 918 | } 919 | 920 | section#editor div#panes > iframe { 921 | height: 100vh; 922 | position: fixed; 923 | top: 0px; 924 | right: 0px; 925 | width: 40%; 926 | } 927 | 928 | section#editor .add-file-menu input { 929 | max-width: 3em; 930 | margin-left: -1em; 931 | cursor: pointer; 932 | } 933 | 934 | section#editor .add-file-menu { 935 | color: #e5b413; 936 | } 937 | 938 | section#editor .file-content { 939 | position: absolute; 940 | display: flex; 941 | top: 200px; 942 | bottom: 20px; 943 | text-align: center; 944 | justify-content: center; 945 | align-items: center; 946 | width: inherit; 947 | } 948 | 949 | section#editor .file-content > img { 950 | max-width: 100%; 951 | max-height: 100%; 952 | } 953 | 954 | section#editor .file-content > div { 955 | } 956 | 957 | /*** Code mirror ***/ 958 | 959 | .CodeMirror { 960 | background: none !important; 961 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 962 | font-size: 18px; 963 | } 964 | 965 | .CodeMirror-gutters { 966 | background: rgba(255,255,255,0.1) !important; 967 | } 968 | 969 | .CodeMirror-dialog-top { 970 | background-color: #232a25; 971 | } 972 | 973 | input.CodeMirror-search-field { 974 | font-family: "Courier New", "Courier", "Courier 10 Pitch", monospace; 975 | font-size: 18px; 976 | } 977 | 978 | .color-warn { 979 | color: #C64438 !important; 980 | } 981 | 982 | /*** popout ***/ 983 | 984 | #slingcode-frame { 985 | background-color: white; 986 | border: 0px solid transparent; 987 | position: fixed; 988 | top: 0; 989 | left: 0; 990 | bottom: 0; 991 | right: 0; 992 | width: 100%; 993 | height: 100%; 994 | } 995 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:source-paths ["src"] 2 | :dependencies [[reagent "0.10.0"] 3 | [org.clojure/core.async "1.1.587"]] 4 | :dev-http {8000 "public/"} 5 | :builds {:app {:target :browser 6 | :output-dir "public/js" 7 | :asset-path "js" 8 | :modules {:main {:init-fn slingcode.main/main!}} 9 | :devtools {:after-load slingcode.main/reload! 10 | :preloads [shadow.remote.runtime.cljs.browser]} 11 | :release {:output-dir "build/js"}}}} 12 | -------------------------------------------------------------------------------- /src/default-apps/8bit-interface.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/8bit-interface.zip -------------------------------------------------------------------------------- /src/default-apps/banana-dungeon-game.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/banana-dungeon-game.zip -------------------------------------------------------------------------------- /src/default-apps/chromium-dinosaur-game.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/chromium-dinosaur-game.zip -------------------------------------------------------------------------------- /src/default-apps/hello-world.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/hello-world.zip -------------------------------------------------------------------------------- /src/default-apps/jquery-ui-demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/jquery-ui-demo.zip -------------------------------------------------------------------------------- /src/default-apps/leaflet-map.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/leaflet-map.zip -------------------------------------------------------------------------------- /src/default-apps/mithril-todomvc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/mithril-todomvc.zip -------------------------------------------------------------------------------- /src/default-apps/party-like-its-98.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/party-like-its-98.zip -------------------------------------------------------------------------------- /src/default-apps/preact-demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/preact-demo.zip -------------------------------------------------------------------------------- /src/default-apps/savings-calculator.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/savings-calculator.zip -------------------------------------------------------------------------------- /src/default-apps/text-log.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/text-log.zip -------------------------------------------------------------------------------- /src/default-apps/tiny-spreadsheet.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/tiny-spreadsheet.zip -------------------------------------------------------------------------------- /src/default-apps/widgets-order-form.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr15m/slingcode/aef42bf097aef82f2eda297f4fe63ddd2763ff83/src/default-apps/widgets-order-form.zip -------------------------------------------------------------------------------- /src/resourceupdater.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', function(event) { 2 | var myblob = (document.location.href.indexOf("blob:" + event.origin) == 0); 3 | var parentsent = (window.parent.location.href.indexOf(event.origin) == 0); 4 | var reference = event.data["reference"]; 5 | var kind = event.data["kind"]; 6 | var url = event.data["url"]; 7 | if (myblob && parentsent && reference && kind && url) { 8 | console.log("Slingcode live reloading tag:", event.origin, kind, reference, url); 9 | var selector = '[data-slingcode-reference="' + reference + '"]'; 10 | var el = document.querySelector(selector); 11 | var boss = el.parentElement; 12 | if (kind == "link") { 13 | el.setAttribute("href", url); 14 | } else if (kind == "script") { 15 | var elnew = document.createElement("script"); 16 | elnew.setAttribute("data-slingcode-reference", reference); 17 | elnew.setAttribute("src", url); 18 | boss.appendChild(elnew); 19 | boss.removeChild(el); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/site/ENV: -------------------------------------------------------------------------------- 1 | NGINX_SERVER_NAME=slingcode.net 2 | NGINX_INCLUDE_FILE=nginx.conf 3 | NGINX_HTTPS_ONLY=1 4 | -------------------------------------------------------------------------------- /src/site/Procfile: -------------------------------------------------------------------------------- 1 | static: / 2 | analytics: make app-analytics.html analytics.html; sleep 3600 3 | -------------------------------------------------------------------------------- /src/site/nginx.conf: -------------------------------------------------------------------------------- 1 | access_log $LOG_ROOT/$APP/access.log; 2 | error_log $LOG_ROOT/$APP/error.log warn; 3 | 4 | location /publish { 5 | sendfile on; 6 | sendfile_max_chunk 1m; 7 | tcp_nopush on; 8 | directio 8m; 9 | aio threads; 10 | default_type "text/html"; 11 | alias /home/piku/.piku/apps/slingcode/publish.html; 12 | } 13 | 14 | location /revision.txt { 15 | default_type "text/plain"; 16 | alias /home/piku/.piku/apps/slingcode/revision.txt; 17 | add_header Access-Control-Allow-Origin *; 18 | } 19 | -------------------------------------------------------------------------------- /src/slingcode-bootleg.clj: -------------------------------------------------------------------------------- 1 | (let [template (html "../build/index.html") 2 | css (slurp "../build/style.min.css") 3 | logo (slurp "../build/logo-b64-href.txt") 4 | js (slurp "../build/js/main.js")] 5 | (-> template 6 | (enlive/at [:link#style] (enlive/substitute (convert-to [:style css] :hickory))) 7 | (enlive/at [:link.rm] nil) 8 | (enlive/at [:link#favicon] (enlive/substitute (convert-to [:link {:rel "icon" :href logo}] :hickory))) 9 | (enlive/at [:script#entrypoint] (enlive/substitute (convert-to [:script js] :hickory))))) 10 | -------------------------------------------------------------------------------- /src/slingcode-embed.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/slingcode-site-bootleg.clj: -------------------------------------------------------------------------------- 1 | (let [template (html "../build/index.html") 2 | template (enlive/at template [:head] (enlive/append (html "slingcode-social.html" :hickory-seq))) 3 | template (enlive/at template [:link] (fn [t] (update-in t [:attrs :href] (fn [a] (str "public/" a))))) 4 | template (enlive/at template [:link.rm] nil) 5 | template (enlive/at template [:script] (enlive/substitute nil)) 6 | static (html "slingcode-static.html") 7 | static (enlive/at static [:section#about] (enlive/content (markdown (str "../" (last *command-line-args*)) :hickory-seq))) 8 | static (enlive/at static [:p#gh-logo] (enlive/substitute nil)) 9 | static (enlive/at static [:section#about] (enlive/prepend (convert-to [:img {:src "public/img/computers-in-our-lives.jpg"}] :hickory-seq))) 10 | static (enlive/at static [:p#youtube] (enlive/substitute (html "slingcode-embed.html" :hickory-seq))) 11 | ;static (enlive/at static [:section#about] (enlive/prepend (convert-to [:div [:p.title "Slingcode personal computing platform."]] :hickory-seq))) 12 | ] 13 | (enlive/at template [:body] (enlive/content static))) 14 | -------------------------------------------------------------------------------- /src/slingcode-social.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/slingcode-static.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 16 |
17 | 18 | 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/slingcode/boilerplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled. 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |
Hello World.
15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /src/slingcode/icons.cljs: -------------------------------------------------------------------------------- 1 | (ns slingcode.icons) 2 | 3 | (def i 4 | {:link-out "M1408 928v320q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h704q14 0 23 9t9 23v64q0 14-9 23t-23 9h-704q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-320q0-14 9-23t23-9h64q14 0 23 9t9 23zm384-864v512q0 26-19 45t-45 19-45-19l-176-176-652 652q-10 10-23 10t-23-10l-114-114q-10-10-10-23t10-23l652-652-176-176q-19-19-19-45t19-45 45-19h512q26 0 45 19t19 45z" 5 | :code "M681 1399l-50 50q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l466-466q10-10 23-10t23 10l50 50q10 10 10 23t-10 23l-393 393 393 393q10 10 10 23t-10 23zm591-1067l-373 1291q-4 13-15.5 19.5t-23.5 2.5l-62-17q-13-4-19.5-15.5t-2.5-24.5l373-1291q4-13 15.5-19.5t23.5-2.5l62 17q13 4 19.5 15.5t2.5 24.5zm657 651l-466 466q-10 10-23 10t-23-10l-50-50q-10-10-10-23t10-23l393-393-393-393q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l466 466q10 10 10 23t-10 23z" 6 | :share "M1472 989v259q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h255q13 0 22.5 9.5t9.5 22.5q0 27-26 32-77 26-133 60-10 4-16 4h-112q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-214q0-19 18-29 28-13 54-37 16-16 35-8 21 9 21 29zm237-496l-384 384q-18 19-45 19-12 0-25-5-39-17-39-59v-192h-160q-323 0-438 131-119 137-74 473 3 23-20 34-8 2-12 2-16 0-26-13-10-14-21-31t-39.5-68.5-49.5-99.5-38.5-114-17.5-122q0-49 3.5-91t14-90 28-88 47-81.5 68.5-74 94.5-61.5 124.5-48.5 159.5-30.5 196.5-11h160v-192q0-42 39-59 13-5 25-5 26 0 45 19l384 384q19 19 19 45t-19 45z" 7 | :download "M1344 1344q0-26-19-45t-45-19-45 19-19 45 19 45 45 19 45-19 19-45zm256 0q0-26-19-45t-45-19-45 19-19 45 19 45 45 19 45-19 19-45zm128-224v320q0 40-28 68t-68 28h-1472q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h465l135 136q58 56 136 56t136-56l136-136h464q40 0 68 28t28 68zm-325-569q17 41-14 70l-448 448q-18 19-45 19t-45-19l-448-448q-31-29-14-70 17-39 59-39h256v-448q0-26 19-45t45-19h256q26 0 45 19t19 45v448h256q42 0 59 39z" 8 | :times "M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z" 9 | :search "M1216 832q0-185-131.5-316.5t-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5 316.5-131.5 131.5-316.5zm512 832q0 52-38 90t-90 38q-54 0-90-38l-343-342q-179 124-399 124-143 0-273.5-55.5t-225-150-150-225-55.5-273.5 55.5-273.5 150-225 225-150 273.5-55.5 273.5 55.5 225 150 150 225 55.5 273.5q0 220-124 399l343 343q37 37 37 90z" 10 | :clone "M1664 1632v-1088q0-13-9.5-22.5t-22.5-9.5h-1088q-13 0-22.5 9.5t-9.5 22.5v1088q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5-9.5t9.5-22.5zm128-1088v1088q0 66-47 113t-113 47h-1088q-66 0-113-47t-47-113v-1088q0-66 47-113t113-47h1088q66 0 113 47t47 113zm-384-384v160h-128v-160q0-13-9.5-22.5t-22.5-9.5h-1088q-13 0-22.5 9.5t-9.5 22.5v1088q0 13 9.5 22.5t22.5 9.5h160v128h-160q-66 0-113-47t-47-113v-1088q0-66 47-113t113-47h1088q66 0 113 47t47 113z" 11 | :bars "M1664 1344v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45zm0-512v128q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h1408q26 0 45 19t19 45z" 12 | :pencil "M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z" 13 | :play "M1576 927l-1328 738q-23 13-39.5 3t-16.5-36v-1472q0-26 16.5-36t39.5 3l1328 738q23 13 23 31t-23 31z" 14 | :stop "M1664 192v1408q0 26-19 45t-45 19h-1408q-26 0-45-19t-19-45v-1408q0-26 19-45t45-19h1408q26 0 45 19t19 45z" 15 | :paper-plane "M1764 11q33 24 27 64l-256 1536q-5 29-32 45-14 8-31 8-11 0-24-5l-453-185-242 295q-18 23-49 23-13 0-22-4-19-7-30.5-23.5t-11.5-36.5v-349l864-1059-1069 925-395-162q-37-14-40-55-2-40 32-59l1664-960q15-9 32-9 20 0 36 11z" 16 | :back "M1408 960v-128q0-26-19-45t-45-19h-502l189-189q19-19 19-45t-19-45l-91-91q-18-18-45-18t-45 18l-362 362-91 91q-18 18-18 45t18 45l91 91 362 362q18 18 45 18t45-18l91-91q18-18 18-45t-18-45l-189-189h502q26 0 45-19t19-45zm256-64q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"}) 17 | 18 | (defn component-icon [n] 19 | [:svg.icon {:width 64 :height 32 :viewBox "0 0 2048 1792"} [:path {:d (i n)}]]) 20 | -------------------------------------------------------------------------------- /src/slingcode/logo.svg: -------------------------------------------------------------------------------- 1 | ../../public/logo.svg -------------------------------------------------------------------------------- /src/slingcode/main.cljs: -------------------------------------------------------------------------------- 1 | (ns slingcode.main 2 | (:require 3 | [cljs.core.async :refer (chan put! close! "Slingcode start.") 34 | 35 | (aset js/window "onerror" (fn [error] (tap> {"message" (aget error "message") 36 | "filename" (aget error "filename") 37 | "lineno" (aget error "lineno") 38 | "colno" (aget error "colno") 39 | "error" (js->clj (aget error "error"))}))) 40 | 41 | (defonce state (r/atom {})) 42 | (def dom-parser (js/DOMParser.)) 43 | (def re-uuid (js/RegExp. "([a-f0-9]+(-|$)){5}" "g")) 44 | (def re-zip-app-files (js/RegExp. "(.*?)/(.*)")) 45 | (def re-css-url (js/RegExp. "url\\([\"']{0,1}(.*?)[\"']{0,1}\\)" "gi")) 46 | (def re-script-url (js/RegExp. "[\"'](.*?)[\"']" "gi")) 47 | 48 | (def boilerplate (rc/inline "slingcode/boilerplate.html")) 49 | (def not-found-app (rc/inline "slingcode/not-found.html")) 50 | (def resource-updater-source (rc/inline "resourceupdater.js")) 51 | (def logo (rc/inline "slingcode/logo.svg")) 52 | (def revision (rc/inline "slingcode/revision.txt")) 53 | (def default-apps-base64-blob (rc/inline "default-apps.zip.b64")) 54 | (def blocked-message {:level :warning 55 | :text (clojure.string/join "\n" 56 | ["We couldn't open the app window." 57 | "Sometimes adblockers mistakenly do this." 58 | "Try disabling your adblocker" 59 | "for this site and refresh."])}) 60 | (def default-signaling-servers ["wss://hub.bugout.link" 61 | "wss://tracker.openwebtorrent.com" 62 | "wss://tracker.btorrent.xyz"]) 63 | (def default-settings {"signaling-servers" default-signaling-servers}) 64 | 65 | ; only old Safari iOS needs this check 66 | (def can-make-files 67 | (try 68 | (when (js/File. #js ["hello."] "hello.txt" #js {:type "text/plain"}) true) 69 | (catch :default e false))) 70 | 71 | ; check if this platform can do webtorrenting 72 | (def can-p2p webtorrent/WEBRTC_SUPPORT) 73 | 74 | (js/console.log "can-make-files?" can-make-files) 75 | (js/console.log "can-p2p?" can-p2p) 76 | 77 | 78 | ; ***** functions ***** ; 79 | 80 | 81 | (defn make-file [content file-name args] 82 | (let [blob-content (clj->js [content]) 83 | last-modified (or (aget content "lastModified") (js/Date.)) 84 | args (clj->js args)] 85 | (aset args "lastModified" last-modified) 86 | (if can-make-files 87 | (js/File. blob-content file-name args) 88 | (let [f (js/Blob. blob-content args)] 89 | (aset f "name" file-name) 90 | (aset f "lastModified" last-modified) 91 | f)))) 92 | 93 | (defn make-boilerplate-files [] 94 | [(make-file boilerplate "index.html" {:type "text/html"})]) 95 | 96 | (defn extract-id [store-key] 97 | (let [id (.pop (.split store-key "/"))] 98 | (when (.match id re-uuid) [id store-key]))) 99 | 100 | (defn get-index-file [files] 101 | (first (filter #(= (.-name %) "index.html") files))) 102 | 103 | (defn base64-to-blob [data-uri] 104 | ; TODO: remove this when old iPads pepper the netherworld 105 | (let [[header base64-string] (.split data-uri ",") 106 | byte-string (js/atob base64-string) 107 | content-type (-> header (.split ":") second (.split ";") first) 108 | ab (js/ArrayBuffer. (.-length byte-string)) 109 | ia (js/Uint8Array. ab)] 110 | (doseq [i (range (.-length byte-string))] 111 | (aset ia i (.charCodeAt byte-string i))) 112 | (js/Blob. (clj->js [ab]) (clj->js {:type content-type})))) 113 | 114 | (defn get-file-contents [file result-type] 115 | (if file 116 | (if (aget file "text") 117 | (case result-type 118 | :array-buffer (.arrayBuffer file) 119 | :binary-string (.arrayBuffer file) 120 | (.text file)) 121 | ; wrapper hack to support older chrome versions 122 | ; TODO: remove when support is good across browsers 123 | (js/Promise. (fn [res rej] 124 | (let [fr (js/FileReader.)] 125 | (aset fr "onload" (fn [done] (res (.-result fr)))) 126 | (case result-type 127 | :array-buffer (.readAsArrayBuffer fr file) 128 | :binary-string (.readAsBinaryString fr file) 129 | :data-url (.readAsDataURL fr file) 130 | (.readAsText fr file)))))) 131 | (js/Promise. (fn [res rej] (res (if (= result-type :array-buffer) (js/ArrayBuffer. []) "")))))) 132 | 133 | (defn retrieve-files [store app-id] 134 | (if can-make-files 135 | (.getItem store (str "app/" app-id)) 136 | ; Safari why (TODO: remove when fn section above is removed) 137 | (js/Promise. 138 | (fn [res err] 139 | (go 140 | (let [files (js->clj (clj (> stored-apps 151 | (filter (fn [[app-id files]] (not (some #(= app-id %) app-order)))) 152 | (map first) 153 | vec)] 154 | (vec (concat (vec (or app-order [])) not-in-app-order)))) 155 | 156 | (defn get-files-map [files] 157 | (into {} (map (fn [f] {(.-name f) f}) files))) 158 | 159 | (defn extract-icon-url [dom files] 160 | (let [files-map (get-files-map files) 161 | icon-url (.querySelector dom "link[rel*='icon']") 162 | icon-url (if icon-url (.getAttribute icon-url "href")) 163 | icon-file (get files-map icon-url) 164 | icon-url (if icon-file (js/window.URL.createObjectURL icon-file) icon-url)] 165 | icon-url)) 166 | 167 | (defn get-apps-data [store] 168 | (go 169 | (let [store-keys (remove nil? (map extract-id ( (if tags (.getAttribute tags "content") "") (.split ","))) 184 | app {id 185 | {:title title 186 | :description description 187 | :tags (js->clj tags) 188 | :files (vec files) 189 | :icon-url icon-url}}] 190 | app))) 191 | store-keys)) 192 | result-chans (when app-chans (async/map merge app-chans)) 193 | result (if result-chans (dissoc ( n 198 | (.toLowerCase) 199 | (.replace (js/RegExp. "[^\\w ]+" "g") "") 200 | (.replace (js/RegExp. " +" "g") "-"))) 201 | 202 | (defn make-zip [store id title] 203 | (go 204 | (let [slug (make-slug title) 205 | zip (JSZip.) 206 | folder (.folder zip slug) 207 | files (> tags 233 | (filter 234 | (fn [tag] 235 | (let [k (.getAttribute tag lookup)] 236 | (and k (get-blob-url file-blobs k))))) 237 | (vec))) 238 | 239 | (defn replace-tag-attribute [file-blobs tag lookup] 240 | (let [tag-url (.getAttribute tag lookup) 241 | tag-url-patched (or (get-blob-url file-blobs tag-url) tag-url) 242 | reference-id (str (random-uuid))] 243 | (.setAttribute tag lookup tag-url-patched) 244 | (.setAttribute tag "data-slingcode-reference" reference-id) 245 | {tag-url [reference-id]})) 246 | 247 | (defn replace-text-refs [file-blobs text regex] 248 | (clojure.string/replace 249 | text regex 250 | (fn [[full inner]] 251 | (let [matching-blob-url (get-blob-url file-blobs inner)] 252 | (if matching-blob-url 253 | (.replace full inner matching-blob-url) 254 | full))))) 255 | 256 | (defn replace-tag-text [file-blobs tag regex] 257 | (aset tag "textContent" 258 | (replace-text-refs file-blobs 259 | (aget tag "textContent") 260 | regex))) 261 | 262 | (defn replace-file-refs [file-blobs file-name file content-type regex] 263 | (go 264 | (if (= (.-type file) content-type) 265 | (let [src (\n" (-> dom .-documentElement .-outerHTML))] 307 | [(make-file updated-dom-string (.-name file) {:type (.-type file)}) 308 | {:file-blobs file-blobs 309 | :tags {:scripts script-references 310 | :links style-references}}])))) 311 | 312 | (defn get-run-frame-reference [state app-id] 313 | (if (get-in state [:editing :iframe]) 314 | (-> (js/document.getElementById "slingcode-embedded-run-frame") .-contentWindow) 315 | (-> state :windows (get app-id)))) 316 | 317 | (defn in-string [a b] 318 | (>= (.indexOf (if a (.toLowerCase a) "") b) 0)) 319 | 320 | (defn filter-search [apps search] 321 | (let [search (if search (.toLowerCase search) "")] 322 | (into {} (filter 323 | (fn [[k v]] 324 | (or (= search "") 325 | (nil? search) 326 | (in-string (.toLowerCase (or (v :title) "")) search) 327 | (in-string (.toLowerCase (or (v :description) "")) search))) 328 | apps)))) 329 | 330 | 331 | ; ***** mutating functions ***** ; 332 | 333 | 334 | (defn store-app-order! [store app-order] 335 | (js/Promise. 336 | (fn [res err] 337 | (go 338 | (res (js->clj (js app-order))))))))) 339 | 340 | (defn store-files! [store app-id files] 341 | (if can-make-files 342 | ; TODO: warn user if storage full or error writing 343 | (.setItem store (str "app/" app-id) (clj->js files)) 344 | ; all of the following complex serialization and deserialization 345 | ; is neccessary because of the way iOS Safari 9 + localForage 346 | ; handle Files embedded within a deeper structure 347 | ; TODO: remove when all browsers support localForage blob arrays 348 | (js/Promise. 349 | (fn [res err] 350 | (go 351 | (let [file-chans (map (fn [f] 352 | (go 353 | (let [content {:name (.-name f) 354 | :type (.-type f) 355 | :lastModified (.-lastModified f) 356 | :content (js files)))))))))) 360 | 361 | (defn set-main-window-content! [state document files index-file] 362 | (go 363 | (let [content ( document (.getElementById "slingcode-frame")) 369 | title-element (-> document (.getElementsByTagName "title") js/Array.prototype.slice.call first) 370 | icon-element (-> document (.querySelector "link[rel*='icon']")) 371 | [index-file references] ( % (assoc-in [:editing :references] references)))))) 376 | 377 | (defn update-main-window-content! [{:keys [state store] :as app-data} files app-id] 378 | (js/console.log "updating main window content") 379 | (let [file (if files 380 | (get-index-file files) 381 | (make-file not-found-app "index.html" {:type "text/html"})) 382 | run-frame (get-run-frame-reference @state app-id)] 383 | (js/console.log "updating main window content (window)" run-frame) 384 | (when run-frame 385 | (set-main-window-content! state (-> run-frame .-document) files file)))) 386 | 387 | (defn update-refs! [{:keys [state store] :as app-data} files app-id file-index] 388 | (go 389 | (let [run-frame (get-run-frame-reference @state app-id) 390 | frame (when run-frame (-> run-frame .-document (.getElementById "slingcode-frame"))) 391 | [index-file updated-references] (js {"reference" reference-id 406 | "kind" (.substr (name k) 0 (dec (count (name k)))) 407 | "url" (js/window.URL.createObjectURL file)}) 408 | "*"))))))) 409 | 410 | (defn save-handler! [{:keys [state store] :as app-data} app-id file-index cm] 411 | (let [content (when (and cm (aget cm "getValue")) (.getValue cm)) 412 | files (get-in @state [:editing :files]) 413 | files (vec (map-indexed (fn [i f] 414 | (if (and (= @file-index i) content) 415 | (make-file content (.-name f) {:type (get-valid-type f)}) 416 | f)) 417 | files))] 418 | (go 419 | ( % 429 | (assoc :apps apps) 430 | (assoc :app-order app-order) 431 | (assoc-in [:editing :files] files))) 432 | (if (= (.-name file) "index.html") 433 | (update-main-window-content! app-data files app-id) 434 | (update-refs! app-data files app-id @file-index)))))) 435 | 436 | (defn remove-file! [{:keys [state store] :as app-data} app-id file-index] 437 | (let [files (get-in @state [:editing :files]) 438 | files (vec (concat (subvec files 0 file-index) (subvec files (inc file-index))))] 439 | (go 440 | ( % 443 | (assoc :apps apps) 444 | (assoc-in [:editing :files] files) 445 | (assoc-in [:editing :tab-index] (js/Math.max 0 (dec file-index))))))))) 446 | 447 | (defn create-editor! [dom-node src content-type] 448 | (let [config {:lineNumbers true 449 | :matchBrackets true 450 | ;:autofocus true 451 | :value (or src "") 452 | :theme "erlang-dark" 453 | :autoCloseBrackets true 454 | :mode content-type} 455 | cm (CodeMirror 456 | dom-node 457 | (clj->js config))] 458 | cm)) 459 | 460 | (defn init-cm! [{:keys [state] :as app-data} id file-index tab-index dom-node] 461 | (aset (.-commands CodeMirror) "save" (partial save-handler! app-data id tab-index)) 462 | (go 463 | (let [files (-> @state :editing :files)] 464 | (if (< file-index (count files)) 465 | (let [file (nth files file-index) 466 | src ( @state :windows (get id)) 537 | win (if (not (and win (.-closed win))) win) 538 | win (or win (js/window.open (str "?app=" id) (str "window-" id)))] 539 | (when win 540 | (.focus win) 541 | (swap! state assoc-in [:windows id] win)) 542 | ; let the user know if the window was blocked from opening 543 | (js/setTimeout 544 | (fn [] (when (aget win "closed") 545 | (swap! state assoc :message blocked-message))) 546 | 250))) 547 | 548 | (defn toggle-app-iframe! [{:keys [state store] :as app-data} ev] 549 | (.preventDefault ev) 550 | (swap! state update-in [:editing :iframe] not)) 551 | 552 | (defn edit-app! [{:keys [state store history] :as app-data} app-id files ev] 553 | (when ev 554 | (.preventDefault ev)) 555 | (go 556 | (let [files (vec (or files ( js/navigator .-platform (.match "Mac")) (.-metaKey ev) (.-ctrlKey ev)) 586 | target (.-target ev) 587 | tag (-> target .-tagName .toLowerCase)] 588 | (when (and ctrl S app-id (= mode :edit) (not= tag "textarea")) 589 | (save-file! app-data tab-index app-id ev)))) 590 | 591 | (defn delete-app! [{:keys [state store] :as app-data} id ev] 592 | (.preventDefault ev) 593 | (when (js/confirm "Are you sure you want to delete this app?") 594 | (go-home! app-data ev) 595 | (go 596 | (delete instead?") 610 | (when (js/confirm "Are you sure you want to delete this file?") 611 | (remove-file! app-data app-id @tab-index))))) 612 | 613 | (defn download-zip! [{:keys [state store] :as app-data} id title ev] 614 | (.preventDefault ev) 615 | (go 616 | (let [zipfile ( app second :files) nil) 626 | (swap! state assoc 627 | :message {:level :success :text (str "Added " (count added-apps) " apps.")} 628 | :add-menu nil))))) 629 | 630 | (defn toggle-screen! [state which ev] 631 | (.preventDefault ev) 632 | (swap! state 633 | (fn [s] 634 | (let [mode (s :mode) 635 | mode-last (s :mode-last) 636 | s (if (= mode which) 637 | (-> s 638 | (assoc :mode mode-last) 639 | (dissoc :mode-last)) 640 | (assoc s :mode which :mode-last mode))] 641 | (dissoc s :burger-menu))))) 642 | 643 | (defn toggle-app-actions-menu! [state app-id] 644 | (swap! state update-in [:actions-menu] #(if (= % app-id) nil app-id))) 645 | 646 | (defn toggle-add-menu! [state ev] 647 | (.preventDefault ev) 648 | (swap! state update-in [:add-menu] not)) 649 | 650 | (defn initiate-zip-upload! [{:keys [state store] :as app-data} ev] 651 | (.preventDefault ev) 652 | (let [files (js/Array.from (-> ev .-target .-files))] 653 | (add-zip-file! app-data (first files) false))) 654 | 655 | (defn increment-filename [f] 656 | (let [[_ file-part _ increment extension] (.exec #"(.*?)(-([0-9]+)){0,1}(?:\.([^.]+))?$" f)] 657 | (str file-part "-" (inc (int increment)) (and extension ".") extension))) 658 | 659 | (defn ensure-unique-filename [files file-name] 660 | (let [file-names (set (map #(.-name %) files))] 661 | (loop [f file-name] 662 | (if (contains? file-names f) 663 | (recur (increment-filename f)) 664 | f)))) 665 | 666 | (defn add-file! [{:keys [state store] :as app-data} file] 667 | (swap! state 668 | #(-> % 669 | (update-in [:editing :files] conj file) 670 | (assoc-in [:editing :tab-index] (count (get-in % [:editing :files]))))) 671 | (let [app-id (get-in @state [:editing :id]) 672 | tab-index (r/cursor state [:editing :tab-index])] 673 | (save-handler! app-data app-id tab-index nil))) 674 | 675 | (defn add-selected-file! [{:keys [state store] :as app-data} ev] 676 | (.preventDefault ev) 677 | (let [files (js/Array.from (-> ev .-target .-files)) 678 | file (first files) 679 | file-name (ensure-unique-filename (-> @state :editing :files) (.-name file)) 680 | file-type (get-valid-type file) 681 | file (make-file file file-name {:type file-type})] 682 | (add-file! app-data file))) 683 | 684 | (defn create-empty-file! [{:keys [state store] :as app-data} ev] 685 | (.preventDefault ev) 686 | (let [file-name (js/prompt "Filename:") 687 | file (when (and file-name (not= file-name "")) 688 | (make-file "" file-name {:type (or (mime-types/lookup file-name) "text/plain")}))] 689 | (when file 690 | (add-file! app-data file)))) 691 | 692 | (defn handle-paste! [{:keys [state store] :as app-data} ev] 693 | ;(js/console.log "handle-paste!") 694 | (let [clipboard (or (.-clipboardData ev) (.-clipboardData js/window)) 695 | ;pasted-text (.getData clipboard "Text") ; if data coming in is text 696 | pasted-files (aget clipboard "files") 697 | pasted-file (aget pasted-files 0) 698 | pasted-file-type (when pasted-file (aget pasted-file "type")) 699 | pasted-file-name (when pasted-file (aget pasted-file "name")) 700 | files (r/cursor state [:editing :files]) 701 | tab-index (r/cursor state [:editing :tab-index]) 702 | current-file (nth @files @tab-index) 703 | current-file-type (aget current-file "type") 704 | app-id (-> @state :editing :id) 705 | ; retain filename if types match 706 | file-name (if (= current-file-type pasted-file-type) 707 | (aget current-file "name") 708 | (ensure-unique-filename (-> @state :editing :files) pasted-file-name))] 709 | (when (and (> @tab-index 0) pasted-file) 710 | (when (js/confirm (str "Pasting a " 711 | (-> pasted-file .-type (.split "/") .pop) 712 | ".\n" 713 | "Replace the current file?")) 714 | (swap! files (fn [existing-files] 715 | (map-indexed (fn [i f] 716 | (if (= i @tab-index) 717 | (make-file pasted-file file-name (clj->js {:type pasted-file-type})) 718 | f)) existing-files))) 719 | (save-handler! app-data app-id tab-index nil))))) 720 | 721 | (defn reset-slingcode! [store ev] 722 | (.preventDefault ev) 723 | (when (js/confirm 724 | "WARNING!\nCompletely reset Slingcode and delete all apps?") 725 | (go 726 | ( js/window .-location .reload)))) 728 | 729 | (defn check-for-new-version! [state ev] 730 | (.preventDefault ev) 731 | (swap! state assoc :update-check :checking) 732 | (go 733 | (let [result ( @state :windows (get app-id))] 750 | (when win (.close win))))) 751 | 752 | (defn remove-signaling-server! [original-settings s ev] 753 | (.preventDefault ev) 754 | (swap! original-settings update-in ["signaling-servers"] 755 | (fn [servers] 756 | (vec 757 | (keep-indexed 758 | (fn [idx server] (when (not= idx s) server)) 759 | servers))))) 760 | 761 | (defn save-settings! [{:keys [state store] :as app-data} original-settings ev] 762 | (js/console.log "Yes hello") 763 | (.preventDefault ev) 764 | (go 765 | (js/console.log "And here" (clj->js @original-settings)) 766 | (js @original-settings))) 767 | (swap! state assoc :settings @original-settings) 768 | (js/console.log "saved") 769 | (toggle-screen! state :settings ev))) 770 | 771 | ; ***** send / receive ***** ; 772 | 773 | (defn room-name-from-secret [secret] 774 | (clojure.string/join " " 775 | (concat 776 | ["slingcode" "exchange"] 777 | (niceware/bytesToPassphrase (.slice (nacl/hash (nacl/hash secret)) 0 16))))) 778 | 779 | (defn extract-torrent-file [torrent] 780 | (let [c (chan) 781 | f (aget (.-files torrent) 0)] 782 | (go 783 | (.getBlob f (fn [err blob] 784 | (put! c blob) 785 | (close! c)))) 786 | c)) 787 | 788 | (defn seed-webtorrent! [bugout-instance f title] 789 | (let [webtorrent-instance (.-wt bugout-instance) 790 | announce (clj->js {"announce" (.-announce bugout-instance)}) 791 | encrypted-file (make-file f (make-slug title) #js {:type "application/octet-stream"}) 792 | c (chan)] 793 | (js/console.log "webtorrent instance" webtorrent-instance announce) 794 | (.on webtorrent-instance "error" (fn [err] (js/console.log "WebTorrent Error" err))) 795 | (go 796 | (.seed webtorrent-instance 797 | encrypted-file 798 | announce 799 | (fn [torrent] 800 | (js/console.log "torrent seeded" torrent) 801 | (put! c torrent) 802 | (close! c)))) 803 | c)) 804 | 805 | (defn one-time-secret-first-half [bugout-address-raw hmac-key] 806 | (.slice (nacl-auth bugout-address-raw hmac-key) 0 4)) 807 | 808 | (defn stop-sending-receiving! [{:keys [state store] :as app-data} mode ev] 809 | (when ev (.preventDefault ev)) 810 | (let [bugout (get-in @state [mode :bugout-instance]) 811 | webtorrent (when bugout (aget bugout "wt"))] 812 | (when bugout 813 | (.close bugout) 814 | (.destroy webtorrent))) 815 | (swap! state dissoc mode (when ev :mode)) 816 | (js/console.log "stop-sending-receiving!")) 817 | 818 | (defn receive-app! [{:keys [state store] :as app-data} human-readable-one-time-secret ev] 819 | (when ev (.preventDefault ev)) 820 | (swap! state assoc :mode :receive :receive {:status {:initiated true}}) 821 | (when can-p2p 822 | (let [human-readable-one-time-secret (vec (.split human-readable-one-time-secret " ")) 823 | passphrase-tokens (filter #(and % (not= % "")) human-readable-one-time-secret) 824 | one-time-secret (niceware/passphraseToBytes (clj->js passphrase-tokens)) 825 | bugout-address-hash-prefix (.slice one-time-secret 0 4) 826 | bugout-secret (.slice one-time-secret 4 8) 827 | hmac-key (nacl/hash bugout-secret) 828 | room-name (room-name-from-secret one-time-secret) 829 | bugout-instance (Bugout. room-name (clj->js {:announce (get-in @state [:settings "signaling-servers"])})) 830 | webtorrent-instance (.-wt bugout-instance)] 831 | (swap! state assoc-in [:receive :bugout-instance] bugout-instance) 832 | (.on bugout-instance "seen" 833 | (fn [address] 834 | (.send bugout-instance address (clj->js {"secret" human-readable-one-time-secret})) 835 | (swap! state assoc-in [:receive :status :seen] true))) 836 | (.on bugout-instance "message" 837 | (fn [address message] 838 | (let [torrent-hash (aget message "torrent-hash") 839 | encryption-key (js/Uint8Array.from (aget message "encryption-key")) 840 | encryption-nonce (js/Uint8Array.from (aget message "encryption-nonce")) 841 | bugout-address-raw (bs58/decode address) 842 | bugout-address-hash-prefix-check (one-time-secret-first-half bugout-address-raw hmac-key)] 843 | (when (and torrent-hash 844 | encryption-key 845 | encryption-nonce 846 | (nacl/verify bugout-address-hash-prefix bugout-address-hash-prefix-check)) 847 | (.add webtorrent-instance 848 | (str "magnet:?xt=urn:btih:" torrent-hash) 849 | (clj->js {"announce" (.-announce bugout-instance)}) 850 | (fn [torrent] 851 | (swap! state assoc-in [:receive :status :downloading] true) 852 | (.on torrent "done" 853 | (fn [] 854 | (.send bugout-instance address (clj->js {"secret" human-readable-one-time-secret 855 | "done" true})) 856 | (swap! state assoc-in [:receive :status :done] true) 857 | (go 858 | (let [file-name (str (aget (first (.-files torrent)) "name") ".zip") 859 | encrypted-zip (js {:keyPair bugout-keypair :announce (get-in @state [:settings "signaling-servers"])})) 894 | torrent (js {"encryption-key" (js/Array.from encryption-key) 911 | "encryption-nonce" (js/Array.from encryption-nonce) 912 | "torrent-hash" (.-infoHash torrent)})) 913 | (swap! state assoc-in [:send :status :replied] true))))))) 914 | 915 | (.on torrent "upload" 916 | (fn [byte-count] 917 | (swap! state assoc-in [:send :status :sending] true))) 918 | 919 | (swap! state assoc :send {:title title 920 | :secret human-readable-one-time-secret 921 | :bugout-instance bugout-instance 922 | :status {}}))))) 923 | 924 | (defn enable-scan-camera! [{:keys [state store] :as app-data} el] 925 | (js/console.log "enable-scan-camera!" el) 926 | (if el 927 | (let [scanner (zx/lib.BrowserQRCodeReader.)] 928 | (aset js/window "slingcode-qr-scanner" scanner) 929 | (.decodeFromInputVideoDeviceContinuously 930 | scanner 931 | js/undefined 932 | "qrcam" 933 | (fn [result err] 934 | (when result 935 | (.reset scanner) 936 | (js/console.log result) 937 | (let [text (aget result "text") 938 | qs (.pop (.split text "?")) 939 | qs-params (URLSearchParams. qs) 940 | secret (.get qs-params "receive")] 941 | (if secret 942 | (receive-app! app-data secret nil))))))) 943 | (let [scanner (aget js/window "slingcode-qr-scanner")] 944 | (when scanner 945 | (.stopContinuousDecode scanner) 946 | (.stopAsyncDecode scanner) 947 | (.stopStreams scanner) 948 | (.reset scanner))))) 949 | 950 | ; ***** views ***** ; 951 | 952 | (defn component-no-p2p [state] 953 | [:section#send.screen 954 | [:p "Sorry, your browser doesn't support peer-to-peer WebRTC connections."] 955 | [:button {:on-click #(swap! state dissoc :mode)} "Ok"]]) 956 | 957 | (defn component-receive [{:keys [state] :as app-data}] 958 | (let [secret (r/cursor state [:receive :secret]) 959 | receive-scan (r/cursor state [:receive :scan]) 960 | status (or (get-in @state [:receive :status]) {}) 961 | bugout (get-in @state [:receive :bugout-instance]) 962 | completed-class {:class "completed"}] 963 | (if can-p2p 964 | (if (status :initiated) 965 | [:section#send.screen 966 | [:p (str "Ready to receive.")] 967 | (when (not (status :done)) [:div#send-spinner "Receiving..."]) 968 | [:ul 969 | [:li completed-class "Connection listening."] 970 | [:li (when (status :seen) completed-class) "Seen other device."] 971 | [:li (when (status :downloading) completed-class) "Downloading the app."] 972 | [:li (when (status :done) completed-class) "Done."]] 973 | [:button {:on-click (partial stop-sending-receiving! app-data :receive)} (if (status :done) "Ok" "Cancel")]] 974 | [:section#send.screen 975 | [:p "Enter the 'send secret' from the other device, or scan the QR code, to start receiving."] 976 | [:div.qr 977 | (if @receive-scan 978 | [:div 979 | [:p "Scan QR to receive app."] 980 | [:video {:id "qrcam" :ref (partial enable-scan-camera! app-data)}]] 981 | [:input#send-secret {:value @secret 982 | :placeholder "Enter 'send secret'..." 983 | :on-change #(reset! secret (-> % .-target .-value))}]) 984 | [:div.input-group 985 | [:button {:on-click #(swap! receive-scan not)} (if @receive-scan "Input 'send secret'" "Scan a QR code")]]] 986 | [:div.input-group 987 | (when (not @receive-scan) [:button {:on-click (partial receive-app! app-data @secret)} "Receive"]) 988 | [:button {:on-click (partial stop-sending-receiving! app-data :receive)} "Cancel"]]]) 989 | [component-no-p2p state]))) 990 | 991 | (defn render-qr-code [secret-phrase base-url el] 992 | (js/console.log "render-qr-code" secret-phrase el) 993 | (when (and el (= (.-length (.-children el)) 0)) 994 | (let [code-writer (zx/lib.BrowserQRCodeSvgWriter.)] 995 | (.writeToDom code-writer el 996 | (str base-url 997 | "?receive=" 998 | (js/encodeURIComponent secret-phrase)) 999 | 300 300)))) 1000 | 1001 | (defn component-secret [secret secret-field base-url] 1002 | (when secret 1003 | (let [secret-phrase (clojure.string/join " " secret)] 1004 | [:div.secret-container 1005 | [:p "Select 'receive' on your other device."] 1006 | [:p "Then enter or scan this 'send secret' to connect:"] 1007 | [:input#send-secret {:value secret-phrase :read-only true :ref #(reset! secret-field %)}] 1008 | [:button#copy {:on-click (fn [ev] 1009 | (.select @secret-field) 1010 | (.execCommand js/document "copy") 1011 | (js/alert "Send secret copied!"))} "Copy"] 1012 | [:div.qr 1013 | [:div#qrcode {:ref (partial render-qr-code secret-phrase base-url)}] 1014 | [:p "scan to receive"]]]))) 1015 | 1016 | (defn component-send [{:keys [state base-url] :as app-data}] 1017 | (let [status (or (get-in @state [:send :status]) {}) 1018 | bugout (get-in @state [:send :bugout-instance]) 1019 | secret (get-in @state [:send :secret]) 1020 | secret-field (r/atom nil) 1021 | title (get-in @state [:send :title]) 1022 | completed-class {:class "completed"}] 1023 | (if can-p2p 1024 | [:section#send.screen 1025 | (if bugout 1026 | [:div 1027 | [:p [:strong (str "Ready to send" (when title (str " '" title "'")) ".")]] 1028 | [component-secret secret secret-field base-url] 1029 | (when (not (status :done)) [:div#send-spinner "Sending..."]) 1030 | [:ul 1031 | [:li completed-class "Connection listening."] 1032 | [:li (when (status :seen) completed-class) "Seen other device."] 1033 | [:li (when (status :replied) completed-class) "Replied to request."] 1034 | [:li (when (status :sending) completed-class) "Sending data."] 1035 | [:li (when (status :done) completed-class) "Done."]] 1036 | [:button {:on-click (partial stop-sending-receiving! app-data :send)} (if (status :done) "Ok" "Cancel")]] 1037 | [:div 1038 | [:p "Preparing files for sharing."] 1039 | [:p#send-spinner "Preparing..."] 1040 | [:button {:on-click (partial stop-sending-receiving! app-data :send)} "Cancel"]])] 1041 | [component-no-p2p state]))) 1042 | 1043 | (defn component-upload [{:keys [state] :as app-data}] 1044 | [:div "Load a zip file" 1045 | [:button {:on-click #(swap! state dissoc :mode :edit)} "Ok"]]) 1046 | 1047 | (defn component-filename [editor files i tab-index] 1048 | (let [active (= i @tab-index) 1049 | file (nth @files i) 1050 | n (.-name file)] 1051 | [:li {:class (when active "active") 1052 | :on-click (partial switch-tab! editor tab-index i)} 1053 | [:span n]])) 1054 | 1055 | (defn component-codemirror-block [{:keys [state] :as app-data} app-id f i tab-index] 1056 | [:div.editor 1057 | {:style {:display (if (= i @tab-index) "flex" "none")} 1058 | :ref (partial init-cm! app-data app-id i tab-index)}]) 1059 | 1060 | (defn dropdown-menu-state [menu-state id] 1061 | {:class (if (= @menu-state id) "open") 1062 | :on-click #(swap! menu-state (fn [old-state] (if (= old-state id) nil id)))}) 1063 | 1064 | (defn file-load-li [app-data uniq] 1065 | [:li 1066 | [:input {:type "file" 1067 | :id (str "add-file-" uniq) 1068 | :accept "image/*,text/*,application/json,application/javascript" 1069 | :on-change (partial add-selected-file! app-data)}] 1070 | [:label {:for (str "add-file-" uniq)} "Load"]]) 1071 | 1072 | (defn file-create-li [app-data] 1073 | [:li {:on-click (partial create-empty-file! app-data)} "Create"]) 1074 | 1075 | (defn component-editor [{:keys [state] :as app-data}] 1076 | (let [files (r/cursor state [:editing :files]) 1077 | tab-index (r/cursor state [:editing :tab-index]) 1078 | menu-state (r/cursor state [:editing :menu-state]) 1079 | file-count (range (count @files)) 1080 | app-id (-> @state :editing :id) 1081 | iframe (-> @state :editing :iframe) 1082 | app-window (-> @state :windows (get app-id))] 1083 | [:section#editor.screen 1084 | [:ul#file-menu {:on-mouse-leave #(reset! menu-state nil)} 1085 | [:a.back {:href "#" 1086 | :on-click (partial go-home! app-data)} 1087 | [component-icon :back]] 1088 | [:li.topmenu (dropdown-menu-state menu-state :file) "File" 1089 | [:ul 1090 | [:li [:a {:href "#" :on-click (partial save-file! app-data tab-index app-id)} "Save"]] 1091 | ; [:li [:a.color-warn {:href "#"} "rename"]] 1092 | [:li [:a.color-warn {:href "#" :on-click (partial delete-file! app-data app-id tab-index)} "Delete"]] 1093 | [file-load-li app-data "top"] 1094 | [file-create-li app-data]]] 1095 | [:li.topmenu (dropdown-menu-state menu-state :app) "App" 1096 | [:ul 1097 | [:li [:a {:href "https://slingcode.net/publish" :target "_blank"} "Publish"]] 1098 | [:li [:a.color-warn {:href "#" :on-click (partial delete-app! app-data app-id)} "Delete"]]]] 1099 | (if (or iframe app-window) 1100 | [:li.topmenu.button 1101 | {:on-click (partial close-run-frame! app-data app-id)} 1102 | [component-icon :stop] "Stop"] 1103 | [:li.topmenu.button (dropdown-menu-state menu-state :run) 1104 | [component-icon :play] "Run" 1105 | [:ul 1106 | [:li {:on-click (partial open-app-tab! app-data app-id)} 1107 | (if 1108 | (and app-window (not (aget app-window "closed"))) 1109 | "Switch to tab" 1110 | "In a new tab")] 1111 | [:li {:on-click (partial toggle-app-iframe! app-data)} 1112 | (if iframe 1113 | "Close view" 1114 | "Next to code")]]])] 1115 | [:ul#files {:class (if iframe "out")} 1116 | (doall (for [i file-count] 1117 | (let [f (nth @files i) 1118 | editor (get-in @state [:editing :editors i])] 1119 | (with-meta 1120 | [component-filename editor files i tab-index] 1121 | {:key (.-name f)})))) 1122 | (when (< (count @files) 9) 1123 | [:li.add-file-menu.topmenu (merge {:on-mouse-leave #(reset! menu-state nil)} 1124 | (dropdown-menu-state menu-state :add-file)) "+" 1125 | [:ul 1126 | [file-load-li app-data "sub"] 1127 | [file-create-li app-data]]])] 1128 | [:div#panes {:class (if iframe "out")} 1129 | [:div 1130 | (doall (for [i file-count] 1131 | (let [file (nth @files i) 1132 | filename (.-name file) 1133 | content-type (get-valid-type file)] 1134 | (cond 1135 | (= (.indexOf content-type "image/") 0) (when (= i @tab-index) 1136 | [:div.file-content {:key i} [:img {:src (js/window.URL.createObjectURL file)}]]) 1137 | :else (with-meta [component-codemirror-block app-data app-id file i tab-index] {:key i})))))] 1138 | (when iframe 1139 | [:iframe {:src (str "?app=" app-id) :id "slingcode-embedded-run-frame"}])]])) 1140 | 1141 | (defn component-list-app [{:keys [state] :as app-data} app-id app] 1142 | [:div.app 1143 | [:div.columns 1144 | [:div.column.action-buttons 1145 | [:div [:button {:on-click (partial open-app-tab! app-data app-id) :title "Run app"} [component-icon :play]]] 1146 | [:div [:button {:on-click (partial edit-app! app-data app-id nil) :title "Edit app code"} [component-icon :pencil]]] 1147 | [:div [:button {:on-click (partial send-app! app-data app-id (app :files) (app :title)) :title "Send app"} [component-icon :paper-plane]]] 1148 | [:div [:button {:on-click (partial clone-app! app-data app (str (random-uuid)) (app :files)) :title "Clone app"} [component-icon :clone]]] 1149 | [:div [:button {:on-click (partial download-zip! app-data app-id (app :title)) :title "Download app zip"} [component-icon :download]]]] 1150 | [:div.column 1151 | [:span.app-icon {:on-click (partial open-app-tab! app-data app-id)} 1152 | (if (app :icon-url) 1153 | [:img {:src (app :icon-url)}] 1154 | [:svg {:width 64 :height 64} [:circle {:cx 32 :cy 32 :r 32 :fill "#555"}]])] 1155 | [:a.title {:href (str "?app=" app-id) 1156 | :on-click (partial open-app-tab! app-data app-id) 1157 | :target (str "window-" app-id)} 1158 | [:p.title (app :title) [:span {:class "link-out"} [component-icon :link-out]]]] 1159 | [:p (app :description)]]]]) 1160 | 1161 | (defn component-about [state] 1162 | [:section#about.screen 1163 | [:p.title "Slingcode"] 1164 | [:p "Personal computing platform."] 1165 | [:p "Copyright Chris McCormick, 2020."] 1166 | [:ul 1167 | [:li [:a {:href "https://twitter.com/mccrmx"} "@mccrmx"]] 1168 | [:li [:a {:href "https://mccormick.cx"} "mccormick.cx"]]] 1169 | [:p.light "Revision: " revision] 1170 | [:p [:a {:href "https://slingcode.net/" :target "_blank"} "slingcode.net"]] 1171 | [:button {:on-click (partial toggle-screen! state :about)} "Ok"] 1172 | [:div#dedication "For S & O."]]) 1173 | 1174 | (defn component-settings [{:keys [state store] :as app-data} original-settings update-check-status] 1175 | (let [signaling-servers (get @original-settings "signaling-servers") 1176 | update-check (@state :update-check)] 1177 | (if (not (nil? update-check)) 1178 | [:section#settings.screen 1179 | [:p.title "Check for updates"] 1180 | (if (= update-check :checking) 1181 | [:div#loading "Checking..."] 1182 | (if (= update-check revision) 1183 | [:div [:p "You have the latest version already."]] 1184 | [:div 1185 | [:p "Revision " update-check " of " [:a {:href "https://slingcode.net/slingcode.html" :download "slingcode.html"} "slingcode.html"] " is available."] 1186 | [:p "(Right click and 'Save link as' to download it)."] 1187 | [:p "Your revision is " revision]])) 1188 | [:div.input-group 1189 | [:button {:on-click #(swap! state dissoc :update-check)} (if (= update-check :checking) "Cancel" "Ok")]]] 1190 | [:section#settings.screen 1191 | [:p.title "Settings"] 1192 | [:div.input-group 1193 | [:p "Update & reset"] 1194 | [:ul 1195 | [:li [:button.success {:on-click (partial check-for-new-version! state)} "Check for update"]] 1196 | [:li [:button.warning {:on-click (partial reset-slingcode! store)} "Reset Slingcode"]]]] 1197 | [:div.input-group 1198 | [:p "WebTorrent signaling servers"] 1199 | [:ul#signaling-servers 1200 | (for [s (range (count signaling-servers))] 1201 | [:li {:key s} 1202 | [:button.remove {:on-click (partial remove-signaling-server! original-settings s)} "x"] 1203 | [:input {:value (nth signaling-servers s) 1204 | :on-change #(swap! original-settings assoc-in ["signaling-servers" s] (-> % .-target .-value))}]]) 1205 | [:li [:button.success 1206 | {:on-click #(swap! original-settings update-in ["signaling-servers"] conj "wss://")} "+"]]] 1207 | [:p 1208 | "These are used for the p2p send and receive functionality. " 1209 | "Learn how to " [:a {:href "https://github.com/webtorrent/bittorrent-tracker/" 1210 | :target "_blank"} "run your own signaling server"] 1211 | "."]] 1212 | [:div.input-group 1213 | [:button {:on-click (partial save-settings! app-data original-settings)} "Ok"] 1214 | [:button {:on-click (partial toggle-screen! state :settings)} "Cancel"]]]))) 1215 | 1216 | (defn component-download [state] 1217 | (let [zipfile (@state :zipfile)] 1218 | [:section#about.screen 1219 | [:p.title "Download"] 1220 | [:p [:a {:href (js/window.URL.createObjectURL zipfile) :download (.-name zipfile)} (.-name zipfile)]] 1221 | [:button {:on-click #(swap! state dissoc :mode :zipfile)} "Done"]])) 1222 | 1223 | (defn component-main [{:keys [state] :as app-data}] 1224 | (let [apps (r/cursor state [:apps]) 1225 | burger-menu (r/cursor state [:burger-menu]) 1226 | app-order (@state :app-order) 1227 | mode (@state :mode)] 1228 | [:div {:tab-index 0 1229 | :on-paste (partial handle-paste! app-data) 1230 | :on-key-down (partial intercept-browser-save! app-data mode)} 1231 | [:section#header 1232 | [:div#logo 1233 | [:img {:src (str "data:image/svg+xml;base64," (js/btoa logo))}] 1234 | [:span "Slingcode"] 1235 | [:nav (when (nil? mode) [:a {:href "#" :on-click (partial toggle-burger-menu! burger-menu)} [component-icon :bars]])] 1236 | (when @burger-menu 1237 | [:ul#burger-menu 1238 | [:li [:a {:href "#" :on-click (partial toggle-screen! state :settings)} "Settings"]] 1239 | [:li [:a {:href "#" :on-click (partial toggle-screen! state :about)} "About"]] 1240 | [:li [:a {:href "https://slingcode.net/screencasts.html" :target "_blank"} "Screencasts"]] 1241 | [:li [:a {:href "https://slingcode.net/#news-signup" :target "_blank"} "Newsletter"]]]) 1242 | [:svg#lines {:width "100%" :height "60px"} 1243 | [:path {:fill-opacity 0 :stroke-width 2 :stroke-linecap "round" :stroke-linejoin "round" :d "m 0,52 100,0 50,-50 5000,0"}] 1244 | [:path {:fill-opacity 0 :stroke-width 2 :stroke-linecap "round" :stroke-linejoin "round" :d "m 0,57 103,0 50,-50 5000,0"}]]]] 1245 | 1246 | (when (@state :message) 1247 | [:div.message-wrapper {:class (name (-> @state :message :level))} 1248 | [:div.message {:on-click (fn [ev] (.preventDefault ev) (swap! state dissoc :message))} 1249 | [component-icon :times] (-> @state :message :text)]]) 1250 | 1251 | (case mode 1252 | :about [component-about state] 1253 | :settings [component-settings app-data (r/atom (get-in @state [:settings]))] 1254 | :edit [component-editor app-data] 1255 | :upload [component-upload app-data] 1256 | :send [component-send app-data] 1257 | :receive [component-receive app-data] 1258 | :download [component-download state] 1259 | nil [:section#apps.screen 1260 | [:section#tags 1261 | 1262 | [:div#search 1263 | [:input {:placeholder "Filter" 1264 | :on-change #(swap! state assoc :search (-> % .-target .-value)) 1265 | :value (@state :search)}] 1266 | [:span.icon-search [component-icon :search]] 1267 | (when (and (@state :search) (not= (@state :search) "")) 1268 | [:span.icon-times {:on-click #(swap! state dissoc :search)} 1269 | [component-icon :times]])]] 1270 | 1271 | (let [apps-filtered (filter-search @apps (@state :search))] 1272 | (for [id (reverse app-order)] 1273 | (let [app (get apps-filtered id)] 1274 | (when app 1275 | [:div {:key id} [component-list-app app-data id app]])))) 1276 | 1277 | (when (@state :add-menu) 1278 | [:div#add-menu {:on-mouse-leave (partial toggle-add-menu! state)} 1279 | [:ul 1280 | [:li {:on-click (partial edit-app! app-data (str (random-uuid)) (make-boilerplate-files))} 1281 | [:a {:href "#"} 1282 | "New app"]] 1283 | [:li [:input {:type "file" 1284 | :name "upload-zip" 1285 | :accept "application/zip" 1286 | :on-change (partial initiate-zip-upload! app-data)}] 1287 | [:label "Upload zip"]] 1288 | [:li {:on-click (fn [ev] (.preventDefault ev) (swap! state assoc :mode :receive))} 1289 | [:a {:href "#"} 1290 | "Receive app"]]]]) 1291 | 1292 | [:button#add-app {:on-click (partial toggle-add-menu! state)} (if (@state :add-menu) "x" "+")]])])) 1293 | 1294 | (defn component-child-container [{:keys [state query] :as app-data} files file app-id] 1295 | [:iframe#slingcode-frame 1296 | {:ref (fn [el] 1297 | ; if we were spawned by a slingcode editor then ask the editor to reload us 1298 | ; otherwise set our own content 1299 | (let [parent-window (.-opener js/window) 1300 | parent-iframe (.-parent js/parent)] 1301 | (cond 1302 | parent-window 1303 | (.postMessage parent-window #js {:action "reload" :app-id app-id :sender-type "window"} "*") 1304 | parent-iframe 1305 | (.postMessage parent-iframe #js {:action "reload" :app-id app-id} "*") 1306 | :else 1307 | (set-main-window-content! state js/document files file))))}]) 1308 | 1309 | ; ***** browser message handlers ***** ; 1310 | 1311 | (defn receive-message! [{:keys [state store] :as app-data} message] 1312 | (js/console.log "received message" message) 1313 | (let [action (aget message "data" "action") 1314 | app-id (aget message "data" "app-id") 1315 | sender-type (aget message "sender-type")] 1316 | (cond (= action "reload") 1317 | (go 1318 | (let [files ( js/document .-location .-href) 1324 | win) 1325 | (when (= sender-type "window") 1326 | (swap! state update-in [:windows] assoc app-id win)) 1327 | (update-main-window-content! app-data files app-id))) 1328 | (= action "unload") 1329 | (swap! state update-in [:windows] dissoc app-id)))) 1330 | 1331 | (defn receive-popstate! [{:keys [state] :as app-data} ev] 1332 | (js/console.log "popstate" (.-state ev)) 1333 | (let [popstate (.-state ev) 1334 | mode (aget popstate "mode") 1335 | app-id (aget popstate "app-id")] 1336 | (case mode 1337 | "edit" (edit-app! app-data app-id nil ev) 1338 | (go-home! app-data ev)))) 1339 | 1340 | ; ***** init ***** ; 1341 | 1342 | (defn reload! [] 1343 | (println "reload!") 1344 | (let [qs (-> js/document .-location .-search) 1345 | qs-params (URLSearchParams. qs) 1346 | history (.-history js/window) 1347 | location (-> js/window .-location) 1348 | base-url (str (.-protocol location) "//" (.-host location) (.-pathname location))] 1349 | (go 1350 | (let [store (.createInstance localforage #js {:name "slingcode-apps"}) 1351 | app-data {:state state :store store :base-url base-url :history history} 1352 | el (js/document.getElementById "app") 1353 | ;_ (tap> {"CLEARED STORE" (clj (js default-apps)) 1391 | (js/console.log "Current state:" (clj->js (deref (app-data :state)))) 1392 | (tap> {"apps" ((deref (app-data :state)) :apps)}) 1393 | (if receive-code 1394 | (do 1395 | (.replaceState history #js {"mode" "home"} (.-title js/document) base-url) 1396 | (receive-app! app-data receive-code nil)) 1397 | (if edit-app-id 1398 | (edit-app! app-data edit-app-id nil nil))) 1399 | (rdom/render [component-main app-data] el))))))) 1400 | 1401 | (defn main! [] 1402 | (println "main!") 1403 | (reload!)) 1404 | -------------------------------------------------------------------------------- /src/slingcode/not-found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello World. 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 |

Not found

16 |

No app could be loaded.

17 |

Did you forget to save first?

18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /src/slingcode/zxingwrap.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "lib": window["RTCPeerConnection"] ? require("@zxing/library") : null, 3 | }; 4 | --------------------------------------------------------------------------------