├── services ├── image.png ├── .gitignore ├── frontend ├── index.js ├── utils │ └── nanoevent.js ├── components │ └── input.js ├── styles │ ├── reset.css │ └── style.css ├── plugins │ └── seeder.js └── views │ └── main.js ├── bundles └── index.html ├── readme.md ├── LICENSE ├── server.js ├── lib ├── store.js ├── router.js └── archiver.js ├── package.json └── install.sh /services: -------------------------------------------------------------------------------- 1 | node /home/pi/seeder/server.js 2 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/new-computers/seeder/HEAD/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | archiver 3 | pids 4 | bundles/bundle.js 5 | data.json 6 | archives 7 | package-lock.json 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | const choo = require('choo') 2 | 3 | const app = choo() 4 | 5 | app.use(require('./plugins/seeder')) 6 | 7 | app.route('/', require('./views/main')) 8 | 9 | app.mount('body') 10 | -------------------------------------------------------------------------------- /bundles/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | seeder 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/utils/nanoevent.js: -------------------------------------------------------------------------------- 1 | // Maybe this should be a module? 2 | module.exports = function (el, e, handler) { 3 | try { 4 | el.removeEventListener(e, handler) 5 | el.addEventListener(e, handler) 6 | } catch (err) { 7 | console.error(err) 8 | } 9 | 10 | return el 11 | } 12 | -------------------------------------------------------------------------------- /frontend/components/input.js: -------------------------------------------------------------------------------- 1 | const Nanocomponent = require('nanocomponent') 2 | const html = require('nanohtml') 3 | 4 | module.exports = class Input extends Nanocomponent { 5 | constructor(placeholder) { 6 | super() 7 | this.placeholder = placeholder 8 | } 9 | 10 | createElement() { 11 | return html` 12 | 13 | ` 14 | } 15 | 16 | update() { 17 | return false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # seeder 2 | A Raspberry Pi DAT seeder (early-development stage). 3 | 4 | ![seeder ui](image.png) 5 | 6 | It's just a seeder, without HTTP mirroring or anything special, yet. 7 | 8 | ## installation 9 | ``` 10 | $ curl -o- https://raw.githubusercontent.com/new-computers/seeder/master/install.sh | bash 11 | ``` 12 | It'll install everything you need. 13 | 14 | ## usage 15 | The installation script should add a service to ```systemd```: a web server running on port 80 that's the frontend for the seeder and that also handles and seeds the archives. 16 | 17 | Navigate to the Pi's address (in my case it's ```seeder.local```) and manage your sources. 18 | 19 | After you remove a source it'll still seed it until you restart the Pi or the service. 20 | 21 | ## development 22 | 23 | Clone the repo, then: 24 | ``` 25 | npm install 26 | npm run build 27 | npm start dev 28 | ``` 29 | -------------------------------------------------------------------------------- /frontend/styles/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, font, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | dl, dt, dd, ol, ul, li, 7 | fieldset, form, label, legend, 8 | table, caption, tbody, tfoot, thead, tr, th, td { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | outline: 0; 13 | font-weight: inherit; 14 | font-style: inherit; 15 | font-size: 100%; 16 | font-family: inherit; 17 | vertical-align: baseline; 18 | } 19 | /* remember to define focus styles! */ 20 | :focus { 21 | outline: 0; 22 | } 23 | body { 24 | line-height: 1; 25 | color: black; 26 | background: white; 27 | } 28 | ol, ul { 29 | list-style: none; 30 | } 31 | /* tables still need 'cellspacing="0"' in the markup */ 32 | table { 33 | border-collapse: separate; 34 | border-spacing: 0; 35 | } 36 | caption, th, td { 37 | text-align: left; 38 | font-weight: normal; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ""; 43 | } 44 | blockquote, q { 45 | quotes: "" ""; 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 New Computer Working Group 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 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const koa = require('koa') 2 | const serve = require('koa-static') 3 | const bodyParser = require('koa-bodyparser') 4 | 5 | const Store = require('./lib/store') 6 | const router = require('./lib/router') 7 | const Archiver = require('./lib/archiver') 8 | 9 | const app = new koa() 10 | 11 | var port = 80 12 | var root = '/home/pi/seeder' 13 | 14 | if (process.argv.length > 2 && process.argv[2] === 'dev') { 15 | port = 8080 16 | root = '.' 17 | } 18 | 19 | const store = new Store(root + '/data.json') 20 | const archiver = new Archiver(root, store) 21 | const r = router(store, archiver) 22 | 23 | store.read() 24 | archiver.all() 25 | 26 | app.use(serve(root + '/bundles')) 27 | app.use(bodyParser()) 28 | app.use(r.routes()) 29 | .use(r.allowedMethods()) 30 | 31 | console.log('The frontend is served on port ' + port + '.\n') 32 | app.listen(port) 33 | 34 | // Timeout for seeded sites 35 | check_timeouts() 36 | setInterval(check_timeouts, 2 * 3600000) 37 | 38 | function check_timeouts() { 39 | var now = new Date().getTime() 40 | 41 | var feeds = store.get('feeds') 42 | 43 | for (var i = 0; i < feeds.length; i++) { 44 | if (feeds[i].timeout && feeds[i].timeout < now) { 45 | archiver.remove(feeds[i].url) 46 | 47 | feeds.splice(store.indexOf(feeds[i].url), 1) 48 | } 49 | } 50 | 51 | store.set('feeds', feeds) 52 | store.write() 53 | } 54 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | function Store(path) { 4 | this.data = { 5 | feeds: [] 6 | } 7 | 8 | this.temp = {} 9 | 10 | this.get = function (key) { 11 | return this.data[key] 12 | } 13 | 14 | this.set = function (key, value) { 15 | this.data[key] = value 16 | return value 17 | } 18 | 19 | this.indexOf = function (url) { 20 | for (var i = 0; i < this.data.feeds.length; i++) { 21 | if (this.data.feeds[i].url === url) { 22 | return i 23 | } 24 | } 25 | } 26 | 27 | this.get_temp = function (key) { 28 | return this.temp[key] || {} 29 | } 30 | 31 | this.set_temp = function (key, value) { 32 | this.temp[key] = value 33 | } 34 | 35 | this.delete_temp = function(key) { 36 | delete this.temp[key] 37 | } 38 | 39 | this.read = function () { 40 | try { 41 | var data = fs.readFileSync(path, 'utf8') 42 | data = JSON.parse(data) 43 | 44 | this.data = { 45 | feeds: [] 46 | } 47 | for (var i = 0; i < data.feeds.length; i++) { 48 | if (typeof data.feeds[i] === 'object') { 49 | this.data.feeds.push(data.feeds[i]) 50 | } else { 51 | this.data.feeds.push({ 52 | url: data.feeds[i], 53 | paused: false 54 | }) 55 | } 56 | } 57 | } catch (err) {} 58 | } 59 | 60 | this.write = function () { 61 | fs.writeFile(path, JSON.stringify(this.data, null, '\t'), err => { 62 | if (err) { 63 | throw err 64 | } 65 | }) 66 | } 67 | 68 | return this 69 | } 70 | 71 | module.exports = Store 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seeder", 3 | "version": "0.1.0", 4 | "description": "A Dat seeder service", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "xo", 8 | "start": "node server.js", 9 | "watch": "watchify -t sheetify -o bundles/bundle.js frontend/index.js", 10 | "build": "browserify -t sheetify -o bundles/bundle.js frontend/index.js", 11 | "update": "git pull origin master && npm run build && systemctl restart dat" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/new-computers/seeder.git" 16 | }, 17 | "author": "kodedninja", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/new-computers/seeder/issues" 21 | }, 22 | "homepage": "https://github.com/new-computers/seeder#readme", 23 | "xo": { 24 | "semicolon": false, 25 | "no-var": false, 26 | "rules": { 27 | "no-var": 0, 28 | "no-undef": 0, 29 | "camelcase": 0, 30 | "capitalized-comment": 0, 31 | "new-cap": 0, 32 | "no-negated-condition": 0 33 | } 34 | }, 35 | "dependencies": { 36 | "dat-node": "^3.5.8", 37 | "dayjs": "^1.5.15", 38 | "du": "^0.1.0", 39 | "fs-extra": "^5.0.0", 40 | "koa": "^2.5.0", 41 | "koa-bodyparser": "^4.2.0", 42 | "koa-router": "^7.4.0", 43 | "koa-static": "^4.0.2", 44 | "parse-dat-url": "^3.0.1", 45 | "sheetify": "^7.3.1" 46 | }, 47 | "devDependencies": { 48 | "browserify": "^16.1.1", 49 | "choo": "^6.11.0-preview1", 50 | "nanocomponent": "^6.5.1", 51 | "nanohtml": "^1.2.2", 52 | "watchify": "^3.11.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | s_has() { 4 | type "${1-}" > /dev/null 2>&1 5 | } 6 | 7 | # install node 8 | if s_has "node" && s_has "npm" ; then 9 | echo "node installed" 10 | else 11 | echo "0. node is not installed. Installing..." 12 | 13 | # install nvm 14 | if s_has "nvm" ; then 15 | echo "nvm installed." 16 | else 17 | echo "0.a. Installing nvm..." 18 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 19 | source ~/.bashrc 20 | 21 | echo "nvm installed!" 22 | fi 23 | 24 | # install latest node version 25 | nvm install node 26 | nvm use node 27 | 28 | n=$(which node); \ 29 | n=${n%/bin/node}; \ 30 | chmod -R 755 $n/bin/*; \ 31 | sudo cp -r $n/{bin,lib,share} /usr/local 32 | 33 | echo "Finished! :|" 34 | fi 35 | 36 | # download seeder 37 | echo "1. Downloading seeder..." 38 | 39 | cd /home/pi 40 | git clone https://github.com/new-computers/seeder 41 | cd seeder 42 | 43 | echo "Finished! :|" 44 | 45 | # install dependencies 46 | echo "2. Installing dependencies..." 47 | 48 | sudo npm i -g add-to-systemd lil-pids 49 | sudo npm install 50 | 51 | echo "Finished! :)" 52 | 53 | # build 54 | echo "3. Building..." 55 | 56 | sudo npm run build 57 | 58 | echo "Finished! :>" 59 | 60 | # add to systemd 61 | echo "4. Setting up your Pi..." 62 | sudo add-to-systemd dat $(which lil-pids) /home/pi/seeder/services /home/pi/seeder/pids 63 | sudo systemctl start dat 64 | 65 | echo "Finished! :D" 66 | 67 | # test 68 | # TODO 69 | 70 | echo "" 71 | echo "seeder was successfully installed to your Pi." 72 | echo "Open the frontend in a web-browser, by going to its IP address or to http://$(hostname -s).local/" 73 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | function r(store, archiver) { 4 | const router = new Router() 5 | 6 | router.get('/feeds', async ctx => { 7 | var feeds = store.get('feeds') 8 | 9 | ctx.response.set('Content-Type', 'application/json') 10 | ctx.body = {feeds} 11 | }) 12 | 13 | router.post('/feeds', async ctx => { 14 | var feeds = store.get('feeds') 15 | 16 | var obj = { 17 | url: ctx.request.body.url, 18 | paused: false 19 | } 20 | 21 | if (ctx.request.body.timeout) { 22 | obj.timeout = ctx.request.body.timeout 23 | } 24 | 25 | feeds.push(obj) 26 | archiver.one(obj) 27 | 28 | store.set('feeds', feeds) 29 | store.write() 30 | 31 | ctx.response.set('Content-Type', 'application/json') 32 | ctx.body = {success: true} 33 | }) 34 | 35 | router.post('/remove-feed', async ctx => { 36 | var feeds = store.get('feeds') 37 | feeds.splice(store.indexOf(ctx.request.body.url), 1) 38 | 39 | store.set('feeds', feeds) 40 | store.write() 41 | 42 | archiver.remove(ctx.request.body.url) 43 | 44 | ctx.response.set('Content-Type', 'application/json') 45 | ctx.body = {success: true} 46 | }) 47 | 48 | router.get('/stats/:feed', async ctx => { 49 | var stats = store.get_temp(ctx.params.feed) 50 | 51 | ctx.response.set('Content-Type', 'application/json') 52 | ctx.body = JSON.stringify(stats) 53 | }) 54 | 55 | router.post('/pause', async ctx => { 56 | var feeds = store.get('feeds') 57 | var id = store.indexOf(ctx.request.body.url) 58 | 59 | if (feeds[id].paused) { 60 | archiver.resume(ctx.request.body.url) 61 | } else { 62 | archiver.pause(ctx.request.body.url) 63 | } 64 | 65 | feeds[id].paused = !feeds[id].paused 66 | 67 | store.set('feeds', feeds) 68 | store.write() 69 | 70 | ctx.response.set('Content-Type', 'application/json') 71 | ctx.body = {success: true} 72 | }) 73 | 74 | // Currently not used, but left it here in case it'll be needed 75 | router.post('/timeout', async ctx => { 76 | var feeds = store.get('feeds') 77 | var id = store.indexOf(ctx.request.body.url) 78 | 79 | feeds[id].timeout = ctx.request.body.timeout 80 | 81 | store.set('feeds', feeds) 82 | store.write() 83 | 84 | ctx.response.set('Content-Type', 'application/json') 85 | ctx.body = {success: true} 86 | }) 87 | 88 | return router 89 | } 90 | 91 | module.exports = r 92 | -------------------------------------------------------------------------------- /lib/archiver.js: -------------------------------------------------------------------------------- 1 | const Dat = require('dat-node') 2 | const fs = require('fs-extra') 3 | const du = require('du') 4 | 5 | function Archiver(root, store) { 6 | const t = this 7 | try { 8 | fs.mkdirSync(root + '/archives') 9 | } catch (err) {} 10 | 11 | this.dats = {} 12 | 13 | this.one = async function (feed) { 14 | var url = feed.url 15 | 16 | var dirname = url.replace('dat://', '') 17 | .replace(/\//g, '') 18 | 19 | try { 20 | fs.mkdirSync(root + '/archives/' + dirname) 21 | } catch (err) {} 22 | 23 | Dat(root + '/archives/' + dirname, {key: url}, async (err, dat) => { 24 | if (err) { 25 | var data = store.get_temp(dirname) 26 | data.peers = -1 27 | store.set_temp(dirname, data) 28 | 29 | throw err 30 | } 31 | 32 | console.log('Seeding ' + url) 33 | t.dats[dirname] = dat 34 | 35 | var network = dat.joinNetwork() 36 | var stats = dat.trackStats() 37 | 38 | network.on('connection', () => { 39 | var data = store.get_temp(dirname) 40 | data.peers = stats.peers.total 41 | store.set_temp(dirname, data) 42 | }) 43 | 44 | du(root + '/archives/' + dirname, (err, size) => { 45 | var data = store.get_temp(dirname) 46 | data.size = bytesToSize(size) 47 | store.set_temp(dirname, data) 48 | }) 49 | 50 | if (feed.http && feed.http.port && typeof feed.http.port == 'number') { 51 | console.log(dirname + ' is mirrored to HTTP on port ' + feed.http.port) 52 | dat.serveHttp({ 53 | port: feed.http.port, 54 | live: true 55 | }) 56 | } 57 | 58 | if (feed.paused) { 59 | dat.pause() 60 | } 61 | }) 62 | } 63 | 64 | this.remove = async function (url) { 65 | var dirname = url.replace('dat://', '') 66 | .replace(/\//g, '') 67 | console.log('Removed ' + url) 68 | if (this.dats[dirname]) { 69 | this.dats[dirname].pause() 70 | this.dats[dirname].close() 71 | delete this.dats[dirname] 72 | } 73 | 74 | store.delete_temp(dirname) 75 | 76 | try { 77 | fs.removeSync(root + '/archives/' + dirname) 78 | } catch (err) {} 79 | } 80 | 81 | this.pause = function (url) { 82 | var dirname = url.replace('dat://', '') 83 | .replace(/\//g, '') 84 | this.dats[dirname].pause() 85 | } 86 | 87 | this.resume = function (url) { 88 | var dirname = url.replace('dat://', '') 89 | .replace(/\//g, '') 90 | this.dats[dirname].resume() 91 | } 92 | 93 | this.all = function () { 94 | const feeds = store.get('feeds') 95 | 96 | feeds.forEach(this.one) 97 | } 98 | 99 | return this 100 | } 101 | 102 | function bytesToSize(bytes) { 103 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] 104 | if (bytes === 0) { 105 | return '0 Byte' 106 | } 107 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) 108 | return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i] 109 | } 110 | 111 | module.exports = Archiver 112 | -------------------------------------------------------------------------------- /frontend/plugins/seeder.js: -------------------------------------------------------------------------------- 1 | const parse = require('parse-dat-url') 2 | const dayjs = require('dayjs') 3 | 4 | module.exports = (state, emitter) => { 5 | state.feeds = [] 6 | state.stats = {} 7 | // State.open = true 8 | state.newUrl = {val: 75, text: 'forever'} 9 | fetch() 10 | 11 | emitter.on('feeds:fetch', fetch) 12 | 13 | emitter.on('feeds:cancel', () => { 14 | state.open = false 15 | emitter.emit('render') 16 | }) 17 | 18 | emitter.on('feeds:adjustTime', () => { 19 | // State.open = true 20 | // state.text = text 21 | emitter.emit('render') 22 | }) 23 | 24 | emitter.on('feeds:add', url => { 25 | if (!state.open) { 26 | state.open = true 27 | state.opened = true // True only at the first render after open 28 | emitter.emit('render') 29 | } else { 30 | try { 31 | url = parse(url) 32 | 33 | if (url.protocol !== 'dat:') { 34 | return 35 | } 36 | 37 | if (state.feeds.indexOf(url.href) !== -1) { 38 | return 39 | } 40 | 41 | var data = { 42 | url: url.href 43 | } 44 | 45 | switch (state.newUrl.val.toString()) { 46 | case '50': 47 | data.timeout = dayjs().add(1, 'M').valueOf() 48 | break 49 | case '25': 50 | data.timeout = dayjs().add(1, 'w').valueOf() 51 | text = '1 week' 52 | break 53 | case '0': 54 | data.timeout = dayjs().add(1, 'd').valueOf() 55 | text = '1 day' 56 | break 57 | } 58 | 59 | window.fetch('/feeds', { 60 | body: JSON.stringify(data), 61 | headers: { 62 | 'content-type': 'application/json' 63 | }, 64 | method: 'POST' 65 | }) 66 | .then(res => res.json()) 67 | .then(data => { 68 | if (data.success) { 69 | state.newUrl = {val: 75, text: 'forever'} // Reset 70 | 71 | state.feeds.push({url: url.href}) 72 | emitter.emit('render') 73 | stats(url.href) 74 | } 75 | }) 76 | state.open = false 77 | } catch (err) { 78 | console.error(err) 79 | } 80 | } 81 | }) 82 | 83 | emitter.on('feeds:remove', url => { 84 | window.fetch('/remove-feed', { 85 | body: JSON.stringify({url}), 86 | headers: { 87 | 'content-type': 'application/json' 88 | }, 89 | method: 'POST' 90 | }) 91 | .then(res => res.json()) 92 | .then(data => { 93 | if (data.success) { 94 | state.feeds.splice(index(url), 1) 95 | delete state.stats[url] 96 | emitter.emit('render') 97 | } 98 | }) 99 | }) 100 | 101 | emitter.on('feeds:stats', stats) 102 | 103 | emitter.on('feeds:pause', url => { 104 | window.fetch('/pause', { 105 | body: JSON.stringify({url}), 106 | headers: { 107 | 'content-type': 'application/json' 108 | }, 109 | method: 'POST' 110 | }) 111 | .then(res => res.json()) 112 | .then(data => { 113 | if (data.success) { 114 | state.feeds[index(url)].paused = !state.feeds[index(url)].paused 115 | emitter.emit('render') 116 | } 117 | }) 118 | }) 119 | 120 | function fetch() { 121 | window.fetch('/feeds') 122 | .then(res => res.json()) 123 | .then(data => { 124 | state.feeds = data.feeds 125 | emitter.emit('render') 126 | 127 | state.feeds.forEach(f => { 128 | stats(f.url) 129 | }) 130 | }) 131 | } 132 | 133 | function stats(url) { 134 | var dirname = url.replace('dat://', '').replace(/\//g, '') 135 | 136 | window.fetch('/stats/' + dirname) 137 | .then(res => res.json()) 138 | .then(data => { 139 | state.stats[url] = data 140 | emitter.emit('render') 141 | 142 | // Recheck after 10 seconds 143 | setTimeout(() => { 144 | stats(url) 145 | }, 10000) 146 | }) 147 | } 148 | 149 | function index(url) { 150 | for (var i = 0; i < state.feeds.length; i++) { 151 | if (state.feeds[i].url === url) { 152 | return i 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /frontend/views/main.js: -------------------------------------------------------------------------------- 1 | const html = require('nanohtml') 2 | var css = require('sheetify') 3 | const nanoevent = require('../utils/nanoevent') 4 | 5 | const Input = require('../components/input') 6 | 7 | const url = new Input('dat://') 8 | 9 | css('../styles/reset.css') 10 | css('../styles/style.css') 11 | module.exports = view 12 | 13 | function view(state, emit) { 14 | var known_url = window.location.origin.replace('http://', '') // Location.hostname doesn't include the port 15 | return html` 16 | 17 |
18 |

you are known as ${known_url}

19 |
20 | feedback ↗ 21 |
22 |
23 |
24 |
25 | ${description(state)} 26 | ${addDatControl(state, emit)} 27 |
28 |
29 | ${urlInput(state, emit)} 30 |
31 |
32 | ${state.feeds.map(feed)} 33 |
34 |
35 | 36 | ` 37 | function addDatControl(state, emit) { 38 | if (!state.open) { 39 | return html` 40 |

+ add a dat

41 | ` 42 | } 43 | return html` 44 |

cancel 45 | save

46 | ` 47 | 48 | function cancel(e) { 49 | e.preventDefault() 50 | emit('feeds:cancel', state) 51 | } 52 | } 53 | 54 | function adjustSeeding(state, emit) { 55 | return html` 56 |
57 |

seed this site ${state.newUrl.val === 75 ? '' : 'for'} ${state.newUrl.text}

58 | 59 |
60 | ` 61 | 62 | function writeVal(e) { 63 | val = e.target.value 64 | switch (val.toString()) { 65 | case '75': 66 | text = 'forever' 67 | break 68 | case '50': 69 | text = '1 month' 70 | break 71 | case '25': 72 | text = '1 week' 73 | break 74 | case '0': 75 | text = '1 day' 76 | break 77 | default: 78 | text = 'whyyy' 79 | } 80 | 81 | state.newUrl.val = val 82 | state.newUrl.text = text 83 | emit('feeds:adjustTime', state) 84 | } 85 | } 86 | 87 | function urlInput(state, emit) { 88 | if (state.open) { 89 | if (state.opened) { 90 | setTimeout(() => { // Had to trick this 91 | url.element.focus() 92 | }, 100) 93 | state.opened = false 94 | } 95 | 96 | return html` 97 |
98 | ${nanoevent(url.render(), 'keydown', keydown)} 99 | ${adjustSeeding(state, emit)} 100 |
101 | ` 102 | } 103 | return html` 104 |
105 | ${nanoevent(url.render(), 'keydown', keydown)} 106 |

some placeholder text

107 |
108 | ` 109 | 110 | function keydown(e) { 111 | if (e.keyCode === 13 || e.which === 13) { 112 | submit(e) 113 | } 114 | } 115 | } 116 | 117 | function submit(e) { 118 | e.preventDefault() 119 | emit('feeds:add', url.element.value) 120 | url.element.value = '' 121 | } 122 | 123 | function description(state) { 124 | var s = sum(state.stats) 125 | if (state.feeds.length === 0) { 126 | return html`

add a dat url to start peering it →

` 127 | } 128 | return html`

you are seeding ${state.feeds.length} ${plural('site', state.feeds.length)} to ${s} ${plural('peer', s)} :)

` 129 | 130 | function plural(word, value) { 131 | return word + (value === 1 ? '' : 's') 132 | } 133 | 134 | function sum(obj) { 135 | var sum = 0 136 | for (var el in obj) { 137 | if (obj.hasOwnProperty(el)) { 138 | if (obj[el].peers && obj[el].peers !== -1) { 139 | sum += parseFloat(obj[el].peers) 140 | } 141 | } 142 | } 143 | return sum 144 | } 145 | } 146 | 147 | function feed(fd) { 148 | var dotcolor = fd.paused ? 'yellow' : 'green' 149 | var default_peers = '0' 150 | var error = false 151 | 152 | if (state.stats[fd.url] && state.stats[fd.url].peers === -1) { 153 | dotcolor = 'red' 154 | default_peers = '' 155 | error = true 156 | } 157 | 158 | return html` 159 |
160 |
161 |
162 | ${fd.url} 163 |
164 |
165 |
${state.stats[fd.url] ? state.stats[fd.url].size : ''}
166 | ${!error ? html`${!fd.paused ? '=' : '+'}` : ''} 167 | 168 |
${(!fd.paused && state.stats[fd.url] && !error) ? state.stats[fd.url].peers : default_peers}
169 |
170 |
171 | ` 172 | 173 | function click(e) { 174 | e.preventDefault() 175 | emit('feeds:remove', fd.url) 176 | } 177 | 178 | function pause(e) { 179 | e.preventDefault() 180 | emit('feeds:pause', fd.url) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /frontend/styles/style.css: -------------------------------------------------------------------------------- 1 | /* @import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:300,400');*/ 2 | 3 | @import url('https://rsms.me/inter/inter-ui.css'); 4 | 5 | body{ 6 | font-size: 24px; 7 | font-family: -apple-system, BlinkMacSystemFont, "Inter UI", sans-serif !important; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | font-weight: 300; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin: 0px; 16 | padding: 2rem; 17 | /* background-color: #171717; 18 | color: #f1f1f1; */ 19 | } 20 | 21 | header{ 22 | width: 100%; 23 | display: flex; 24 | justify-content: space-between; 25 | width: 100%; 26 | /* position: sticky; 27 | top: 2rem; */ 28 | /* background-color: white; */ 29 | } 30 | 31 | main{ 32 | width: 100%; 33 | } 34 | 35 | a{ 36 | color: blue; 37 | text-decoration: none; 38 | border: none !important; 39 | word-wrap: break-word; 40 | padding: 0px !important; 41 | margin: 0px; 42 | } 43 | 44 | a:hover{ 45 | text-decoration: underline; 46 | } 47 | 48 | p{ 49 | margin: 0px; 50 | padding: 0px; 51 | } 52 | 53 | .border{ 54 | border: 1px solid rgba(0,0,0,.2); 55 | transition: border 0.2s ease; 56 | 57 | } 58 | 59 | .border:hover { 60 | border: 1px solid rgba(0,0,0,.4); 61 | } 62 | 63 | .add{ 64 | display: flex; 65 | justify-content: space-between; 66 | padding: 2rem 0; 67 | /* position: sticky; 68 | top: 100px; */ 69 | /* background-color: white; */ 70 | } 71 | 72 | .entry { 73 | display: none; 74 | } 75 | 76 | .info { 77 | width: 20%; 78 | display: flex; 79 | justify-content: flex-end; 80 | align-items: center; 81 | } 82 | 83 | .size { 84 | margin-right: 1rem; 85 | font-size: 16px; 86 | opacity: 0; 87 | } 88 | 89 | .toggle{ 90 | width: 12px; 91 | transform: rotate(90deg); 92 | margin-right: 1rem; 93 | color: rgba(0,0,0,.4); 94 | /* margin-top: 2px; */ 95 | opacity: 0; 96 | } 97 | 98 | .toggle:hover{ 99 | text-decoration: none; 100 | color: rgba(0,0,0,1); 101 | } 102 | 103 | .remove{ 104 | margin-right: 1rem; 105 | color: rgba(0,0,0,.4); 106 | margin-top: -2px; 107 | opacity: 0; 108 | color: salmon; 109 | } 110 | 111 | .remove:after{ 112 | content: '×'; 113 | } 114 | 115 | .remove:hover{ 116 | text-decoration: none; 117 | } 118 | 119 | .remove:hover:after{ 120 | color: rgba(0,0,0,1); 121 | } 122 | 123 | .urlInput { 124 | margin-bottom: 2rem; 125 | } 126 | 127 | .slider { 128 | display: flex; 129 | flex-direction: row; 130 | align-items: center; 131 | } 132 | 133 | .slider p{ 134 | margin-right: 1rem; 135 | white-space: nowrap; 136 | } 137 | 138 | .control{ 139 | padding-left: 2rem; 140 | } 141 | 142 | .pin{ 143 | margin-bottom: 2rem; 144 | padding: 1rem; 145 | display: flex; 146 | justify-content: space-between; 147 | align-items: center; 148 | overflow: hidden; 149 | } 150 | 151 | .pin:hover .toggle{ 152 | opacity: 1; 153 | } 154 | 155 | .pin:hover .remove{ 156 | opacity: 1; 157 | } 158 | 159 | .pin:hover .size { 160 | opacity: 1; 161 | } 162 | 163 | .pin:last-child{ 164 | margin-bottom: 0px; 165 | } 166 | 167 | .url{ 168 | /* margin-top: -2px; */ 169 | max-width: 60vw; 170 | overflow: hidden; 171 | white-space: nowrap; 172 | height: 1.1em; 173 | text-overflow: ellipsis; 174 | 175 | } 176 | 177 | .seed{ 178 | display: flex; 179 | justify-content: flex-start; 180 | align-items: center; 181 | } 182 | 183 | .seed:hover:after{ 184 | content: '↗'; 185 | color: rgba(0,0,0,.5); 186 | text-decoration: none; 187 | margin-left: .5rem; 188 | /* margin-top: 2px; */ 189 | } 190 | 191 | .dot{ 192 | width: 12px !important; 193 | height: 12px; 194 | border-radius: 100px; 195 | margin-right: 1rem; 196 | background-color: #171717; 197 | } 198 | 199 | .green{ 200 | background-color: #6FCF97; 201 | } 202 | 203 | .yellow{ 204 | background-color: #ffe949; 205 | } 206 | 207 | .red { 208 | background-color: salmon; 209 | } 210 | 211 | .d { 212 | width: 20px; 213 | height: 20px; 214 | border-radius: 100px; 215 | margin-left: 2rem; 216 | border: 3px solid #6FCF97; 217 | -webkit-animation: sk-scaleout 3.0s infinite ease-in-out; 218 | animation: sk-scaleout 3.0s infinite ease-in-out; 219 | } 220 | 221 | .green{ 222 | color: #6FCF97; 223 | } 224 | 225 | @-webkit-keyframes sk-scaleout { 226 | 0% { -webkit-transform: scale(0) } 227 | 100% { 228 | -webkit-transform: scale(1.2); 229 | opacity: 0; 230 | } 231 | } 232 | 233 | @keyframes sk-scaleout { 234 | 0% { 235 | -webkit-transform: scale(0); 236 | transform: scale(0); 237 | } 100% { 238 | -webkit-transform: scale(1.2); 239 | transform: scale(1.2); 240 | opacity: 0; 241 | } 242 | } 243 | 244 | 245 | .input { 246 | margin-bottom: 2rem; 247 | padding: 3rem; 248 | } 249 | 250 | .cancel{ 251 | color: #ccc; 252 | } 253 | 254 | .cencel:hover{ 255 | text-decoration: underline; 256 | } 257 | 258 | .save{ 259 | margin-left: 1rem; 260 | } 261 | 262 | .save:hover{ 263 | text-decoration: underline; 264 | } 265 | 266 | .urlInput{ 267 | color: #000; 268 | font-size: 24px; 269 | font-weight: 300; 270 | width: 100%; 271 | padding: 0px; 272 | padding-bottom: .5rem; 273 | outline: none; 274 | border: none; 275 | border-bottom: 1px solid #ccc; 276 | } 277 | 278 | 279 | input[type="range"] { 280 | -webkit-appearance: none; 281 | max-width: 100%; 282 | margin: 11.5px 0; 283 | margin-top: 5px; 284 | overflow: visible; 285 | flex-grow: 1; 286 | } 287 | 288 | input[type="range"]:focus { 289 | outline: none; 290 | } 291 | 292 | input[type="range"]::-webkit-slider-runnable-track { 293 | width: 100%; 294 | height: 1px; 295 | cursor: pointer; 296 | box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0px 0px 1px rgba(13, 13, 13, 0); 297 | background: #000000; 298 | border-radius: 0px; 299 | border: 0px solid #010101; 300 | } 301 | 302 | input[type="range"]::-webkit-slider-thumb { 303 | box-shadow: 0px 0px 0px rgba(0, 0, 0, 0), 0px 0px 0px rgba(13, 13, 13, 0); 304 | border: 1px solid #000000; 305 | height: 24px; 306 | width: 24px; 307 | border-radius: 50px; 308 | background: #ffffff; 309 | cursor: pointer; 310 | -webkit-appearance: none; 311 | margin-top: -11.5px; 312 | } 313 | 314 | input[type="range"]:focus::-webkit-slider-runnable-track { 315 | background: #171717; 316 | } 317 | 318 | /* input[type='range'] { 319 | box-sizing: border-box; 320 | border: 0px solid transparent; 321 | padding: 0px; 322 | margin: 0px; 323 | background: repeating-linear-gradient(90deg, #00000050, #00000050 1px, transparent 1px, transparent 25%) no-repeat 50% 50%; 324 | } */ 325 | --------------------------------------------------------------------------------