diff --git a/main/menu.js b/main/menu.js index 664774a1..14cf1f87 100644 --- a/main/menu.js +++ b/main/menu.js @@ -116,6 +116,11 @@ function decreasePlaybackRate () { } } +// Open the preferences window +function showPreferences () { + windows.main.send('dispatch', 'preferences') +} + function onWindowShow () { log('onWindowShow') getMenuItem('Full Screen').enabled = true @@ -269,6 +274,14 @@ function getAppMenuTemplate () { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' + }, + { + type: 'separator' + }, + { + label: 'Preferences', + accelerator: 'CmdOrCtrl+,', + click: () => showPreferences() } ] }, @@ -411,6 +424,14 @@ function getAppMenuTemplate () { { type: 'separator' }, + { + label: 'Preferences', + accelerator: 'Cmd+,', + click: () => showPreferences() + }, + { + type: 'separator' + }, { label: 'Services', role: 'services', diff --git a/renderer/index.css b/renderer/index.css index 7153a074..e5667cf6 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -911,6 +911,176 @@ video::-webkit-media-text-track-container { margin-right: 4px !important; } +/* + * 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 .btn { + display: inline-block; + -webkit-appearance: button; + margin: 0; + font-weight: normal; + text-align: center; + vertical-align: middle; + cursor: pointer; + 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; + cursor: pointer; +} + +.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;; + cursor: pointer; +} + + /* * MEDIA OVERLAY / AUDIO DETAILS */ diff --git a/renderer/index.js b/renderer/index.js index 855dc3ee..3cea58e2 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -346,6 +346,25 @@ function dispatch (action, ...args) { if (action === 'exitModal') { state.modal = null } + if (action === 'preferences') { + state.location.go({ + url: 'preferences', + onbeforeload: function (cb) { + // initialize preferences + state.window.title = 'Preferences' + state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}}) + cb() + }, + onbeforeunload: function (cb) { + // save state after preferences + savePreferences() + cb() + } + }) + } + if (action === 'updatePreferences') { + updatePreferences(args[0], args[1] /* property, value */) + } if (action === 'updateAvailable') { updateAvailable(args[0] /* version */) } @@ -523,6 +542,23 @@ function resumeTorrents () { .forEach((x) => startTorrentingSummary(x)) } +function updatePreferences (property, value) { + var path = property.split('.') + var key = state.unsaved.prefs + for (var i = 0; i < path.length - 1; i++) { + if (typeof key[path[i]] === 'undefined') { + key[path[i]] = {} + } + key = key[path[i]] + } + key[path[i]] = value +} + +function savePreferences () { + state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs) + saveState() +} + // Don't write state.saved to file more than once a second function saveStateThrottled () { if (state.saveStateTimeout) return @@ -620,7 +656,7 @@ var instantIoRegex = /^(https:\/\/)?instant\.io\/#/ function addTorrent (torrentId) { backToList() var torrentKey = state.nextTorrentKey++ - var path = state.saved.downloadPath + var path = state.saved.prefs.downloadPath if (torrentId.path) { // Use path string instead of W3C File object torrentId = torrentId.path @@ -742,7 +778,7 @@ function startTorrentingSummary (torrentSummary) { if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++ // Use Downloads folder by default - var path = s.path || state.saved.downloadPath + var path = s.path || state.saved.prefs.downloadPath var torrentID if (s.torrentFileName) { // Load torrent file from disk @@ -1191,7 +1227,7 @@ function saveTorrentFileAs (torrentSummary) { var newFileName = `${path.parse(torrentSummary.name).name}.torrent` var opts = { title: 'Save Torrent File', - defaultPath: path.join(state.saved.downloadPath, newFileName), + defaultPath: path.join(state.saved.prefs.downloadPath, newFileName), filters: [ { name: 'Torrent Files', extensions: ['torrent'] }, { name: 'All Files', extensions: ['*'] } diff --git a/renderer/state.js b/renderer/state.js index ba660701..40796f62 100644 --- a/renderer/state.js +++ b/renderer/state.js @@ -266,9 +266,11 @@ function getDefaultSavedState () { ] } ], - downloadPath: config.IS_PORTABLE - ? path.join(config.CONFIG_PATH, 'Downloads') - : remote.app.getPath('downloads') + prefs: { + downloadPath: config.IS_PORTABLE + ? path.join(config.CONFIG_PATH, 'Downloads') + : remote.app.getPath('downloads') + } } } diff --git a/renderer/views/app.js b/renderer/views/app.js index 4722e059..d5dd1cc1 100644 --- a/renderer/views/app.js +++ b/renderer/views/app.js @@ -8,7 +8,8 @@ var Header = require('./header') var Views = { 'home': require('./torrent-list'), 'player': require('./player'), - 'create-torrent': require('./create-torrent-page') + 'create-torrent': require('./create-torrent-page'), + 'preferences': require('./preferences') } var Modals = { 'open-torrent-address-modal': require('./open-torrent-address-modal'), diff --git a/renderer/views/header.js b/renderer/views/header.js index 5dd302af..ab777ca7 100644 --- a/renderer/views/header.js +++ b/renderer/views/header.js @@ -37,7 +37,7 @@ function Header (state) { } function getAddButton () { - if (state.location.url() !== 'player') { + if (state.location.url() === 'home') { return hx` + ${sections} + + ` +} + +function renderSection (definition) { + var controls = [] + + definition.controls.forEach(function (controlDefinition) { + controls.push(controlDefinition.renderer(controlDefinition)) + }) + + return hx` +
+
+
${definition.icon}${definition.title}
+
help_outline${definition.description}
+
+ ${controls} +
+
+
+ ` +} + +function renderFileSelector (definition) { + var value = getStateValue(definition.property) + return hx` +
+
+ +
+ + +
+
+
+ ` + function filePickerHandler () { + dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) { + if (!Array.isArray(filenames)) return + if (!definition.validator || definition.validator(filenames[0])) { + setStateValue(definition.property, filenames[0]) + } + }) + } +} + +/* +function renderCheckbox (definition) { + var checked = getStateValue(definition.property) + var checkbox = checked ? 'check_box' : 'check_box_outline_blank' + return hx` +
+
+
+ +
${definition.description}
+
+
+
+ ` + function checkboxHandler (e) { + setStateValue(definition.property, !getStateValue(definition.property)) + } +} +*/ + +function getPreferenceDefinitions () { + return [ + { + title: 'General', + description: 'These are WebTorrent Desktop main preferences. Will put a very long text to check if it overflows correctly.', + icon: 'settings', + controls: [ + { + label: 'Download Path', + description: 'Directory where the files will be stored. Please, check if it has enough space!', + property: 'downloadPath', + renderer: renderFileSelector, + placeholder: 'Your downloads directory ie: $HOME/Downloads', + options: { + title: 'Select download directory.', + properties: [ 'openDirectory' ] + }, + validator: function (value) { + return fs.existsSync(value) + } + } + ] + }/*, + { + title: 'Interface', + description: 'Here you can change the way the application looks and beahaves.', + icon: 'view_compact', + controls: [ + { + label: 'Disable Tray', + description: 'This option gives you the chance to quit immediately and don\'t send the application to background when you close it.', + property: 'interface.disableTray', + renderer: renderCheckbox + } + ] + }*/ + ] +} + +function getStateValue (property) { + var path = property.split('.') + var key = prefState + for (var i = 0; i < path.length - 1; i++) { + if (typeof key[path[i]] === 'undefined') { + return '' + } + key = key[path[i]] + } + return key[path[i]] +} + +function setStateValue (property, value) { + dispatch('updatePreferences', property, value) +} diff --git a/renderer/views/torrent-list.js b/renderer/views/torrent-list.js index 258078c0..7bcd2dc2 100644 --- a/renderer/views/torrent-list.js +++ b/renderer/views/torrent-list.js @@ -142,7 +142,8 @@ function TorrentList (state) { // of the play button, unless already showing a spinner there: var positionElem var willShowSpinner = torrentSummary.playStatus === 'requested' - var defaultFile = torrentSummary.files[torrentSummary.defaultPlayFileIndex] + var defaultFile = torrentSummary.files && + torrentSummary.files[torrentSummary.defaultPlayFileIndex] if (defaultFile && defaultFile.currentTime && !willShowSpinner) { var fraction = defaultFile.currentTime / defaultFile.duration positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')