├── .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 |
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 | 
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 | VIDEO
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 | VIDEO
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 | VIDEO
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 | VIDEO
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 | VIDEO
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 |
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 | VIDEO
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 |
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 |
--------------------------------------------------------------------------------