Add preferences page
* For now, the prefs page has just a single option, Downloads Folder * For now, you can't type in a folder, you must use the chooser * Further fixes coming om master * Written by @ChrisMorrisOrg and @grunjol, rebased by @dcposch
This commit is contained in:
21
main/menu.js
21
main/menu.js
@@ -116,6 +116,11 @@ function decreasePlaybackRate () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open the preferences window
|
||||||
|
function showPreferences () {
|
||||||
|
windows.main.send('dispatch', 'preferences')
|
||||||
|
}
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
log('onWindowShow')
|
log('onWindowShow')
|
||||||
getMenuItem('Full Screen').enabled = true
|
getMenuItem('Full Screen').enabled = true
|
||||||
@@ -269,6 +274,14 @@ function getAppMenuTemplate () {
|
|||||||
label: 'Select All',
|
label: 'Select All',
|
||||||
accelerator: 'CmdOrCtrl+A',
|
accelerator: 'CmdOrCtrl+A',
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => showPreferences()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -411,6 +424,14 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'Cmd+,',
|
||||||
|
click: () => showPreferences()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Services',
|
label: 'Services',
|
||||||
role: 'services',
|
role: 'services',
|
||||||
|
|||||||
@@ -911,6 +911,176 @@ video::-webkit-media-text-track-container {
|
|||||||
margin-right: 4px !important;
|
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
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -346,6 +346,25 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'exitModal') {
|
if (action === 'exitModal') {
|
||||||
state.modal = null
|
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') {
|
if (action === 'updateAvailable') {
|
||||||
updateAvailable(args[0] /* version */)
|
updateAvailable(args[0] /* version */)
|
||||||
}
|
}
|
||||||
@@ -523,6 +542,23 @@ function resumeTorrents () {
|
|||||||
.forEach((x) => startTorrentingSummary(x))
|
.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
|
// Don't write state.saved to file more than once a second
|
||||||
function saveStateThrottled () {
|
function saveStateThrottled () {
|
||||||
if (state.saveStateTimeout) return
|
if (state.saveStateTimeout) return
|
||||||
@@ -620,7 +656,7 @@ var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
|||||||
function addTorrent (torrentId) {
|
function addTorrent (torrentId) {
|
||||||
backToList()
|
backToList()
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
var path = state.saved.downloadPath
|
var path = state.saved.prefs.downloadPath
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
@@ -742,7 +778,7 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// Use Downloads folder by default
|
||||||
var path = s.path || state.saved.downloadPath
|
var path = s.path || state.saved.prefs.downloadPath
|
||||||
|
|
||||||
var torrentID
|
var torrentID
|
||||||
if (s.torrentFileName) { // Load torrent file from disk
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
@@ -1191,7 +1227,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||||
var opts = {
|
var opts = {
|
||||||
title: 'Save Torrent File',
|
title: 'Save Torrent File',
|
||||||
defaultPath: path.join(state.saved.downloadPath, newFileName),
|
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
|
|||||||
@@ -266,9 +266,11 @@ function getDefaultSavedState () {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
downloadPath: config.IS_PORTABLE
|
prefs: {
|
||||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
downloadPath: config.IS_PORTABLE
|
||||||
: remote.app.getPath('downloads')
|
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||||
|
: remote.app.getPath('downloads')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ var Header = require('./header')
|
|||||||
var Views = {
|
var Views = {
|
||||||
'home': require('./torrent-list'),
|
'home': require('./torrent-list'),
|
||||||
'player': require('./player'),
|
'player': require('./player'),
|
||||||
'create-torrent': require('./create-torrent-page')
|
'create-torrent': require('./create-torrent-page'),
|
||||||
|
'preferences': require('./preferences')
|
||||||
}
|
}
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function Header (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.location.url() !== 'player') {
|
if (state.location.url() === 'home') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
155
renderer/views/preferences.js
Normal file
155
renderer/views/preferences.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
module.exports = Preferences
|
||||||
|
|
||||||
|
var fs = require('fs-extra')
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
var remote = require('electron').remote
|
||||||
|
var dialog = remote.dialog
|
||||||
|
|
||||||
|
var prefState
|
||||||
|
|
||||||
|
function Preferences (state) {
|
||||||
|
prefState = state.unsaved.prefs
|
||||||
|
var definitions = getPreferenceDefinitions()
|
||||||
|
var sections = []
|
||||||
|
|
||||||
|
definitions.forEach(function (sectionDefinition) {
|
||||||
|
sections.push(renderSection(sectionDefinition))
|
||||||
|
})
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div class='preferences'>
|
||||||
|
${sections}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSection (definition) {
|
||||||
|
var controls = []
|
||||||
|
|
||||||
|
definition.controls.forEach(function (controlDefinition) {
|
||||||
|
controls.push(controlDefinition.renderer(controlDefinition))
|
||||||
|
})
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<section class='section preferences-panel'>
|
||||||
|
<div class='section-container'>
|
||||||
|
<div class='section-heading'><i.icon>${definition.icon}</i>${definition.title}</div>
|
||||||
|
<div class='help text'><i.icon>help_outline</i>${definition.description}</div>
|
||||||
|
<div class='section-body'>
|
||||||
|
${controls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFileSelector (definition) {
|
||||||
|
var value = getStateValue(definition.property)
|
||||||
|
return hx`
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<label class='control-label'>
|
||||||
|
<div class='preference-title'>${definition.label}</div>
|
||||||
|
<div class='preference-description'>${definition.description}</div>
|
||||||
|
</label>
|
||||||
|
<div class='controls'>
|
||||||
|
<input type='text' class='file-picker-text'
|
||||||
|
id=${definition.property} placeholder=${definition.placeholder}
|
||||||
|
readonly='readonly'
|
||||||
|
value=${value}/>
|
||||||
|
<button class='btn' onclick=${filePickerHandler}><i.icon>folder_open</i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
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`
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<div class='checkbox'>
|
||||||
|
<label class='control-label'>
|
||||||
|
<i.icon onclick=${checkboxHandler}>${checkbox}</i>
|
||||||
|
<div class='preference-title' onclick=${checkboxHandler}>${definition.label}</div>
|
||||||
|
</label>
|
||||||
|
<div class='preference-description'>${definition.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -142,7 +142,8 @@ function TorrentList (state) {
|
|||||||
// of the play button, unless already showing a spinner there:
|
// of the play button, unless already showing a spinner there:
|
||||||
var positionElem
|
var positionElem
|
||||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||||
var defaultFile = torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
var defaultFile = torrentSummary.files &&
|
||||||
|
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
||||||
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
||||||
var fraction = defaultFile.currentTime / defaultFile.duration
|
var fraction = defaultFile.currentTime / defaultFile.duration
|
||||||
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
|
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||||
|
|||||||
Reference in New Issue
Block a user