Merge pull request #66 from Stremio/settings-page

Settings page
This commit is contained in:
Nikola Hristov 2019-10-01 11:27:00 +03:00 committed by GitHub
commit d7ec4e5643
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 685 additions and 592 deletions

View file

@ -0,0 +1,62 @@
const React = require('react');
const PropTypes = require('prop-types');
const { Modal } = require('stremio-router');
const Button = require('stremio/common/Button');
const ColorPicker = require('stremio/common/ColorPicker');
const useBinaryState = require('stremio/common/useBinaryState');
const Icon = require('stremio-icons/dom');
const styles = require('./styles');
const ColorInput = ({ className, id, value, onChange }) => {
const [colorInputVisible, showColorInput, closeColorInput] = useBinaryState(false);
const [selectedColor, setSelectedColor] = React.useState(value);
const confirmColorInput = React.useCallback((event) => {
if(typeof onChange === "function") {
event.nativeEvent.value = selectedColor;
onChange(event);
}
closeColorInput();
}, [selectedColor, onChange]);
React.useEffect(() => {
setSelectedColor(value);
}, [value, colorInputVisible]);
const modalBackgroundOnClick = React.useCallback((event) => {
if(event.target === event.currentTarget) {
closeColorInput();
}
}, []);
return (
<React.Fragment>
<Button className={className} title={selectedColor} onClick={showColorInput} style={{ backgroundColor: value }}></Button>
{
colorInputVisible
?
<Modal className={styles['color-input-modal']} onMouseDown={modalBackgroundOnClick}>
<div className={styles['color-input-container']}>
<Button onClick={closeColorInput}>
<Icon className={styles['x-icon']} icon={'ic_x'} />
</Button>
<h1>Choose a color:</h1>
<ColorPicker className={styles['color-input']} value={selectedColor} onChange={setSelectedColor} />
<Button className={styles['button']} data-id={id} onClick={confirmColorInput}>Select</Button>
</div>
</Modal>
:
null
}
</React.Fragment>
);
};
ColorInput.propTypes = {
className: PropTypes.string,
id: PropTypes.string.isRequired,
value: PropTypes.string,
onChange: PropTypes.func
};
module.exports = ColorInput;

View file

@ -0,0 +1,4 @@
const ColorInput = require('./ColorInput');
module.exports = ColorInput;

View file

@ -0,0 +1,49 @@
.color-input-modal {
background-color: var(--color-backgrounddarker40);
display: flex;
flex-direction: column;
.color-input-container {
position: relative;
padding: 1rem;
background-color: var(--color-surfacelighter);
margin: auto;
* {
overflow: visible;
}
.x-icon {
position: absolute;
top: 1rem;
right: 1rem;
width: 1rem;
height: 1rem;
fill: var(--color-surfacedark);
}
h1 {
font-size: 1.2rem;
}
.color-input {
margin: 1rem auto 0 auto;
:global(.a-color-picker-stack):not(:global(.a-color-picker-row-top)) canvas, :global(.a-color-picker-circle) {
border: solid 1px var(--color-surfacedark);
}
:global(.a-color-picker-circle) {
box-shadow: 0 0 .2rem var(--color-surfacedark);
}
}
.button {
text-align: center;
color: var(--color-surfacelighter);
background-color: var(--color-signal5);
padding: 1rem;
margin-top: 1rem;
}
}
}

View file

@ -1,6 +1,7 @@
const Button = require('./Button');
const Checkbox = require('./Checkbox');
const ColorPicker = require('./ColorPicker');
const ColorInput = require('./ColorInput');
const Dropdown = require('./Dropdown');
const MainNavBar = require('./MainNavBar');
const MetaItem = require('./MetaItem');
@ -25,6 +26,7 @@ module.exports = {
Button,
Checkbox,
ColorPicker,
ColorInput,
Dropdown,
MainNavBar,
MetaItem,

View file

@ -0,0 +1,196 @@
const React = require('react');
const PropTypes = require('prop-types');
const { Button, Dropdown, Checkbox, ColorInput } = require('stremio/common');
const Icon = require('stremio-icons/dom');
const classnames = require('classnames');
const styles = require('./styles');
const SectionsList = React.forwardRef(({ className, sections, preferences, onPreferenceChanged, onScroll }, ref) => {
const toggleCheckbox = (id) => {
onPreferenceChanged(id, !preferences[id]);
};
const colorChanged = React.useCallback((event) => {
const id = event.currentTarget.dataset.id;
const color = event.nativeEvent.value;
onPreferenceChanged(id, color);
}, [onPreferenceChanged]);
const updateDropdown = React.useCallback((event) => {
var data = event.currentTarget.dataset;
onPreferenceChanged(data.name, data.value);
}, [onPreferenceChanged]);
const checkUser = React.useCallback((event) => {
if(! preferences.user) {
// Here in Stremio 4 we show a toast with a message, asking the anonymous user to log in/register
console.log('No user found');
event.preventDefault();
}
}, []);
// Determines whether the link should be opened in new window or in the current one.
const getTargetFor = url => ['//', 'http://', 'https://', 'file://', 'ftp://', 'mailto:', 'magnet:']
.some(scheme => url.startsWith(scheme)) ? '_blank' : '_self'
// TODO: If we get the user data after initialization, these should be wrapped in React.useState and set by React.useEffect
const changePasswordUrl = preferences.user && 'https://www.strem.io/reset-password/' + preferences.user.email;
const webCalUrl = preferences.user && 'webcal://www.strem.io/calendar/' + preferences.user._id + '.ics';
const sectionsElements = sections.map((section) =>
<div key={section.id} ref={section.ref} className={styles['section']} data-section={section.id}>
<div className={styles['section-header']}>{section.id}</div>
{(section.inputs || [])
.map((input) => {
if (input.type === 'user') {
return (
<React.Fragment key={'user'}>
<div className={classnames(styles['input-container'], styles['user-container'])}>
{
!preferences.user
?
<div style={{ backgroundImage: `url('/images/anonymous.png')` }} className={styles['avatar']} />
:
<div style={{ backgroundImage: `url('${preferences.user.avatar}'), url('/images/default_avatar.png')` }} className={styles['avatar']} />
}
<div className={styles['email']}>{!preferences.user ? 'Anonymous user' : preferences.user.email}</div>
</div>
<div className={classnames(styles['input-container'], styles['button-container'])}>
<Button className={styles['button']} href={'#/intro'}>
<div className={styles['label']}>{preferences.user ? 'LOG OUT' : 'SIGN IN'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={changePasswordUrl} target={'_blank'} onClick={checkUser}>
<div className={styles['label']}>{'Change password'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['text-container'])}>
<div className={styles['text']}>
<div className={styles['label']}>{'Import options'}</div>
</div>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={'https://www.stremio.com/#TODO:install-facebook-addon'} target={'_blank'} onClick={checkUser}>
<div className={styles['label']}>{'Import from Facebook'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={'https://www.stremio.com/#TODO:export-user-data'} target={'_blank'} onClick={checkUser}>
<div className={styles['label']}>{'Export user data'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={webCalUrl} target={'_blank'} onClick={checkUser}>
<div className={styles['label']}>{'Subscribe to calendar'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={'https://stremio.zendesk.com/'} target={'_blank'}>
<div className={styles['label']}>{'Contact support'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['link-container'])}>
<Button className={styles['link']} href={'https://docs.google.com/forms/d/e/1FAIpQLScubrlTpDMIPUUBlhZ5lwcXl3HxzKfunIMCX5Jnp-cDyglWjQ/viewform?usp=sf_link'} target={'_blank'}>
<div className={styles['label']}>{'Request account deletion'}</div>
</Button>
</div>
<div className={classnames(styles['input-container'], styles['button-container'])}>
<div className={styles['input-header']}>{'Trakt Scrobbling'}</div>
<Button className={styles['button']}>
<Icon className={styles['icon']} icon={'ic_trackt'} />
<div className={styles['label']}>{preferences.user && preferences.user.trakt ? 'ALREADY UTHENTIATED' : 'AUTHENTIATE'}</div>
</Button>
</div>
</React.Fragment>
);
} else if (input.type === 'select') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['select-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Dropdown options={input.options} selected={[preferences[input.id]]} name={input.id} key={input.id} className={styles['dropdown']} onSelect={updateDropdown} />
</div>
);
} else if (input.type === 'link') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['link-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Button ref={input.ref} className={styles['link']} href={input.href} target={getTargetFor(input.href)}>
<div className={styles['label']}>{input.label}</div>
</Button>
</div>
);
} else if (input.type === 'button') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['button-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Button ref={input.ref} className={styles['button']} href={input.href}>
{input.icon ? <Icon className={styles['icon']} icon={input.icon} /> : null}
<div className={styles['label']}>{input.label}</div>
</Button>
</div>
);
} else if (input.type === 'checkbox') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['checkbox-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Checkbox ref={input.ref} className={styles['checkbox']} checked={preferences[input.id]} onClick={toggleCheckbox.bind(null, input.id)}>
<div className={styles['label']}>{input.label}</div>
</Checkbox>
</div>
);
} else if (input.type === 'static-text') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['text-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<div className={styles['text']}>
{input.icon ? <Icon className={styles[input.icon === 'ic_x' ? 'x-icon' : 'icon']} icon={input.icon} /> : null}
<div className={styles['label']}>{input.label}</div>
</div>
</div>
);
} else if (input.type === 'color') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['color-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<ColorInput className={styles['color-picker']} id={input.id} value={preferences[input.id]} onChange={colorChanged} />
</div>
);
}
})}
</div>
);
return (
<div ref={ref} className={className} onScroll={onScroll}>
{sectionsElements}
</div>
);
});
SectionsList.propTypes = {
className: PropTypes.string,
sections: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
ref: PropTypes.shape({
current: PropTypes.object,
}).isRequired,
inputs: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.string.isRequired,
id: PropTypes.string,
header: PropTypes.string,
label: PropTypes.string,
icon: PropTypes.string,
href: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
})),
})),
})),
preferences: PropTypes.object,
onPreferenceChanged: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
};
module.exports = SectionsList;

View file

@ -0,0 +1,3 @@
const SectionsList = require('./SectionsList');
module.exports = SectionsList;

View file

@ -0,0 +1,181 @@
:import('~stremio/common/Checkbox/styles.less') {
checkbox-icon: icon;
}
.section {
padding: 4rem 2rem;
.section-header {
margin: 0 1.5rem 1.5rem 1.5rem;
font-size: 2rem;
color: var(--color-surfacelighter);
}
.input-container {
margin: 1.5rem;
display: flex;
flex-direction: column;
.input-header {
margin-bottom: 0.5rem;
color: var(--color-surfacelighter);
}
.checkbox-icon {
flex: none;
width: 1.2rem;
height: 1.2rem;
fill: var(--color-surfacelight);
}
&.user-container {
flex-direction: row;
align-items: center;
.avatar {
margin: 0 1rem;
width: 4.2rem;
height: 4.2rem;
border-radius: 50%;
border: var(--focusable-border-size) solid var(--color-primary);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.email {
color: var(--color-surfacelighter);
}
}
&.select-container {
.dropdown {
height: 3rem;
width: var(--input-width);
}
}
&.link-container {
margin: 1rem 1.5rem;
.link {
display: block;
color: var(--color-secondarylight);
&:focus {
color: var(--color-surface);
}
&:hover {
color: var(--color-surfacelighter);
}
}
}
&.button-container {
.button {
padding: 0.7rem;
width: var(--input-width);
min-height: calc(var(--input-width) * 0.09);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-primary);
cursor: pointer;
.icon {
width: 1.4rem;
height: 100%;
margin-right: 0.5rem;
fill: var(--color-surfacelighter);
}
.label {
color: var(--color-surfacelighter);
}
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
border-color: transparent;
background-color: var(--color-primarylight);
}
}
}
&.checkbox-container {
.checkbox {
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
.label {
width: 100%;
margin-left: 0.5rem;
color: var(--color-surfacelight);
}
&:focus,
&:hover {
.checkbox-icon {
fill: var(--color-surfacelighter);
}
.label {
color: var(--color-surfacelighter);
}
}
}
}
&.text-container {
.text {
display: flex;
flex-direction: row;
align-items: center;
.icon {
margin-right: 0.5rem;
width: 1rem;
height: 1rem;
fill: var(--color-signal5);
}
.x-icon {
margin-right: 0.5rem;
width: 1rem;
height: 1rem;
fill: var(--color-signal2);
}
.label {
width: 100%;
color: var(--color-surfacelighter);
}
}
}
&.color-container {
.color-picker {
width: var(--input-width);
height: calc(var(--input-width) * 0.08);
cursor: pointer;
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
border-color: transparent;
}
}
}
}
>:last-child {
margin-bottom: 0;
}
}

View file

@ -0,0 +1,28 @@
const React = require('react');
const PropTypes = require('prop-types');
const { Button } = require('stremio/common');
const classnames = require('classnames');
const styles = require('./styles');
const SectionsSelector = ({ className, sections, selectedSectionId, onSelectedSection }) => {
return (
<div className={className}>
{sections.map((section) =>
<Button key={section.id} className={classnames(styles['section-label'], { [styles['selected']]: selectedSectionId === section.id })} type={'button'} data-section={section.id} onClick={onSelectedSection}>
{section.id}
</Button>
)}
</div>
);
};
SectionsSelector.propTypes = {
className: PropTypes.string,
sections: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
})),
selectedSectionId: PropTypes.string.isRequired,
onSelectedSection: PropTypes.func.isRequired,
};
module.exports = SectionsSelector;

View file

@ -0,0 +1,3 @@
const SectionsSelector = require('./SectionsSelector');
module.exports = SectionsSelector;

View file

@ -0,0 +1,26 @@
.section-label {
padding: 1rem;
font-size: 1.1rem;
border: calc(var(--focusable-border-size) * 0.5) solid transparent;
color: var(--color-surfacelight);
cursor: pointer;
&.selected {
color: var(--color-surfacelighter);
background-color: var(--color-background);
&:hover {
background-color: var(--color-background);
}
}
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
color: var(--color-surfacelighter);
background-color: var(--color-surface20);
border-color: transparent;
}
}

View file

@ -1,312 +1,68 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Icon from 'stremio-icons/dom';
import { Input, Popup, Checkbox } from 'stremio/common';
import classnames from 'classnames';
import styles from './styles';
const React = require('react');
const { NavBar } = require('stremio/common');
const styles = require('./styles');
const SectionsSelector = require('./SectionsSelector');
const SectionsList = require('./SectionsList');
const { settingsSections } = require('./constants');
const useSettings = require('./useSettings');
const SECTIONS_ORDER = {
'General': 1,
'Player': 2,
'Streaming': 3
};
const devTestWithUser = true;
class Settings extends Component {
constructor(props) {
super(props);
const Settings = () => {
const [preferences, setPreferences] = useSettings(devTestWithUser);
const sections = React.useMemo(()=>Object.keys(settingsSections)
.map((section) => ({
id: section,
inputs: settingsSections[section],
ref: React.createRef()
})), []);
const [selectedSectionId, setSelectedSectionId] = React.useState(sections[0].id);
const scrollContainerRef = React.useRef(null);
this.scrollContainerRef = React.createRef();
/////////////////
this.state = {
selectedSectionId: null,
sections: [],
inputs: []
};
const updatePreference = (option, value) => {
setPreferences({ ...preferences, [option]: value });
}
componentDidMount() {
this.settingsOnUpdate([
{ section: 'General', label: 'Username', type: 'user', avatar: '', email: '' },
{ section: 'General', label: 'LOG OUT', type: 'button' },
{ section: 'General', label: 'Change password', type: 'link', href: '' },
{ section: 'General', label: 'Import options', type: 'static-text' },
{ section: 'General', label: 'Import from Facebook', type: 'link', href: '' },
{ section: 'General', label: 'Export user data', type: 'link', href: '' },
{ section: 'General', label: 'Subscribe to calendar', type: 'link', href: '' },
{ section: 'General', label: 'Contact support', type: 'link', href: 'https://stremio.zendesk.com/' },
{ section: 'General', label: 'Request account deletion', type: 'link', href: 'https://docs.google.com/forms/d/e/1FAIpQLScubrlTpDMIPUUBlhZ5lwcXl3HxzKfunIMCX5Jnp-cDyglWjQ/viewform?usp=sf_link' },
{ section: 'General', header: 'Trakt Scrobbling', label: 'AUTHENTICATE', type: 'button', icon: 'ic_trackt' },
{ section: 'General', header: 'UI Language', label: 'UI Language', type: 'select', options: ['Български език', 'English', 'Deutsch', 'Español', 'Italiano'], value: 'English' },
{ section: 'Player', label: 'ADD-ONS', type: 'button', icon: 'ic_addons' },
{ section: 'Player', header: 'Default Subtitles Language', label: 'Default Subtitles Language', type: 'select', options: ['English', 'Nederlands', 'Avesta', 'Български език', 'Deutsch', 'Español', 'Italiano'], value: 'English' },
{ section: 'Player', header: 'Default Subtitles Size', label: 'Default Subtitles Size', type: 'select', options: ['72%', '80%', '100%', '120%', '140%', '160%', '180%'], value: '100%' },
{ section: 'Player', header: 'Subtitles Background', label: 'Subtitles background', type: 'select', options: ['None', 'Solid', 'Transparent'], value: 'None' },
{ section: 'Player', header: 'Subtitles color', label: 'Subtitles color', type: 'color', color: '#FFFFFF' },
{ section: 'Player', header: 'Subtitles outline color', label: 'Subtitles outline color', type: 'color', color: '#000000' },
{ section: 'Player', label: 'Auto-play next episode', type: 'checkbox', value: true },
{ section: 'Player', label: 'Pause playback when minimized', type: 'checkbox', value: false },
{ section: 'Player', label: 'Hardware-accelerated decoding', type: 'checkbox', value: true },
{ section: 'Player', label: 'Launch player in a separate window (advanced)', type: 'checkbox', value: true },
{ section: 'Streaming', header: 'Caching', label: 'Caching', type: 'select', options: ['No Caching', '2GB', '5GB', '10GB'], value: '2GB' },
{ section: 'Streaming', header: 'Torrent Profile', label: 'Torrent Profile', type: 'select', options: ['Default', 'Soft', 'Fast'], value: 'Default' },
{ section: 'Streaming', header: 'Streaming server URL: http://127.0.0.1:11470', label: 'Streaming server is available.', type: 'static-text', icon: 'ic_check' }
]);
}
settingsOnUpdate = (settings) => {
this.setState(({ selectedSectionId, inputs }) => {
const sections = settings.map(({ section }) => section)
.filter((section, index, sections) => sections.indexOf(section) === index)
.sort(function(a, b) {
const valueA = SECTIONS_ORDER[a];
const valueB = SECTIONS_ORDER[b];
if (!isNaN(valueA) && !isNaN(valueB)) return valueA - valueB;
if (!isNaN(valueA)) return -1;
if (!isNaN(valueB)) return 1;
return a - b;
})
.map((section) => ({
id: section,
ref: React.createRef()
}));
var sectionId = null;
if (selectedSectionId !== null && sections.find(({ id }) => id === selectedSectionId)) {
sectionId = selectedSectionId
} else if (sections.length > 0) {
sectionId = sections[0].id
}
return {
selectedSectionId: sectionId,
sections: sections,
inputs: settings.map((setting) => ({
...setting,
id: setting.label,
ref: React.createRef(),
active: !!(inputs.find(({ id }) => id === setting.label) || {}).active
}))
}
const changeSection = React.useCallback((event) => {
const currentSectionId = event.currentTarget.dataset.section;
const section = sections.find((section) => section.id === currentSectionId);
//setSelectedSectionId(currentSectionId);
scrollContainerRef.current.scrollTo({
top: section.ref.current.offsetTop,
behavior: 'smooth'
});
}
}, [sections]);
changeSection = (event) => {
this.setState({ selectedSectionId: event.currentTarget.dataset.section }, () => {
const section = this.state.sections.find((section) => section.id === this.state.selectedSectionId);
this.scrollContainerRef.current.scrollTo({
top: section.ref.current.offsetTop
});
});
}
onScroll = () => {
if (this.state.sections.length <= 0) {
return;
}
if (this.scrollContainerRef.current.scrollTop + this.scrollContainerRef.current.clientHeight === this.scrollContainerRef.current.scrollHeight) {
this.setState({ selectedSectionId: this.state.sections[this.state.sections.length - 1].id });
const sectionListOnScorll = React.useCallback((event) => {
const scrollContainer = event.currentTarget;
if (scrollContainer.scrollTop + scrollContainer.clientHeight === scrollContainer.scrollHeight) {
setSelectedSectionId(sections[sections.length - 1].id);
} else {
for (let i = this.state.sections.length - 1; i >= 0; i--) {
if (this.state.sections[i].ref.current.offsetTop <= this.scrollContainerRef.current.scrollTop) {
this.setState({ selectedSectionId: this.state.sections[i].id });
for (let i = sections.length - 1;i >= 0;i--) {
if (sections[i].ref.current.offsetTop <= scrollContainer.scrollTop) {
setSelectedSectionId(sections[i].id);
break;
}
}
}
}
}, [sections]);
shouldComponentUpdate(nextProps, nextState) {
return nextState.selectedSectionId !== this.state.selectedSectionId ||
nextState.sections !== this.state.sections ||
nextState.inputs !== this.state.inputs;
}
componentDidUpdate(prevProps, prevState) {
this.state.inputs.forEach((input) => {
input.ref.current && !input.active && input.ref.current.close && input.ref.current.close();
});
}
activate = (id) => {
this.setState(({ inputs }) => ({
inputs: inputs.map((input) => ({
...input,
active: id === input.id
}))
}));
}
deactivate = (id) => {
this.setState(({ inputs }) => ({
inputs: inputs.map((input) => ({
...input,
active: id === input.id ? false : input.active
}))
}));
}
toggleCheckbox = (id) => {
this.setState(({ inputs }) => ({
inputs: inputs.map((input) => ({
...input,
value: id === input.id ? !input.value : input.value
}))
}));
}
onChange = (event) => {
var data = event.currentTarget.dataset;
this.setState(({ inputs }) => {
return {
inputs: inputs.map((input) => ({
...input,
value: data.id === input.id ? data.option : input.value,
active: false
}))
}
})
}
renderPopup({ ref, activate, deactivate, active, value, id, options, onClick }) {
return (
<Popup ref={ref} className={'popup-container'} onOpen={activate.bind(null, id)} onClose={deactivate.bind(null, id)}>
<Popup.Label>
<Input className={classnames(styles['bar-button'], { 'active': active })} type={'button'}>
<div className={styles['value']}>{value}</div>
<Icon className={styles['icon']} icon={'ic_arrow_down'} />
</Input>
</Popup.Label>
<Popup.Menu>
<div className={styles['popup-content']}>
{options.map((option) =>
<div key={option} className={classnames(styles['option'], { [styles['selected']]: value === option })} data-option={option} data-id={id} onClick={onClick}>{option}</div>
)}
</div>
</Popup.Menu>
</Popup>
);
}
render() {
return (
return (
<div className={styles['settings-parent-container']}>
<NavBar
className={styles['nav-bar']}
backButton={true}
addonsButton={true}
fullscreenButton={true}
navMenu={true} />
<div className={styles['settings-container']}>
<div className={styles['side-menu']}>
{this.state.sections.map((section) =>
<Input key={section.id} className={classnames(styles['section-label'], { [styles['selected']]: this.state.selectedSectionId === section.id })} type={'button'} data-section={section.id} onClick={this.changeSection}>
{section.id}
</Input>
)}
</div>
<div ref={this.scrollContainerRef} className={styles['scroll-container']} onScroll={this.onScroll}>
{this.state.sections.map((section) =>
<div key={section.id} ref={section.ref} className={styles['section']} data-section={section.id}>
<div className={styles['section-header']}>{section.id}</div>
{this.state.inputs
.filter((input) => input.section === section.id)
.map((input) => {
if (input.type === 'user') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['user-container'])}>
{
input.email.length === 0
?
<div style={{ backgroundImage: `url('/images/anonymous.png')` }} className={styles['avatar']} />
:
<div style={{ backgroundImage: `url('${this.props.avatar}'), url('/images/default_avatar.png')` }} className={styles['avatar']} />
}
<div className={styles['email']}>{input.email.length === 0 ? 'Anonymous user' : input.email}</div>
</div>
);
} else if (input.type === 'select') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['select-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
{this.renderPopup({
ref: input.ref,
activate: this.activate,
deactivate: this.deactivate,
active: input.active,
id: input.id,
value: input.value,
options: input.options,
onClick: this.onChange
})}
</div>
);
} else if (input.type === 'link') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['link-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Input ref={input.ref} className={styles['link']} type={input.type} href={input.href} target={'_blank'}>{input.label}</Input>
</div>
);
} else if (input.type === 'button') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['button-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Input ref={input.ref} className={styles['button']} type={input.type}>
{input.icon ? <Icon className={styles['icon']} icon={input.icon} /> : null}
<div className={styles['label']}>{input.label}</div>
</Input>
</div>
);
} else if (input.type === 'checkbox') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['checkbox-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Checkbox ref={input.ref} className={styles['checkbox']} checked={input.value} onClick={this.toggleCheckbox.bind(null, input.label)}>
<div className={styles['label']}>{input.label}</div>
</Checkbox>
</div>
);
} else if (input.type === 'static-text') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['text-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<div className={styles['text']}>
{input.icon ? <Icon className={styles[input.icon === 'ic_x' ? 'x-icon' : 'icon']} icon={input.icon} /> : null}
<div className={styles['label']}>{input.label}</div>
</div>
</div>
);
} else if (input.type === 'color') {
return (
<div key={input.id} className={classnames(styles['input-container'], styles['color-container'])}>
{input.header ? <div className={styles['input-header']}>{input.header}</div> : null}
<Input className={styles['color-picker']} type={input.type} defaultValue={input.color} tabIndex={'-1'} />
</div>
);
}
})}
</div>
)}
</div>
<SectionsSelector className={styles['side-menu']} sections={sections} selectedSectionId={selectedSectionId} onSelectedSection={changeSection} />
<SectionsList ref={scrollContainerRef} className={styles['scroll-container']} sections={sections} preferences={preferences} onPreferenceChanged={updatePreference} onScroll={sectionListOnScorll} />
</div>
);
}
}
</div>
);
};
Settings.propTypes = {
settingsConfiguration: PropTypes.arrayOf(PropTypes.shape({
section: PropTypes.string.isRequired,
header: PropTypes.string,
label: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
avatar: PropTypes.string,
email: PropTypes.string,
href: PropTypes.string,
icon: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.string),
value: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string
])
})).isRequired
}
Settings.defaultProps = {
settingsConfiguration: []
}
export default Settings;
module.exports = Settings;

View file

@ -0,0 +1,27 @@
const settingsSections = {
'General': [
{ 'type': 'user' },
{ 'header': 'UI Language', 'label': 'UI Language', 'type': 'select', 'options': [{ 'label': 'Български език', 'value': 'bul' }, { 'label': 'English', 'value': 'eng' }, { 'label': 'Deutsch', 'value': 'ger' }, { 'label': 'Español', 'value': 'esp' }, { 'label': 'Italiano', 'value': 'ita' }], 'id': 'ui_language' },
],
'Player': [
{ 'label': 'ADD-ONS', 'type': 'button', 'icon': 'ic_addons', 'id': 'add-ons', 'href': '#/addons' },
{ 'header': 'Default Subtitles Language', 'label': 'Default Subtitles Language', 'type': 'select', 'options': [{ 'label': 'Български език', 'value': 'bul' }, { 'label': 'English', 'value': 'eng' }, { 'label': 'Deutsch', 'value': 'ger' }, { 'label': 'Español', 'value': 'esp' }, { 'label': 'Italiano', 'value': 'ita' }], 'id': 'default_subtitles_language' },
{ 'header': 'Default Subtitles Size', 'label': 'Default Subtitles Size', 'type': 'select', 'options': [{ 'label': '72%', 'value': '72%' }, { 'label': '80%', 'value': '80%' }, { 'label': '100%', 'value': '100%' }, { 'label': '120%', 'value': '120%' }, { 'label': '140%', 'value': '140%' }, { 'label': '160%', 'value': '160%' }, { 'label': '180%', 'value': '180%' }], 'id': 'default_subtitles_size' },
{ 'header': 'Subtitles Background', 'label': 'Subtitles background', 'type': 'select', 'options': [{ 'label': 'None', 'value': '' }, { 'label': 'Solid', 'value': 'solid' }, { 'label': 'Transparent', 'value': 'transparent' }], 'id': 'subtitles_background' },
{ 'header': 'Subtitles color', 'label': 'Subtitles color', 'type': 'color', 'id': 'subtitles_color' },
{ 'header': 'Subtitles outline color', 'label': 'Subtitles outline color', 'type': 'color', 'id': 'subtitles_outline_color' },
{ 'label': 'Auto-play next episode', 'type': 'checkbox', 'id': 'auto-play_next_episode' },
{ 'label': 'Pause playback when minimized', 'type': 'checkbox', 'id': 'pause_playback_when_minimized' },
{ 'label': 'Hardware-accelerated decoding', 'type': 'checkbox', 'id': 'hardware-accelerated_decoding' },
{ 'label': 'Launch player in a separate window (advanced)', 'type': 'checkbox', 'id': 'launch_player_in_a_separate_window_(advanced)' },
],
'Streaming': [
{ 'header': 'Caching', 'label': 'Caching', 'type': 'select', 'options': [{ 'label': 'No Caching', 'value': '' }, { 'label': '2GB', 'value': '2048' }, { 'label': '5GB', 'value': '5120' }, { 'label': '10GB', 'value': '10240' }], 'id': 'caching' },
{ 'header': 'Torrent Profile', 'label': 'Torrent Profile', 'type': 'select', 'options': [{ 'label': 'Default', 'value': 'profile-default' }, { 'label': 'Soft', 'value': 'profile-soft' }, { 'label': 'Fast', 'value': 'profile-fast' }], 'id': 'torrent_profile' },
{ 'header': 'Streaming server URL: http://127.0.0.1:11470', 'label': 'Streaming server is available.', 'type': 'static-text', 'icon': 'ic_check', 'id': 'streaming_server_is_available.' }
]
};
module.exports = {
settingsSections,
};

View file

@ -1,3 +1,3 @@
import Settings from './Settings';
const Settings = require('./Settings');
export default Settings;
module.exports = Settings;

View file

@ -1,307 +1,41 @@
.settings-container, :global(.popup-container) {
--spacing: 16px;
--input-width: 500px;
font-size: 14px;
}
.settings-container {
.settings-parent-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
background-color: var(--color-background);
.side-menu {
padding: var(--spacing);
width: 17em;
display: flex;
flex-direction: column;
background-color: var(--color-backgroundlighter);
.section-label {
padding: var(--spacing);
font-size: 1.1em;
border: calc(var(--focusable-border-size) * 0.5) solid transparent;
color: var(--color-surfacelight);
cursor: pointer;
&.selected {
color: var(--color-surfacelighter);
background-color: var(--color-background);
&:hover {
background-color: var(--color-background);
}
}
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
color: var(--color-surfacelighter);
background-color: var(--color-surface20);
border-color: transparent;
}
}
.nav-bar {
flex: none;
align-self: stretch;
}
.scroll-container {
padding: 0 calc(var(--spacing) * 2);
flex: 1;
overflow-y: auto;
.settings-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
background-color: var(--color-backgroundlight);
--input-width: 35rem;
.side-menu {
padding: 1rem;
width: 17rem;
display: flex;
flex-direction: column;
background-color: var(--color-backgroundlighter);
.section {
padding: calc(var(--spacing) * 4) calc(var(--spacing) * 2);
.section-header {
margin: 0 calc(var(--spacing) * 1.5) calc(var(--spacing) * 1.5) calc(var(--spacing) * 1.5);
font-size: 2em;
color: var(--color-surfacelighter);
}
.input-container {
margin: calc(var(--spacing) * 1.5);
display: flex;
flex-direction: column;
.input-header {
margin-bottom: calc(var(--spacing) * 0.5);
color: var(--color-surfacelighter);
}
&.user-container {
flex-direction: row;
align-items: center;
.avatar {
margin: 0 var(--spacing);
width: 4.2em;
height: 4.2em;
border-radius: 50%;
border: var(--focusable-border-size) solid var(--color-primary);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.email {
color: var(--color-surfacelighter);
}
}
&.select-container {
.bar-button {
padding: calc(var(--spacing) * 0.5);
width: var(--input-width);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: calc(var(--focusable-border-size) * 0.5) solid var(--color-primary);
cursor: pointer;
.value {
width: 100%;
color: var(--color-surface);
}
.icon {
width: var(--spacing);
height: var(--spacing);
fill: var(--color-surface);
}
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
background-color: var(--color-backgroundlight);
.value {
color: var(--color-surfacelighter);
}
.icon {
fill: var(--color-surfacelighter);
}
}
&:global(.active) {
background-color: var(--color-surfacelighter);
.value {
color: var(--color-backgrounddarker);
}
.icon {
fill: var(--color-backgrounddarker);
}
}
}
}
&.link-container {
margin: var(--spacing) calc(var(--spacing) * 1.5);
.link {
display: block;
color: var(--color-secondarylight);
&:focus {
color: var(--color-surface);
}
&:hover {
color: var(--color-surfacelighter);
}
}
}
&.button-container {
.button {
padding: calc(var(--spacing) * 0.7);
width: var(--input-width);
min-height: calc(var(--input-width) * 0.09);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-primary);
border: calc(var(--focusable-border-size) * 0.5) solid transparent;
cursor: pointer;
.icon {
width: 1.4em;
height: 100%;
margin-right: calc(var(--spacing) * 0.5);
fill: var(--color-surfacelighter);
}
.label {
max-width: 30em;
color: var(--color-surfacelighter);
}
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
border-color: transparent;
background-color: var(--color-primarylight);
}
}
}
&.checkbox-container {
.checkbox {
--icon-size: 1.2em;
--icon-color: var(--color-surface);
--icon-background-color: transparent;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
.label {
width: 100%;
margin-left: 0.5em;
color: var(--color-surfacelight);
}
&:global(.checked) {
--icon-color: var(--color-surfacelight);
--icon-background-color: var(--color-primary);
}
&:focus, &:hover {
--icon-color: var(--color-surfacelighter);
.label {
color: var(--color-surfacelighter);
}
}
}
}
&.text-container {
.text {
display: flex;
flex-direction: row;
align-items: center;
.icon {
margin-right: calc(var(--spacing) * 0.5);
width: var(--spacing);
height: var(--spacing);
fill: var(--color-signal5);
}
.x-icon {
margin-right: calc(var(--spacing) * 0.5);
width: var(--spacing);
height: var(--spacing);
fill: var(--color-signal2);
}
.label {
width: 100%;
color: var(--color-surfacelighter);
}
}
}
&.color-container {
.color-picker {
width: var(--input-width);
height: calc(var(--input-width) * 0.08);
border: calc(var(--focusable-border-size) * 0.5) solid transparent;
cursor: pointer;
&:focus {
border-color: var(--color-surfacelighter);
}
&:hover {
border-color: transparent;
}
}
}
}
>:last-child {
margin-bottom: 0;
}
}
>:not(:last-child) {
border-bottom: calc(var(--focusable-border-size) * 0.5) solid var(--color-primary);
.scroll-container {
padding: 0 2rem;
flex: 1;
overflow-y: auto;
>:not(:last-child) {
border-bottom: calc(var(--focusable-border-size) * 0.5) solid var(--color-primary);
}
}
}
}
:global(.popup-container) {
.popup-content {
width: var(--input-width);
border: calc(var(--focusable-border-size) * 0.5) solid var(--color-primary);
background-color: var(--color-surfacelighter);
.option {
padding: calc(var(--spacing) * 0.5);
width: 100%;
color: var(--color-backgrounddarker);
cursor: pointer;
&.selected {
color: var(--color-surfacelighter);
background-color: var(--color-primarydark);
}
&:hover {
color: var(--color-surfacelighter);
background-color: var(--color-primary);
}
}
}
}

View file

@ -0,0 +1,22 @@
const React = require('react');
module.exports = (devTestWithUser) => React.useState({
"user": devTestWithUser ? {
"_id": "neo",
"email": "neo@example.com",
"avatar": "https://www.thenational.ae/image/policy:1.891803:1566372420/AC17-Matrix-20-04.jpg?f=16x9&w=1200&$p$f$w=5867e40",
} : null,
"ui_language": "eng",
"default_subtitles_language": "bul",
"default_subtitles_size": "100%",
"subtitles_background": "",
"subtitles_color": "#ffffff",
"subtitles_outline_color": "#000",
"auto-play_next_episode": true,
"pause_playback_when_minimized": false,
"hardware-accelerated_decoding": true,
"launch_player_in_a_separate_window_(advanced)": true,
"caching": "2048",
"torrent_profile": "profile-default",
"streaming_server_is_available.": true,
});