Commit 572440feaf959755763efb726087066a6f5b29db
quick dump
Matt McKegg committed on 10/27/2016, 3:07:41 AMFiles changed
.gitignore | added |
README.md | added |
api/index.js | added |
assets/base.html | added |
assets/img/emoji | added |
index.js | added |
lib/context-menu.js | added |
lib/h.js | added |
lib/make-single-instance.js | added |
lib/serve-blobs.js | added |
lib/ssb-server.js | added |
lib/window.js | added |
main-window.js | added |
package.json | added |
styles/base.mcss | added |
styles/index.js | added |
styles/main-window.mcss | added |
.gitignore | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +node_modules |
README.md | ||
---|---|---|
@@ -1,0 +1,26 @@ | ||
1 | +patchwork-next | |
2 | +=== | |
3 | + | |
4 | +Very early **work-in-progress** attempt at remaking [Patchwork](https://github.com/ssbc/patchwork) using [patchbay](https://github.com/dominictarr/patchbay) and UX/ideas from [ferment](https://github.com/mmckegg/ferment). | |
5 | + | |
6 | +The goal is to make a standalone, easy to install, "social" view into the ssb world. | |
7 | + | |
8 | +## Install and run | |
9 | + | |
10 | +```shell | |
11 | +$ git clone https://github.com/mmckegg/patchwork-next | |
12 | +$ cd patchwork-next | |
13 | +$ npm install | |
14 | +$ npm start | |
15 | +``` | |
16 | + | |
17 | +## TODO | |
18 | + | |
19 | +- [ ] Preserve scroll on back button | |
20 | +- [ ] Main navigation buttons | |
21 | +- [ ] Easy navigation sidebar | |
22 | +- [ ] Contacts sidebar | |
23 | +- [ ] Lots of styling | |
24 | +- [ ] Compressed feed (the algorithm :wink:) | |
25 | + - [ ] Thread previews in feed [figure out how to do this streaming] | |
26 | +- [ ] Add more todos! |
api/index.js | ||
---|---|---|
@@ -1,0 +1,124 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var ssbKeys = require('ssb-keys') | |
3 | +var ref = require('ssb-ref') | |
4 | + | |
5 | +function Hash (onHash) { | |
6 | + var buffers = [] | |
7 | + return pull.through(function (data) { | |
8 | + buffers.push('string' === typeof data | |
9 | + ? new Buffer(data, 'utf8') | |
10 | + : data | |
11 | + ) | |
12 | + }, function (err) { | |
13 | + if(err && !onHash) throw err | |
14 | + var b = buffers.length > 1 ? Buffer.concat(buffers) : buffers[0] | |
15 | + var h = '&'+ssbKeys.hash(b) | |
16 | + onHash && onHash(err, h) | |
17 | + }) | |
18 | +} | |
19 | +//uncomment this to use from browser... | |
20 | +//also depends on having ssb-ws installed. | |
21 | +//var createClient = require('ssb-lite') | |
22 | + | |
23 | +var createFeed = require('ssb-feed') | |
24 | +var cache = CACHE = {} | |
25 | + | |
26 | +module.exports = function (sbot, opts) { | |
27 | + var connection_status = [] | |
28 | + var keys = opts.keys | |
29 | + | |
30 | + var internal = { | |
31 | + getLatest: function (id, cb) { | |
32 | + sbot.getLatest(id, cb) | |
33 | + }, | |
34 | + add: function (msg, cb) { | |
35 | + sbot.add(msg, cb) | |
36 | + } | |
37 | + } | |
38 | + | |
39 | + var feed = createFeed(internal, keys, {remote: true}) | |
40 | + | |
41 | + setImmediate((x) => { | |
42 | + connection_status.forEach(fn => fn()) | |
43 | + }) | |
44 | + | |
45 | + return { | |
46 | + connection_status: connection_status, | |
47 | + sbot_blobs_add: function (cb) { | |
48 | + return pull( | |
49 | + Hash(function (err, id) { | |
50 | + if(err) return cb(err) | |
51 | + //completely UGLY hack to tell when the blob has been sucessfully written... | |
52 | + var start = Date.now(), n = 5 | |
53 | + ;(function next () { | |
54 | + setTimeout(function () { | |
55 | + sbot.blobs.has(id, function (err, has) { | |
56 | + if(has) return cb(null, id) | |
57 | + if(n--) next() | |
58 | + else cb(new Error('write failed')) | |
59 | + }) | |
60 | + }, Date.now() - start) | |
61 | + })() | |
62 | + }), | |
63 | + sbot.blobs.add() | |
64 | + ) | |
65 | + }, | |
66 | + sbot_links: function (query) { | |
67 | + return sbot.links(query) | |
68 | + }, | |
69 | + sbot_links2: function (query) { | |
70 | + return sbot.links2.read(query) | |
71 | + }, | |
72 | + sbot_query: function (query) { | |
73 | + return sbot.query.read(query) | |
74 | + }, | |
75 | + sbot_log: function (opts) { | |
76 | + return pull( | |
77 | + sbot.createLogStream(opts), | |
78 | + pull.through(function (e) { | |
79 | + CACHE[e.key] = CACHE[e.key] || e.value | |
80 | + }) | |
81 | + ) | |
82 | + }, | |
83 | + sbot_user_feed: function (opts) { | |
84 | + return sbot.createUserStream(opts) | |
85 | + }, | |
86 | + sbot_get: function (key, cb) { | |
87 | + if(CACHE[key]) cb(null, CACHE[key]) | |
88 | + else sbot.get(key, function (err, value) { | |
89 | + if(err) return cb(err) | |
90 | + cb(null, CACHE[key] = value) | |
91 | + }) | |
92 | + }, | |
93 | + sbot_gossip_peers: function (cb) { | |
94 | + sbot.gossip.peers(cb) | |
95 | + }, | |
96 | + //liteclient won't have permissions for this | |
97 | + sbot_gossip_connect: function (opts, cb) { | |
98 | + sbot.gossip.connect(opts, cb) | |
99 | + }, | |
100 | + sbot_publish: function (content, cb) { | |
101 | + if(content.recps) | |
102 | + content = ssbKeys.box(content, content.recps.map(function (e) { | |
103 | + return ref.isFeed(e) ? e : e.link | |
104 | + })) | |
105 | + else if(content.mentions) | |
106 | + content.mentions.forEach(function (mention) { | |
107 | + if(ref.isBlob(mention.link)) { | |
108 | + sbot.blobs.push(mention.link, function (err) { | |
109 | + if(err) console.error(err) | |
110 | + }) | |
111 | + } | |
112 | + }) | |
113 | + | |
114 | + feed.add(content, function (err, msg) { | |
115 | + if(err) console.error(err) | |
116 | + else if(!cb) console.log(msg) | |
117 | + cb && cb(err, msg) | |
118 | + }) | |
119 | + }, | |
120 | + sbot_whoami: function (cb) { | |
121 | + sbot.whoami(cb) | |
122 | + } | |
123 | + } | |
124 | +} |
assets/img/emoji | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +../../node_modules/emoji-named-characters/pngs |
index.js | ||
---|---|---|
@@ -1,0 +1,100 @@ | ||
1 | +process.on('uncaughtException', function (err) { | |
2 | + console.log(err) | |
3 | + process.exit() | |
4 | +}) | |
5 | + | |
6 | +var electron = require('electron') | |
7 | +var openWindow = require('./lib/window') | |
8 | +var createSbot = require('./lib/ssb-server') | |
9 | +var serveBlobs = require('./lib/serve-blobs') | |
10 | +var makeSingleInstance = require('./lib/make-single-instance') | |
11 | +var pull = require('pull-stream') | |
12 | +var pullFile = require('pull-file') | |
13 | +var Path = require('path') | |
14 | +var fs = require('fs') | |
15 | +var defaultMenu = require('electron-default-menu') | |
16 | +var Menu = electron.Menu | |
17 | +var dataUriToBuffer = require('data-uri-to-buffer') | |
18 | +var extend = require('xtend') | |
19 | +var ssbKeys = require('ssb-keys') | |
20 | + | |
21 | +var windows = { | |
22 | + dialogs: new Set() | |
23 | +} | |
24 | + | |
25 | +var context = null | |
26 | +if (process.argv.includes('--use-global-ssb') || process.argv.includes('-g')) { | |
27 | + context = setupContext('ssb', { | |
28 | + server: false | |
29 | + }) | |
30 | +} else { | |
31 | + makeSingleInstance(windows, openMainWindow) | |
32 | + context = setupContext('ssb') | |
33 | +} | |
34 | + | |
35 | +electron.ipcMain.on('add-blob', (ev, id, path, cb) => { | |
36 | + pull( | |
37 | + path.startsWith('data:') ? pull.values([dataUriToBuffer(path)]) : pullFile(path), | |
38 | + context.sbot.blobs.add((err, hash) => { | |
39 | + if (err) return ev.sender.send('response', id, err) | |
40 | + ev.sender.send('response', id, null, hash) | |
41 | + }) | |
42 | + ) | |
43 | +}) | |
44 | + | |
45 | +electron.app.on('ready', function () { | |
46 | + Menu.setApplicationMenu(Menu.buildFromTemplate(defaultMenu(electron.app, electron.shell))) | |
47 | + openMainWindow() | |
48 | +}) | |
49 | + | |
50 | +electron.app.on('activate', function (e) { | |
51 | + openMainWindow() | |
52 | +}) | |
53 | + | |
54 | +function openMainWindow () { | |
55 | + if (!windows.main) { | |
56 | + windows.main = openWindow(context, Path.join(__dirname, 'main-window.js'), { | |
57 | + minWidth: 800, | |
58 | + width: 1024, | |
59 | + height: 768, | |
60 | + titleBarStyle: 'hidden-inset', | |
61 | + title: 'Ferment', | |
62 | + show: true, | |
63 | + backgroundColor: '#444', | |
64 | + webPreferences: { | |
65 | + experimentalFeatures: true | |
66 | + }, | |
67 | + icon: './ferment-logo.png' | |
68 | + }) | |
69 | + windows.main.setSheetOffset(40) | |
70 | + windows.main.on('closed', function () { | |
71 | + windows.main = null | |
72 | + }) | |
73 | + } | |
74 | +} | |
75 | + | |
76 | +function setupContext (appName, opts) { | |
77 | + var ssbConfig = require('ssb-config/inject')(appName, extend({ | |
78 | + port: 8008, | |
79 | + blobsPort: 7777 | |
80 | + }, opts)) | |
81 | + | |
82 | + if (opts && opts.server === false) { | |
83 | + return { | |
84 | + config: ssbConfig | |
85 | + } | |
86 | + } else { | |
87 | + ssbConfig.keys = ssbKeys.loadOrCreateSync(Path.join(ssbConfig.path, 'secret')) | |
88 | + var context = { | |
89 | + sbot: createSbot(ssbConfig), | |
90 | + config: ssbConfig | |
91 | + } | |
92 | + ssbConfig.manifest = context.sbot.getManifest() | |
93 | + serveBlobs(context) | |
94 | + fs.writeFileSync(Path.join(ssbConfig.path, 'manifest.json'), JSON.stringify(ssbConfig.manifest)) | |
95 | + console.log(`Address: ${context.sbot.getAddress()}`) | |
96 | + return context | |
97 | + } | |
98 | + | |
99 | + return ssbConfig | |
100 | +} |
lib/context-menu.js | ||
---|---|---|
@@ -1,0 +1,44 @@ | ||
1 | +var electron = require('electron') | |
2 | +var Menu = electron.remote.Menu | |
3 | +var MenuItem = electron.remote.MenuItem | |
4 | +var BrowserWindow = electron.remote.BrowserWindow | |
5 | + | |
6 | +window.addEventListener('contextmenu', function (e) { | |
7 | + module.exports(null, null, e) | |
8 | +}, false) | |
9 | + | |
10 | +module.exports = function (context, item, ev) { | |
11 | + ev.preventDefault() | |
12 | + ev.stopPropagation() | |
13 | + var menu = new Menu() | |
14 | + menu.append(new MenuItem({ | |
15 | + label: 'Reload', | |
16 | + click: function (item, focusedWindow) { | |
17 | + if (focusedWindow) { | |
18 | + focusedWindow.reload() | |
19 | + } | |
20 | + } | |
21 | + })) | |
22 | + menu.append(new MenuItem({ | |
23 | + type: 'separator' | |
24 | + })) | |
25 | + menu.append(new MenuItem({ | |
26 | + label: 'Inspect Element', | |
27 | + click: function () { | |
28 | + var x = ev.clientX | |
29 | + var y = ev.clientY | |
30 | + BrowserWindow.getFocusedWindow().inspectElement(x, y) | |
31 | + } | |
32 | + })) | |
33 | + | |
34 | + if (item && item.id) { | |
35 | + menu.append(new MenuItem({ | |
36 | + label: 'Copy SSB ID', | |
37 | + click: function () { | |
38 | + electron.clipboard.writeText(item.id) | |
39 | + } | |
40 | + })) | |
41 | + } | |
42 | + | |
43 | + menu.popup(electron.remote.getCurrentWindow()) | |
44 | +} |
lib/h.js | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +module.exports = require('micro-css/h')(require('@mmckegg/mutant/html-element')) |
lib/make-single-instance.js | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +var electron = require('electron') | |
2 | +module.exports = function (windows, openMainWindow) { | |
3 | + if (electron.app.makeSingleInstance((commandLine, workingDirectory) => { | |
4 | + if (windows.main) { | |
5 | + if (windows.main.isMinimized()) windows.main.restore() | |
6 | + windows.main.focus() | |
7 | + } else { | |
8 | + openMainWindow() | |
9 | + } | |
10 | + })) { | |
11 | + electron.app.quit() | |
12 | + } | |
13 | +} |
lib/serve-blobs.js | ||
---|---|---|
@@ -1,0 +1,60 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var cat = require('pull-cat') | |
3 | +var toPull = require('stream-to-pull-stream') | |
4 | +var ident = require('pull-identify-filetype') | |
5 | +var mime = require('mime-types') | |
6 | +var URL = require('url') | |
7 | +var http = require('http') | |
8 | + | |
9 | +module.exports = function (context, cb) { | |
10 | + return http.createServer(ServeBlobs(context.sbot)).listen(context.config.blobsPort, cb) | |
11 | +} | |
12 | + | |
13 | +function ServeBlobs (sbot) { | |
14 | + return function (req, res, next) { | |
15 | + var parsed = URL.parse(req.url, true) | |
16 | + var hash = decodeURIComponent(parsed.pathname.slice(1)) | |
17 | + sbot.blobs.want(hash, function (_, has) { | |
18 | + if (!has) return respond(res, 404, 'File not found') | |
19 | + // optional name override | |
20 | + if (parsed.query.name) { | |
21 | + res.setHeader('Content-Disposition', 'inline; filename=' + encodeURIComponent(parsed.query.name)) | |
22 | + } | |
23 | + | |
24 | + // serve | |
25 | + res.setHeader('Content-Security-Policy', BlobCSP()) | |
26 | + respondSource(res, sbot.blobs.get(hash), false) | |
27 | + }) | |
28 | + } | |
29 | +} | |
30 | + | |
31 | +function respondSource (res, source, wrap) { | |
32 | + if (wrap) { | |
33 | + res.writeHead(200, {'Content-Type': 'text/html'}) | |
34 | + pull( | |
35 | + cat([ | |
36 | + pull.once('<html><body><script>'), | |
37 | + source, | |
38 | + pull.once('</script></body></html>') | |
39 | + ]), | |
40 | + toPull.sink(res) | |
41 | + ) | |
42 | + } else { | |
43 | + pull( | |
44 | + source, | |
45 | + ident(function (type) { | |
46 | + if (type) res.writeHead(200, {'Content-Type': mime.lookup(type)}) | |
47 | + }), | |
48 | + toPull.sink(res) | |
49 | + ) | |
50 | + } | |
51 | +} | |
52 | + | |
53 | +function respond (res, status, message) { | |
54 | + res.writeHead(status) | |
55 | + res.end(message) | |
56 | +} | |
57 | + | |
58 | +function BlobCSP () { | |
59 | + return 'default-src none; sandbox' | |
60 | +} |
lib/ssb-server.js | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +module.exports = require('scuttlebot') | |
2 | + .use(require('scuttlebot/plugins/master')) | |
3 | + .use(require('scuttlebot/plugins/gossip')) | |
4 | + .use(require('scuttlebot/plugins/friends')) | |
5 | + .use(require('scuttlebot/plugins/replicate')) | |
6 | + .use(require('ssb-blobs')) | |
7 | + .use(require('scuttlebot/plugins/invite')) | |
8 | + .use(require('scuttlebot/plugins/block')) | |
9 | + .use(require('scuttlebot/plugins/local')) | |
10 | + .use(require('scuttlebot/plugins/logging')) | |
11 | + .use(require('scuttlebot/plugins/private')) | |
12 | + .use(require('ssb-links')) | |
13 | + .use(require('ssb-query')) |
lib/window.js | ||
---|---|---|
@@ -1,0 +1,92 @@ | ||
1 | +var Path = require('path') | |
2 | +var electron = require('electron') | |
3 | +var extend = require('xtend/mutable') | |
4 | + | |
5 | +module.exports = function Window (context, path, opts) { | |
6 | + var window = new electron.BrowserWindow(extend({ | |
7 | + show: false | |
8 | + }, opts)) | |
9 | + | |
10 | + window.setMenu(null) | |
11 | + electron.ipcMain.on('ready-to-show', handleReadyToShow) | |
12 | + | |
13 | + window.webContents.on('dom-ready', function () { | |
14 | + window.webContents.executeJavaScript(` | |
15 | + var electron = require('electron') | |
16 | + var rootView = require(${JSON.stringify(path)}) | |
17 | + var insertCss = require('insert-css') | |
18 | + var h = require('../lib/h') | |
19 | + var createClient = require('ssb-client') | |
20 | + | |
21 | + require('../lib/context-menu') | |
22 | + insertCss(require('../styles')) | |
23 | + electron.webFrame.setZoomLevelLimits(1, 1) | |
24 | + | |
25 | + var config = ${JSON.stringify(context.config)} | |
26 | + var data = ${JSON.stringify(opts.data)} | |
27 | + var title = ${JSON.stringify(opts.title || 'Patchwork')} | |
28 | + | |
29 | + document.documentElement.querySelector('head').appendChild( | |
30 | + h('title', title) | |
31 | + ) | |
32 | + | |
33 | + var shouldShow = ${opts.show !== false} | |
34 | + var shouldConnect = ${opts.connect !== false} | |
35 | + | |
36 | + if (shouldConnect) { | |
37 | + createClient(config.keys, config, (err, client) => { | |
38 | + if (err) { | |
39 | + electron.remote.getGlobal('console').log(err) | |
40 | + throw err | |
41 | + } else { | |
42 | + render(client) | |
43 | + } | |
44 | + }) | |
45 | + } else { | |
46 | + render() | |
47 | + } | |
48 | + | |
49 | + function render (client) { | |
50 | + try { | |
51 | + document.documentElement.replaceChild(h('body', [ | |
52 | + rootView(client, config, data) | |
53 | + ]), document.body) | |
54 | + } catch (ex) { | |
55 | + electron.ipcRenderer.send('ready-to-show') | |
56 | + throw ex | |
57 | + } | |
58 | + shouldShow && electron.ipcRenderer.send('ready-to-show') | |
59 | + } | |
60 | + `) | |
61 | + }) | |
62 | + | |
63 | + // setTimeout(function () { | |
64 | + // window.show() | |
65 | + // }, 3000) | |
66 | + | |
67 | + window.webContents.on('will-navigate', function (e, url) { | |
68 | + e.preventDefault() | |
69 | + electron.shell.openExternal(url) | |
70 | + }) | |
71 | + | |
72 | + window.webContents.on('new-window', function (e, url) { | |
73 | + e.preventDefault() | |
74 | + electron.shell.openExternal(url) | |
75 | + }) | |
76 | + | |
77 | + window.on('closed', function () { | |
78 | + electron.ipcMain.removeListener('ready-to-show', handleReadyToShow) | |
79 | + }) | |
80 | + | |
81 | + window.loadURL('file://' + Path.join(__dirname, '..', 'assets', 'base.html')) | |
82 | + return window | |
83 | + | |
84 | + // scoped | |
85 | + | |
86 | + function handleReadyToShow (ev) { | |
87 | + if (ev.sender === window) { | |
88 | + window.show() | |
89 | + electron.ipcMain.removeListener('ready-to-show', handleReadyToShow) | |
90 | + } | |
91 | + } | |
92 | +} |
main-window.js | ||
---|---|---|
@@ -1,0 +1,116 @@ | ||
1 | +var combine = require('depject') | |
2 | +var Modules = require('patchbay/modules') | |
3 | +var SbotApi = require('./api') | |
4 | +var extend = require('xtend') | |
5 | +var h = require('./lib/h') | |
6 | +var plugs = require('patchbay/plugs') | |
7 | +var Value = require('@mmckegg/mutant/value') | |
8 | +var when = require('@mmckegg/mutant/when') | |
9 | +var computed = require('@mmckegg/mutant/computed') | |
10 | + | |
11 | +module.exports = function (ssbClient, config) { | |
12 | + var api = SbotApi(ssbClient, config) | |
13 | + var modules = combine(extend(Modules, { | |
14 | + 'sbot-api.js': api, | |
15 | + 'blob-url.js': { | |
16 | + blob_url: function (link) { | |
17 | + var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.blobsPort}` | |
18 | + if (typeof link.link === 'string') { | |
19 | + link = link.link | |
20 | + } | |
21 | + return `${prefix}/${encodeURIComponent(link)}` | |
22 | + } | |
23 | + } | |
24 | + })) | |
25 | + | |
26 | + var screenView = plugs.first(modules.plugs.screen_view) | |
27 | + var forwardHistory = [] | |
28 | + var backHistory = [] | |
29 | + var views = { | |
30 | + '/public': screenView('/public') | |
31 | + } | |
32 | + var canGoForward = Value(false) | |
33 | + var canGoBack = Value(false) | |
34 | + var currentView = Value(['/public']) | |
35 | + var rootElement = computed(currentView, (data) => { | |
36 | + if (Array.isArray(data)) { | |
37 | + return views[data[0]] | |
38 | + } | |
39 | + }) | |
40 | + | |
41 | + window.onhashchange = function (ev) { | |
42 | + setView(window.location.hash.substring(1)) | |
43 | + } | |
44 | + | |
45 | + var mainElement = h('div.main', [ | |
46 | + rootElement | |
47 | + ]) | |
48 | + | |
49 | + return h('MainWindow', { | |
50 | + classList: [ '-' + process.platform ] | |
51 | + }, [ | |
52 | + h('div.top', [ | |
53 | + h('span.history', [ | |
54 | + h('a', { | |
55 | + 'ev-click': goBack, | |
56 | + classList: [ when(canGoBack, '-active') ] | |
57 | + }, '<'), | |
58 | + h('a', { | |
59 | + 'ev-click': goForward, | |
60 | + classList: [ when(canGoForward, '-active') ] | |
61 | + }, '>') | |
62 | + ]), | |
63 | + h('span.appTitle', ['Patchwork']) | |
64 | + ]), | |
65 | + mainElement | |
66 | + ]) | |
67 | + | |
68 | + // scoped | |
69 | + | |
70 | + function goBack () { | |
71 | + if (backHistory.length) { | |
72 | + canGoForward.set(true) | |
73 | + forwardHistory.push(currentView()) | |
74 | + currentView.set(backHistory.pop()) | |
75 | + canGoBack.set(backHistory.length > 0) | |
76 | + } | |
77 | + } | |
78 | + | |
79 | + function goForward () { | |
80 | + if (forwardHistory.length) { | |
81 | + backHistory.push(currentView()) | |
82 | + currentView.set(forwardHistory.pop()) | |
83 | + canGoForward.set(forwardHistory.length > 0) | |
84 | + canGoBack.set(true) | |
85 | + } | |
86 | + } | |
87 | + | |
88 | + function setView (view, ...args) { | |
89 | + var newView = [view, ...args] | |
90 | + if (!isSame(newView, currentView())) { | |
91 | + if (!views[view]) { | |
92 | + views[view] = screenView(view, ...args) | |
93 | + } | |
94 | + | |
95 | + canGoForward.set(false) | |
96 | + canGoBack.set(true) | |
97 | + forwardHistory.length = 0 | |
98 | + backHistory.push(currentView()) | |
99 | + currentView.set(newView) | |
100 | + currentView().scrollTop = 0 | |
101 | + } | |
102 | + } | |
103 | +} | |
104 | + | |
105 | +function isSame (a, b) { | |
106 | + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { | |
107 | + for (var i = 0; i < a.length; i++) { | |
108 | + if (a[i] !== b[i]) { | |
109 | + return false | |
110 | + } | |
111 | + } | |
112 | + return true | |
113 | + } else if (a === b) { | |
114 | + return true | |
115 | + } | |
116 | +} |
package.json | ||
---|---|---|
@@ -1,0 +1,33 @@ | ||
1 | +{ | |
2 | + "name": "patchwork-next", | |
3 | + "version": "0.0.0", | |
4 | + "description": "", | |
5 | + "main": "index.js", | |
6 | + "scripts": { | |
7 | + "test": "echo \"Error: no test specified\" && exit 1", | |
8 | + "start": "electron index.js", | |
9 | + "postinstall": "npm run rebuild", | |
10 | + "rebuild": "npm rebuild --runtime=electron --target=1.4.3 --abi=50 --disturl=https://atom.io/download/atom-shell" | |
11 | + }, | |
12 | + "author": "", | |
13 | + "license": "ISC", | |
14 | + "dependencies": { | |
15 | + "@mmckegg/mutant": "^3.6.1", | |
16 | + "data-uri-to-buffer": "0.0.4", | |
17 | + "electron": "^1.4.4", | |
18 | + "electron-default-menu": "^1.0.0", | |
19 | + "insert-css": "^1.0.0", | |
20 | + "micro-css": "^0.6.2", | |
21 | + "patchbay": "^3.5.0", | |
22 | + "pull-file": "^1.0.0", | |
23 | + "pull-identify-filetype": "^1.1.0", | |
24 | + "pull-stream": "^3.4.5", | |
25 | + "scuttlebot": "^9.2.0", | |
26 | + "ssb-blobs": "^0.1.7", | |
27 | + "ssb-keys": "^7.0.0", | |
28 | + "ssb-links": "^2.0.0", | |
29 | + "ssb-query": "^0.1.1", | |
30 | + "ssb-ref": "^2.6.2", | |
31 | + "ssb-ws": "^0.6.2" | |
32 | + } | |
33 | +} |
styles/base.mcss | ||
---|---|---|
@@ -1,0 +1,101 @@ | ||
1 | +html, body { | |
2 | + background: #ccc | |
3 | + margin: 0 | |
4 | + font-family: caption, sans-serif | |
5 | + overflow: hidden | |
6 | + height: 100% | |
7 | + font-size: 12px | |
8 | + -webkit-user-select: none | |
9 | + color: #2b2b2b | |
10 | +} | |
11 | + | |
12 | +body { | |
13 | + display: flex | |
14 | + flex-direction: column | |
15 | + line-height: 1.2 | |
16 | +} | |
17 | + | |
18 | +h1 { | |
19 | + color: #C0C0C0 | |
20 | + font-size: 200% | |
21 | + margin: 4px 0 | |
22 | + font-weight: normal | |
23 | + text-shadow: 0px 0px 2px black | |
24 | + flex: 1 | |
25 | +} | |
26 | + | |
27 | +select { | |
28 | + font-size: 80% | |
29 | + display: inline-block | |
30 | + padding: 2px 4px | |
31 | + height: 18px | |
32 | + border: 1px solid #A9A9A9 | |
33 | + background: #666 svg(dropArrow) no-repeat right | |
34 | + -webkit-appearance: none | |
35 | + color: #FFF | |
36 | + padding-right: 12px | |
37 | + border-radius: 0 | |
38 | + | |
39 | + :hover { | |
40 | + background-image: svg(dropArrow -active) | |
41 | + } | |
42 | + | |
43 | + @svg dropArrow { | |
44 | + width: 12px | |
45 | + height: 6px | |
46 | + content: "<path d='M2,0 L10,0 L6,6 Z' />" | |
47 | + | |
48 | + path { | |
49 | + fill: #888 | |
50 | + } | |
51 | + | |
52 | + -active { | |
53 | + path { | |
54 | + fill: #DDD | |
55 | + } | |
56 | + } | |
57 | + } | |
58 | +} | |
59 | + | |
60 | +input { | |
61 | + [type='text'] { | |
62 | + font-size: 80% | |
63 | + display: inline-block | |
64 | + padding: 2px 4px | |
65 | + height: 18px | |
66 | + border: 1px solid #A9A9A9 | |
67 | + background: #333 | |
68 | + color: #FFF | |
69 | + padding-right: 12px | |
70 | + border-radius: 0 | |
71 | + } | |
72 | +} | |
73 | + | |
74 | +::-webkit-file-upload-button { | |
75 | + font-family: inherit | |
76 | +} | |
77 | + | |
78 | +a { | |
79 | + color: #ebf7b1 | |
80 | + text-decoration: none | |
81 | + | |
82 | + code { | |
83 | + color: #8EC1FC | |
84 | + } | |
85 | + | |
86 | + :hover { | |
87 | + text-decoration: underline | |
88 | + } | |
89 | +} | |
90 | + | |
91 | +* + h1 { | |
92 | + margin-top: 16px | |
93 | +} | |
94 | + | |
95 | +* { | |
96 | + box-sizing:border-box | |
97 | +} | |
98 | + | |
99 | +input, textarea, keygen, select, button { | |
100 | + font-family: '.SFNSText-Regular', sans-serif | |
101 | +} |
styles/index.js | ||
---|---|---|
@@ -1,0 +1,19 @@ | ||
1 | +var fs = require('fs') | |
2 | +var path = require('path') | |
3 | +var compile = require('micro-css') | |
4 | +var result = '' | |
5 | +var additional = '' | |
6 | + | |
7 | +fs.readdirSync(__dirname).forEach(function (file) { | |
8 | + if (/\.mcss$/i.test(file)) { | |
9 | + result += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n' | |
10 | + } | |
11 | + | |
12 | + if (/\.css$/i.test(file)) { | |
13 | + additional += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n' | |
14 | + } | |
15 | +}) | |
16 | + | |
17 | +additional += fs.readFileSync(require.resolve('patchbay/style.css'), 'utf8') | |
18 | + | |
19 | +module.exports = compile(result) + additional |
styles/main-window.mcss | ||
---|---|---|
@@ -1,0 +1,158 @@ | ||
1 | +MainWindow { | |
2 | + height: 100% | |
3 | + display: flex | |
4 | + flex-direction: column | |
5 | + | |
6 | + -darwin { | |
7 | + div.top { | |
8 | + padding-left: 70px | |
9 | + } | |
10 | + div.top { | |
11 | + span.appTitle { | |
12 | + margin-right: 40px; | |
13 | + } | |
14 | + } | |
15 | + } | |
16 | + | |
17 | + -win32 { | |
18 | + div.top { | |
19 | + padding-right: 70px | |
20 | + } | |
21 | + } | |
22 | + | |
23 | + div.top { | |
24 | + -webkit-app-region: drag; | |
25 | + display: flex; | |
26 | + background: #fff; | |
27 | + padding: 10px; | |
28 | + border-bottom: 2px solid #e2e2e2; | |
29 | + box-shadow: 0 0 3px #7f7f7f; | |
30 | + position: relative; | |
31 | + | |
32 | + span { | |
33 | + a { | |
34 | + padding: 2px 8px; | |
35 | + border: 2px solid #bbbbbb; | |
36 | + border-radius: 4px; | |
37 | + background: #a7a6a6; | |
38 | + color: #000000; | |
39 | + font-size: 120%; | |
40 | + cursor: pointer; | |
41 | + margin-left: 5px; | |
42 | + | |
43 | + :hover { | |
44 | + text-decoration: none | |
45 | + color: black | |
46 | + border-color: #888 | |
47 | + } | |
48 | + | |
49 | + -selected { | |
50 | + border-color: #444 | |
51 | + background: #CCC | |
52 | + color: black | |
53 | + } | |
54 | + | |
55 | + -add { | |
56 | + border-color: #498849 | |
57 | + background-color: #255D24 | |
58 | + text-shadow: 1px 1px 1px #000 | |
59 | + color: white | |
60 | + | |
61 | + :active { | |
62 | + background-color: #1F331F !important | |
63 | + } | |
64 | + | |
65 | + :hover { | |
66 | + background-color: #356D34 | |
67 | + border-color: #4CB54C | |
68 | + } | |
69 | + } | |
70 | + } | |
71 | + } | |
72 | + | |
73 | + span.history { | |
74 | + a { | |
75 | + opacity: 0.3 | |
76 | + | |
77 | + -active { | |
78 | + opacity: 1 | |
79 | + } | |
80 | + } | |
81 | + | |
82 | + a + a { | |
83 | + margin-left: 0 | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + span.appTitle { | |
88 | + flex: 1; | |
89 | + text-align: center; | |
90 | + font-size: 130%; | |
91 | + color: #757575; | |
92 | + letter-spacing: 0.1em; | |
93 | + font-weight: bold; | |
94 | + font-weight: normal; | |
95 | + } | |
96 | + } | |
97 | + | |
98 | + div.info { | |
99 | + a.message { | |
100 | + display: block | |
101 | + padding: 10px | |
102 | + background: #314427 | |
103 | + transition: color 0.2s, background-color 0.2s | |
104 | + | |
105 | + :hover { | |
106 | + text-decoration: none | |
107 | + color: white | |
108 | + background: #315427 | |
109 | + } | |
110 | + } | |
111 | + box-shadow: 0 0 3px black | |
112 | + overflow: hidden | |
113 | + animation: 0.5s slide-in | |
114 | + position: relative | |
115 | + } | |
116 | + | |
117 | + div.main { | |
118 | + flex: 1 | |
119 | + overflow: auto | |
120 | + display: flex | |
121 | + | |
122 | + div { | |
123 | + flex: 1 | |
124 | + } | |
125 | + } | |
126 | + | |
127 | + div.bottom { | |
128 | + position: relative | |
129 | + box-shadow: 0 0 3px #222 | |
130 | + background: #222 | |
131 | + align-items: center | |
132 | + display: flex | |
133 | + padding: 5px | |
134 | + | |
135 | + audio { | |
136 | + color: #EEE | |
137 | + | |
138 | + ::-webkit-media-controls-panel { | |
139 | + background: transparent | |
140 | + } | |
141 | + | |
142 | + ::-webkit-media-controls-current-time-display { | |
143 | + color: inherit | |
144 | + } | |
145 | + | |
146 | + width: 100% | |
147 | + } | |
148 | + } | |
149 | +} | |
150 | + | |
151 | +@keyframes slide-in { | |
152 | + 0% { | |
153 | + max-height: 0 | |
154 | + } | |
155 | + 100% { | |
156 | + max-height: 100px | |
157 | + } | |
158 | +} |
Built with git-ssb-web