diff --git a/package.json b/package.json index e6958bec..1e99e687 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "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", @@ -47,6 +48,8 @@ "devDependencies": { "babel-cli": "^6.11.4", "babel-plugin-syntax-jsx": "^6.13.0", + "babel-plugin-transform-es2015-destructuring": "^6.9.0", + "babel-plugin-transform-object-rest-spread": "^6.8.0", "babel-plugin-transform-react-jsx": "^6.8.0", "cross-zip": "^2.0.1", "electron-osx-sign": "^0.3.0", diff --git a/src/.babelrc b/src/.babelrc index 9d3c6b01..db07c35e 100644 --- a/src/.babelrc +++ b/src/.babelrc @@ -1,6 +1,8 @@ { "plugins": [ "syntax-jsx", + "transform-es2015-destructuring", + "transform-object-rest-spread", "transform-react-jsx" ] } diff --git a/src/renderer/views/Button.js b/src/renderer/views/Button.js new file mode 100644 index 00000000..a8aa09d3 --- /dev/null +++ b/src/renderer/views/Button.js @@ -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 ( + + ) + } +} + +module.exports = Button diff --git a/src/renderer/views/PathSelector.js b/src/renderer/views/PathSelector.js new file mode 100644 index 00000000..d5013a97 --- /dev/null +++ b/src/renderer/views/PathSelector.js @@ -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 ( +
+
{this.props.label}:
+ + +
+ ) + } +} + +module.exports = PathSelector diff --git a/src/renderer/views/PreferencesPage.js b/src/renderer/views/PreferencesPage.js new file mode 100644 index 00000000..5e338523 --- /dev/null +++ b/src/renderer/views/PreferencesPage.js @@ -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 ( +
+ + + {DownloadPathSelector(state)} + + + + + {ExternalPlayerPathSelector(state)} + + +
+ ) + } +} + +// {ExternalPlayerCheckbox(state)} +// {DefaultAppCheckbox(state)} + +class PreferencesSection extends React.Component { + static get propTypes () { + return { + title: React.PropTypes.string + } + } + + render () { + return ( +
+

{this.props.title}

+ {this.props.children} +
+ ) + } +} + +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 new file mode 100644 index 00000000..24e26252 --- /dev/null +++ b/src/renderer/views/TextInput.js @@ -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 ( + + ) + } +} + +module.exports = TextInput diff --git a/src/renderer/views/app.js b/src/renderer/views/app.js index 33b4f193..f6f3677a 100644 --- a/src/renderer/views/app.js +++ b/src/renderer/views/app.js @@ -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 = { diff --git a/src/renderer/views/preferences.js b/src/renderer/views/preferences.js deleted file mode 100644 index b309feb0..00000000 --- a/src/renderer/views/preferences.js +++ /dev/null @@ -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 ( -
- {renderGeneralSection(state)} - {renderPlaybackSection(state)} -
- ) - } -} - -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 = [( - - )] - 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 || '', - 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 : ( -
- help_outline{definition.description} -
- ) - return ( -
-
-
- {definition.icon}{definition.title} -
- {helpElem} -
- {controls} -
-
-
- ) -} - -function renderCheckbox (definition, value, callback) { - var iconClass = 'icon clickable' - if (value) iconClass += ' enabled' - - return ( -
-
- -
- -
-
-
- ) - 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 = [( - - ), ( - - )] - 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 ( -
-
- -
- {controls} -
-
-
- ) -} diff --git a/static/css/components/Button.css b/static/css/components/Button.css new file mode 100644 index 00000000..b05108ef --- /dev/null +++ b/static/css/components/Button.css @@ -0,0 +1,76 @@ +.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 new file mode 100644 index 00000000..7ca86d68 --- /dev/null +++ b/static/css/components/PathSelector.css @@ -0,0 +1,17 @@ +.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 new file mode 100644 index 00000000..7289a314 --- /dev/null +++ b/static/css/components/TextInput.css @@ -0,0 +1,18 @@ +.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/main.css b/static/css/main.css index 3c03b0eb..8e97f5e3 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -75,12 +75,12 @@ table { height: 100%; display: flex; flex-flow: column; - background: rgb(40, 40, 40); + background: rgb(30, 30, 30); animation: fadein 0.5s; } .app:not(.is-focused) { - background: rgb(50, 50, 50); + background: rgb(40, 40, 40); } /* @@ -93,7 +93,7 @@ table { font-weight: 400; src: local('Material Icons'), local('MaterialIcons-Regular'), - url(MaterialIcons-Regular.woff2) format('woff2'); + url('../MaterialIcons-Regular.woff2') format('woff2'); } .icon { @@ -170,7 +170,7 @@ table { .header { background: rgb(40, 40, 40); - border-bottom: 1px solid rgb(20, 20, 20); + border-bottom: 1px solid rgb(30, 30, 30); height: 38px; /* vertically center OS menu buttons (OS X) */ padding-top: 7px; overflow: hidden; @@ -247,7 +247,7 @@ table { overflow-x: hidden; overflow-y: overlay; flex: 1 1 auto; - margin-top: 37px; + margin-top: 38px; } .app.view-player .content { @@ -369,64 +369,10 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */ text-align: center; } -button { /* Rectangular text buttons */ - background: transparent; - margin-left: 10px; - padding: 0; - border: none; - border-radius: 3px; - font-size: 14px; - font-weight: bold; - color: #aaa; - outline: none; -} - -button.button-flat { - color: #222; - padding: 7px 18px; -} - -button.button-flat.light { - color: #eee; -} - -button.button-flat:hover, -button.button-flat:focus { /* Material design: focused */ - background-color: rgba(153, 153, 153, 0.2); -} - -button.button-flat:active { /* Material design: pressed */ - background-color: rgba(153, 153, 153, 0.4); -} - -button.button-raised { - background-color: #2196f3; - color: #eee; - padding: 7px 18px; -} - -button.button-raised:hover, -button.button-raised:focus { - background-color: #38a0f5; -} - -button.button-raised:active { - background-color: #51abf6; -} - /* * OTHER FORM ELEMENT DEFAULTS */ -input[type='text'] { - background: transparent; - width: 300px; - padding: 6px; - border: 1px solid #bbb; - border-radius: 3px; - outline: none; -} - /* * TORRENT LIST */ @@ -438,7 +384,7 @@ input[type='text'] { background-position: center; transition: -webkit-filter 0.1s ease-out; position: relative; - animation: fadein .4s; + animation: fadein 0.5s; } .torrent, @@ -921,185 +867,6 @@ video::-webkit-media-text-track-container { font-weight: bold; } -/* - * Preferences page, based on Atom settings style - */ - -.preferences { - font-size: 12px; - line-height: calc(10/7); -} - -.preferences .text { - color: #a8a8a8; -} - -.preferences .icon { - color: rgba(170, 170, 170, 0.6); - font-size: 16px; - margin-right: 0.2em; -} - -.preferences .icon.enabled { - color: yellow; -} - -.preferences .btn { - display: inline-block; - -webkit-appearance: button; - margin: 0; - font-weight: normal; - text-align: center; - vertical-align: middle; - border-color: #cccccc; - border-radius: 3px; - color: #9da5b4; - text-shadow: none; - border: 1px solid #181a1f; - background-color: #3d3d3d; - white-space: initial; - font-size: 0.889em; - line-height: 1; - padding: 0.5em 0.75em; -} - -.preferences .btn .icon { - margin: 0; - color: #a8a8a8; -} - -.preferences .help .icon { - vertical-align: sub; -} - - -.preferences .preferences-panel .control-group + .control-group { - margin-top: 1.5em; -} - -.preferences .section { - padding: 20px; - border-top: 1px solid #181a1f; -} - -.preferences .section:first { - border-top: 0px; -} - -.preferences .section:first-child, -.preferences .section:last-child { - padding: 20px; -} - -.preferences .section.section:empty { - padding: 0; - border-top: none; -} - -.preferences .section-container { - width: 100%; - max-width: 800px; -} - -.preferences section .section-heading, -.preferences .section .section-heading { - margin-bottom: 10px; - color: #dcdcdc; - font-size: 1.75em; - font-weight: bold; - line-height: 1; - -webkit-user-select: none; - cursor: default; -} - -.preferences .sub-section-heading.icon:before, -.preferences .section-heading.icon:before { - margin-right: 8px; -} - -.preferences .section-heading-count { - margin-left: .5em; -} - -.preferences .section-body { - margin-top: 20px; -} - -.preferences .sub-section { - margin: 20px 0; -} - -.preferences .sub-section .sub-section-heading { - color: #dcdcdc; - font-size: 1.4em; - font-weight: bold; - line-height: 1; - -webkit-user-select: none; -} - -.preferences .preferences-panel label { - color: #a8a8a8; -} - -.preferences .preferences-panel .control-group + .control-group { - margin-top: 1.5em; -} - -.preferences .preferences-panel .control-group .editor-container { - margin: 0; -} - -.preferences .preference-title { - font-size: 1.2em; - -webkit-user-select: none; - display: inline-block; -} - -.preferences .preference-description { - color: rgba(170, 170, 170, 0.6); - -webkit-user-select: none; - cursor: default; -} - -.preferences input { - font-size: 1.1em; - line-height: 1.15em; - max-height: none; - width: 100%; - padding-left: 0.5em; - border-radius: 3px; - color: #a8a8a8; - border: 1px solid #181a1f; - background-color: #1b1d23; -} - -.preferences input::-webkit-input-placeholder { - color: rgba(170, 170, 170, 0.6); -} - -.preferences .control-group input { - margin-top: 0.2em; -} - -.preferences .control-group input.file-picker-text { - width: calc(100% - 40px); -} - -.preferences .control-group .checkbox .icon { - font-size: 1.5em; - margin: 0; - vertical-align: text-bottom; -} - -.preferences .checkbox { - width: auto; -} - -.checkbox-label { - vertical-align: top; -} - - /* * MEDIA OVERLAY / AUDIO DETAILS */ diff --git a/static/css/pages/PreferencesPage.css b/static/css/pages/PreferencesPage.css new file mode 100644 index 00000000..28405ba7 --- /dev/null +++ b/static/css/pages/PreferencesPage.css @@ -0,0 +1,17 @@ +.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/main.html b/static/main.html index 4b41b8ba..9399e248 100644 --- a/static/main.html +++ b/static/main.html @@ -3,8 +3,15 @@ + WebTorrent Desktop - + + + + + + +