From 1a01d7ed9247171ea46b4520b239c94dadd955ec Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Mon, 22 Aug 2016 17:53:29 -0700 Subject: [PATCH] Use Material UI; improve Preferences Page New principles for our UI: - Components should use inline styles whenever possible - Let's shrink the size of main.css to < 100 lines over time so it just contains typography and basic styles - Always require just the individual component that is needed from Material UI so that the whole library doesn't get loaded (important for startup perf) --- bin/check-deps.js | 9 +- package.json | 3 +- src/renderer/main.js | 5 +- src/renderer/views/Button.js | 40 ---- src/renderer/views/PathSelector.js | 75 ++++--- src/renderer/views/PreferencesPage.js | 263 +++++++++++++------------ src/renderer/views/TextInput.js | 34 ---- src/renderer/views/app.js | 23 ++- static/css/components/Button.css | 76 ------- static/css/components/PathSelector.css | 17 -- static/css/components/TextInput.css | 18 -- static/css/pages/PreferencesPage.css | 17 -- static/{css => }/main.css | 22 ++- static/main.html | 15 +- 14 files changed, 234 insertions(+), 383 deletions(-) delete mode 100644 src/renderer/views/Button.js delete mode 100644 src/renderer/views/TextInput.js delete mode 100644 static/css/components/Button.css delete mode 100644 static/css/components/PathSelector.css delete mode 100644 static/css/components/TextInput.css delete mode 100644 static/css/pages/PreferencesPage.css rename static/{css => }/main.css (97%) diff --git a/bin/check-deps.js b/bin/check-deps.js index f4a2dafc..1423f73c 100755 --- a/bin/check-deps.js +++ b/bin/check-deps.js @@ -46,11 +46,14 @@ var BUILT_IN_ELECTRON_MODULES = [ 'electron' ] var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES) var EXECUTABLE_DEPS = [ - 'gh-release', - 'standard', 'babel-cli', 'babel-plugin-syntax-jsx', - 'babel-plugin-transform-react-jsx' + 'babel-plugin-transform-es2015-destructuring', + 'babel-plugin-transform-object-rest-spread', + 'babel-plugin-transform-react-jsx', + 'gh-release', + 'nodemon', + 'standard' ] main() diff --git a/package.json b/package.json index 1e99e687..f9448c9a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "application-config": "^1.0.0", "bitfield": "^1.0.2", "chromecasts": "^1.8.0", - "classnames": "^2.2.5", "create-torrent": "^3.24.5", "deep-equal": "^1.0.1", "dlnacasts": "^0.1.0", @@ -29,12 +28,14 @@ "iso-639-1": "^1.2.1", "languagedetect": "^1.1.1", "location-history": "^1.0.0", + "material-ui": "^0.15.4", "musicmetadata": "^2.0.2", "network-address": "^1.1.0", "parse-torrent": "^5.7.3", "prettier-bytes": "^1.0.1", "react": "^15.2.1", "react-dom": "^15.2.1", + "react-tap-event-plugin": "^1.0.0", "run-parallel": "^1.1.6", "semver": "^5.1.0", "simple-concat": "^1.0.0", diff --git a/src/renderer/main.js b/src/renderer/main.js index 54e27385..5659c382 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -5,9 +5,12 @@ crashReporter.init() const dragDrop = require('drag-drop') const electron = require('electron') +const fs = require('fs') const React = require('react') const ReactDOM = require('react-dom') -const fs = require('fs') + +// Required by Material UI -- adds `onTouchTap` event +require('react-tap-event-plugin')() const config = require('../config') const App = require('./views/app') diff --git a/src/renderer/views/Button.js b/src/renderer/views/Button.js deleted file mode 100644 index a8aa09d3..00000000 --- a/src/renderer/views/Button.js +++ /dev/null @@ -1,40 +0,0 @@ -const c = require('classnames') -const React = require('react') - -class Button extends React.Component { - static get propTypes () { - return { - className: React.PropTypes.string, - onClick: React.PropTypes.func, - theme: React.PropTypes.oneOf(['light', 'dark']), - type: React.PropTypes.oneOf(['default', 'flat', 'raised']) - } - } - - static get defaultProps () { - return { - theme: 'light', - type: 'default' - } - } - - render () { - const { theme, type, className, ...other } = this.props - return ( - - ) - } -} - -module.exports = Button diff --git a/src/renderer/views/PathSelector.js b/src/renderer/views/PathSelector.js index d5013a97..5069eed1 100644 --- a/src/renderer/views/PathSelector.js +++ b/src/renderer/views/PathSelector.js @@ -1,38 +1,33 @@ -const c = require('classnames') const electron = require('electron') const React = require('react') const remote = electron.remote -const Button = require('./Button') -const TextInput = require('./TextInput') +const RaisedButton = require('material-ui/RaisedButton').default +const TextField = require('material-ui/TextField').default class PathSelector extends React.Component { static get propTypes () { return { className: React.PropTypes.string, - defaultValue: React.PropTypes.string.isRequired, dialog: React.PropTypes.object, - label: React.PropTypes.string.isRequired, - onChange: React.PropTypes.func + displayValue: React.PropTypes.string, + id: React.PropTypes.string, + onChange: React.PropTypes.func, + title: React.PropTypes.string.isRequired, + value: React.PropTypes.string.isRequired } } constructor (props) { super(props) - - this.state = { - value: props.defaultValue - } - this.handleClick = this.handleClick.bind(this) } handleClick () { var opts = Object.assign({ - defaultPath: this.state.value, - properties: [ 'openFile', 'openDirectory' ], - title: this.props.label + defaultPath: this.props.value, + properties: [ 'openFile', 'openDirectory' ] }, this.props.dialog) remote.dialog.showOpenDialog( @@ -40,28 +35,52 @@ class PathSelector extends React.Component { opts, (filenames) => { if (!Array.isArray(filenames)) return - this.setState({value: filenames[0]}) this.props.onChange && this.props.onChange(filenames[0]) } ) } render () { + const id = this.props.title.replace(' ', '-').toLowerCase() return ( -
-
{this.props.label}:
- - + {this.props.title}: +
+ + ) } diff --git a/src/renderer/views/PreferencesPage.js b/src/renderer/views/PreferencesPage.js index 5e338523..2ba3532d 100644 --- a/src/renderer/views/PreferencesPage.js +++ b/src/renderer/views/PreferencesPage.js @@ -1,33 +1,142 @@ const React = require('react') const path = require('path') -const PathSelector = require('./PathSelector') +const Checkbox = require('material-ui/Checkbox').default +const colors = require('material-ui/styles/colors') +const RaisedButton = require('material-ui/RaisedButton').default +const PathSelector = require('./PathSelector') const {dispatch} = require('../lib/dispatcher') class PreferencesPage extends React.Component { - render () { - var state = this.props.state + constructor () { + super() + + this.handleDownloadPathChange = + this.handleDownloadPathChange.bind(this) + + this.handleOpenExternalPlayerChange = + this.handleOpenExternalPlayerChange.bind(this) + + this.handleExternalPlayerPathChange = + this.handleExternalPlayerPathChange.bind(this) + } + + downloadPathSelector () { return ( -
+ + + + ) + } + + handleDownloadPathChange (filePath) { + dispatch('updatePreferences', 'downloadPath', filePath) + } + + openExternalPlayerCheckbox () { + return ( + + + + ) + } + + handleOpenExternalPlayerChange (e, isChecked) { + dispatch('updatePreferences', 'openExternalPlayer', !isChecked) + } + + externalPlayerPathSelector () { + const playerName = path.basename( + this.props.state.unsaved.prefs.externalPlayerPath || 'VLC' + ) + + const description = this.props.state.unsaved.prefs.openExternalPlayer + ? `Torrent media files will always play in ${playerName}.` + : `Torrent media files will play in ${playerName} if WebTorrent cannot ` + + 'play them.' + + return ( + +

{description}

+ +
+ ) + } + + handleExternalPlayerPathChange (filePath) { + if (path.extname(filePath) === '.app') { + // Mac: Use executable in packaged .app bundle + filePath += '/Contents/MacOS/' + path.basename(filePath, '.app') + } + dispatch('updatePreferences', 'externalPlayerPath', filePath) + } + + setDefaultAppButton () { + return ( + +

WebTorrent is not currently the default torrent app.

+ +
+ ) + } + + handleSetDefaultApp () { + window.alert('TODO') + // var isFileHandler = state.unsaved.prefs.isFileHandler + // dispatch('updatePreferences', 'isFileHandler', !isFileHandler) + } + + render () { + return ( +
- - {DownloadPathSelector(state)} - + {this.downloadPathSelector()} - - {ExternalPlayerPathSelector(state)} - + {this.openExternalPlayerCheckbox()} + {this.externalPlayerPathSelector()} + + + {this.setDefaultAppButton()}
) } } -// {ExternalPlayerCheckbox(state)} -// {DefaultAppCheckbox(state)} - class PreferencesSection extends React.Component { static get propTypes () { return { @@ -37,8 +146,18 @@ class PreferencesSection extends React.Component { render () { return ( -
-

{this.props.title}

+
+

{this.props.title}

{this.props.children}
) @@ -48,119 +167,15 @@ class PreferencesSection extends React.Component { class Preference extends React.Component { render () { return ( -
+
{this.props.children}
) } } -function DownloadPathSelector (state) { - return ( - - ) - - function handleChange (filePath) { - dispatch('updatePreferences', 'downloadPath', filePath) - } -} - -function ExternalPlayerPathSelector (state) { - return ( - '} - onChange={handleChange} - /> - ) - - function handleChange (filePath) { - if (path.extname(filePath) === '.app') { - // Get executable in packaged mac app - var name = path.basename(filePath, '.app') - filePath += '/Contents/MacOS/' + name - } - dispatch('updatePreferences', 'externalPlayerPath', filePath) - } -} - -// function ExternalPlayerCheckbox (state) { -// return renderCheckbox({ -// label: 'Play in External Player', -// description: 'Media will play in external player', -// property: 'openExternalPlayer', -// value: state.saved.prefs.openExternalPlayer -// }, -// state.unsaved.prefs.openExternalPlayer, -// function (value) { -// dispatch('updatePreferences', 'openExternalPlayer', value) -// }) -// } - -// function renderCheckbox (definition, value, callback) { -// var iconClass = 'icon clickable' -// if (value) iconClass += ' enabled' - -// return ( -//
-//
-// -//
-// -//
-//
-//
-// ) -// function handleClick () { -// callback(!value) -// } -// } - -// function DefaultAppCheckbox (state) { -// var definition = { -// key: 'file-handlers', -// label: 'Handle Torrent Files' -// } -// var buttonText = state.unsaved.prefs.isFileHandler -// ? 'Remove default app for torrent files' -// : 'Make WebTorrent the default app for torrent files' -// var controls = [( -// -// )] -// return renderControlGroup(definition, controls) - -// function toggleFileHandlers () { -// var isFileHandler = state.unsaved.prefs.isFileHandler -// dispatch('updatePreferences', 'isFileHandler', !isFileHandler) -// } -// } - module.exports = PreferencesPage diff --git a/src/renderer/views/TextInput.js b/src/renderer/views/TextInput.js deleted file mode 100644 index 24e26252..00000000 --- a/src/renderer/views/TextInput.js +++ /dev/null @@ -1,34 +0,0 @@ -const c = require('classnames') -const React = require('react') - -class TextInput extends React.Component { - static get propTypes () { - return { - theme: React.PropTypes.oneOf('light', 'dark'), - className: React.PropTypes.string - } - } - - static get defaultProps () { - return { - theme: 'light' - } - } - - render () { - const { className, theme, ...other } = this.props - return ( - - ) - } -} - -module.exports = TextInput diff --git a/src/renderer/views/app.js b/src/renderer/views/app.js index f6f3677a..94d4f687 100644 --- a/src/renderer/views/app.js +++ b/src/renderer/views/app.js @@ -1,5 +1,9 @@ const React = require('react') +const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default +const getMuiTheme = require('material-ui/styles/getMuiTheme').default +const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default + const Header = require('./header') const Views = { @@ -16,8 +20,11 @@ const Modals = { 'unsupported-media-modal': require('./unsupported-media-modal') } -module.exports = class App extends React.Component { +var muiTheme = getMuiTheme(Object.assign(darkBaseTheme, { + fontFamily: 'BlinkMacSystemFont, \'Helvetica Neue\', Helvetica, sans-serif' +})) +module.exports = class App extends React.Component { constructor (props) { super(props) this.state = props.state @@ -47,12 +54,14 @@ module.exports = class App extends React.Component { if (hideControls) cls.push('hide-video-controls') var vdom = ( -
-
- {this.getErrorPopover()} -
{this.getView()}
- {this.getModal()} -
+ +
+
+ {this.getErrorPopover()} +
{this.getView()}
+ {this.getModal()} +
+
) return vdom diff --git a/static/css/components/Button.css b/static/css/components/Button.css deleted file mode 100644 index b05108ef..00000000 --- a/static/css/components/Button.css +++ /dev/null @@ -1,76 +0,0 @@ -.Button { - -webkit-app-region: no-drag; - background: transparent; - border-radius: 3px; - border: none; - color: #aaa; - cursor: default; - font-size: 14px; - font-weight: bold; - margin-left: 10px; - outline: none; - padding: 0; - padding: 7px 18px; - text-transform: uppercase; -} - -/** - * Default button - */ - -.Button.default { - color: #222; - background-color: rgba(153, 153, 153, 0.1); -} - -.Button.default.dark { - color: #eee; -} - -.Button.default:hover, -.Button.default:focus { /* Material design: focused */ - background-color: rgba(153, 153, 153, 0.2); -} - -.Button.default:active { /* Material design: pressed */ - background-color: rgba(153, 153, 153, 0.4); -} - -/** - * Flat button - */ - -.Button.flat { - color: #222; -} - -.Button.flat.dark { - color: #eee; -} - -.Button.flat:hover, -.Button.flat:focus { - background-color: rgba(153, 153, 153, 0.2); -} - -.Button.flat:active { - background-color: rgba(153, 153, 153, 0.4); -} - -/** - * Raised button - */ - -.Button.raised { - background-color: #2196f3; - color: #eee; -} - -.Button.raised:hover, -.Button.raised:focus { - background-color: #38a0f5; -} - -.Button.raised:active { - background-color: #51abf6; -} diff --git a/static/css/components/PathSelector.css b/static/css/components/PathSelector.css deleted file mode 100644 index 7ca86d68..00000000 --- a/static/css/components/PathSelector.css +++ /dev/null @@ -1,17 +0,0 @@ -.PathSelector { - align-items: center; - display: flex; - width: 100%; -} - -.PathSelector .label { - margin-right: 10px; - flex: 1 1 auto; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.PathSelector .input { - flex: 5 5 200px; -} diff --git a/static/css/components/TextInput.css b/static/css/components/TextInput.css deleted file mode 100644 index 7289a314..00000000 --- a/static/css/components/TextInput.css +++ /dev/null @@ -1,18 +0,0 @@ -.TextInput { - background: rgba(255, 255, 255, 0.8); - border-radius: 3px; - font-size: 14px; - outline: none; - padding: 3px 6px; - width: 200px; - -webkit-app-region: no-drag; - cursor: default; -} - -.TextInput.light { - border: 1px solid rgba(0, 0, 0, 0.4); -} - -.TextInput.dark { - border: 1px solid rgba(0, 0, 0, 0.8); -} diff --git a/static/css/pages/PreferencesPage.css b/static/css/pages/PreferencesPage.css deleted file mode 100644 index 28405ba7..00000000 --- a/static/css/pages/PreferencesPage.css +++ /dev/null @@ -1,17 +0,0 @@ -.PreferencesPage { - color: rgba(255, 255, 255, 0.6); - margin: 0 20px; -} - -.PreferencesSection:not(:last-child) { - margin-top: 20px; - margin-bottom: 40px; -} - -.PreferencesSection .title { - color: rgba(255, 255, 255, 0.9); -} - -.Preference:not(:last-child) { - margin-bottom: 10px; -} diff --git a/static/css/main.css b/static/main.css similarity index 97% rename from static/css/main.css rename to static/main.css index 8e97f5e3..36959f3b 100644 --- a/static/css/main.css +++ b/static/main.css @@ -20,7 +20,7 @@ body { } .app { - color: #FFF; + color: #FAFAFA; /* grey50 */ font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif; font-size: 14px; line-height: 1.5em; @@ -91,9 +91,7 @@ table { font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: local('Material Icons'), - local('MaterialIcons-Regular'), - url('../MaterialIcons-Regular.woff2') format('woff2'); + src: url('MaterialIcons-Regular.woff2') format('woff2'); } .icon { @@ -1025,3 +1023,19 @@ video::-webkit-media-text-track-container { height: 32px; margin: 4px 0 0 4px; } + +/** + * Use this class on Material UI components to get correct native app behavior: + * + * - Dragging the button should NOT drag the entire app window + * - The cursor should be default, not a pointer (hand) like on the web + */ + +.control { + -webkit-app-region: no-drag; + cursor: default !important; +} + +.control * { + cursor: default !important; +} diff --git a/static/main.html b/static/main.html index 9399e248..702d5f3e 100644 --- a/static/main.html +++ b/static/main.html @@ -3,22 +3,11 @@ - WebTorrent Desktop - - - - - - - + -
- - +