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