Preferences page rehaul: use React components, UI improvements
This commit is contained in:
40
src/renderer/views/Button.js
Normal file
40
src/renderer/views/Button.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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
|
||||
70
src/renderer/views/PathSelector.js
Normal file
70
src/renderer/views/PathSelector.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const c = require('classnames')
|
||||
const electron = require('electron')
|
||||
const React = require('react')
|
||||
|
||||
const remote = electron.remote
|
||||
|
||||
const Button = require('./Button')
|
||||
const TextInput = require('./TextInput')
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}, this.props.dialog)
|
||||
|
||||
remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
opts,
|
||||
(filenames) => {
|
||||
if (!Array.isArray(filenames)) return
|
||||
this.setState({value: filenames[0]})
|
||||
this.props.onChange && this.props.onChange(filenames[0])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
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}
|
||||
>
|
||||
Change…
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PathSelector
|
||||
166
src/renderer/views/PreferencesPage.js
Normal file
166
src/renderer/views/PreferencesPage.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const React = require('react')
|
||||
const path = require('path')
|
||||
|
||||
const PathSelector = require('./PathSelector')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
class PreferencesPage extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
return (
|
||||
<div className='PreferencesPage'>
|
||||
<PreferencesSection title='Downloads'>
|
||||
<Preference>
|
||||
{DownloadPathSelector(state)}
|
||||
</Preference>
|
||||
</PreferencesSection>
|
||||
<PreferencesSection title='Playback'>
|
||||
<Preference>
|
||||
{ExternalPlayerPathSelector(state)}
|
||||
</Preference>
|
||||
</PreferencesSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// {ExternalPlayerCheckbox(state)}
|
||||
// {DefaultAppCheckbox(state)}
|
||||
|
||||
class PreferencesSection extends React.Component {
|
||||
static get propTypes () {
|
||||
return {
|
||||
title: React.PropTypes.string
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='PreferencesSection'>
|
||||
<h2 className='title'>{this.props.title}</h2>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Preference extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className='Preference'>
|
||||
{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
|
||||
34
src/renderer/views/TextInput.js
Normal file
34
src/renderer/views/TextInput.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
@@ -6,7 +6,7 @@ const Views = {
|
||||
'home': require('./torrent-list'),
|
||||
'player': require('./player'),
|
||||
'create-torrent': require('./create-torrent'),
|
||||
'preferences': require('./preferences')
|
||||
'preferences': require('./PreferencesPage')
|
||||
}
|
||||
|
||||
const Modals = {
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
const React = require('react')
|
||||
const remote = require('electron').remote
|
||||
const dialog = remote.dialog
|
||||
const path = require('path')
|
||||
|
||||
const {dispatch} = require('../lib/dispatcher')
|
||||
|
||||
module.exports = class Preferences extends React.Component {
|
||||
render () {
|
||||
var state = this.props.state
|
||||
return (
|
||||
<div className='preferences'>
|
||||
{renderGeneralSection(state)}
|
||||
{renderPlaybackSection(state)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function renderGeneralSection (state) {
|
||||
return renderSection({
|
||||
key: 'general',
|
||||
title: 'General',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderDownloadPathSelector(state),
|
||||
renderFileHandlers(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderPlaybackSection (state) {
|
||||
return renderSection({
|
||||
title: 'Playback',
|
||||
description: '',
|
||||
icon: 'settings'
|
||||
}, [
|
||||
renderOpenExternalPlayerSelector(state),
|
||||
renderExternalPlayerSelector(state)
|
||||
])
|
||||
}
|
||||
|
||||
function renderDownloadPathSelector (state) {
|
||||
return renderFileSelector({
|
||||
key: 'download-path',
|
||||
label: 'Download Path',
|
||||
description: 'Data from torrents will be saved here',
|
||||
property: 'downloadPath',
|
||||
options: {
|
||||
title: 'Select download directory',
|
||||
properties: [ 'openDirectory' ]
|
||||
}
|
||||
},
|
||||
state.unsaved.prefs.downloadPath,
|
||||
function (filePath) {
|
||||
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||
})
|
||||
}
|
||||
|
||||
function renderFileHandlers (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)
|
||||
}
|
||||
}
|
||||
|
||||
function renderExternalPlayerSelector (state) {
|
||||
return renderFileSelector({
|
||||
label: 'External Media Player',
|
||||
description: 'Progam that will be used to play media externally',
|
||||
property: 'externalPlayerPath',
|
||||
options: {
|
||||
title: 'Select media player executable',
|
||||
properties: [ 'openFile' ]
|
||||
}
|
||||
},
|
||||
state.unsaved.prefs.externalPlayerPath || '<VLC>',
|
||||
function (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 renderOpenExternalPlayerSelector (state) {
|
||||
return renderCheckbox({
|
||||
key: 'open-external-player',
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// Renders a prefs section.
|
||||
// - definition should be {icon, title, description}
|
||||
// - controls should be an array of vdom elements
|
||||
function renderSection (definition, controls) {
|
||||
var helpElem = !definition.description ? null : (
|
||||
<div key='help' className='help text'>
|
||||
<i className='icon'>help_outline</i>{definition.description}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<section key={definition.key} className='section preferences-panel'>
|
||||
<div className='section-container'>
|
||||
<div key='heading' className='section-heading'>
|
||||
<i className='icon'>{definition.icon}</i>{definition.title}
|
||||
</div>
|
||||
{helpElem}
|
||||
<div key='body' className='section-body'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a file chooser
|
||||
// - defition should be {label, description, options}
|
||||
// options are passed to dialog.showOpenDialog
|
||||
// - value should be the current pref, a file or folder path
|
||||
// - callback takes a new file or folder path
|
||||
function renderFileSelector (definition, value, callback) {
|
||||
var controls = [(
|
||||
<input
|
||||
type='text'
|
||||
className='file-picker-text'
|
||||
key={definition.property}
|
||||
id={definition.property}
|
||||
disabled='disabled'
|
||||
value={value} />
|
||||
), (
|
||||
<button
|
||||
key={definition.property + '-btn'}
|
||||
className='btn'
|
||||
onClick={handleClick}>
|
||||
<i className='icon'>folder_open</i>
|
||||
</button>
|
||||
)]
|
||||
return renderControlGroup(definition, controls)
|
||||
|
||||
function handleClick () {
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||
if (!Array.isArray(filenames)) return
|
||||
callback(filenames[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderControlGroup (definition, controls) {
|
||||
return (
|
||||
<div key={definition.key} className='control-group'>
|
||||
<div className='controls'>
|
||||
<label className='control-label'>
|
||||
<div className='preference-title'>{definition.label}</div>
|
||||
<div className='preference-description'>{definition.description}</div>
|
||||
</label>
|
||||
<div className='controls'>
|
||||
{controls}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user