-
+
Add addon
- {dropdowns.map((dropdown) => (
-
+ {dropdowns.map((dropdown, index) => (
+
))}
@@ -46,21 +56,107 @@ const Addons = ({ urlParams, queryParams }) => {
/>
-
+
{
- addons.filter(({ name }) => query.length === 0 || (typeof name === 'string' && name.includes(query)))
- .map((addon) => (
-
- ))
+ error !== null ?
+
+ {error.type}{error.type === 'Other' ? ` - ${error.content}` : null}
+
+ :
+ Array.isArray(addons) ?
+ addons.filter((addon) => query.length === 0 ||
+ ((typeof addon.manifest.name === 'string' && addon.manifest.name.toLowerCase().includes(query.toLowerCase())) ||
+ (typeof addon.manifest.description === 'string' && addon.manifest.description.toLowerCase().includes(query.toLowerCase()))
+ ))
+ .map((addon, index) => (
+
setSelectedAddon(addon.transportUrl)}
+ onShareButtonClicked={() => setSharedAddon(addon)}
+ />
+ ))
+ :
+
+ Loading
+
}
+ {
+ addAddonModalOpened ?
+
setAddAddonModalOpened(false)
+ }
+ },
+ {
+ label: 'Add',
+ props: {
+ title: 'Add',
+ onClick: onAddButtonClicked
+ }
+ }
+ ]}
+ onCloseRequest={() => setAddAddonModalOpened(false)}
+ >
+
+
+ :
+ null
+ }
{
selectedAddon !== null ?
-
-
-
+
+
+
+ :
+ null
+ }
+ {
+ sharedAddon !== null ?
+
setSharedAddon(null)}>
+ setSharedAddon(null)}
+ />
+
:
null
}
diff --git a/src/routes/Addons/styles.less b/src/routes/Addons/styles.less
index 5ba0a6f00..d97a130ea 100644
--- a/src/routes/Addons/styles.less
+++ b/src/routes/Addons/styles.less
@@ -21,6 +21,7 @@
display: flex;
flex-direction: row;
margin: 2rem;
+ overflow: visible;
.add-button-container {
flex: none;
@@ -112,29 +113,41 @@
width: 100%;
margin-bottom: 2rem;
}
+
+ .message-container {
+ padding: 0 2rem;
+ font-size: 2rem;
+ color: var(--color-surfacelighter);
+ }
}
}
}
-.addon-prompt-modal-container {
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: var(--color-background60);
+.add-addon-prompt-container {
+ width: 30rem;
- .addon-prompt-container {
- flex: none;
- display: flex;
- flex-direction: column;
- justify-content: center;
- width: 50rem;
- height: 80%;
-
- .addon-prompt {
- flex-grow: 0;
- flex-shrink: 1;
- flex-basis: auto;
- align-self: stretch;
- }
+ .url-content {
+ flex: 1;
+ width: 100%;
+ padding: 0.5rem;
+ font-size: 0.9rem;
+ color: var(--color-surfacedark);
+ border: thin solid var(--color-surface);
}
+
+ .cancel-button {
+ background-color: var(--color-surfacedark);
+ }
+}
+
+.addon-prompt-container {
+ width: 50rem;
+
+ .cancel-button {
+ background-color: var(--color-surfacedark);
+ }
+}
+
+.share-prompt-container {
+ width: 30rem;
}
\ No newline at end of file
diff --git a/src/routes/Addons/useAddons.js b/src/routes/Addons/useAddons.js
index 3435c0dae..154af9940 100644
--- a/src/routes/Addons/useAddons.js
+++ b/src/routes/Addons/useAddons.js
@@ -1,72 +1,129 @@
const React = require('react');
+const { useServices } = require('stremio/services');
-const CATEGORIES = ['official', 'community', 'my'];
-const DEFAULT_CATEGORY = 'community';
-const DEFAULT_TYPE = 'all';
+const DEFAULT_TYPE = 'movie';
+const DEFAULT_CATEGORY = 'thirdparty';
-const useAddons = (category, type) => {
- category = CATEGORIES.includes(category) ? category : DEFAULT_CATEGORY;
- type = typeof type === 'string' && type.length > 0 ? type : DEFAULT_TYPE;
- const addons = React.useMemo(() => {
- return [
- {
- id: 'com.linvo.cinemeta',
- name: 'Cinemeta',
- description: 'The official add-on for movie and series catalogs',
- types: ['movie', 'series'],
- version: '2.12.1',
- transportUrl: 'https://v3-cinemeta.strem.io/manifest.json',
- installed: true,
- official: true
- },
- {
- id: 'com.linvo.cinemeta2',
- name: 'Cinemeta2',
- logo: '/images/intro_background.jpg',
- description: 'The official add-on for movie and series catalogs',
- types: ['movie', 'series'],
- version: '2.12.2',
- transportUrl: 'https://v2-cinemeta.strem.io/manifest.json',
- installed: false,
- official: false
+const useAddons = (urlParams, queryParams) => {
+ const { core } = useServices();
+ const [addons, setAddons] = React.useState([[], [], [], [], null]);
+ const installAddon = React.useCallback(descriptor => {
+ core.dispatch({
+ action: 'AddonOp',
+ args: {
+ addonOp: 'Install',
+ args: descriptor
}
- ];
+ });
}, []);
- const onSelect = React.useCallback((event) => {
- const { name, value } = event.currentTarget.dataset;
- if (name === 'category') {
- const nextCategory = CATEGORIES.includes(value) ? value : '';
- window.location.replace(`#/addons/${nextCategory}/${type}`);
- } else if (name === 'type') {
- const nextType = typeof value === 'string' ? value : '';
- window.location.replace(`#/addons/${category}/${nextType}`);
- }
- }, [category, type]);
- const categoryDropdown = React.useMemo(() => {
- const selected = CATEGORIES.includes(category) ? [category] : [];
- const options = CATEGORIES
- .map((category) => ({ label: category, value: category }));
- return {
- name: 'category',
- selected,
- options,
- onSelect
+ const uninstallAddon = React.useCallback(descriptor => {
+ core.dispatch({
+ action: 'AddonOp',
+ args: {
+ addonOp: 'Remove',
+ args: {
+ transport_url: descriptor.transportUrl
+ }
+ }
+ });
+ }, []);
+ React.useEffect(() => {
+ const type = typeof urlParams.type === 'string' && urlParams.type.length > 0 ? urlParams.type : DEFAULT_TYPE;
+ const category = typeof urlParams.category === 'string' && urlParams.category.length > 0 ? urlParams.category : DEFAULT_CATEGORY;
+ const onNewState = () => {
+ const state = core.getState();
+ [...new Set(
+ ['all'].concat(...state.ctx.content.addons.map(addon => addon.manifest.types))
+ )]
+ .map((type) => (
+ {
+ is_selected: urlParams.category === 'my' && urlParams.type === type,
+ name: 'my',
+ load: {
+ base: 'https://v3-cinemeta.strem.io/manifest.json',
+ path: {
+ resource: 'addon_catalog',
+ type_name: type,
+ id: 'my',
+ extra: []
+ }
+ }
+ })
+ )
+ .forEach(addon => state.addons.catalogs.push(addon));
+ const selectAddon = (transportUrl) => {
+ window.location = `#/addons/${category}/${type}?addon=${transportUrl}`;
+ };
+ const selectInputs = [
+ {
+ selected: state.addons.catalogs
+ .filter(({ is_selected }) => is_selected)
+ .map(({ load }) => load.path.id),
+ options: state.addons.catalogs
+ .filter((catalog, index, catalogs) => {
+ return catalogs.map(ctg => ctg.name).indexOf(catalog.name) === index;
+ })
+ .map(({ name, load }) => ({
+ value: load.path.id,
+ label: name
+ })),
+ onSelect: (event) => {
+ const load = state.addons.catalogs.find(({ load: { path: { id } } }) => {
+ return id === event.value;
+ }).load;
+ window.location = `#/addons/${encodeURIComponent(load.path.id)}/${encodeURIComponent(load.path.type_name)}`;
+ }
+ },
+ {
+ selected: state.addons.catalogs
+ .filter(({ is_selected }) => is_selected)
+ .map(({ load }) => JSON.stringify(load)),
+ options: state.addons.catalogs
+ .filter(({ load: { path: { id } } }) => {
+ return id === category;
+ })
+ .map(({ load }) => ({
+ value: JSON.stringify(load),
+ label: load.path.type_name
+ })),
+ onSelect: (event) => {
+ const load = JSON.parse(event.value);
+ window.location = `#/addons/${encodeURIComponent(load.path.id)}/${encodeURIComponent(load.path.type_name)}`;
+ }
+ }
+ ];
+ const installedAddons = state.ctx.is_loaded ? state.ctx.content.addons : [];
+ const addonsItems = urlParams.category === 'my' ?
+ installedAddons.filter(addon => urlParams.type === 'all' || addon.manifest.types.includes(urlParams.type))
+ :
+ state.addons.content.type === 'Ready' ?
+ state.addons.content.content
+ :
+ [];
+ const error = state.addons.content.type === 'Err' && !state.ctx.is_loaded ? state.addons.content.content : null;
+ setAddons([addonsItems, selectInputs, selectAddon, installedAddons, error]);
};
- }, [category, onSelect]);
- const typeDropdown = React.useMemo(() => {
- const selected = typeof type === 'string' && type.length > 0 ? [type] : [];
- const options = ['all', 'movie', 'series', 'channel']
- .concat(selected)
- .filter((type, index, types) => types.indexOf(type) === index)
- .map((type) => ({ label: type, value: type }));
- return {
- name: 'type',
- selected,
- options,
- onSelect
+ core.on('NewModel', onNewState);
+ core.dispatch({
+ action: 'Load',
+ args: {
+ load: 'CatalogFiltered',
+ args: {
+ base: 'https://v3-cinemeta.strem.io/manifest.json',
+ path: {
+ resource: 'addon_catalog',
+ type_name: type,
+ id: category,
+ extra: []
+ }
+ }
+ }
+ });
+ return () => {
+ core.off('NewModel', onNewState);
};
- }, [type, onSelect]);
- return [addons, [categoryDropdown, typeDropdown]];
+ }, [urlParams, queryParams]);
+ return [addons, installAddon, uninstallAddon];
};
module.exports = useAddons;
diff --git a/src/routes/Addons/useSelectedAddon.js b/src/routes/Addons/useSelectedAddon.js
index 501b1754e..5bffaec37 100644
--- a/src/routes/Addons/useSelectedAddon.js
+++ b/src/routes/Addons/useSelectedAddon.js
@@ -12,19 +12,24 @@ const useSelectedAddon = (transportUrl) => {
return;
}
- fetch(transportUrl)
+ fetch(transportUrl) // TODO
.then((resp) => resp.json())
- .then((manifest) => setAddon({ ...manifest, transportUrl }));
+ .then((manifest) => setAddon({ manifest, transportUrl, flags: {} }));
}, [transportUrl]);
const clear = React.useCallback(() => {
if (active) {
const { pathname, search } = UrlUtils.parse(locationHash.slice(1));
- const queryParams = new URLSearchParams(search);
+ const queryParams = new URLSearchParams(search || '');
queryParams.delete('addon');
- window.location.replace(`#${pathname}?${queryParams.toString()}`);
+ if ([...queryParams].length !== 0) {
+ window.location.replace(`#${pathname}?${queryParams.toString()}`);
+ } else {
+ window.location.replace(`#${pathname}`);
+ }
+ setAddon(null);
}
- }, [active]);
- return [addon, clear];
+ }, [active, locationHash]);
+ return [addon, clear, setAddon];
};
module.exports = useSelectedAddon;
diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js
index 9f1ad474f..beb35e26d 100644
--- a/src/routes/Intro/Intro.js
+++ b/src/routes/Intro/Intro.js
@@ -3,14 +3,15 @@ const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { useRouteFocused } = require('stremio-router');
const { Button } = require('stremio/common');
+const { useServices } = require('stremio/services');
const CredentialsTextInput = require('./CredentialsTextInput');
const ConsentCheckbox = require('./ConsentCheckbox');
const styles = require('./styles');
-const LOGIN_FORM = 'LOGIN_FORM';
-const SIGNUP_FORM = 'SIGNUP_FORM';
+const SIGNUP_FORM = 'signup';
-const Intro = () => {
+const Intro = ({ queryParams }) => {
+ const { core } = useServices();
const routeFocused = useRouteFocused();
const emailRef = React.useRef();
const passwordRef = React.useRef();
@@ -22,17 +23,20 @@ const Intro = () => {
const [state, dispatch] = React.useReducer(
(state, action) => {
switch (action.type) {
- case 'switch-form':
- return {
- form: state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM,
- email: '',
- password: '',
- confirmPassword: '',
- termsAccepted: false,
- privacyPolicyAccepted: false,
- marketingAccepted: false,
- error: ''
- };
+ case 'set-form':
+ if (state.form !== action.form) {
+ return {
+ form: action.form,
+ email: '',
+ password: '',
+ confirmPassword: '',
+ termsAccepted: false,
+ privacyPolicyAccepted: false,
+ marketingAccepted: false,
+ error: ''
+ };
+ }
+ return state;
case 'change-credentials':
return {
...state,
@@ -55,7 +59,7 @@ const Intro = () => {
}
},
{
- form: SIGNUP_FORM,
+ form: queryParams.get('form'),
email: '',
password: '',
confirmPassword: '',
@@ -65,27 +69,115 @@ const Intro = () => {
error: ''
}
);
- const loginWithFacebook = React.useCallback(() => {
- alert('TODO: Facebook login');
+ React.useEffect(() => {
+ const onEvent = ({ event, args }) => {
+ if (event === 'CtxActionErr') {
+ dispatch({ type: 'error', error: args[1].args.message });
+ }
+ if (event === 'CtxChanged') {
+ const state = core.getState();
+ if (state.ctx.content.auth !== null) {
+ window.location.replace('/');
+ }
+ }
+ };
+ core.on('Event', onEvent);
+ return () => {
+ core.off('Event', onEvent);
+ };
}, []);
+ const loginWithFacebook = React.useCallback(() => {
+ FB.login((response) => {
+ if (response.status === 'connected') {
+ fetch('https://www.strem.io/fb-login-with-token/' + encodeURIComponent(response.authResponse.accessToken), { timeout: 10 * 1000 })
+ .then((resp) => {
+ if (resp.status < 200 || resp.status >= 300) {
+ throw new Error('Login failed at getting token from Stremio with status ' + resp.status);
+ } else {
+ return resp.json();
+ }
+ })
+ .then(() => {
+ core.dispatch({
+ action: 'UserOp',
+ args: {
+ userOp: 'Login',
+ args: {
+ email: state.email,
+ password: response.authResponse.accessToken
+ }
+ }
+ });
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ }
+ });
+ }, [state.email, state.password]);
const loginWithEmail = React.useCallback(() => {
if (typeof state.email !== 'string' || state.email.length === 0) {
dispatch({ type: 'error', error: 'Invalid email' });
return;
}
-
- alert('TODO: Login');
+ if (state.password.length === 0) {
+ dispatch({ type: 'error', error: 'Invalid password' });
+ return;
+ }
+ core.dispatch({
+ action: 'UserOp',
+ args: {
+ userOp: 'Login',
+ args: {
+ email: state.email,
+ password: state.password
+ }
+ }
+ });
}, [state.email, state.password]);
const loginAsGuest = React.useCallback(() => {
if (!state.termsAccepted) {
dispatch({ type: 'error', error: 'You must accept the Terms of Service' });
return;
}
-
- alert('TODO: Guest login');
- }, [state.termsAccepted, state.privacyPolicyAccepted, state.marketingAccepted]);
+ core.dispatch({
+ action: 'UserOp',
+ args: {
+ userOp: 'Logout'
+ }
+ });
+ location = '#/';
+ }, [state.termsAccepted]);
const signup = React.useCallback(() => {
- alert('TODO: Signup');
+ if (!state.termsAccepted) {
+ dispatch({ type: 'error', error: 'You must accept the Terms of Service' });
+ return;
+ }
+ if (!state.privacyPolicyAccepted) {
+ dispatch({ type: 'error', error: 'You must accept the Privacy Policy' });
+ return;
+ }
+ if (state.password !== state.confirmPassword) {
+ dispatch({ type: 'error', error: 'Passwords do not match' });
+ return;
+ }
+ core.dispatch({
+ action: 'UserOp',
+ args: {
+ userOp: 'Register',
+ args: {
+ email: state.email,
+ password: state.password,
+ gdpr_consent: {
+ tos: state.termsAccepted,
+ privacy: state.privacyPolicyAccepted,
+ marketing: state.marketingAccepted,
+ time: new Date(),
+ from: 'web'
+ }
+ }
+ }
+ });
}, [state.email, state.password, state.confirmPassword, state.termsAccepted, state.privacyPolicyAccepted, state.marketingAccepted]);
const emailOnChange = React.useCallback((event) => {
dispatch({
@@ -130,9 +222,9 @@ const Intro = () => {
const toggleMarketingAccepted = React.useCallback(() => {
dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' });
}, []);
- const switchForm = React.useCallback(() => {
- dispatch({ type: 'switch-form' });
- }, []);
+ React.useEffect(() => {
+ dispatch({ type: 'set-form', form: queryParams.get('form') });
+ }, [queryParams]);
React.useEffect(() => {
if (typeof state.error === 'string' && state.error.length > 0) {
errorRef.current.scrollIntoView();
@@ -229,7 +321,7 @@ const Intro = () => {
:
null
}
-
+
{state.form === SIGNUP_FORM ? 'LOG IN' : 'SING UP WITH EMAIL'}
diff --git a/src/routes/Settings/SectionsList/SectionsList.js b/src/routes/Settings/SectionsList/SectionsList.js
index c835738b2..d56de298a 100644
--- a/src/routes/Settings/SectionsList/SectionsList.js
+++ b/src/routes/Settings/SectionsList/SectionsList.js
@@ -1,28 +1,36 @@
const React = require('react');
const PropTypes = require('prop-types');
-const { Button, Dropdown, Checkbox, ColorInput } = require('stremio/common');
+const { Button, Multiselect, 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]);
+ onPreferenceChanged(id, preferences[id] === 'true' ? 'false' : 'true');
};
const colorChanged = React.useCallback((event) => {
- const id = event.currentTarget.dataset.id;
- const color = event.nativeEvent.value;
+ const id = event.dataset.id;
+ const color = event.value;
onPreferenceChanged(id, color);
}, [onPreferenceChanged]);
const updateDropdown = React.useCallback((event) => {
- var data = event.currentTarget.dataset;
- onPreferenceChanged(data.name, data.value);
+ const name = event.dataset.name;
+ const value = event.reactEvent.currentTarget.dataset.value;
+ onPreferenceChanged(name, value);
}, [onPreferenceChanged]);
+ const updateStreamingDropdown = React.useCallback((event) => {
+ const name = event.dataset.name;
+ const value = event.reactEvent.currentTarget.dataset.value;
+ const newPrefs = { ...preferences.streaming, [name]: value };
+ onPreferenceChanged('streaming', newPrefs);
+ }, [onPreferenceChanged, preferences.streaming]);
+
const checkUser = React.useCallback((event) => {
- if(! preferences.user) {
+ 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();
@@ -37,6 +45,45 @@ const SectionsList = React.forwardRef(({ className, sections, preferences, onPre
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 formatBytes = inBytes => {
+ if (inBytes === '0') return 'no caching';
+ if (inBytes === 'Infinity') return '∞';
+
+ const bytes = parseInt(inBytes, 10);
+
+ const kilo = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const power = Math.floor(Math.log(bytes) / Math.log(kilo));
+
+ // More than 1024 yotta bytes
+ if (power >= sizes.length) {
+ power = sizes.length - 1;
+ }
+ return parseFloat((bytes / Math.pow(kilo, power)).toFixed(2)) + ' ' + sizes[power];
+ }
+ const cacheSizes = ['0', '2147483648', '5368709120', '10737418240', 'Infinity'];
+ const mkCacheSizeOptions = sizes => sizes.map(size => ({
+ label: formatBytes(size), // TODO: translation
+ value: size.toString(),
+ }))
+ const supportedProfiles = ['default', 'soft', 'fast'];
+ const mkProfiles = profiles => profiles.map(profile => ({
+ label: profile[0].toUpperCase() + profile.slice(1).toLowerCase(), // TODO: translation
+ value: profile,
+ }))
+ const [cachingOptions, setCachingOptions] = React.useState(mkProfiles(supportedProfiles));
+ const [streamingProfiles, setStreamingProfiles] = React.useState(mkProfiles(supportedProfiles));
+ React.useEffect(() => {
+ if (!preferences.streaming || typeof preferences.streaming.cacheSize === 'undefined') return;
+ setCachingOptions(mkCacheSizeOptions([...new Set(cacheSizes.concat(preferences.streaming.cacheSize))]));
+ }, [preferences.streaming && preferences.streaming.cacheSize]);
+ React.useEffect(() => {
+ if (preferences.streaming && preferences.streaming.profile && !supportedProfiles.includes(preferences.streaming.profile)) {
+ setStreamingProfiles(mkProfiles(supportedProfiles.concat(preferences.streaming.profile)));
+ }
+ }, [preferences.streaming && preferences.streaming.profile]);
+
const sectionsElements = sections.map((section) =>
{section.id}
@@ -104,11 +151,49 @@ const SectionsList = React.forwardRef(({ className, sections, preferences, onPre
);
+ } else if (input.type === 'streaming') {
+ return (
+ preferences.streaming_loaded
+ ?
+
+ {
+ // The streaming server settings are shown only if server is available
+ preferences.streaming_error
+ ?
+ null
+ :
+
+
+
+
+ }
+ {/* From here there is only presentation */}
+
+
Streaming server URL: {preferences.server_url}
+
+
+
+
+
{'Streaming server is ' + (preferences.streaming_error ? 'not ' : '') + 'available.'}{preferences.streaming_error && ' Reason: ' + preferences.streaming_error}
+
+
+
+ :
+
+
Loading streaming settgins...
+
+ );
} else if (input.type === 'select') {
return (
{input.header ?
{input.header}
: null}
-
+
);
} else if (input.type === 'link') {
@@ -134,7 +219,7 @@ const SectionsList = React.forwardRef(({ className, sections, preferences, onPre
return (
{input.header ?
{input.header}
: null}
-
+
{input.label}
@@ -153,7 +238,13 @@ const SectionsList = React.forwardRef(({ className, sections, preferences, onPre
return (
{input.header ?
{input.header}
: null}
-
+
+
+ );
+ } else if (input.type === 'info') {
+ return (
+
+
{input.header} {preferences[input.id]}
);
}
diff --git a/src/routes/Settings/SectionsList/styles.less b/src/routes/Settings/SectionsList/styles.less
index d7b647535..85e687d99 100644
--- a/src/routes/Settings/SectionsList/styles.less
+++ b/src/routes/Settings/SectionsList/styles.less
@@ -1,20 +1,32 @@
:import('~stremio/common/Checkbox/styles.less') {
checkbox-icon: icon;
}
+:import('~stremio/common/Multiselect/styles.less') {
+ menu-container: menu-container;
+}
+
+.menu-container {
+ max-height: 30rem;
+ overflow-y: auto;
+}
.section {
- padding: 4rem 2rem;
+ padding: 4rem 0;
+ margin: 0 2rem;
+ width: var(--input-width);
+ overflow: visible;
.section-header {
- margin: 0 1.5rem 1.5rem 1.5rem;
+ margin: 1.5rem 0;
font-size: 2rem;
color: var(--color-surfacelighter);
}
.input-container {
- margin: 1.5rem;
+ margin: 2rem 0;
display: flex;
flex-direction: column;
+ overflow: visible;
.input-header {
margin-bottom: 0.5rem;
@@ -51,14 +63,13 @@
&.select-container {
.dropdown {
height: 3rem;
- width: var(--input-width);
}
}
&.link-container {
- margin: 1rem 1.5rem;
-
+ margin: 0;
.link {
+ padding: .75rem 0;
display: block;
color: var(--color-secondarylight);
@@ -75,7 +86,6 @@
&.button-container {
.button {
padding: 0.7rem;
- width: var(--input-width);
min-height: calc(var(--input-width) * 0.09);
display: flex;
align-items: center;
@@ -132,10 +142,12 @@
}
&.text-container {
+ margin: 0;
.text {
display: flex;
flex-direction: row;
align-items: center;
+ padding: .75rem 0;
.icon {
margin-right: 0.5rem;
@@ -160,7 +172,7 @@
&.color-container {
.color-picker {
- width: var(--input-width);
+ box-shadow: inset 0px 0px .2rem 0px var(--color-surfacelighter20);
height: calc(var(--input-width) * 0.08);
cursor: pointer;
diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js
index d7018c9d3..90d3a725d 100644
--- a/src/routes/Settings/Settings.js
+++ b/src/routes/Settings/Settings.js
@@ -6,21 +6,20 @@ const SectionsList = require('./SectionsList');
const { settingsSections } = require('./constants');
const useSettings = require('./useSettings');
-const devTestWithUser = true;
-
const Settings = () => {
- const [preferences, setPreferences] = useSettings(devTestWithUser);
- const sections = React.useMemo(()=>Object.keys(settingsSections)
+ const [preferences, setPreferences] = useSettings();
+ const [dynamicSections, setDynamicSections] = React.useState(settingsSections);
+ // TODO: The Streaming section should be handled separately
+ const sections = React.useMemo(()=>Object.keys(dynamicSections)
.map((section) => ({
id: section,
- inputs: settingsSections[section],
+ inputs: dynamicSections[section],
ref: React.createRef()
- })), []);
+ })), [dynamicSections]);
+
const [selectedSectionId, setSelectedSectionId] = React.useState(sections[0].id);
const scrollContainerRef = React.useRef(null);
- /////////////////
-
const updatePreference = (option, value) => {
setPreferences({ ...preferences, [option]: value });
}
diff --git a/src/routes/Settings/constants.js b/src/routes/Settings/constants.js
index 563bbd8c2..9a3a95b9d 100644
--- a/src/routes/Settings/constants.js
+++ b/src/routes/Settings/constants.js
@@ -1,27 +1,28 @@
+const languageOptions = [ { "label": "аҧсуа бызшәа", "value": "abk" }, { "label": "Afaraf", "value": "aar" }, { "label": "Afrikaans", "value": "afr" }, { "label": "Akan", "value": "aka" }, { "label": "gjuha shqipe", "value": "sqi" }, { "label": "አማርኛ", "value": "amh" }, { "label": "العربية", "value": "ara" }, { "label": "aragonés", "value": "arg" }, { "label": "Հայերեն", "value": "hye" }, { "label": "অসমীয়া", "value": "asm" }, { "label": "авар мацӀ", "value": "ava" }, { "label": "avesta", "value": "ave" }, { "label": "aymar aru", "value": "aym" }, { "label": "azərbaycan dili", "value": "aze" }, { "label": "bamanankan", "value": "bam" }, { "label": "башҡорт теле", "value": "bak" }, { "label": "euskara", "value": "eus" }, { "label": "беларуская мова", "value": "bel" }, { "label": "বাংলা", "value": "ben" }, { "label": "भोजपुरी", "value": "bih" }, { "label": "Bislama", "value": "bis" }, { "label": "bosanski jezik", "value": "bos" }, { "label": "brezhoneg", "value": "bre" }, { "label": "български език", "value": "bul" }, { "label": "ဗမာစာ", "value": "mya" }, { "label": "català", "value": "cat" }, { "label": "Chamoru", "value": "cha" }, { "label": "нохчийн мотт", "value": "che" }, { "label": "chiCheŵa", "value": "nya" }, { "label": "中文 (Zhōngwén)", "value": "zho" }, { "label": "чӑваш чӗлхи", "value": "chv" }, { "label": "Kernewek", "value": "cor" }, { "label": "corsu", "value": "cos" }, { "label": "ᓀᐦᐃᔭᐍᐏᐣ", "value": "cre" }, { "label": "hrvatski jezik", "value": "hrv" }, { "label": "čeština", "value": "ces" }, { "label": "dansk", "value": "dan" }, { "label": "ދިވެހި", "value": "div" }, { "label": "Nederlands", "value": "nld" }, { "label": "རྫོང་ཁ", "value": "dzo" }, { "label": "English", "value": "eng" }, { "label": "Esperanto", "value": "epo" }, { "label": "eesti", "value": "est" }, { "label": "Eʋegbe", "value": "ewe" }, { "label": "føroyskt", "value": "fao" }, { "label": "vosa Vakaviti", "value": "fij" }, { "label": "suomi", "value": "fin" }, { "label": "français", "value": "fre" }, { "label": "Fulfulde", "value": "ful" }, { "label": "galego", "value": "glg" }, { "label": "ქართული", "value": "kat" }, { "label": "Deutsch", "value": "ger" }, { "label": "ελληνικά", "value": "ell" }, { "label": "Avañe'ẽ", "value": "grn" }, { "label": "ગુજરાતી", "value": "guj" }, { "label": "Kreyòl ayisyen", "value": "hat" }, { "label": "Hausa", "value": "hau" }, { "label": "עברית", "value": "heb" }, { "label": "Otjiherero", "value": "her" }, { "label": "हिन्दी", "value": "hin" }, { "label": "Hiri Motu", "value": "hmo" }, { "label": "magyar", "value": "hun" }, { "label": "Interlingua", "value": "ina" }, { "label": "Bahasa Indonesia", "value": "ind" }, { "label": "Interlingue", "value": "ile" }, { "label": "Gaeilge", "value": "gle" }, { "label": "Asụsụ Igbo", "value": "ibo" }, { "label": "Iñupiaq", "value": "ipk" }, { "label": "Ido", "value": "ido" }, { "label": "Íslenska", "value": "isl" }, { "label": "italiano", "value": "ita" }, { "label": "ᐃᓄᒃᑎᑐᑦ", "value": "iku" }, { "label": "日本語 (にほんご)", "value": "jpn" }, { "label": "basa Jawa", "value": "jav" }, { "label": "kalaallisut", "value": "kal" }, { "label": "ಕನ್ನಡ", "value": "kan" }, { "label": "Kanuri", "value": "kau" }, { "label": "कश्मीरी", "value": "kas" }, { "label": "қазақ тілі", "value": "kaz" }, { "label": "ខ្មែរ", "value": "khm" }, { "label": "Gĩkũyũ", "value": "kik" }, { "label": "Ikinyarwanda", "value": "kin" }, { "label": "Кыргызча", "value": "kir" }, { "label": "коми кыв", "value": "kom" }, { "label": "KiKongo", "value": "kon" }, { "label": "한국어 (韓國語)", "value": "kor" }, { "label": "Kurdî", "value": "kur" }, { "label": "Kuanyama", "value": "kua" }, { "label": "latine", "value": "lat" }, { "label": "Lëtzebuergesch", "value": "ltz" }, { "label": "Luganda", "value": "lug" }, { "label": "Limburgs", "value": "lim" }, { "label": "Lingála", "value": "lin" }, { "label": "ພາສາລາວ", "value": "lao" }, { "label": "lietuvių kalba", "value": "lit" }, { "label": "Tshiluba", "value": "lub" }, { "label": "latviešu valoda", "value": "lav" }, { "label": "Gaelg", "value": "glv" }, { "label": "македонски јазик", "value": "mkd" }, { "label": "fiteny malagasy", "value": "mlg" }, { "label": "bahasa Melayu", "value": "msa" }, { "label": "മലയാളം", "value": "mal" }, { "label": "Malti", "value": "mlt" }, { "label": "te reo Māori", "value": "mri" }, { "label": "मराठी", "value": "mar" }, { "label": "Kajin M̧ajeļ", "value": "mah" }, { "label": "монгол", "value": "mon" }, { "label": "Ekakairũ Naoero", "value": "nau" }, { "label": "Diné bizaad", "value": "nav" }, { "label": "Norsk bokmål", "value": "nob" }, { "label": "isiNdebele", "value": "nde" }, { "label": "नेपाली", "value": "nep" }, { "label": "Owambo", "value": "ndo" }, { "label": "Norsk nynorsk", "value": "nno" }, { "label": "Norsk", "value": "nor" }, { "label": "ꆈꌠ꒿ Nuosuhxop", "value": "iii" }, { "label": "isiNdebele", "value": "nbl" }, { "label": "occitan", "value": "oci" }, { "label": "ᐊᓂᔑᓈᐯᒧᐎᓐ", "value": "oji" }, { "label": "ѩзыкъ словѣньскъ", "value": "chu" }, { "label": "Afaan Oromoo", "value": "orm" }, { "label": "ଓଡ଼ିଆ", "value": "ori" }, { "label": "ирон æвзаг", "value": "oss" }, { "label": "ਪੰਜਾਬੀ", "value": "pan" }, { "label": "पाऴि", "value": "pli" }, { "label": "فارسی", "value": "fas" }, { "label": "język polski", "value": "pol" }, { "label": "پښتو", "value": "pus" }, { "label": "português", "value": "por" }, { "label": "português Brazil", "value": "pob" }, { "label": "Runa Simi", "value": "que" }, { "label": "rumantsch grischun", "value": "roh" }, { "label": "Ikirundi", "value": "run" }, { "label": "limba română", "value": "ron" }, { "label": "русский язык", "value": "rus" }, { "label": "संस्कृतम्", "value": "san" }, { "label": "sardu", "value": "srd" }, { "label": "सिन्धी", "value": "snd" }, { "label": "Davvisámegiella", "value": "sme" }, { "label": "gagana fa'a Samoa", "value": "smo" }, { "label": "yângâ tî sängö", "value": "sag" }, { "label": "српски језик", "value": "srp" }, { "label": "Gàidhlig", "value": "gla" }, { "label": "chiShona", "value": "sna" }, { "label": "සිංහල", "value": "sin" }, { "label": "slovenčina", "value": "slk" }, { "label": "slovenski jezik", "value": "slv" }, { "label": "Soomaaliga", "value": "som" }, { "label": "Sesotho", "value": "sot" }, { "label": "español", "value": "spa" }, { "label": "Basa Sunda", "value": "sun" }, { "label": "Kiswahili", "value": "swa" }, { "label": "SiSwati", "value": "ssw" }, { "label": "Svenska", "value": "swe" }, { "label": "தமிழ்", "value": "tam" }, { "label": "తెలుగు", "value": "tel" }, { "label": "тоҷикӣ", "value": "tgk" }, { "label": "ไทย", "value": "tha" }, { "label": "ትግርኛ", "value": "tir" }, { "label": "བོད་ཡིག", "value": "bod" }, { "label": "Türkmen", "value": "tuk" }, { "label": "Wikang Tagalog", "value": "tgl" }, { "label": "Setswana", "value": "tsn" }, { "label": "faka Tonga", "value": "ton" }, { "label": "Türkçe", "value": "tur" }, { "label": "Xitsonga", "value": "tso" }, { "label": "татар теле", "value": "tat" }, { "label": "Twi", "value": "twi" }, { "label": "Reo Tahiti", "value": "tah" }, { "label": "Uyƣurqə", "value": "uig" }, { "label": "українська мова", "value": "ukr" }, { "label": "اردو", "value": "urd" }, { "label": "O'zbek", "value": "uzb" }, { "label": "Tshivenḓa", "value": "ven" }, { "label": "Tiếng Việt", "value": "vie" }, { "label": "Volapük", "value": "vol" }, { "label": "walon", "value": "wln" }, { "label": "Cymraeg", "value": "cym" }, { "label": "Wollof", "value": "wol" }, { "label": "Frysk", "value": "fry" }, { "label": "isiXhosa", "value": "xho" }, { "label": "ייִדיש", "value": "yid" }, { "label": "Yorùbá", "value": "yor" }, { "label": "Saɯ cueŋƅ", "value": "zha" }, { "label": "isiZulu", "value": "zul" } ];
+
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' },
+ { 'id': 'user', 'type': 'user' },
+ { 'id': 'language', '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': 'show_vid_overview', 'label': 'Show videos overview', 'type': 'checkbox' },
],
'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)' },
+ { 'id': 'add-ons', 'label': 'ADD-ONS', 'type': 'button', 'icon': 'ic_addons', 'href': '#/addons' },
+ { 'id': 'subtitles_language', 'header': 'Default Subtitles Language', 'label': 'Default Subtitles Language', 'type': 'select', 'options': languageOptions },
+ { 'id': 'subtitles_size', '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': 'subtitles_background', 'header': 'Subtitles Background', 'label': 'Subtitles background', 'type': 'select', 'options': [{ 'label': 'None', 'value': '' }, { 'label': 'Solid', 'value': 'solid' }, { 'label': 'Transparent', 'value': 'transparent' }] },
+ { 'id': 'subtitles_color', 'header': 'Subtitles color', 'label': 'Subtitles color', 'type': 'color' },
+ { 'id': 'subtitles_outline_color', 'header': 'Subtitles outline color', 'label': 'Subtitles outline color', 'type': 'color' },
+ { 'id': 'autoplay_next_vid', 'label': 'Auto-play next episode', 'type': 'checkbox' },
+ { 'id': 'pause_on_lost_focus', 'label': 'Pause playback when not in focus', 'type': 'checkbox' },
+ { 'id': 'use_external_player', 'label': 'Launch player in a separate window (advanced)', 'type': 'checkbox' },
],
'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.' }
+ { 'id': 'streaming', 'type': 'streaming' },
]
};
+
module.exports = {
settingsSections,
};
diff --git a/src/routes/Settings/useSettings.js b/src/routes/Settings/useSettings.js
index bca76cb46..4f71eba0e 100644
--- a/src/routes/Settings/useSettings.js
+++ b/src/routes/Settings/useSettings.js
@@ -1,22 +1,68 @@
const React = require('react');
+const { useServices } = require('stremio/services');
-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,
-});
\ No newline at end of file
+const IGNORED_SETTINGS = Object.freeze(['user', 'streaming']);
+
+module.exports = () => {
+ const { core } = useServices();
+
+ const [settings, setSettings] = React.useState({
+ user: null,
+ streaming: {},
+ streaming_loaded: false,
+ streaming_error: ""
+ });
+
+ React.useEffect(() => {
+ const onNewState = () => {
+ const { ctx, streaming_server_settings } = core.getState()
+ try {
+ const newSettings = {
+ ...settings,
+ ...ctx.content.settings,
+ user: ctx.content.auth ? ctx.content.auth.user : null,
+ streaming: streaming_server_settings && streaming_server_settings.ready || {},
+ streaming_loaded: streaming_server_settings && !!(streaming_server_settings.error || streaming_server_settings.ready),
+ streaming_error: streaming_server_settings && streaming_server_settings.error || "",
+ };
+ setSettings(newSettings);
+ } catch (e) {
+ console.log('Cannot update settings state', e);
+ }
+ };
+ const onStoreError = ({ event, args }) => {
+ if (event !== "SettingsStoreError") return;
+ // TODO: Notify with maybe a toast?
+ console.log(args)
+ }
+
+ core.on('NewModel', onNewState);
+ core.on('Event', onStoreError);
+
+ onNewState();
+
+ return () => {
+ // Destructor function
+ core.off('NewModel', onNewState);
+ core.off('Event', onStoreError);
+ };
+ }, []);
+
+ const setTheSettings = React.useCallback(newSettings => {
+ const event = { action: 'Settings', args: { args: {} } };
+ // This can be done with React.useEffect and newSettings.streaming as dependency
+ const streamingServerSettingChanged = settings.streaming && Object.keys(newSettings.streaming)
+ .some(prop => settings.streaming[prop] !== newSettings.streaming[prop]);
+ if (streamingServerSettingChanged) {
+ event.args = { settings: 'StoreStreamingServer', args: newSettings.streaming };
+ } else {
+ event.args.settings = 'Store';
+ Object.keys(newSettings)
+ .filter(prop => !IGNORED_SETTINGS.includes(prop))
+ .forEach(key => event.args.args[key] = newSettings[key].toString());
+ }
+ core.dispatch(event);
+ }, [settings])
+
+ return [settings, setTheSettings];
+};
diff --git a/storybook/stories/Addon/InstalledAddon.js b/storybook/stories/Addon/InstalledAddon.js
index 65970946a..24400fdd7 100644
--- a/storybook/stories/Addon/InstalledAddon.js
+++ b/storybook/stories/Addon/InstalledAddon.js
@@ -1,6 +1,20 @@
const React = require('react');
const { storiesOf } = require('@storybook/react');
+const { action } = require('@storybook/addon-actions');
+const Addon = require('stremio/routes/Addons/Addon');
+const styles = require('./styles');
storiesOf('Addon', module).add('Installed', () => (
-
Installed addon
+
));
diff --git a/storybook/stories/Addon/NotInstalledAddon.js b/storybook/stories/Addon/NotInstalledAddon.js
index 9c9a6a8de..5d5130198 100644
--- a/storybook/stories/Addon/NotInstalledAddon.js
+++ b/storybook/stories/Addon/NotInstalledAddon.js
@@ -1,6 +1,20 @@
const React = require('react');
const { storiesOf } = require('@storybook/react');
+const { action } = require('@storybook/addon-actions');
+const Addon = require('stremio/routes/Addons/Addon');
+const styles = require('./styles');
storiesOf('Addon', module).add('NotInstalled', () => (
-
Not installed addon
+
));
diff --git a/storybook/stories/Addon/styles.less b/storybook/stories/Addon/styles.less
new file mode 100644
index 000000000..f300a05f0
--- /dev/null
+++ b/storybook/stories/Addon/styles.less
@@ -0,0 +1,3 @@
+.installed-addon-container, .not-installed-addon-container {
+ margin: 10px;
+}
\ No newline at end of file