diff --git a/app.js b/app.js index 7ef7536c..767ecd03 100644 --- a/app.js +++ b/app.js @@ -1,56 +1,34 @@ const persist = require('choo-persist') -const mount = require('choo/mount') const log = require('choo-log') const css = require('sheetify') -const xtend = require('xtend') const choo = require('choo') +const xtend = Object.assign -const params = require('./lib/param-router') +require('./lib/monkeypatch') css('dat-colors') css('tachyons') css('./public/css/base.css') css('./public/css/colors.css') -const opts = { - filter: (state) => { - state = xtend(state) - delete state.repos - return state +const app = choo() + +app.use(persist({ + filter: function (state) { + return xtend({}, state, { repos: {} }) } +})) + +if (process.env.NODE_ENV === 'development') { + app.use(log()) } -persist(opts, (p) => { - const app = choo() - app.use(onError('error:display')) - app.use(p) +app.use(require('./models/welcome')) +app.use(require('./models/drag-drop')) +app.use(require('./models/repos')) +app.use(require('./models/error')) - if (process.env.NODE_ENV === 'development') { - app.use(log()) - } +app.route('/', require('./pages/main')) + +app.mount('body div') - app.model(require('./models/main-view')()) - app.model(require('./models/window')()) - app.model(require('./models/repos')()) - app.model(require('./models/error')()) - - app.router([ - ['/', params({ - default: require('./pages/main') - })] - ]) - - mount('body div', app.start()) -}) - -function onError (action) { - return { - onError: function (err, state, createSend) { - var send = createSend('handleError') - send(action, err.message, function (err) { - // if we hit this point the error handler has failed and we should crash - if (err) throw err - }) - } - } -} diff --git a/elements/empty.js b/elements/empty.js new file mode 100644 index 00000000..c62323dd --- /dev/null +++ b/elements/empty.js @@ -0,0 +1,82 @@ +var html = require('choo/html') +var css = require('sheetify') + +var icon = require('../elements/icon') + +module.exports = EmptyState + +const skeleton = css` + :host { + position: relative; + .skeleton { + position: fixed; + top: 3.5rem; + left: 1.25rem; + width: 232px; + max-width: 100vw; + } + .dotted-lines { + position: absolute; + top: .25rem; + right: 5.5rem; + width: 17rem; + z-index: 3; + } + .create-new-dat, + .link { + position: absolute; + width: 15rem; + } + .create-new-dat { + top: 14.5rem; + right: 4rem; + svg { + display: inline-block; + width: 2rem; + height: 2rem; + } + } + .link { + top: 6rem; + right: 8.5rem; + color: red; + svg { + display: inline-block; + width: 2rem; + height: 2rem; + margin-bottom: -.75rem; + } + } + } +` + +function EmptyState () { + return html` +
+ +
+ + +
+ ${icon('create-new-dat', { class: 'color-green-disabled' })} +

Create New Dat

+

+ … or select one of your local +
+ datasets and start sharing it. +

+
+
+
+ ` +} diff --git a/elements/status.js b/elements/status.js index 27f1e6e2..8c908509 100644 --- a/elements/status.js +++ b/elements/status.js @@ -79,7 +79,7 @@ const progressSubline = css` } ` -module.exports = function (dat, stats, send) { +module.exports = function (dat, stats) { if (dat.owner && dat.importer) { return html`
Watching for updates…
` } diff --git a/elements/table.js b/elements/table.js index 7344dfbf..365fabb0 100644 --- a/elements/table.js +++ b/elements/table.js @@ -112,7 +112,7 @@ const table = css` module.exports = tableElement -function tableElement (dats, send) { +function tableElement (dats, emit) { return html`
@@ -128,7 +128,7 @@ function tableElement (dats, send) { ${dats.map(function (dat) { - return row(dat, send) + return row(dat, emit) })}
@@ -136,7 +136,7 @@ function tableElement (dats, send) { ` } -function row (dat, send) { +function row (dat, emit) { const stats = dat.stats && dat.stats.get() var peers = (dat.network) ? dat.network.connected @@ -153,7 +153,7 @@ function row (dat, send) { : 'stale' : 'paused' - const togglePause = () => send('repos:togglePause', dat) + const togglePause = () => emit('toggle pause', dat) const hexContent = { loading: button.icon('loading', { @@ -181,13 +181,13 @@ function row (dat, send) { var finderButton = button.icon('Open in Finder', { icon: icon('open-in-finder'), class: 'row-action', - onclick: () => send('repos:open', dat) + onclick: () => emit('open dat', dat) }) var linkButton = button.icon('Share Dat', { icon: icon('link'), class: 'row-action', - onclick: () => send('repos:share', dat) + onclick: () => emit('share dat', dat) }) var deleteButton = button.icon('Remove Dat', { @@ -203,7 +203,7 @@ function row (dat, send) { target = target.parentNode } assert.equal(typeof id, 'string', 'elements/table.deleteButton: id should be type string') - send('repos:remove', { key: id }) + emit('remove dat', { key: id }) } }) @@ -236,7 +236,7 @@ function row (dat, send) { - ${status(dat, stats, send)} + ${status(dat, stats)} ${stats.size} diff --git a/elements/welcome.js b/elements/welcome.js new file mode 100644 index 00000000..a9d96307 --- /dev/null +++ b/elements/welcome.js @@ -0,0 +1,36 @@ +var html = require('choo/html') +var css = require('sheetify') + +var button = require('./button') + +module.exports = WelcomeScreen + +const welcome = css` + :host { + height: 100vh; + background-color: var(--color-neutral); + color: var(--color-white); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + -webkit-app-region: drag; + } +` + +function WelcomeScreen (methods) { + const onexit = methods.onexit + const onLoad = methods.onload + + return html` +
+ +

+ Share data on the distributed web. +

+ ${button.green('Get Started', { onclick: onexit })} +
+ ` +} + diff --git a/index.js b/index.js index 4755088c..729f4497 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const window = require('electron-window') const Env = require('envobj') const path = require('path') const doctor = require('dat-doctor') -const { Writable } = require('stream') +const Writable = require('stream').Writable const autoUpdater = require('./lib/auto-updater') const colors = require('dat-colors') diff --git a/lib/dat-manager.js b/lib/dat-manager.js new file mode 100644 index 00000000..67bdb4f4 --- /dev/null +++ b/lib/dat-manager.js @@ -0,0 +1,145 @@ +const ipc = require('electron').ipcRenderer +const encoding = require('dat-encoding') +const assert = require('assert') +const mkdirp = require('mkdirp') +const waterfall = require('run-waterfall') + +module.exports = createManager + +// creates a wrapper for all dats. Handles stats, and updates choo's internal +// state whenever a mutation happens +function createManager ({ multidat, dbPaused, downloadsDir }, onupdate) { + assert.ok(multidat, 'models/repos: multidat should exist') + assert.ok(onupdate, 'models/repos: onupdate should exist') + + // add stats to all recreated dats + var dats = multidat.list() + dats.forEach(initDat) + onupdate(null, dats) + + return { + create: create, + close: close, + closeAll: closeAll, + pause: pause, + resume: resume, + togglePause: togglePause + } + + function create (dir, opts, cb) { + if (!cb) { + cb = opts + opts = {} + } + + assert.equal(typeof dir, 'string', 'dat-manager: dir should be a string') + assert.equal(typeof opts, 'object', 'dat-manager: opts should be a object') + assert.equal(typeof cb, 'function', 'dat-manager: cb should be a function') + + mkdirp(dir, function (err) { + if (err) return cb(err) + + opts = Object.assign(opts, { + watch: true, + resume: true, + ignoreHidden: true, + compareFileContent: true + }) + + multidat.create(dir, opts, function (err, dat) { + if (err) return cb(err) + initDat(dat) + update() + cb(null, dat) + }) + }) + } + + function close (key, cb) { + dbPaused.write(key, false, function (err) { + if (err) return cb(err) + multidat.close(key, function (err) { + if (err) return cb(err) + update() + cb() + }) + }) + } + + function closeAll (cb) { + var tasks = multidat.list().map(function (dat, i) { + return function () { + var done = arguments[arguments.length - 1] + dat.close(done) + } + }) + waterfall(tasks, cb) + } + + function pause (dat, cb) { + var key = encoding.toStr(dat.key) + dat.leaveNetwork() + dbPaused.write(key, true, cb) + } + + function resume (dat, cb) { + var key = encoding.toStr(dat.key) + dat.joinNetwork() + dbPaused.write(key, false, cb) + } + + function togglePause (dat, cb) { + var key = encoding.toStr(dat.key) + dbPaused.read(function (err, paused) { + if (err) return cb(err) + if (paused[key]) resume(dat, cb) + else pause(dat, cb) + }) + } + + function update () { + var dats = multidat.list().slice() + dats.forEach(function (dat) { + var stats = dat.stats && dat.stats.get() + dat.progress = (!stats) + ? 0 + : (stats.blocksTotal) + ? Math.min(1, stats.blocksProgress / stats.blocksTotal) + : 0 + }) + + var incomplete = dats.filter(function (dat) { + return dat.network && dat.progress < 1 + }) + var progress = incomplete.length + ? incomplete.reduce(function (acc, dat) { + return acc + dat.progress + }, 0) / incomplete.length + : 1 + if (progress === 1) progress = -1 // deactivate + + ipc.send('progress', progress) + onupdate(null, dats) + } + + function initDat (dat) { + const key = encoding.toStr(dat.key) + dbPaused.read((err, paused) => { + if (err) throw err + if (!paused[key]) { + dat.joinNetwork() + } + }) + + dat.metadata = {} + + const updates = multidat.readManifest(dat) + updates.on('manifest', function (manifest) { + dat.metadata.title = manifest.title + dat.metadata.author = manifest.author + update() + }) + + dat.on('update', update) + } +} diff --git a/lib/monkeypatch.js b/lib/monkeypatch.js new file mode 100644 index 00000000..75753d92 --- /dev/null +++ b/lib/monkeypatch.js @@ -0,0 +1,9 @@ +// required for electron integration tests +if (process.env.RUNNING_IN_SPECTRON) { + const dialog = require('electron').remote.dialog + const path = require('path') + + dialog.showOpenDialog = (opts, cb) => { + return [path.join(__dirname, '..', 'tests', 'fixtures')] + } +} diff --git a/lib/param-router.js b/lib/param-router.js deleted file mode 100644 index 6e0846b1..00000000 --- a/lib/param-router.js +++ /dev/null @@ -1,20 +0,0 @@ -var assert = require('assert') - -module.exports = router - -function router (routes) { - assert.equal(typeof routes, 'object', 'param-router: routes should be type object') - assert.equal(typeof routes.default, 'function', 'param-router: routes.default should be type function') - - return function (state, prev, send) { - var searchKeys = Object.keys(state.location.search) - var len = searchKeys.length - for (var i = 0; i < len; i++) { - var key = searchKeys[i] - var route = routes[key] - if (route) return route.call(routes, state, prev, send) - } - - return routes.default(state, prev, send) - } -} diff --git a/models/drag-drop.js b/models/drag-drop.js new file mode 100644 index 00000000..fbd094a3 --- /dev/null +++ b/models/drag-drop.js @@ -0,0 +1,24 @@ +const explain = require('explain-error') +const fs = require('fs') + +module.exports = dragDropModel + +function dragDropModel (state, bus) { + window.ondragover = function (e) { + e.preventDefault() + } + window.ondrop = function (e) { + e.preventDefault() + var dirname = e.dataTransfer && + e.dataTransfer.files && + e.dataTransfer.files[0] && + e.dataTransfer.files[0].path + if (!dirname) return + fs.stat(dirname, (err, stat) => { + if (err) return bus.emit('error', explain(err, 'models/window: fs.stat error on dirname')) + if (!stat.isDirectory()) return + bus.emit('create dat', dirname) + }) + } +} + diff --git a/models/error.js b/models/error.js index e056942f..770e350f 100644 --- a/models/error.js +++ b/models/error.js @@ -3,31 +3,15 @@ const xhr = require('xhr') const version = require('../package.json').version const Modal = require('../elements/modal') -module.exports = model +module.exports = errorModel -function model () { - return { - namespace: 'error', - state: { - message: null - }, - subscriptions: { - onUncaughtException: onUncaughtException - }, - effects: { - display: display, - quit: quit - } - } -} - -function display (state, error, send, done) { - const message = error.message || error - const modal = Modal.error()(message) - document.body.appendChild(modal) -} +function errorModel (state, bus) { + bus.on('error', function (err) { + const message = err.message || err + const modal = Modal.error()(message) + document.body.appendChild(modal) + }) -function onUncaughtException (send, done) { process.on('uncaughtException', function (err) { if (err._thrown) return @@ -48,8 +32,8 @@ function onUncaughtException (send, done) { if (err) console.error(err) }) - const modal = Modal.crash()(() => { - send('error:quit', done) + const modal = Modal.crash()(function () { + ipc.sendSync('quit') }) document.body.appendChild(modal) @@ -58,6 +42,3 @@ function onUncaughtException (send, done) { }) } -function quit (state, data, send, done) { - ipc.sendSync('quit') -} diff --git a/models/main-view.js b/models/main-view.js deleted file mode 100644 index 6c7be88e..00000000 --- a/models/main-view.js +++ /dev/null @@ -1,31 +0,0 @@ -var assert = require('assert') - -module.exports = createModel - -function createModel (cb) { - return { - namespace: 'mainView', - effects: { - loadWelcomeScreenPerhaps: loadWelcomeScreenPerhaps - }, - state: { - welcome: true - }, - reducers: { - toggleWelcomeScreen: toggleWelcomeScreen - } - } -} - -function loadWelcomeScreenPerhaps (state, action, send, done) { - send('repos:shareState', function (err, reposState) { - if (err) return done(err) - if (reposState.values.length) return done() - send('mainView:toggleWelcomeScreen', { toggle: true }, done) - }) -} - -function toggleWelcomeScreen (state, data, action) { - assert.equal(typeof data.toggle, 'boolean', 'models/main-view: toggle should be a boolean') - return { welcome: data.toggle } -} diff --git a/models/repos.js b/models/repos.js index 45683b58..bbbbdf99 100644 --- a/models/repos.js +++ b/models/repos.js @@ -10,318 +10,141 @@ const Multidat = require('multidat') const minimist = require('minimist') const Worker = require('dat-worker') const toilet = require('toiletdb') -const assert = require('assert') const mkdirp = require('mkdirp') +const assert = require('assert') +const xtend = require('xtend') const path = require('path') const Modal = require('../elements/modal') +const createManager = require('../lib/dat-manager') -if (process.env.RUNNING_IN_SPECTRON) { - dialog.showOpenDialog = (opts, cb) => { - return [path.join(__dirname, '..', 'tests', 'fixtures')] - } -} +var argv = minimist(remoteProcess.argv.slice(2)) +var downloadsDir = (argv.data) + ? argv.data + : path.join(app.getPath('downloads'), '/dat') -module.exports = createModel +module.exports = reposModel -function createModel () { - let manager = null - let dbPaused = null +function reposModel (state, bus) { + state.repos = xtend({ + downloadsDir: downloadsDir, + removalKey: null, + ready: false, + values: [] + }, state.repos) - const argv = minimist(remoteProcess.argv.slice(2)) - const downloadsDir = (argv.data) - ? argv.data - : path.join(app.getPath('downloads'), '/dat') + var manager = null - return { - namespace: 'repos', - state: { - downloadsDir: downloadsDir, - removalKey: null, - ready: false, - values: [] - }, - subscriptions: { - start: startMultidat, - onIpc: handleIpc - }, - reducers: { - ready: multidatReady, - update: updateDats - }, - effects: { - create: createDat, - remove: removeDat, - open: openDirectory, - share: shareDat, - clone: cloneDat, - shareState: shareState, - togglePause: togglePause - } + function onerror (err) { + if (err) bus.emit('error', err) } // boot multidat, create the ~/Downloads/dat directory - function startMultidat (send, done) { - const dbLocation = argv.db || path.join(process.env.HOME, '.dat-desktop') - const dbMultidriveFile = path.join(dbLocation, 'dats.json') - const dbPausedFile = path.join(dbLocation, 'paused.json') + var dbLocation = argv.db || path.join(process.env.HOME, '.dat-desktop') + var dbMultidriveFile = path.join(dbLocation, 'dats.json') + var dbPausedFile = path.join(dbLocation, 'paused.json') - const tasks = [ - function (next) { - mkdirp(dbLocation, next) - }, - function (_, next) { - mkdirp(downloadsDir, next) - }, - function (_, next) { - const dbMultidrive = toilet(dbMultidriveFile) - dbPaused = toilet(dbPausedFile) - Multidat(dbMultidrive, { - dat: Worker, - stdout: ConsoleStream(), - stderr: ConsoleStream() - }, next) - }, - function (multidat, next) { - send('repos:ready', function (err) { - if (err) return next(err) - next(null, multidat) - }) - }, - function (multidat, next) { - manager = createManager(multidat, function (err, dats) { - if (err) return done(err) - send('repos:update', dats, next) + var tasks = [ + function (next) { + mkdirp(dbLocation, next) + }, + function (_, next) { + mkdirp(downloadsDir, next) + }, + function (_, next) { + var dbMultidrive = toilet(dbMultidriveFile) + Multidat(dbMultidrive, { + dat: Worker, + stdout: ConsoleStream(), + stderr: ConsoleStream() + }, next) + }, + function (multidat, done) { + var dbPaused = toilet(dbPausedFile) + manager = createManager({ + multidat, + dbPaused + }, function (err, dats) { + if (err) return bus.emit('error', err) + state.repos.values = dats + state.repos.ready = true + bus.emit('render') + }) + window.addEventListener('beforeunload', onBeforeUnload) + function onBeforeUnload (ev) { + ev.returnValue = false + window.removeEventListener('beforeunload', onBeforeUnload) + manager.closeAll(function () { + app.quit() }) - window.addEventListener('beforeunload', onBeforeUnload) - function onBeforeUnload (ev) { - ev.returnValue = false - window.removeEventListener('beforeunload', onBeforeUnload) - manager.closeAll(function () { - app.quit() - }) - } - }, - function (_, next) { - // show the welcome screen if you start without any dats - send('mainView:loadWelcomeScreenPerhaps', done) } - ] - - waterfall(tasks, done) - } - - // share state with external model - function shareState (state, data, send, done) { - done(null, state) - } - - // signal multidat is ready to accept values - function multidatReady (state, data) { - return { ready: true } - } - - // update the values with then new values from the manager - function updateDats (state, data) { - return { values: data } - } + bus.emit('repos loaded') + done() + } + ] - // handle IPC events from the server - function handleIpc (send, done) { - ipc.on('link', (event, url) => { - const key = encoding.decode(url) - send('repos:clone', key, done) - }) - ipc.on('log', (ev, str) => console.log(str)) - ipc.send('ready') - } + waterfall(tasks, onerror) // open the dat archive in the native filesystem explorer - function openDirectory (state, data, send, done) { - assert.ok(data.path, 'repos-model.openDirectory: data.path should exist') - var pathname = 'file://' + path.resolve(data.path) - shell.openExternal(pathname, done) - } + bus.on('open dat', function (dat) { + var pathname = 'file://' + path.resolve(dat.path) + shell.openExternal(pathname, onerror) + }) // choose a directory and convert it to a dat archive - function createDat (state, data, send, done) { - var pathname = data + bus.on('create dat', function (pathname) { if (!pathname) { var files = dialog.showOpenDialog({ properties: ['openDirectory'] }) - if (!files || !files.length) return done() + if (!files || !files.length) return pathname = files[0] } - manager.create(pathname, done) - } + manager.create(pathname, onerror) + }) - function removeDat (state, data, send, done) { - const key = data.key - const modal = Modal.confirm()(function () { - dbPaused.write(key, false, function (err) { - if (err) return done(err) - manager.close(key, done) - }) - }) - document.body.appendChild(modal) - } - - // copy a dat share link to clipboard and open a modal - function shareDat (state, data, send, done) { - assert.ok(data.key, 'repos-model.shareDat: data.key should exist') - const encodedKey = encoding.encode(data.key) - const modal = Modal.link()(encodedKey) - document.body.appendChild(modal) - } - - function cloneDat (state, data, send, done) { - assert.ok(Buffer.isBuffer(data) || typeof data === 'string', 'repos-model.cloneDat: data should be a buffer or a string') + bus.on('clone dat', function (key) { + cloneDat(key) + }) + ipc.on('link', function (event, url) { + cloneDat(url) + }) + function cloneDat (_key) { try { - var key = encoding.decode(data).toString('hex') + var key = encoding.toStr(_key) } catch (e) { - return done(new Error("The value you entered doesn't appear to be a valid Dat link")) - } - - mkdirp(state.downloadsDir, function (err) { - if (err) return done(err) - var dir = path.join(state.downloadsDir, key) - - mkdirp(dir, function (err) { - if (err) return done(err) - - var opts = { key: key } - manager.create(dir, opts, done) - }) - }) - } - - function togglePause (state, data, send, done) { - const dat = data - const key = encoding.toStr(dat.key) - - dbPaused.read((err, paused) => { - if (err) return done(err) - if (paused[key]) resume() - else pause() - }) - - function resume () { - dat.joinNetwork() - dbPaused.write(key, false, done) + return onerror(new Error("The value you entered doesn't appear to be a valid Dat link")) } - function pause () { - dat.leaveNetwork() - dbPaused.write(key, true, done) - } + var dir = path.join(state.repos.downloadsDir, key) + var opts = { key: key } + manager.create(dir, opts, onerror) } - // creates a wrapper for all dats. Handles stats, and updates choo's internal - // state whenever a mutation happens - function createManager (multidat, onupdate) { - assert.ok(multidat, 'models/repos: multidat should exist') - assert.ok(onupdate, 'models/repos: onupdate should exist') - - // add stats to all recreated dats - var dats = multidat.list() - dats.forEach(initDat) - onupdate(null, dats) - - return { - create: create, - close: close, - closeAll: closeAll - } - - function create (dir, opts, cb) { - if (!cb) { - cb = opts - opts = {} - } - - assert.equal(typeof dir, 'string', 'models/repos: dat-manager: dir should be a string') - assert.equal(typeof opts, 'object', 'models/repos: dat-manager: opts should be a object') - assert.equal(typeof cb, 'function', 'models/repos: dat-manager: cb should be a function') - - opts = Object.assign(opts, { - watch: true, - resume: true, - ignoreHidden: true, - compareFileContent: true - }) - - multidat.create(dir, opts, function (err, dat) { - if (err) return cb(err) - initDat(dat) - update() - cb(null, dat) - }) - } - - function close (key, cb) { - multidat.close(key, function (err) { - if (err) return cb(err) - update() - cb() - }) - } - - function closeAll (cb) { - var tasks = multidat.list().map(function (dat, i) { - return function () { - var done = arguments[arguments.length - 1] - dat.close(done) - } - }) - waterfall(tasks, cb) - } - - function update () { - var dats = multidat.list().slice() - dats.forEach(function (dat) { - var stats = dat.stats && dat.stats.get() - dat.progress = (!stats) - ? 0 - : (stats.blocksTotal) - ? Math.min(1, stats.blocksProgress / stats.blocksTotal) - : 0 - }) - - var incomplete = dats.filter(function (dat) { - return dat.network && dat.progress < 1 - }) - var progress = incomplete.length - ? incomplete.reduce(function (acc, dat) { - return acc + dat.progress - }, 0) / incomplete.length - : 1 - if (progress === 1) progress = -1 // deactivate - - ipc.send('progress', progress) - onupdate(null, dats) - } - - function initDat (dat) { - const key = encoding.toStr(dat.key) - dbPaused.read((err, paused) => { - if (err) throw err - if (!paused[key]) { - dat.joinNetwork() - } - }) + // copy a dat share link to clipboard and open a modal + bus.on('share dat', function (dat) { + assert.ok(dat.key, 'repos-model.shareDat: data.key should exist') + const encodedKey = encoding.toStr(dat.key) + const modal = Modal.link()(encodedKey) + document.body.appendChild(modal) + }) - dat.metadata = {} + bus.on('toggle pause', function (dat) { + manager.togglePause(dat, onerror) + }) - const updates = multidat.readManifest(dat) - updates.on('manifest', function (manifest) { - dat.metadata.title = manifest.title - dat.metadata.author = manifest.author - update() + bus.on('remove dat', function (dat) { + const modal = Modal.confirm()(function () { + manager.close(dat.key, function (err) { + if (err) return onerror(err) + bus.emit('render') }) + }) + document.body.appendChild(modal) + }) - dat.on('update', update) - } - } + // handle IPC events from the server + ipc.on('log', (ev, str) => console.log(str)) + ipc.send('ready') } diff --git a/models/welcome.js b/models/welcome.js new file mode 100644 index 00000000..2a2c9e96 --- /dev/null +++ b/models/welcome.js @@ -0,0 +1,20 @@ +var xtend = Object.assign + +module.exports = welcomeModel + +function welcomeModel (state, bus) { + state.welcome = xtend({ + show: false + }, state.welcome) + + bus.on('repos loaded', function () { + if (state.repos.values.length) return + state.welcome.show = true + bus.emit('render') + }) + + bus.on('hide welcome screen', function () { + state.welcome.show = false + bus.emit('render') + }) +} diff --git a/models/window.js b/models/window.js deleted file mode 100644 index 5153d199..00000000 --- a/models/window.js +++ /dev/null @@ -1,30 +0,0 @@ -const explain = require('explain-error') -const fs = require('fs') - -module.exports = createModel - -function createModel () { - return { - namespace: 'window', - subscriptions: { - 'window-file-drop': dropFile - } - } -} - -function dropFile (send, done) { - window.ondragover = (e) => e.preventDefault() - window.ondrop = (e) => { - e.preventDefault() - const dirname = e.dataTransfer && - e.dataTransfer.files && - e.dataTransfer.files[0] && - e.dataTransfer.files[0].path - if (!dirname) return - fs.stat(dirname, (err, stat) => { - if (err) return done(explain(err, 'models/window: fs.stat error on dirname')) - if (!stat.isDirectory()) return - send('repos:create', dirname, done) - }) - } -} diff --git a/package.json b/package.json index 257db60a..ea3f4289 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "author": "Dat Team", "dependencies": { "base-elements": "^2.2.1", - "choo": "^4.0.0-6", - "choo-log": "^3.0.1", - "choo-persist": "^2.0.0", + "choo": "^5.0.1", + "choo-log": "^5.0.1", + "choo-persist": "^3.0.0", "console-stream": "juliangruber/console-stream#fix/console", "dat-colors": "^3.3.0", "dat-doctor": "^1.2.2", diff --git a/pages/main.js b/pages/main.js index dd6404ac..e9267642 100644 --- a/pages/main.js +++ b/pages/main.js @@ -1,86 +1,26 @@ 'use strict' const html = require('choo/html') -const css = require('sheetify') const Header = require('../elements/header') -const button = require('../elements/button') const sprite = require('../elements/sprite') const Table = require('../elements/table') -const icon = require('../elements/icon') - -const skeleton = css` - :host { - position: relative; - .skeleton { - position: fixed; - top: 3.5rem; - left: 1.25rem; - width: 232px; - max-width: 100vw; - } - .dotted-lines { - position: absolute; - top: .25rem; - right: 5.5rem; - width: 17rem; - z-index: 3; - } - .create-new-dat, - .link { - position: absolute; - width: 15rem; - } - .create-new-dat { - top: 14.5rem; - right: 4rem; - svg { - display: inline-block; - width: 2rem; - height: 2rem; - } - } - .link { - top: 6rem; - right: 8.5rem; - color: red; - svg { - display: inline-block; - width: 2rem; - height: 2rem; - margin-bottom: -.75rem; - } - } - } -` - -const welcome = css` - :host { - height: 100vh; - background-color: var(--color-neutral); - color: var(--color-white); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - -webkit-app-region: drag; - } -` +const Welcome = require('../elements/welcome') +const Empty = require('../elements/empty') module.exports = mainView // render the main view // (obj, obj, fn) -> html -function mainView (state, prev, send) { - const showWelcomeScreen = state.mainView.welcome +function mainView (state, emit) { + const showWelcomeScreen = state.welcome.show const dats = state.repos.values const isReady = state.repos.ready const header = Header({ isReady: isReady, - oncreate: () => send('repos:create'), - onimport: (link) => send('repos:clone', link) + oncreate: () => emit('create dat'), + onimport: (link) => emit('clone dat', link) }) document.title = 'Dat Desktop' @@ -90,10 +30,10 @@ function mainView (state, prev, send) { return html`
${sprite()} - ${WelcomeScreen({ + ${Welcome({ onexit: () => { window.removeEventListener('keydown', captureKeyEvent) - send('mainView:toggleWelcomeScreen', { toggle: false }) + emit('hide welcome screen') }, onload: () => { window.addEventListener('keydown', captureKeyEvent) @@ -108,7 +48,7 @@ function mainView (state, prev, send) {
${sprite()} ${header} - ${EmptyState()} + ${Empty()}
` } @@ -117,7 +57,7 @@ function mainView (state, prev, send) {
${sprite()} ${header} - ${Table(dats, send)} + ${Table(dats, emit)}
` @@ -125,53 +65,7 @@ function mainView (state, prev, send) { const key = e.code if (key === 'Enter' || key === 'Space') { window.removeEventListener('keydown', captureKeyEvent) - send('mainView:toggleWelcomeScreen', { toggle: false }) + emit('hide welcome screen') } } } - -function WelcomeScreen (methods) { - const onexit = methods.onexit - const onLoad = methods.onload - - return html` -
- -

- Share data on the distributed web. -

- ${button.green('Get Started', { onclick: onexit })} -
- ` -} - -function EmptyState () { - return html` -
- -
- - -
- ${icon('create-new-dat', { class: 'color-green-disabled' })} -

Create New Dat

-

- … or select one of your local -
- datasets and start sharing it. -

-
-
-
- ` -}