From 9a0f361e14a918804cc752f91726fb43041402ae Mon Sep 17 00:00:00 2001 From: Nate Goldman Date: Fri, 4 Mar 2016 12:42:33 -0800 Subject: [PATCH] separation of concerns --- index.js | 382 +----------------- main/config.js | 7 + main/index.js | 339 ++-------------- main/ipc.js | 68 ++++ main/menu.js | 260 ++++++++++++ main/windows.js | 47 +++ {main => renderer}/index.css | 0 {main => renderer}/index.html | 0 renderer/index.js | 327 +++++++++++++++ {main => renderer}/lib/capture-video-frame.js | 0 {main => renderer}/lib/torrent-poster.js | 0 {main => renderer}/views/app.js | 0 {main => renderer}/views/header.js | 0 {main => renderer}/views/player.js | 0 {main => renderer}/views/torrent-list.js | 0 15 files changed, 733 insertions(+), 697 deletions(-) create mode 100644 main/config.js create mode 100644 main/ipc.js create mode 100644 main/menu.js create mode 100644 main/windows.js rename {main => renderer}/index.css (100%) rename {main => renderer}/index.html (100%) create mode 100644 renderer/index.js rename {main => renderer}/lib/capture-video-frame.js (100%) rename {main => renderer}/lib/torrent-poster.js (100%) rename {main => renderer}/views/app.js (100%) rename {main => renderer}/views/header.js (100%) rename {main => renderer}/views/player.js (100%) rename {main => renderer}/views/torrent-list.js (100%) diff --git a/index.js b/index.js index 18a91203..70e5a365 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,4 @@ -var startTime = Date.now() - -require('debug/browser') - -var debug = require('debug')('index') -var electron = require('electron') -var path = require('path') - -var app = electron.app -var mainWindow, menu +require('./main') // report crashes // require('crash-reporter').start({ @@ -16,374 +7,3 @@ var mainWindow, menu // submitURL: 'https://webtorrent.io/crash-report', // autoSubmit: true // }) - -app.on('open-file', onOpen) -app.on('open-url', onOpen) - -app.on('ready', function () { - createMainWindow() - - menu = electron.Menu.buildFromTemplate(getMenuTemplate()) - electron.Menu.setApplicationMenu(menu) -}) - -app.on('activate', function () { - if (mainWindow) { - mainWindow.show() - } else { - createMainWindow() - } -}) - -app.on('window-all-closed', function () { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -var isQuitting = false -app.on('before-quit', function () { - isQuitting = true -}) - -electron.ipcMain.on('addTorrentFromPaste', function (e) { - addTorrentFromPaste() -}) - -electron.ipcMain.on('setBounds', function (e, bounds) { - setBounds(bounds) -}) - -electron.ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) { - setAspectRatio(aspectRatio, extraSize) -}) - -electron.ipcMain.on('setBadge', function (e, text) { - setBadge(text) -}) - -electron.ipcMain.on('setProgress', function (e, progress) { - setProgress(progress) -}) - -function createMainWindow () { - mainWindow = new electron.BrowserWindow({ - backgroundColor: '#282828', - darkTheme: true, - minWidth: 375, - minHeight: 158, - show: false, - title: 'WebTorrent', - titleBarStyle: 'hidden-inset', - width: 450, - height: 300 - }) - mainWindow.loadURL('file://' + path.join(__dirname, 'main', 'index.html')) - mainWindow.webContents.on('did-finish-load', function () { - setTimeout(function () { - debug('startup time: %sms', Date.now() - startTime) - mainWindow.show() - }, 50) - }) - mainWindow.on('enter-full-screen', onToggleFullScreen) - mainWindow.on('leave-full-screen', onToggleFullScreen) - mainWindow.on('close', function (e) { - if (process.platform === 'darwin' && !isQuitting) { - e.preventDefault() - mainWindow.hide() - } - }) - mainWindow.once('closed', function () { - mainWindow = null - }) -} - -function onOpen (e, torrentId) { - e.preventDefault() - mainWindow.send('addTorrent', torrentId) -} - -function addTorrentFromPaste () { - debug('addTorrentFromPaste') - var torrentIds = electron.clipboard.readText().split('\n') - torrentIds.forEach(function (torrentId) { - torrentId = torrentId.trim() - if (torrentId.length === 0) return - mainWindow.send('addTorrent', torrentId) - }) -} - -function setBounds (bounds) { - debug('setBounds %o', bounds) - if (mainWindow) { - mainWindow.setBounds(bounds, true) - } -} - -function setAspectRatio (aspectRatio, extraSize) { - debug('setAspectRatio %o %o', aspectRatio, extraSize) - if (mainWindow) { - mainWindow.setAspectRatio(aspectRatio, extraSize) - } -} - -// Display string in dock badging area (OS X) -function setBadge (text) { - debug('setBadge %s', text) - app.dock.setBadge(String(text)) -} - -// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. -function setProgress (progress) { - debug('setProgress %s', progress) - if (mainWindow) { - mainWindow.setProgressBar(progress) - } -} - -function toggleFullScreen () { - debug('toggleFullScreen') - if (mainWindow) { - mainWindow.setFullScreen(!mainWindow.isFullScreen()) - onToggleFullScreen() - } -} - -function onToggleFullScreen () { - getMenuItem('Full Screen').checked = mainWindow.isFullScreen() -} - -// Sets whether the window should always show on top of other windows -function toggleAlwaysOnTop () { - debug('toggleAlwaysOnTop %s') - if (mainWindow) { - mainWindow.setAlwaysOnTop(!mainWindow.isAlwaysOnTop()) - getMenuItem('Float on Top').checked = mainWindow.isAlwaysOnTop() - } -} - -function toggleDevTools () { - debug('toggleDevTools') - if (mainWindow) { - mainWindow.toggleDevTools() - } -} - -function reloadWindow () { - debug('reloadWindow') - if (mainWindow) { - startTime = Date.now() - mainWindow.webContents.reloadIgnoringCache() - } -} - -function getMenuTemplate () { - var template = [ - { - label: 'File', - submenu: [ - { - label: 'Create New Torrent...', - accelerator: 'CmdOrCtrl+N', - click: function () { - electron.dialog.showOpenDialog({ - title: 'Select a file or folder for the torrent file.', - properties: [ 'openFile', 'openDirectory', 'multiSelections' ] - }, function (filenames) { - if (!Array.isArray(filenames)) return - mainWindow.send('seed', filenames) - }) - } - }, - { - label: 'Open Torrent File...', - accelerator: 'CmdOrCtrl+O', - click: function () { - electron.dialog.showOpenDialog(mainWindow, { - title: 'Select a .torrent file to open.', - properties: [ 'openFile', 'multiSelections' ] - }, function (filenames) { - if (!Array.isArray(filenames)) return - filenames.forEach(function (filename) { - mainWindow.send('addTorrent', filename) - }) - }) - } - }, - { - label: 'Open Torrent Address...', - accelerator: 'CmdOrCtrl+U', - click: function () { electron.dialog.showMessageBox({ message: 'TODO', buttons: ['OK'] }) } - }, - { - type: 'separator' - }, - { - label: 'Close Window', - accelerator: 'CmdOrCtrl+W', - role: 'close' - } - ] - }, - { - label: 'Edit', - submenu: [ - { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - role: 'cut' - }, - { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - role: 'copy' - }, - { - label: 'Paste Torrent Address', - accelerator: 'CmdOrCtrl+V', - role: 'paste' - }, - { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - role: 'selectall' - } - ] - }, - { - label: 'View', - submenu: [ - { - label: 'Full Screen', - type: 'checkbox', - accelerator: (function () { - if (process.platform === 'darwin') return 'Ctrl+Command+F' - else return 'F11' - })(), - click: toggleFullScreen - }, - { - label: 'Float on Top', - type: 'checkbox', - click: toggleAlwaysOnTop - }, - { - type: 'separator' - }, - { - label: 'Reload', - accelerator: 'CmdOrCtrl+R', - click: reloadWindow - }, - { - label: 'Developer Tools', - accelerator: (function () { - if (process.platform === 'darwin') return 'Alt+Command+I' - else return 'Ctrl+Shift+I' - })(), - click: toggleDevTools - } - ] - }, - { - label: 'Window', - role: 'window', - submenu: [ - { - label: 'Minimize', - accelerator: 'CmdOrCtrl+M', - role: 'minimize' - } - ] - }, - { - label: 'Help', - role: 'help', - submenu: [ - { - label: 'Learn more about WebTorrent', - click: function () { electron.shell.openExternal('https://webtorrent.io') } - }, - { - label: 'Contribute on GitHub', - click: function () { electron.shell.openExternal('https://github.com/feross/webtorrent-app') } - }, - { - type: 'separator' - }, - { - label: 'Report an Issue...', - click: function () { electron.shell.openExternal('https://github.com/feross/webtorrent-app/issues') } - } - ] - } - ] - - if (process.platform === 'darwin') { - var name = app.getName() - template.unshift({ - label: name, - submenu: [ - { - label: 'About ' + name, - role: 'about' - }, - { - type: 'separator' - }, - { - label: 'Services', - role: 'services', - submenu: [] - }, - { - type: 'separator' - }, - { - label: 'Hide ' + name, - accelerator: 'Command+H', - role: 'hide' - }, - { - label: 'Hide Others', - accelerator: 'Command+Alt+H', - role: 'hideothers' - }, - { - label: 'Show All', - role: 'unhide' - }, - { - type: 'separator' - }, - { - label: 'Quit', - accelerator: 'Command+Q', - click: function () { app.quit() } - } - ] - }) - - // Window menu - template[4].submenu.push( - { - type: 'separator' - }, - { - label: 'Bring All to Front', - role: 'front' - } - ) - } - - return template -} - -function getMenuItem (label) { - for (var i = 0; i < menu.items.length; i++) { - var menuItem = menu.items[i].submenu.items.find(function (item) { - return item.label === label - }) - if (menuItem) return menuItem - } -} diff --git a/main/config.js b/main/config.js new file mode 100644 index 00000000..552906b7 --- /dev/null +++ b/main/config.js @@ -0,0 +1,7 @@ +var path = require('path') + +module.exports = { + APP_NAME: 'WebTorrent', + INDEX: 'file://' + path.resolve(__dirname, '..', 'renderer', 'index.html'), + startTime: Date.now() +} diff --git a/main/index.js b/main/index.js index 64b65b6b..43960b75 100644 --- a/main/index.js +++ b/main/index.js @@ -1,327 +1,34 @@ -/* global URL, Blob */ - -var airplay = require('airplay-js') -var chromecasts = require('chromecasts')() -var createTorrent = require('create-torrent') -var dragDrop = require('drag-drop') var electron = require('electron') -var networkAddress = require('network-address') -var path = require('path') -var throttle = require('throttleit') -var torrentPoster = require('./lib/torrent-poster') -var WebTorrent = require('webtorrent') +var menu = require('./menu') +var windows = require('./windows') +var ipc = require('./ipc') +var app = electron.app -var createElement = require('virtual-dom/create-element') -var diff = require('virtual-dom/diff') -var patch = require('virtual-dom/patch') +app.on('open-file', onOpen) +app.on('open-url', onOpen) -var App = require('./views/app') - -var HEADER_HEIGHT = 38 - -// Force use of webtorrent trackers on all torrents -global.WEBTORRENT_ANNOUNCE = createTorrent.announceList - .map(function (arr) { - return arr[0] - }) - .filter(function (url) { - return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0 - }) - -var state = global.state = { - server: null, /* local WebTorrent-to-HTTP server */ - view: { - url: '/', - dock: { - badge: 0, - progress: 0 - }, - devices: { - airplay: null, /* airplay client. finds and manages AppleTVs */ - chromecast: null /* chromecast client. finds and manages Chromecasts */ - }, - client: null, /* the WebTorrent client */ - torrentPlaying: null, /* the torrent we're streaming. see client.torrents */ - // history: [], /* track how we got to the current view. enables Back button */ - // historyIndex: 0, - isFocused: true, - mainWindowBounds: null, /* x y width height */ - title: 'WebTorrent' /* current window title */ - }, - video: { - isPaused: false, - currentTime: 0, /* seconds */ - duration: 1 /* seconds */ - } -} - -var client, currentVDom, rootElement, updateThrottled - -function init () { - client = global.client = new WebTorrent() - client.on('warning', onWarning) - client.on('error', onError) - state.view.client = client - - currentVDom = App(state, dispatch) - rootElement = createElement(currentVDom) - document.body.appendChild(rootElement) - - updateThrottled = throttle(update, 1000) - - dragDrop('body', onFiles) - - chromecasts.on('update', function (player) { - state.view.chromecast = player - update() - }) - - airplay.createBrowser().on('deviceOn', function (player) { - state.view.devices.airplay = player - }).start() - - document.addEventListener('paste', function () { - electron.ipcRenderer.send('addTorrentFromPaste') - }) - - window.addEventListener('focus', function () { - state.view.isFocused = true - if (state.view.dock.badge > 0) electron.ipcRenderer.send('setBadge', '') - state.view.dock.badge = 0 - }) - - window.addEventListener('blur', function () { - state.view.isFocused = false - }) -} -init() - -function update () { - var newVDom = App(state, dispatch) - var patches = diff(currentVDom, newVDom) - rootElement = patch(rootElement, patches) - currentVDom = newVDom - - updateDockIcon() -} - -setInterval(function () { - updateThrottled() -}, 1000) - -function updateDockIcon () { - var progress = state.view.client.progress - var activeTorrentsExist = state.view.client.torrents.some(function (torrent) { - return torrent.progress !== 1 - }) - // Hide progress bar when client has no torrents, or progress is 100% - if (!activeTorrentsExist || progress === 1) { - progress = -1 - } - if (progress !== state.view.dock.progress) { - state.view.dock.progress = progress - electron.ipcRenderer.send('setProgress', progress) - } -} - -function dispatch (action, ...args) { - console.log('dispatch: %s %o', action, args) - if (action === 'addTorrent') { - addTorrent(args[0] /* torrentId */) - } - if (action === 'seed') { - seed(args[0] /* files */) - } - if (action === 'openPlayer') { - openPlayer(args[0] /* torrent */) - } - if (action === 'deleteTorrent') { - deleteTorrent(args[0] /* torrent */) - } - if (action === 'openChromecast') { - openChromecast(args[0] /* torrent */) - } - if (action === 'openAirplay') { - openAirplay(args[0] /* torrent */) - } - if (action === 'setDimensions') { - setDimensions(args[0] /* dimensions */) - } - if (action === 'back') { - if (state.view.url === '/player') { - restoreBounds() - closeServer() - } - state.view.url = '/' - update() - } - if (action === 'playPause') { - state.video.isPaused = !state.video.isPaused - update() - } - if (action === 'playbackJump') { - state.video.jumpToTime = args[0] /* seconds */ - update() - } -} - -electron.ipcRenderer.on('addTorrent', function (e, torrentId) { - addTorrent(torrentId) +app.on('ready', function () { + electron.Menu.setApplicationMenu(menu.appMenu) + windows.createMainWindow(menu) }) -electron.ipcRenderer.on('seed', function (e, files) { - seed(files) +app.on('activate', function () { + if (windows.main) { + windows.main.show() + } else { + windows.createMainWindow(menu) + } }) -function onFiles (files) { - // .torrent file = start downloading the torrent - files.filter(isTorrentFile).forEach(function (torrentFile) { - dispatch('addTorrent', torrentFile) - }) - - // everything else = seed these files - dispatch('seed', files.filter(isNotTorrentFile)) -} - -function isTorrentFile (file) { - var extname = path.extname(file.name).toLowerCase() - return extname === '.torrent' -} - -function isNotTorrentFile (file) { - return !isTorrentFile(file) -} - -function addTorrent (torrentId) { - var torrent = client.add(torrentId) - addTorrentEvents(torrent) -} - -function seed (files) { - if (files.length === 0) return - var torrent = client.seed(files) - addTorrentEvents(torrent) -} - -function addTorrentEvents (torrent) { - torrent.on('infoHash', update) - torrent.on('done', function () { - if (!state.view.isFocused) { - state.view.dock.badge += 1 - electron.ipcRenderer.send('setBadge', state.view.dock.badge) - } - update() - }) - torrent.on('download', updateThrottled) - torrent.on('upload', updateThrottled) - torrent.on('ready', function () { - torrentReady(torrent) - }) - update() -} - -function torrentReady (torrent) { - torrentPoster(torrent, function (err, buf) { - if (err) return onWarning(err) - torrent.posterURL = URL.createObjectURL(new Blob([ buf ], { type: 'image/png' })) - update() - }) - update() -} - -function startServer (torrent, cb) { - // use largest file - state.view.torrentPlaying = torrent.files.reduce(function (a, b) { - return a.length > b.length ? a : b - }) - var index = torrent.files.indexOf(state.view.torrentPlaying) - - var server = torrent.createServer() - server.listen(0, function () { - var port = server.address().port - var urlSuffix = ':' + port + '/' + index - state.server = { - server: server, - localURL: 'http://localhost' + urlSuffix, - networkURL: 'http://' + networkAddress() + urlSuffix - } - cb() - }) -} - -function closeServer () { - state.server.server.destroy() - state.server = null -} - -function openPlayer (torrent) { - startServer(torrent, function () { - state.view.url = '/player' - update() - }) -} - -function deleteTorrent (torrent) { - torrent.destroy(update) -} - -function openChromecast (torrent) { - startServer(torrent, function () { - state.view.chromecast.play(state.server.networkURL, { title: 'WebTorrent — ' + torrent.name }) - state.view.chromecast.on('error', function (err) { - err.message = 'Chromecast: ' + err.message - onError(err) - }) - update() - }) -} - -function openAirplay (torrent) { - startServer(torrent, function () { - state.view.devices.airplay.play(state.server.networkURL, 0, function () {}) - // TODO: handle airplay errors - update() - }) -} - -function setDimensions (dimensions) { - state.view.mainWindowBounds = electron.remote.getCurrentWindow().getBounds() - - // Limit window size to screen size - var workAreaSize = electron.remote.screen.getPrimaryDisplay().workAreaSize - var aspectRatio = dimensions.width / dimensions.height - - var scaleFactor = Math.min( - Math.min(workAreaSize.width / dimensions.width, 1), - Math.min(workAreaSize.height / dimensions.height, 1) - ) - - var width = Math.floor(dimensions.width * scaleFactor) - var height = Math.floor(dimensions.height * scaleFactor) - - height += HEADER_HEIGHT - - // Center window on screen - var x = Math.floor((workAreaSize.width - width) / 2) - var y = Math.floor((workAreaSize.height - height) / 2) - - electron.ipcRenderer.send('setAspectRatio', aspectRatio, {width: 0, height: HEADER_HEIGHT}) - electron.ipcRenderer.send('setBounds', {x, y, width, height}) -} - -function restoreBounds () { - electron.ipcRenderer.send('setAspectRatio', 0) - if (state.view.mainWindowBounds) { - electron.ipcRenderer.send('setBounds', state.view.mainWindowBounds, true) +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') { + app.quit() } -} +}) -function onError (err) { - console.error(err.stack) - window.alert(err.message || err) - update() -} +ipc.init() -function onWarning (err) { - console.log('warning: %s', err.message) +function onOpen (e, torrentId) { + e.preventDefault() + windows.main.send('addTorrent', torrentId) } diff --git a/main/ipc.js b/main/ipc.js new file mode 100644 index 00000000..41485c5f --- /dev/null +++ b/main/ipc.js @@ -0,0 +1,68 @@ +var electron = require('electron') +var debug = require('debug')('webtorrent-app:ipcMain') +var ipcMain = electron.ipcMain +var windows = require('./windows') + +module.exports = { + init: init +} + +function init () { + ipcMain.on('addTorrentFromPaste', function (e) { + addTorrentFromPaste() + }) + + ipcMain.on('setBounds', function (e, bounds) { + setBounds(bounds) + }) + + ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) { + setAspectRatio(aspectRatio, extraSize) + }) + + ipcMain.on('setBadge', function (e, text) { + setBadge(text) + }) + + ipcMain.on('setProgress', function (e, progress) { + setProgress(progress) + }) +} + +function addTorrentFromPaste () { + debug('addTorrentFromPaste') + var torrentIds = electron.clipboard.readText().split('\n') + torrentIds.forEach(function (torrentId) { + torrentId = torrentId.trim() + if (torrentId.length === 0) return + windows.mainWindow.send('addTorrent', torrentId) + }) +} + +function setBounds (bounds) { + debug('setBounds %o', bounds) + if (windows.mainWindow) { + windows.mainWindow.setBounds(bounds, true) + } +} + +function setAspectRatio (aspectRatio, extraSize) { + debug('setAspectRatio %o %o', aspectRatio, extraSize) + if (windows.mainWindow) { + windows.mainWindow.setAspectRatio(aspectRatio, extraSize) + } +} + +// Display string in dock badging area (OS X) +function setBadge (text) { + debug('setBadge %s', text) + electron.app.dock.setBadge(String(text)) +} + +// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. +function setProgress (progress) { + debug('setProgress %s', progress) + if (windows.mainWindow) { + windows.mainWindow.setProgressBar(progress) + } +} diff --git a/main/menu.js b/main/menu.js new file mode 100644 index 00000000..9fcf41d9 --- /dev/null +++ b/main/menu.js @@ -0,0 +1,260 @@ +var electron = require('electron') +var debug = require('debug')('webtorrent-app:menu') +var windows = require('./windows') +var config = require('./config') + +function toggleFullScreen () { + debug('toggleFullScreen') + if (windows.main) { + windows.main.setFullScreen(!windows.main.isFullScreen()) + onToggleFullScreen() + } +} + +function onToggleFullScreen () { + getMenuItem('Full Screen').checked = windows.main.isFullScreen() +} + +// Sets whether the window should always show on top of other windows +function toggleAlwaysOnTop () { + debug('toggleAlwaysOnTop %s') + if (windows.main) { + windows.main.setAlwaysOnTop(!windows.main.isAlwaysOnTop()) + getMenuItem('Float on Top').checked = windows.main.isAlwaysOnTop() + } +} + +function toggleDevTools () { + debug('toggleDevTools') + if (windows.main) { + windows.main.toggleDevTools() + } +} + +function reloadWindow () { + debug('reloadWindow') + if (windows.main) { + config.startTime = Date.now() + windows.main.webContents.reloadIgnoringCache() + } +} + +function getMenuItem (label) { + for (var i = 0; i < appMenu.items.length; i++) { + var menuItem = appMenu.items[i].submenu.items.find(function (item) { + return item.label === label + }) + if (menuItem) return menuItem + } +} + +function getMenuTemplate () { + var template = [ + { + label: 'File', + submenu: [ + { + label: 'Create New Torrent...', + accelerator: 'CmdOrCtrl+N', + click: function () { + electron.dialog.showOpenDialog({ + title: 'Select a file or folder for the torrent file.', + properties: [ 'openFile', 'openDirectory', 'multiSelections' ] + }, function (filenames) { + if (!Array.isArray(filenames)) return + windows.main.send('seed', filenames) + }) + } + }, + { + label: 'Open Torrent File...', + accelerator: 'CmdOrCtrl+O', + click: function () { + electron.dialog.showOpenDialog(windows.main, { + title: 'Select a .torrent file to open.', + properties: [ 'openFile', 'multiSelections' ] + }, function (filenames) { + if (!Array.isArray(filenames)) return + filenames.forEach(function (filename) { + windows.main.send('addTorrent', filename) + }) + }) + } + }, + { + label: 'Open Torrent Address...', + accelerator: 'CmdOrCtrl+U', + click: function () { electron.dialog.showMessageBox({ message: 'TODO', buttons: ['OK'] }) } + }, + { + type: 'separator' + }, + { + label: 'Close Window', + accelerator: 'CmdOrCtrl+W', + role: 'close' + } + ] + }, + { + label: 'Edit', + submenu: [ + { + label: 'Cut', + accelerator: 'CmdOrCtrl+X', + role: 'cut' + }, + { + label: 'Copy', + accelerator: 'CmdOrCtrl+C', + role: 'copy' + }, + { + label: 'Paste Torrent Address', + accelerator: 'CmdOrCtrl+V', + role: 'paste' + }, + { + label: 'Select All', + accelerator: 'CmdOrCtrl+A', + role: 'selectall' + } + ] + }, + { + label: 'View', + submenu: [ + { + label: 'Full Screen', + type: 'checkbox', + accelerator: (function () { + if (process.platform === 'darwin') return 'Ctrl+Command+F' + else return 'F11' + })(), + click: toggleFullScreen + }, + { + label: 'Float on Top', + type: 'checkbox', + click: toggleAlwaysOnTop + }, + { + type: 'separator' + }, + { + label: 'Reload', + accelerator: 'CmdOrCtrl+R', + click: reloadWindow + }, + { + label: 'Developer Tools', + accelerator: (function () { + if (process.platform === 'darwin') return 'Alt+Command+I' + else return 'Ctrl+Shift+I' + })(), + click: toggleDevTools + } + ] + }, + { + label: 'Window', + role: 'window', + submenu: [ + { + label: 'Minimize', + accelerator: 'CmdOrCtrl+M', + role: 'minimize' + } + ] + }, + { + label: 'Help', + role: 'help', + submenu: [ + { + label: 'Learn more about WebTorrent', + click: function () { electron.shell.openExternal('https://webtorrent.io') } + }, + { + label: 'Contribute on GitHub', + click: function () { electron.shell.openExternal('https://github.com/feross/webtorrent-app') } + }, + { + type: 'separator' + }, + { + label: 'Report an Issue...', + click: function () { electron.shell.openExternal('https://github.com/feross/webtorrent-app/issues') } + } + ] + } + ] + + if (process.platform === 'darwin') { + var name = electron.app.getName() + template.unshift({ + label: name, + submenu: [ + { + label: 'About ' + name, + role: 'about' + }, + { + type: 'separator' + }, + { + label: 'Services', + role: 'services', + submenu: [] + }, + { + type: 'separator' + }, + { + label: 'Hide ' + name, + accelerator: 'Command+H', + role: 'hide' + }, + { + label: 'Hide Others', + accelerator: 'Command+Alt+H', + role: 'hideothers' + }, + { + label: 'Show All', + role: 'unhide' + }, + { + type: 'separator' + }, + { + label: 'Quit', + accelerator: 'Command+Q', + click: function () { electron.app.quit() } + } + ] + }) + + // Window menu + template[4].submenu.push( + { + type: 'separator' + }, + { + label: 'Bring All to Front', + role: 'front' + } + ) + } + + return template +} + +var appMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) + +var menu = { + appMenu: appMenu, + onToggleFullScreen: onToggleFullScreen +} + +module.exports = menu diff --git a/main/windows.js b/main/windows.js new file mode 100644 index 00000000..c0c932ca --- /dev/null +++ b/main/windows.js @@ -0,0 +1,47 @@ +var electron = require('electron') +var debug = require('debug')('webtorrent-app:windows') +var config = require('./config') + +var windows = { + main: null, + createMainWindow: createMainWindow +} +var isQuitting = false + +electron.app.on('before-quit', function () { + isQuitting = true +}) + +function createMainWindow (menu) { + windows.main = new electron.BrowserWindow({ + backgroundColor: '#282828', + darkTheme: true, + minWidth: 375, + minHeight: 158, + show: false, + title: config.APP_NAME, + titleBarStyle: 'hidden-inset', + width: 450, + height: 300 + }) + windows.main.loadURL(config.INDEX) + windows.main.webContents.on('did-finish-load', function () { + setTimeout(function () { + debug('startup time: %sms', Date.now() - config.startTime) + windows.main.show() + }, 50) + }) + windows.main.on('enter-full-screen', menu.onToggleFullScreen) + windows.main.on('leave-full-screen', menu.onToggleFullScreen) + windows.main.on('close', function (e) { + if (process.platform === 'darwin' && !isQuitting) { + e.preventDefault() + windows.main.hide() + } + }) + windows.main.once('closed', function () { + windows.main = null + }) +} + +module.exports = windows diff --git a/main/index.css b/renderer/index.css similarity index 100% rename from main/index.css rename to renderer/index.css diff --git a/main/index.html b/renderer/index.html similarity index 100% rename from main/index.html rename to renderer/index.html diff --git a/renderer/index.js b/renderer/index.js new file mode 100644 index 00000000..64b65b6b --- /dev/null +++ b/renderer/index.js @@ -0,0 +1,327 @@ +/* global URL, Blob */ + +var airplay = require('airplay-js') +var chromecasts = require('chromecasts')() +var createTorrent = require('create-torrent') +var dragDrop = require('drag-drop') +var electron = require('electron') +var networkAddress = require('network-address') +var path = require('path') +var throttle = require('throttleit') +var torrentPoster = require('./lib/torrent-poster') +var WebTorrent = require('webtorrent') + +var createElement = require('virtual-dom/create-element') +var diff = require('virtual-dom/diff') +var patch = require('virtual-dom/patch') + +var App = require('./views/app') + +var HEADER_HEIGHT = 38 + +// Force use of webtorrent trackers on all torrents +global.WEBTORRENT_ANNOUNCE = createTorrent.announceList + .map(function (arr) { + return arr[0] + }) + .filter(function (url) { + return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0 + }) + +var state = global.state = { + server: null, /* local WebTorrent-to-HTTP server */ + view: { + url: '/', + dock: { + badge: 0, + progress: 0 + }, + devices: { + airplay: null, /* airplay client. finds and manages AppleTVs */ + chromecast: null /* chromecast client. finds and manages Chromecasts */ + }, + client: null, /* the WebTorrent client */ + torrentPlaying: null, /* the torrent we're streaming. see client.torrents */ + // history: [], /* track how we got to the current view. enables Back button */ + // historyIndex: 0, + isFocused: true, + mainWindowBounds: null, /* x y width height */ + title: 'WebTorrent' /* current window title */ + }, + video: { + isPaused: false, + currentTime: 0, /* seconds */ + duration: 1 /* seconds */ + } +} + +var client, currentVDom, rootElement, updateThrottled + +function init () { + client = global.client = new WebTorrent() + client.on('warning', onWarning) + client.on('error', onError) + state.view.client = client + + currentVDom = App(state, dispatch) + rootElement = createElement(currentVDom) + document.body.appendChild(rootElement) + + updateThrottled = throttle(update, 1000) + + dragDrop('body', onFiles) + + chromecasts.on('update', function (player) { + state.view.chromecast = player + update() + }) + + airplay.createBrowser().on('deviceOn', function (player) { + state.view.devices.airplay = player + }).start() + + document.addEventListener('paste', function () { + electron.ipcRenderer.send('addTorrentFromPaste') + }) + + window.addEventListener('focus', function () { + state.view.isFocused = true + if (state.view.dock.badge > 0) electron.ipcRenderer.send('setBadge', '') + state.view.dock.badge = 0 + }) + + window.addEventListener('blur', function () { + state.view.isFocused = false + }) +} +init() + +function update () { + var newVDom = App(state, dispatch) + var patches = diff(currentVDom, newVDom) + rootElement = patch(rootElement, patches) + currentVDom = newVDom + + updateDockIcon() +} + +setInterval(function () { + updateThrottled() +}, 1000) + +function updateDockIcon () { + var progress = state.view.client.progress + var activeTorrentsExist = state.view.client.torrents.some(function (torrent) { + return torrent.progress !== 1 + }) + // Hide progress bar when client has no torrents, or progress is 100% + if (!activeTorrentsExist || progress === 1) { + progress = -1 + } + if (progress !== state.view.dock.progress) { + state.view.dock.progress = progress + electron.ipcRenderer.send('setProgress', progress) + } +} + +function dispatch (action, ...args) { + console.log('dispatch: %s %o', action, args) + if (action === 'addTorrent') { + addTorrent(args[0] /* torrentId */) + } + if (action === 'seed') { + seed(args[0] /* files */) + } + if (action === 'openPlayer') { + openPlayer(args[0] /* torrent */) + } + if (action === 'deleteTorrent') { + deleteTorrent(args[0] /* torrent */) + } + if (action === 'openChromecast') { + openChromecast(args[0] /* torrent */) + } + if (action === 'openAirplay') { + openAirplay(args[0] /* torrent */) + } + if (action === 'setDimensions') { + setDimensions(args[0] /* dimensions */) + } + if (action === 'back') { + if (state.view.url === '/player') { + restoreBounds() + closeServer() + } + state.view.url = '/' + update() + } + if (action === 'playPause') { + state.video.isPaused = !state.video.isPaused + update() + } + if (action === 'playbackJump') { + state.video.jumpToTime = args[0] /* seconds */ + update() + } +} + +electron.ipcRenderer.on('addTorrent', function (e, torrentId) { + addTorrent(torrentId) +}) + +electron.ipcRenderer.on('seed', function (e, files) { + seed(files) +}) + +function onFiles (files) { + // .torrent file = start downloading the torrent + files.filter(isTorrentFile).forEach(function (torrentFile) { + dispatch('addTorrent', torrentFile) + }) + + // everything else = seed these files + dispatch('seed', files.filter(isNotTorrentFile)) +} + +function isTorrentFile (file) { + var extname = path.extname(file.name).toLowerCase() + return extname === '.torrent' +} + +function isNotTorrentFile (file) { + return !isTorrentFile(file) +} + +function addTorrent (torrentId) { + var torrent = client.add(torrentId) + addTorrentEvents(torrent) +} + +function seed (files) { + if (files.length === 0) return + var torrent = client.seed(files) + addTorrentEvents(torrent) +} + +function addTorrentEvents (torrent) { + torrent.on('infoHash', update) + torrent.on('done', function () { + if (!state.view.isFocused) { + state.view.dock.badge += 1 + electron.ipcRenderer.send('setBadge', state.view.dock.badge) + } + update() + }) + torrent.on('download', updateThrottled) + torrent.on('upload', updateThrottled) + torrent.on('ready', function () { + torrentReady(torrent) + }) + update() +} + +function torrentReady (torrent) { + torrentPoster(torrent, function (err, buf) { + if (err) return onWarning(err) + torrent.posterURL = URL.createObjectURL(new Blob([ buf ], { type: 'image/png' })) + update() + }) + update() +} + +function startServer (torrent, cb) { + // use largest file + state.view.torrentPlaying = torrent.files.reduce(function (a, b) { + return a.length > b.length ? a : b + }) + var index = torrent.files.indexOf(state.view.torrentPlaying) + + var server = torrent.createServer() + server.listen(0, function () { + var port = server.address().port + var urlSuffix = ':' + port + '/' + index + state.server = { + server: server, + localURL: 'http://localhost' + urlSuffix, + networkURL: 'http://' + networkAddress() + urlSuffix + } + cb() + }) +} + +function closeServer () { + state.server.server.destroy() + state.server = null +} + +function openPlayer (torrent) { + startServer(torrent, function () { + state.view.url = '/player' + update() + }) +} + +function deleteTorrent (torrent) { + torrent.destroy(update) +} + +function openChromecast (torrent) { + startServer(torrent, function () { + state.view.chromecast.play(state.server.networkURL, { title: 'WebTorrent — ' + torrent.name }) + state.view.chromecast.on('error', function (err) { + err.message = 'Chromecast: ' + err.message + onError(err) + }) + update() + }) +} + +function openAirplay (torrent) { + startServer(torrent, function () { + state.view.devices.airplay.play(state.server.networkURL, 0, function () {}) + // TODO: handle airplay errors + update() + }) +} + +function setDimensions (dimensions) { + state.view.mainWindowBounds = electron.remote.getCurrentWindow().getBounds() + + // Limit window size to screen size + var workAreaSize = electron.remote.screen.getPrimaryDisplay().workAreaSize + var aspectRatio = dimensions.width / dimensions.height + + var scaleFactor = Math.min( + Math.min(workAreaSize.width / dimensions.width, 1), + Math.min(workAreaSize.height / dimensions.height, 1) + ) + + var width = Math.floor(dimensions.width * scaleFactor) + var height = Math.floor(dimensions.height * scaleFactor) + + height += HEADER_HEIGHT + + // Center window on screen + var x = Math.floor((workAreaSize.width - width) / 2) + var y = Math.floor((workAreaSize.height - height) / 2) + + electron.ipcRenderer.send('setAspectRatio', aspectRatio, {width: 0, height: HEADER_HEIGHT}) + electron.ipcRenderer.send('setBounds', {x, y, width, height}) +} + +function restoreBounds () { + electron.ipcRenderer.send('setAspectRatio', 0) + if (state.view.mainWindowBounds) { + electron.ipcRenderer.send('setBounds', state.view.mainWindowBounds, true) + } +} + +function onError (err) { + console.error(err.stack) + window.alert(err.message || err) + update() +} + +function onWarning (err) { + console.log('warning: %s', err.message) +} diff --git a/main/lib/capture-video-frame.js b/renderer/lib/capture-video-frame.js similarity index 100% rename from main/lib/capture-video-frame.js rename to renderer/lib/capture-video-frame.js diff --git a/main/lib/torrent-poster.js b/renderer/lib/torrent-poster.js similarity index 100% rename from main/lib/torrent-poster.js rename to renderer/lib/torrent-poster.js diff --git a/main/views/app.js b/renderer/views/app.js similarity index 100% rename from main/views/app.js rename to renderer/views/app.js diff --git a/main/views/header.js b/renderer/views/header.js similarity index 100% rename from main/views/header.js rename to renderer/views/header.js diff --git a/main/views/player.js b/renderer/views/player.js similarity index 100% rename from main/views/player.js rename to renderer/views/player.js diff --git a/main/views/torrent-list.js b/renderer/views/torrent-list.js similarity index 100% rename from main/views/torrent-list.js rename to renderer/views/torrent-list.js