├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client └── index.js ├── css └── main.styl ├── idea.md ├── package.json ├── public ├── bundle.js ├── bundle.js.map ├── bundle.min.js ├── bundle.min.js.map ├── main.css ├── recording.gif └── reset.css ├── server ├── index.js └── views │ ├── 404.handlebars │ ├── image.handlebars │ └── index.handlebars └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | db/ 5 | package-lock.json 6 | public/downloads 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | install: npm install -g standard 2 | script: npm test 3 | language: node_js 4 | node_js: 5 | - "7.3.0" 6 | 7 | notifications: 8 | email: false 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Two Bucks Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [:zap: Zapsnap](http://zapsnap.io/) 2 | 3 | [![Build Status](https://travis-ci.org/twobucks/zapsnap.svg?branch=master)](https://travis-ci.org/twobucks/zapsnap) 4 | 5 | Temporary peer to peer screenshot sharing from your browser. 6 | 7 | ## Links 8 | 9 | * [zapsnap-desktop](https://github.com/twobucks/zapsnap-desktop) - MacOS app for taking screenshots 10 | * [seedshot-cli](https://github.com/twobucks/seedshot-cli) - CLI tool for taking screenshots (Linux and MacOS) 11 | 12 | ## What rocks 13 | 14 | * the files are temporary, so we don't waste resources on storing them 15 | * powered by [WebTorrent](https://github.com/feross/webtorrent) 16 | * browser is used for sharing images peer to peer 17 | * when all browsers with the image are closed, the image is gone forever 18 | 19 | ## What sucks 20 | 21 | * browser support, since it depends on [WebTorrent](https://github.com/feross/webtorrent) which doesn't support IE and probably lacks support for majority 22 | of mobile browsers 23 | * each file depends on torrent network so it takes around ~3s to load the image 24 | * no Windows support for taking screenshots 25 | * once you as an owner of an image close the browser, the file might still be available if other peers keep their browser open 26 | 27 | ## Development 28 | 29 | ``` 30 | npm start # will start the server 31 | npm run watch # watch for CSS/JS file changes and build 32 | npm run build # build CSS/JS for production 33 | ``` 34 | 35 | ## Attributions 36 | 37 | Logo created by il Capitano from [Noun Project](https://thenounproject.com/search/?q=zap&i=889349). 38 | 39 | Design by [Benjamin Alijagić](https://twitter.com/benjam1n). 40 | 41 | ## License 42 | 43 | MIT 44 | 45 | ## Sponsors 46 | 47 | Two Bucks Ltd © 2017 48 | 49 | [![https://twobucks.co](https://twobucks.co/assets/images/logo-small.png)](https://twobucks.co) 50 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | var Webtorrent = require('webtorrent') 2 | var client = new Webtorrent() 3 | var DetectRTC = require('detectrtc') 4 | 5 | var img = document.getElementById('seeded') 6 | var downloadedImg = document.getElementById('downloaded') 7 | 8 | var status = document.querySelector('.status') 9 | 10 | var downloadStarted = false 11 | 12 | if (!DetectRTC.isWebRTCSupported) { 13 | document.getElementById('spinner').style.display = 'none' 14 | downloadStarted = true 15 | status.innerHTML = 'This browser is unsupported. Please use a browser with WebRTC support.' 16 | } 17 | 18 | function updateSpeed (torrent) { 19 | const peerCount = parseInt(torrent.wires.length + 1) 20 | let statusHTML = 'Viewing' 21 | 22 | statusHTML += ` ${peerCount}` 23 | if (peerCount === 1) { 24 | statusHTML = `
You are the only one viewing this image.
Copy and send this URL for sharing or close this window to stop sharing.` 25 | } 26 | 27 | status.innerHTML = statusHTML 28 | } 29 | 30 | if (downloadedImg) { 31 | setTimeout(function () { 32 | if (!downloadStarted) { 33 | document.getElementById('spinner').style.display = 'none' 34 | status.innerHTML = 35 | 'Unable to find peers for this image.
Try waiting a little longer or reloading the browser ' + 36 | ' if you think the image is still available.' 37 | } 38 | }, 10000) // 10s 39 | 40 | client.add(downloadedImg.dataset.infoHash, function (torrent) { 41 | torrent.on('download', function () { 42 | updateSpeed(torrent) 43 | }) 44 | torrent.on('upload', function () { 45 | updateSpeed(torrent) 46 | }) 47 | setInterval(function () { 48 | updateSpeed(torrent) 49 | }, 1000) 50 | updateSpeed(torrent) 51 | 52 | console.log('downloading ' + torrent.infoHash) 53 | downloadStarted = true 54 | 55 | torrent.files.forEach(function (file) { 56 | file.getBuffer(function (er, buf) { 57 | document.getElementById('spinner').style.display = 'none' 58 | downloadedImg.src = buf.toString() 59 | }) 60 | }) 61 | }) 62 | } else { 63 | client.seed(Buffer.from(img.src), {name: 'image'}, function (torrent) { 64 | console.log('seeding ' + torrent.infoHash) 65 | torrent.on('download', function () { 66 | updateSpeed(torrent) 67 | }) 68 | torrent.on('upload', function () { 69 | updateSpeed(torrent) 70 | }) 71 | setInterval(function () { 72 | updateSpeed(torrent) 73 | }, 1000) 74 | updateSpeed(torrent) 75 | 76 | var xhr = new window.XMLHttpRequest() 77 | xhr.open('POST', window.location.href, true) 78 | xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8') 79 | 80 | xhr.send(JSON.stringify({ 81 | infoHash: torrent.magnetURI 82 | })) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /css/main.styl: -------------------------------------------------------------------------------- 1 | html, body 2 | height: 100% 3 | min-height: 100%; 4 | margin: 0 5 | 6 | body 7 | background-color: #fffecd; 8 | font-family: 'Roboto', sans-serif; 9 | 10 | body 11 | &:before 12 | content: " "; 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 20px; 18 | background: repeating-linear-gradient( 19 | 45deg, 20 | #fffecd, 21 | #fffecd 30px, 22 | red 30px, 23 | red 60px 24 | ); 25 | 26 | h1 27 | margin: 100px 0 40px 0; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | span 32 | text-indent: 100%; 33 | white-space: nowrap; 34 | overflow: hidden; 35 | #letter-1 36 | transition: .2s all ease; 37 | #letter-2 38 | transition: .4s all ease; 39 | #letter-3 40 | transition: .6s all ease; 41 | #letter-4 42 | transition: .8s all ease; 43 | 44 | h1 svg 45 | margin-left: -18px; 46 | opacity: .70; 47 | transition: .6s all ease; 48 | &:first-child 49 | margin: 0; 50 | path 51 | fill: #000; 52 | 53 | h1 #highVoltage 54 | opacity: .95 55 | position: relative; 56 | top: 10px; 57 | transform: scale(1.4); 58 | 59 | h1.closedCircuit svg 60 | transform: scale(.7); 61 | opacity: 0; 62 | 63 | h1.closedCircuit #highVoltage 64 | width: 0px; 65 | transform: scale(0); 66 | 67 | h1 #highVoltage path 68 | fill: red; 69 | 70 | h2 71 | width: 70%; 72 | margin: 0 auto; 73 | font-weight: 300; 74 | font-size: 48px; 75 | color: rgba(0,0,0,.6); 76 | letter-spacing: -0.04em; 77 | line-height: 1.2; 78 | 79 | .container 80 | text-align: center 81 | min-height: 75% 82 | 83 | .container.image 84 | padding: 20px 85 | 86 | .button 87 | display: inline-block; 88 | margin: 0 0 5px 0; 89 | padding: 18px 32px; 90 | background: red; 91 | color: rgba(255,255,255,1); 92 | font-size: 21px; 93 | font-weight: 800; 94 | //text-transform: uppercase; 95 | letter-spacing: .02em; 96 | border-radius: 4px; 97 | box-shadow: 0 10px 20px 0 rgba(0,0,0,.1); 98 | transition: .4s all; 99 | &:hover { 100 | transform: scale(1.04); 101 | box-shadow: 0 14px 25px 0 rgba(0,0,0,.15); 102 | transition: .4s all; 103 | } 104 | &:hover, &:visited, &:link, &:active { 105 | text-decoration: none 106 | } 107 | &:active { 108 | color: #16a085 !important /* Fallback for older browsers */ 109 | background-color: #fff 110 | } 111 | &.disabled { 112 | //color: #ddd !important 113 | //color: rgba(255, 255, 255, 0.5) !important 114 | cursor: default 115 | line-height: 20px 116 | &:active { 117 | //color: #ddd !important 118 | background-color: transparent !important 119 | } 120 | } 121 | 122 | .button-wrapper 123 | margin: 50px 124 | padding-bottom: 0px 125 | p 126 | font-style: italic; 127 | 128 | @media (min-width: 768px) 129 | .button-wrapper 130 | padding-bottom: 100px 131 | 132 | .faq 133 | width: 30% 134 | margin: 0 auto 135 | max-width: 400px 136 | 137 | .faq a 138 | color: rgba(0,0,0,.8) !important 139 | border-bottom: 2px solid rgba(0,0,0,.2); 140 | font-weight: 700 !important 141 | text-decoration: none 142 | position: relative 143 | z-index: 2 144 | &:before 145 | content: " " 146 | position: absolute 147 | z-index: 1 148 | bottom: 0 149 | left 0 150 | background: rgba(0,0,0,.1) 151 | width: 100%; 152 | height: 0px; 153 | transition: .2s all 154 | &:hover 155 | color: #fff 156 | &:hover:before 157 | height: 20px; 158 | transition: .2s all 159 | 160 | h3 161 | margin: 25px 0 0 0 162 | color: rgba(0,0,0,.65) 163 | 164 | p 165 | margin: 10px 0 0 0 166 | font-size: 18px 167 | color: rgba(0,0,0,.6) 168 | line-height: 1.5 169 | 170 | footer, .push 171 | margin-top: 70px 172 | height: 50px 173 | line-height: 50px 174 | text-align: center 175 | font-weight: 400 176 | font-size: 10px 177 | text-transform: uppercase 178 | letter-spacing: .1em 179 | color: rgba(0,0,0,.6) 180 | font-size: 14px 181 | 182 | footer a 183 | color: rgba(0,0,0,.6) 184 | text-decoration: none 185 | 186 | img 187 | max-width: 100% 188 | 189 | .status 190 | margin-top: 100px 191 | 192 | .spinner 193 | margin: 100px auto; 194 | width: 50px; 195 | height: 40px; 196 | text-align: center; 197 | font-size: 10px; 198 | 199 | 200 | .spinner > div { 201 | background-color: #fff; 202 | height: 100%; 203 | width: 6px; 204 | display: inline-block; 205 | 206 | -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; 207 | animation: sk-stretchdelay 1.2s infinite ease-in-out; 208 | } 209 | 210 | .spinner .rect2 { 211 | -webkit-animation-delay: -1.1s; 212 | animation-delay: -1.1s; 213 | } 214 | 215 | .spinner .rect3 { 216 | -webkit-animation-delay: -1.0s; 217 | animation-delay: -1.0s; 218 | } 219 | 220 | .spinner .rect4 { 221 | -webkit-animation-delay: -0.9s; 222 | animation-delay: -0.9s; 223 | } 224 | 225 | .spinner .rect5 { 226 | -webkit-animation-delay: -0.8s; 227 | animation-delay: -0.8s; 228 | } 229 | 230 | @-webkit-keyframes sk-stretchdelay { 231 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 232 | 20% { -webkit-transform: scaleY(1.0) } 233 | } 234 | 235 | @keyframes sk-stretchdelay { 236 | 0%, 40%, 100% { 237 | transform: scaleY(0.4); 238 | -webkit-transform: scaleY(0.4); 239 | } 20% { 240 | transform: scaleY(1.0); 241 | -webkit-transform: scaleY(1.0); 242 | } 243 | } 244 | 245 | // mobile 246 | 247 | @media (max-width: 768px) 248 | h1 249 | margin: 0 250 | h1 .wrapper 251 | transform: scale(.5) 252 | width: 200%; 253 | margin-left: -50%; 254 | h2 255 | font-size: 30px; 256 | .faq 257 | width: 85% 258 | 259 | // big screen 260 | 261 | @media (min-width: 1600px) 262 | h2 263 | width: 50% 264 | -------------------------------------------------------------------------------- /idea.md: -------------------------------------------------------------------------------- 1 | ## Idea 2 | 3 | The cool thing with Dropbox is that once a screenshot is taken, it's immediately available in my Dropbox folder and the link is ready for sharing. 4 | Ideally, this should all happen on my own machine and things could be shared with Bittorrent. The problem is privacy. There is no privacy if there is Bittorrent, since data is shared all over the network. 5 | 6 | Solution? Server is just a mediator between peers that are sharing files. The request comes, the server gives back the URL for sharing and then you can use that URL for sharing files. You can observe the progress of downloads for that file among peers and remove it at any time. Server is not involved in the download/upload traffic of that file, it goes directly from seeder to leecher. 7 | 8 | ## Features 9 | 10 | ### Server 11 | 12 | 1. Give the file directly to a server, the browser opens the site where a file is living 13 | 2. You are the first viewer or the admin of the file, so you're the only user downloading the file from the server 14 | 3. Once you download the file, it's removed from the server and only continues to live in your current browser session 15 | 4. The seeding process begins, anyone who gets to the URL you're viewing will receive the file from you (with webtorrent) 16 | 5. You get the info about how much peers is viewing the file and when they stop viewing it 17 | 6. Once you close the browser, the file is completely gone 18 | 19 | We basically need to build something similar to [instant.io](https://instant.io/). The problem with instant.io is that you have to manually click and 20 | select the file you want to host. Since we're trying to do it directly from your machine, with the help of that command line tool, we can't use that. 21 | We also don't have access to that button from inside a terminal. Notice that with instant.io, there is no post request that uploads the file anywhere, there's only the file input. 22 | 23 | ### Client 24 | 25 | 1. Take the screenshot 26 | 2. Image is sent to the server 27 | 3. URL opens in your browser with the image you start seeding from your browser 28 | 29 | Check [imgur-screenshot](https://github.com/jomo/imgur-screenshot), which does almost the same thing we need, 30 | just uploads the file to imgur. Also, there exists an even simpler tool for that, but I think it's more for Linux 31 | folks - check [this article to find out more](http://sirupsen.com/a-simple-imgur-bash-screenshot-utility/). 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seedshot", 3 | "version": "1.0.0", 4 | "description": "ephemeral P2P screenshot sharing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard", 8 | "build": "npm run build-css && npm run build-js", 9 | "build-css": "stylus -u nib css/main.styl -o public/ -c", 10 | "build-js": "NODE_ENV=production webpack", 11 | "start": "node server", 12 | "watch": "npm run watch-css & npm run watch-js & DEBUG=instant* nodemon server", 13 | "watch-css": "stylus -u nib css/main.styl -o public/ -w", 14 | "watch-js": "webpack --watch", 15 | "deploy": "rsync -av --progress --exclude=node_modules --exclude=db --exclude=.git . root@82.196.13.85:/home/hrvoje/seedshot", 16 | "ssh": "ssh root@82.196.13.85", 17 | "db": "mongod --dbpath db" 18 | }, 19 | "repository": "twobucks/seedshot", 20 | "author": "Hrvoje Simic ", 21 | "license": "MIT", 22 | "homepage": "https://github.com/twobucks/seedshot#readme", 23 | "dependencies": { 24 | "body-parser": "^1.14.2", 25 | "compression": "^1.6.2", 26 | "connect-busboy": "0.0.2", 27 | "detectrtc": "^1.3.5", 28 | "emoji-favicon": "^0.3.0", 29 | "express": "^4.13.3", 30 | "express-handlebars": "^2.0.1", 31 | "fd-slicer": "^1.0.1", 32 | "http-post": "^0.1.1", 33 | "mongodb": "^1.4.4", 34 | "monk": "^6.0.1", 35 | "multiparty": "^4.1.2", 36 | "uuid": "^2.0.1", 37 | "webtorrent": "^0.98.19" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.25.0", 41 | "babel-loader": "^7.1.1", 42 | "babel-preset-env": "^1.6.0", 43 | "nib": "^1.1.0", 44 | "nodemon": "^1.8.1", 45 | "stylus": "^0.53.0", 46 | "uglifyjs-webpack-plugin": "^0.4.6", 47 | "webpack": "^2.6.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | min-height: 100%; 5 | margin: 0; 6 | } 7 | body { 8 | background-color: #fffecd; 9 | font-family: 'Roboto', sans-serif; 10 | } 11 | body:before { 12 | content: " "; 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 20px; 18 | background: repeating-linear-gradient(45deg, #fffecd, #fffecd 30px, #f00 30px, #f00 60px); 19 | } 20 | h1 { 21 | margin: 100px 0 40px 0; 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: center; 25 | } 26 | h1 span { 27 | text-indent: 100%; 28 | white-space: nowrap; 29 | overflow: hidden; 30 | } 31 | h1 #letter-1 { 32 | transition: 0.2s all ease; 33 | } 34 | h1 #letter-2 { 35 | transition: 0.4s all ease; 36 | } 37 | h1 #letter-3 { 38 | transition: 0.6s all ease; 39 | } 40 | h1 #letter-4 { 41 | transition: 0.8s all ease; 42 | } 43 | h1 svg { 44 | margin-left: -18px; 45 | opacity: 0.7; 46 | transition: 0.6s all ease; 47 | } 48 | h1 svg:first-child { 49 | margin: 0; 50 | } 51 | h1 svg path { 52 | fill: #000; 53 | } 54 | h1 #highVoltage { 55 | opacity: 0.95; 56 | position: relative; 57 | top: 10px; 58 | transform: scale(1.4); 59 | } 60 | h1.closedCircuit svg { 61 | transform: scale(0.7); 62 | opacity: 0; 63 | } 64 | h1.closedCircuit #highVoltage { 65 | width: 0px; 66 | transform: scale(0); 67 | } 68 | h1 #highVoltage path { 69 | fill: #f00; 70 | } 71 | h2 { 72 | width: 70%; 73 | margin: 0 auto; 74 | font-weight: 300; 75 | font-size: 48px; 76 | color: rgba(0,0,0,0.6); 77 | letter-spacing: -0.04em; 78 | line-height: 1.2; 79 | } 80 | .container { 81 | text-align: center; 82 | min-height: 75%; 83 | } 84 | .container.image { 85 | padding: 20px; 86 | } 87 | .button { 88 | display: inline-block; 89 | margin: 0 0 5px 0; 90 | padding: 18px 32px; 91 | background: #f00; 92 | color: #fff; 93 | font-size: 21px; 94 | font-weight: 800; 95 | letter-spacing: 0.02em; 96 | border-radius: 4px; 97 | box-shadow: 0 10px 20px 0 rgba(0,0,0,0.1); 98 | transition: 0.4s all; 99 | } 100 | .button:hover { 101 | transform: scale(1.04); 102 | box-shadow: 0 14px 25px 0 rgba(0,0,0,0.15); 103 | transition: 0.4s all; 104 | } 105 | .button:hover, 106 | .button:visited, 107 | .button:link, 108 | .button:active { 109 | text-decoration: none; 110 | } 111 | .button:active { 112 | color: #16a085 !important /* Fallback for older browsers */; 113 | background-color: #fff; 114 | } 115 | .button.disabled { 116 | cursor: default; 117 | line-height: 20px; 118 | } 119 | .button.disabled:active { 120 | background-color: transparent !important; 121 | } 122 | .button-wrapper { 123 | margin: 50px; 124 | padding-bottom: 0px; 125 | } 126 | .button-wrapper p { 127 | font-style: italic; 128 | } 129 | @media (min-width: 768px) { 130 | .button-wrapper { 131 | padding-bottom: 100px; 132 | } 133 | } 134 | .faq { 135 | width: 30%; 136 | margin: 0 auto; 137 | max-width: 400px; 138 | } 139 | .faq a { 140 | color: rgba(0,0,0,0.8) !important; 141 | border-bottom: 2px solid rgba(0,0,0,0.2); 142 | font-weight: 700 !important; 143 | text-decoration: none; 144 | position: relative; 145 | z-index: 2; 146 | } 147 | .faq a:before { 148 | content: " "; 149 | position: absolute; 150 | z-index: 1; 151 | bottom: 0; 152 | left: 0; 153 | background: rgba(0,0,0,0.1); 154 | width: 100%; 155 | height: 0px; 156 | transition: 0.2s all; 157 | } 158 | .faq a:hover { 159 | color: #fff; 160 | } 161 | .faq a:hover:before { 162 | height: 20px; 163 | transition: 0.2s all; 164 | } 165 | h3 { 166 | margin: 25px 0 0 0; 167 | color: rgba(0,0,0,0.65); 168 | } 169 | p { 170 | margin: 10px 0 0 0; 171 | font-size: 18px; 172 | color: rgba(0,0,0,0.6); 173 | line-height: 1.5; 174 | } 175 | footer, 176 | .push { 177 | margin-top: 70px; 178 | height: 50px; 179 | line-height: 50px; 180 | text-align: center; 181 | font-weight: 400; 182 | font-size: 10px; 183 | text-transform: uppercase; 184 | letter-spacing: 0.1em; 185 | color: rgba(0,0,0,0.6); 186 | font-size: 14px; 187 | } 188 | footer a { 189 | color: rgba(0,0,0,0.6); 190 | text-decoration: none; 191 | } 192 | img { 193 | max-width: 100%; 194 | } 195 | .status { 196 | margin-top: 100px; 197 | } 198 | .spinner { 199 | margin: 100px auto; 200 | width: 50px; 201 | height: 40px; 202 | text-align: center; 203 | font-size: 10px; 204 | } 205 | .spinner > div { 206 | background-color: #fff; 207 | height: 100%; 208 | width: 6px; 209 | display: inline-block; 210 | -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; 211 | animation: sk-stretchdelay 1.2s infinite ease-in-out; 212 | } 213 | .spinner .rect2 { 214 | -webkit-animation-delay: -1.1s; 215 | animation-delay: -1.1s; 216 | } 217 | .spinner .rect3 { 218 | -webkit-animation-delay: -1s; 219 | animation-delay: -1s; 220 | } 221 | .spinner .rect4 { 222 | -webkit-animation-delay: -0.9s; 223 | animation-delay: -0.9s; 224 | } 225 | .spinner .rect5 { 226 | -webkit-animation-delay: -0.8s; 227 | animation-delay: -0.8s; 228 | } 229 | @-webkit-keyframes sk-stretchdelay { 230 | 0%, 40%, 100% { 231 | -webkit-transform: scaleY(0.4); 232 | } 233 | 20% { 234 | -webkit-transform: scaleY(1); 235 | } 236 | } 237 | @media (max-width: 768px) { 238 | h1 { 239 | margin: 0; 240 | } 241 | h1 .wrapper { 242 | transform: scale(0.5); 243 | width: 200%; 244 | margin-left: -50%; 245 | } 246 | h2 { 247 | font-size: 30px; 248 | } 249 | .faq { 250 | width: 85%; 251 | } 252 | } 253 | @media (min-width: 1600px) { 254 | h2 { 255 | width: 50%; 256 | } 257 | } 258 | @-moz-keyframes sk-stretchdelay { 259 | 0%, 40%, 100% { 260 | transform: scaleY(0.4); 261 | -webkit-transform: scaleY(0.4); 262 | } 263 | 20% { 264 | transform: scaleY(1); 265 | -webkit-transform: scaleY(1); 266 | } 267 | } 268 | @-webkit-keyframes sk-stretchdelay { 269 | 0%, 40%, 100% { 270 | transform: scaleY(0.4); 271 | -webkit-transform: scaleY(0.4); 272 | } 273 | 20% { 274 | transform: scaleY(1); 275 | -webkit-transform: scaleY(1); 276 | } 277 | } 278 | @-o-keyframes sk-stretchdelay { 279 | 0%, 40%, 100% { 280 | transform: scaleY(0.4); 281 | -webkit-transform: scaleY(0.4); 282 | } 283 | 20% { 284 | transform: scaleY(1); 285 | -webkit-transform: scaleY(1); 286 | } 287 | } 288 | @keyframes sk-stretchdelay { 289 | 0%, 40%, 100% { 290 | transform: scaleY(0.4); 291 | -webkit-transform: scaleY(0.4); 292 | } 293 | 20% { 294 | transform: scaleY(1); 295 | -webkit-transform: scaleY(1); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /public/recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twobucks/zapsnap/a408ab86a89152436cc16de5c52924fa0b826cd2/public/recording.gif -------------------------------------------------------------------------------- /public/reset.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var exphbs = require('express-handlebars') 3 | var path = require('path') 4 | var multiparty = require('multiparty') 5 | var fs = require('fs') 6 | var uuid = require('uuid') 7 | var db = require('monk')('localhost/seedshot') 8 | var bodyParser = require('body-parser') 9 | var compression = require('compression') 10 | var emojiFavicon = require('emoji-favicon') 11 | 12 | db.then(() => { 13 | console.log('Database connection established.') 14 | }) 15 | 16 | var app = express() 17 | app.use(bodyParser.json()) 18 | app.use(compression()) 19 | app.use(emojiFavicon('zap')) 20 | 21 | app.get('/', function (req, res) { 22 | res.render('index') 23 | }) 24 | 25 | app.get('/404', function (req, res) { 26 | res.render('404') 27 | }) 28 | 29 | app.get('/:uuid', function (req, res) { 30 | var images = db.get('images') 31 | 32 | images.findOne({ uuid: req.params.uuid }).then(function (doc) { 33 | if (doc) { 34 | res.render('image', { 35 | base64: doc.base64, 36 | infoHash: doc.infoHash, 37 | bundlePath: process.env.NODE_ENV === 'production' ? '/public/bundle.min.js' : '/public/bundle.js' 38 | }) 39 | } else { 40 | res.redirect('404') 41 | } 42 | }) 43 | }) 44 | 45 | app.post('/:uuid', function (req, res) { 46 | var hash = req.body.infoHash 47 | var images = db.get('images') 48 | images.findOneAndUpdate( 49 | { uuid: req.params.uuid }, 50 | { uuid: req.params.uuid, infoHash: hash } 51 | ) 52 | res.json({ 53 | infoHash: hash 54 | }) 55 | }) 56 | 57 | app.post('/', function (req, res) { 58 | var form = new multiparty.Form() 59 | var images = db.get('images') 60 | var id = uuid.v1() 61 | form.parse(req, function (err, fields, files) { 62 | if (err) throw err 63 | var path = files['file'][0]['path'] 64 | 65 | fs.readFile(files['file'][0]['path'], function (err, data) { 66 | if (err) throw err 67 | 68 | images.insert({ 69 | uuid: id, 70 | base64: data.toString('base64') 71 | }, function (err, doc) { 72 | if (err) console.log(err) 73 | 74 | fs.unlink(path) 75 | }) 76 | }) 77 | 78 | res.json({ 79 | uuid: id 80 | }) 81 | }) 82 | }) 83 | 84 | app.set('views', path.join(__dirname, 'views')) 85 | app.engine('handlebars', exphbs()) 86 | app.set('view engine', 'handlebars') 87 | app.set('x-powered-by', false) 88 | app.use('/public', express.static(path.join(__dirname, '../public'))) 89 | 90 | var server = app.listen(3000, function () { 91 | var port = server.address().port 92 | 93 | console.log('Example app listening at http://127.0.0.1:%s', port) 94 | }) 95 | -------------------------------------------------------------------------------- /server/views/404.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zapsnap * 404 - Not found 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 404 - Not found 12 |
13 |
14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/views/image.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zapsnap 4 | 5 | 6 | 7 | 8 | 9 |
10 | {{#if infoHash}} 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | {{else}} 20 | 21 | {{/if}} 22 |

23 |
24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /server/views/index.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zapsnap 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

12 | Zapsnap 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |

40 | 41 |

Temporary peer to peer screenshot sharing from your browser

42 | 43 |
44 | 45 |
46 | 47 | Download for MacOS 48 | 49 |

Linux and Windows coming soon

50 |
51 | 52 |
53 | 54 |
55 |

FAQ

56 | 57 |

What do you mean by "temporary"?

58 |

59 | The images are being shared with other people as long as you keep 60 | your browser open. Once you close it, the sharing depends on other 61 | available peers, so the sharing stops once all the browsers are closed. 62 |

63 | 64 |

Which browsers are supported?

65 |

66 | We depend on Webtorrent for browser support. That means we support Chrome, Firefox and Opera. 67 |

68 | 69 |

What's the pricing model?

70 |

71 | It's completely free and open source and the source code is hosted on Github. 72 |

73 | 74 |

How do you afford this?

75 |

76 | We reduce hosting costs by being peer to peer, so the only 77 | expense we have is hosting this server on Digital Ocean, which 78 | costs $10/mo. 79 |

80 | 81 |
82 | 83 | 86 |
87 | 88 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var UglifyJSPlugin = require('uglifyjs-webpack-plugin') 2 | var path = require('path') 3 | 4 | var PROD = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | entry: './client/index.js', 8 | output: { 9 | path: path.join(__dirname, 'public'), 10 | filename: PROD ? 'bundle.min.js' : 'bundle.js' 11 | }, 12 | devtool: 'source-map', 13 | node: { 14 | fs: 'empty' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['env'] 24 | } 25 | } 26 | } 27 | ] 28 | }, 29 | plugins: PROD ? [ 30 | new UglifyJSPlugin({ 31 | compress: { warnings: false } 32 | }) 33 | ] : [] 34 | } 35 | --------------------------------------------------------------------------------