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)
This commit is contained in:
Feross Aboukhadijeh
2016-08-22 17:53:29 -07:00
parent b4976d27f2
commit 1a01d7ed92
14 changed files with 234 additions and 383 deletions

View File

@@ -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')

View File

@@ -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 (
<button
{...other}
className={c(
'Button',
theme,
type,
className
)}
onClick={this.props.onClick}
>
{this.props.children}
</button>
)
}
}
module.exports = Button

View File

@@ -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 (
<div className={c('PathSelector', this.props.className)}>
<div className='label'>{this.props.label}:</div>
<TextInput
className='input'
disabled
value={this.state.value}
/>
<Button
className='button'
theme='dark'
onClick={this.handleClick}
<div
className={this.props.className}
style={{
alignItems: 'center',
display: 'flex',
width: '100%'
}}
>
<div
className='label'
style={{
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
Change
</Button>
{this.props.title}:
</div>
<TextField
className='control'
disabled
id={id}
style={{
flex: '1',
fontSize: 14
}}
value={this.props.displayValue || this.props.value}
/>
<RaisedButton
className='control'
label='Change'
onClick={this.handleClick}
style={{
marginLeft: 10
}}
/>
</div>
)
}

View File

@@ -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 (
<div className='PreferencesPage'>
<Preference>
<PathSelector
dialog={{
title: 'Select download directory',
properties: [ 'openDirectory' ]
}}
onChange={this.handleDownloadPathChange}
title='Download location'
value={this.props.state.unsaved.prefs.downloadPath}
/>
</Preference>
)
}
handleDownloadPathChange (filePath) {
dispatch('updatePreferences', 'downloadPath', filePath)
}
openExternalPlayerCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange}
/>
</Preference>
)
}
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 (
<Preference>
<p>{description}</p>
<PathSelector
dialog={{
title: 'Select media player app',
properties: [ 'openFile' ]
}}
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={this.props.state.unsaved.prefs.externalPlayerPath}
/>
</Preference>
)
}
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 (
<Preference>
<p>WebTorrent is not currently the default torrent app.</p>
<RaisedButton
className='control'
onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default'
/>
</Preference>
)
}
handleSetDefaultApp () {
window.alert('TODO')
// var isFileHandler = state.unsaved.prefs.isFileHandler
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
}
render () {
return (
<div
style={{
color: colors.grey400,
marginLeft: 20,
marginRight: 20
}}
>
<PreferencesSection title='Downloads'>
<Preference>
{DownloadPathSelector(state)}
</Preference>
{this.downloadPathSelector()}
</PreferencesSection>
<PreferencesSection title='Playback'>
<Preference>
{ExternalPlayerPathSelector(state)}
</Preference>
{this.openExternalPlayerCheckbox()}
{this.externalPlayerPathSelector()}
</PreferencesSection>
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
</div>
)
}
}
// {ExternalPlayerCheckbox(state)}
// {DefaultAppCheckbox(state)}
class PreferencesSection extends React.Component {
static get propTypes () {
return {
@@ -37,8 +146,18 @@ class PreferencesSection extends React.Component {
render () {
return (
<div className='PreferencesSection'>
<h2 className='title'>{this.props.title}</h2>
<div
style={{
marginBottom: 30,
marginTop: 30
}}
>
<h2
style={{
color: colors.grey50,
fontSize: 22
}}
>{this.props.title}</h2>
{this.props.children}
</div>
)
@@ -48,119 +167,15 @@ class PreferencesSection extends React.Component {
class Preference extends React.Component {
render () {
return (
<div className='Preference'>
<div
style={{
marginBottom: 10
}}
>
{this.props.children}
</div>
)
}
}
function DownloadPathSelector (state) {
return (
<PathSelector
className='download-path'
label='Download location'
dialog={{
title: 'Select download directory',
properties: [ 'openDirectory' ]
}}
defaultValue={state.unsaved.prefs.downloadPath}
onChange={handleChange}
/>
)
function handleChange (filePath) {
dispatch('updatePreferences', 'downloadPath', filePath)
}
}
function ExternalPlayerPathSelector (state) {
return (
<PathSelector
className='download-path'
label='Player app location'
dialog={{
title: 'Select media player app',
properties: [ 'openFile' ]
}}
defaultValue={state.unsaved.prefs.externalPlayerPath || '<VLC>'}
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 (
// <div key='{definition.key}' className='control-group'>
// <div className='controls'>
// <label className='control-label'>
// <div className='preference-title'>{definition.label}</div>
// </label>
// <div className='controls'>
// <label className='clickable' onClick={handleClick}>
// <i
// className={iconClass}
// id='{definition.property}'
// >
// check_circle
// </i>
// <span className='checkbox-label'>{definition.description}</span>
// </label>
// </div>
// </div>
// </div>
// )
// 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 = [(
// <button key='toggle-handlers'
// className='btn'
// onClick={toggleFileHandlers}>
// {buttonText}
// </button>
// )]
// return renderControlGroup(definition, controls)
// function toggleFileHandlers () {
// var isFileHandler = state.unsaved.prefs.isFileHandler
// dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
// }
// }
module.exports = PreferencesPage

View File

@@ -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 (
<input
{...other}
className={c(
'TextInput',
theme,
className
)}
type='text'
/>
)
}
}
module.exports = TextInput

View File

@@ -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 = (
<div className={'app ' + cls.join(' ')}>
<Header state={state} />
{this.getErrorPopover()}
<div key='content' className='content'>{this.getView()}</div>
{this.getModal()}
</div>
<MuiThemeProvider muiTheme={muiTheme}>
<div className={'app ' + cls.join(' ')}>
<Header state={state} />
{this.getErrorPopover()}
<div key='content' className='content'>{this.getView()}</div>
{this.getModal()}
</div>
</MuiThemeProvider>
)
return vdom