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
-
+
+
+
+
+
+
+