Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/calendar

This commit is contained in:
Tim 2024-10-09 10:43:36 +02:00
commit 77ce94673d
98 changed files with 2439 additions and 832 deletions

View file

@ -1,99 +0,0 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"settings": {
"react": {
"version": "detect"
}
},
"globals": {
"YT": "readonly",
"FB": "readonly",
"cast": "readonly",
"chrome": "readonly"
},
"env": {
"node": true,
"commonjs": true,
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 11,
"ecmaFeatures": {
"jsx": true
}
},
"ignorePatterns": [
"/*",
"!/src"
],
"rules": {
"arrow-parens": "error",
"arrow-spacing": "error",
"block-spacing": "error",
"comma-spacing": "error",
"eol-last": "error",
"eqeqeq": "error",
"func-call-spacing": "error",
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"no-console": [
"error",
{
"allow": [
"warn",
"error"
]
}
],
"no-extra-semi": "error",
"no-eq-null": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-prototype-builtins": "off",
"no-template-curly-in-string": "error",
"no-trailing-spaces": "error",
"no-useless-concat": "error",
"no-unreachable": "error",
"no-unused-vars": [
"error",
{
"varsIgnorePattern": "_"
}
],
"prefer-const": "error",
"quotes": [
"error",
"single"
],
"quote-props": [
"error",
"as-needed",
{
"unnecessary": false
}
],
"semi": "error",
"semi-spacing": "error",
"space-before-blocks": "error",
"valid-typeof": [
"error",
{
"requireStringLiterals": true
}
]
}
}

100
eslint.config.mjs Normal file
View file

@ -0,0 +1,100 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import stylistic from '@stylistic/eslint-plugin'
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
pluginReact.configs.flat.recommended,
{
plugins: {
'@stylistic': stylistic
},
},
{
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}']
},
{
files: ['**/*.js'],
languageOptions: {
sourceType: 'commonjs',
ecmaVersion: 'latest',
}
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
YT: 'readonly',
FB: 'readonly',
cast: 'readonly',
chrome: 'readonly',
}
}
},
{
settings: {
react: {
version: 'detect',
},
},
},
{
rules: {
'no-redeclare': 'off',
'eol-last': 'error',
'eqeqeq': 'error',
'no-console': ['error', {
allow: [
'warn',
'error'
]
}],
}
},
{
rules: {
'@typescript-eslint/no-redeclare': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
'varsIgnorePattern': '_',
'caughtErrorsIgnorePattern': '_',
}
],
}
},
{
rules: {
'@stylistic/arrow-parens': 'error',
'@stylistic/arrow-spacing': 'error',
'@stylistic/block-spacing': 'error',
'@stylistic/comma-spacing': 'error',
'@stylistic/semi-spacing': 'error',
'@stylistic/space-before-blocks': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/func-call-spacing': 'error',
'@stylistic/eol-last': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-multiple-empty-lines': ['error', {
max: 1
}],
'@stylistic/indent': ['error', 4],
'@stylistic/quotes': ['error', 'single'],
}
},
{
rules: {
'react/display-name': 'off',
}
}
];

2448
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.9",
"version": "5.0.0-beta.12",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -25,7 +25,7 @@
"classnames": "2.3.1",
"eventemitter3": "4.0.7",
"filter-invalid-dom-props": "2.1.0",
"hat": "0.0.3",
"hat": "^0.0.3",
"i18next": "^22.4.3",
"langs": "^2.0.0",
"lodash.debounce": "4.0.8",
@ -40,7 +40,7 @@
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd",
"stremio-translations": "github:Stremio/stremio-translations#378218c9617f3e763ba5f6755e4d39c1c158747d",
"url": "0.11.0",
"use-long-press": "^3.1.5"
},
@ -50,6 +50,10 @@
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0",
"@eslint/js": "^9.12.0",
"@stylistic/eslint-plugin": "^2.9.0",
"@stylistic/eslint-plugin-jsx": "^2.9.0",
"@types/hat": "^0.0.4",
"@types/react": "^18.2.9",
"@types/react-dom": "^18.3.0",
"babel-loader": "8.2.3",
@ -58,8 +62,9 @@
"css-loader": "6.5.0",
"cssnano": "5.0.8",
"cssnano-preset-advanced": "5.1.4",
"eslint": "7.32.0",
"eslint-plugin-react": "7.26.1",
"eslint": "^9.12.0",
"eslint-plugin-react": "^7.37.1",
"globals": "^15.10.0",
"html-webpack-plugin": "5.5.0",
"jest": "27.3.1",
"less": "4.1.2",
@ -70,6 +75,7 @@
"terser-webpack-plugin": "5.2.4",
"ts-loader": "^9.5.1",
"typescript": "^5.4.2",
"typescript-eslint": "^8.8.0",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",

View file

@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
@ -162,18 +162,20 @@ const App = () => {
services.core.error instanceof Error ?
<ErrorDialog className={styles['error-container']} />
:
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</TooltipProvider>
</ToastProvider>
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
:
<div className={styles['loader-container']} />
}

View file

@ -4,6 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const ModalDialog = require('stremio/common/ModalDialog');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const { usePlatform } = require('stremio/common/Platform');
const { useServices } = require('stremio/services');
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
const useAddonDetails = require('./useAddonDetails');
@ -43,6 +44,7 @@ function withRemoteAndLocalAddon(AddonDetails) {
const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
const { core } = useServices();
const platform = usePlatform();
const addonDetails = useAddonDetails(transportUrl);
const modalButtons = React.useMemo(() => {
const cancelButton = {
@ -68,7 +70,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
label: 'Configure',
props: {
onClick: (event) => {
window.open(transportUrl.replace('manifest.json', 'configure'));
platform.openExternal(transportUrl.replace('manifest.json', 'configure'));
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'configure',

View file

@ -80,4 +80,4 @@ const BottomSheet = ({ children, title, show }: Props) => {
), document.body);
};
export default BottomSheet;
export default BottomSheet;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import BottomSheet from './BottomSheet';
export default BottomSheet;
export default BottomSheet;

View file

@ -93,6 +93,8 @@ const EXTERNAL_PLAYERS = [
},
];
const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle'];
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES,
@ -110,4 +112,5 @@ module.exports = {
TYPE_PRIORITIES,
ICON_FOR_TYPE,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
};

View file

@ -42,4 +42,4 @@ const Chip = memo(({ label, value, active, onSelect }: Props) => {
);
});
export default Chip;
export default Chip;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Chip from './Chip';
export default Chip;
export default Chip;

View file

@ -34,4 +34,4 @@ const Chips = memo(({ options, selected, onSelect }: Props) => {
);
});
export default Chips;
export default Chips;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Chips from './Chips';
export default Chips;
export default Chips;

View file

@ -37,6 +37,7 @@ const useCoreSuspender = () => {
return React.useContext(CoreSuspenderContext);
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const withCoreSuspender = (Component, Fallback = () => { }) => {
return function withCoreSuspender(props) {
const { core } = useServices();

View file

@ -17,7 +17,8 @@ const DelayedRenderer = ({ children, delay }) => {
};
DelayedRenderer.propTypes = {
children: PropTypes.node
children: PropTypes.node,
delay: PropTypes.number,
};
module.exports = DelayedRenderer;

View file

@ -1,5 +1,7 @@
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
.dropdown {
background: var(--modal-background-color);
display: none;
@ -14,6 +16,8 @@
&.open {
display: block;
max-height: calc(3.2rem * 10);
overflow: auto;
}
.back-button {
@ -27,4 +31,12 @@
width: 1.5rem;
}
}
}
@media only screen and (max-width: @minimum) {
.dropdown {
&.open {
max-height: calc(3.2rem * 7);
}
}
}

View file

@ -32,7 +32,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen
<Icon name={'caret-left'} className={styles['back-button-icon']} />
{t('BACK')}
</Button>
: null
: null
}
{
options
@ -46,10 +46,9 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen
/>
))
}
</div>
);
};
export default Dropdown;
export default Dropdown;

View file

@ -43,4 +43,4 @@ const Option = ({ option, selectedOption, onSelect }: Props) => {
);
};
export default Option;
export default Option;

View file

@ -2,4 +2,4 @@
import Option from './Option';
export default Option;
export default Option;

View file

@ -2,4 +2,4 @@
import Dropdown from './Dropdown';
export default Dropdown;
export default Dropdown;

View file

@ -25,7 +25,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
const onOptionSelect = (value: number) => {
level ? setLevel(level + 1) : onSelect(value), closeMenu();
};
return (
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
<Button
@ -48,10 +48,10 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }
menuOpen={menuOpen}
selectedOption={selectedOption}
/>
: null
: null
}
</div>
);
};
export default MultiselectMenu;
export default MultiselectMenu;

View file

@ -2,4 +2,4 @@
import MultiselectMenu from './MultiselectMenu';
export default MultiselectMenu;
export default MultiselectMenu;

View file

@ -6,4 +6,4 @@ type MultiselectMenuOption = {
default?: boolean;
hidden?: boolean;
level?: MultiselectMenuOption[];
};
};

View file

@ -1,2 +1,2 @@
declare const useLocalSearch: () => { items: LocalSearchItem[], search: (query: string) => void };
export = useLocalSearch;
export = useLocalSearch;

View file

@ -1,2 +1,2 @@
declare const useSearchHistory: () => { items: SearchHistory, clear: () => void };
export = useSearchHistory;
export = useSearchHistory;

View file

@ -20,7 +20,7 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick })
scrollableElements.forEach((element) => {
if (element.scrollTop > 0) {
element.scrollTop = 0;
element.scrollTo({ top: 0, behavior: 'smooth' });
}
});
};

View file

@ -0,0 +1,51 @@
import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
import useShell from './useShell';
import { name, isMobile } from './device';
interface PlatformContext {
name: string;
isMobile: boolean;
openExternal: (url: string) => void;
}
const PlatformContext = createContext<PlatformContext>({} as PlatformContext);
type Props = {
children: JSX.Element;
};
const PlatformProvider = ({ children }: Props) => {
const shell = useShell();
const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
if (shell.active) {
shell.send('open-external', finalUrl);
} else {
window.open(finalUrl, '_blank');
}
} catch (e) {
console.error('Failed to parse external url:', e);
}
};
return (
<PlatformContext.Provider value={{ openExternal, name, isMobile }}>
{children}
</PlatformContext.Provider>
);
};
const usePlatform = () => {
return useContext(PlatformContext);
};
export {
PlatformProvider,
usePlatform
};

View file

@ -0,0 +1,31 @@
import Bowser from 'bowser';
const APPLE_MOBILE_DEVICES = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod',
];
const { userAgent, platform, maxTouchPoints } = globalThis.navigator;
// this detects ipad properly in safari
// while bowser does not
const isIOS = APPLE_MOBILE_DEVICES.includes(platform) || (userAgent.includes('Mac') && 'ontouchend' in document);
// Edge case: iPad is included in this function
// Keep in mind maxTouchPoints for Vision Pro might change in the future
const isVisionOS = userAgent.includes('Macintosh') && maxTouchPoints === 5;
const bowser = Bowser.getParser(userAgent);
const os = bowser.getOSName().toLowerCase();
const name = isVisionOS ? 'visionos' : isIOS ? 'ios' : os || 'unknown';
const isMobile = ['ios', 'android'].includes(name);
export {
name,
isMobile,
};

View file

@ -0,0 +1,5 @@
import { PlatformProvider, usePlatform } from './Platform';
export {
PlatformProvider,
usePlatform,
};

View file

@ -0,0 +1,22 @@
const createId = () => Math.floor(Math.random() * 9999) + 1;
const useShell = () => {
const transport = globalThis?.qt?.webChannelTransport;
const send = (method: string, ...args: (string | number)[]) => {
transport?.send(JSON.stringify({
id: createId(),
type: 6,
object: 'transport',
method: 'onEvent',
args: [method, ...args],
}));
};
return {
active: !!transport,
send,
};
};
export default useShell;

View file

@ -1,5 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
/* eslint-disable @typescript-eslint/no-empty-function */
const React = require('react');
const ToastContext = React.createContext({

View file

@ -20,6 +20,7 @@ const { default: MultiselectMenu } = require('./MultiselectMenu');
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
const { default: HorizontalScroll } = require('./HorizontalScroll');
const PaginationInput = require('./PaginationInput');
const { PlatformProvider, usePlatform } = require('./Platform');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
const Popup = require('./Popup');
const SearchBar = require('./SearchBar');
@ -47,7 +48,6 @@ const useProfile = require('./useProfile');
const useStreamingServer = require('./useStreamingServer');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const platform = require('./platform');
const EventModal = require('./EventModal');
module.exports = {
@ -72,6 +72,8 @@ module.exports = {
HorizontalScroll,
VerticalNavBar,
PaginationInput,
PlatformProvider,
usePlatform,
PlayIconCircleCentered,
Popup,
SearchBar,
@ -102,6 +104,5 @@ module.exports = {
useStreamingServer,
useTorrent,
useTranslate,
platform,
EventModal,
};

View file

@ -1,36 +0,0 @@
// Copyright (C) 2017-2024 Smart code 203358507
// this detects ipad properly in safari
// while bowser does not
const iOS = () => {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
|| (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
};
const Bowser = require('bowser');
const browser = Bowser.parse(window.navigator?.userAgent || '');
// Edge case: iPad is included in this function
// Keep in mind maxTouchPoints for Vision Pro might change in the future
const isVisionProUser = () => {
const isMacintosh = navigator.userAgent.includes('Macintosh');
const hasFiveTouchPoints = navigator.maxTouchPoints === 5;
return isMacintosh && hasFiveTouchPoints;
};
const name = isVisionProUser() ? 'visionos' : (iOS() ? 'ios' : (browser?.os?.name || 'unknown').toLowerCase());
module.exports = {
name,
isMobile: () => {
return name === 'ios' || name === 'android';
}
};

View file

@ -1,8 +1,8 @@
declare const useBinaryState: () => [
declare const useBinaryState: (initialValue?: boolean) => [
boolean,
() => void,
() => void,
() => void,
];
export = useBinaryState;
export = useBinaryState;

View file

@ -1,2 +1,2 @@
declare const useNotifcations: () => Notifications;
export = useNotifcations;
export = useNotifcations;

View file

@ -24,4 +24,4 @@ const useOutsideClick = (callback: () => void) => {
return ref;
};
export default useOutsideClick;
export default useOutsideClick;

View file

@ -1,2 +1,2 @@
declare const useProfile: () => Profile;
export = useProfile;
export = useProfile;

View file

@ -1,2 +1,2 @@
declare const useStreamingServer: () => StreamingServer;
export = useStreamingServer;
export = useStreamingServer;

9
src/modules.d.ts vendored
View file

@ -1,4 +1,7 @@
declare module '*.less';
declare module 'stremio/services';
declare module '*.less' {
const resource: Record<string, string>;
export = resource;
}
declare module 'stremio/common';
declare module 'stremio/common/*';
declare module 'stremio/common/Button';

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState, withCoreSuspender } = require('stremio/common');
const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common');
const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons');
const useRemoteAddons = require('./useRemoteAddons');
@ -15,6 +15,7 @@ const styles = require('./styles');
const Addons = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const platform = usePlatform();
const installedAddons = useInstalledAddons(urlParams);
const remoteAddons = useRemoteAddons(urlParams);
const [addonDetailsTransportUrl, setAddonDetailsTransportUrl] = useAddonDetailsTransportUrl(urlParams, queryParams);
@ -59,7 +60,7 @@ const Addons = ({ urlParams, queryParams }) => {
setAddonDetailsTransportUrl(event.dataset.addon.transportUrl);
}, [setAddonDetailsTransportUrl]);
const onAddonConfigure = React.useCallback((event) => {
window.open(event.dataset.addon.transportUrl.replace('manifest.json', 'configure'));
platform.openExternal(event.dataset.addon.transportUrl.replace('manifest.json', 'configure'));
}, []);
const closeAddonDetails = React.useCallback(() => {
setAddonDetailsTransportUrl(null);

View file

@ -1,2 +1,2 @@
declare const useInstalledAddons: (urlParams: UrlParams) => InstalledAddons;
export = useInstalledAddons;
export = useInstalledAddons;

View file

@ -1,2 +1,2 @@
declare const useRemoteAddons: (urlParams: UrlParams) => RemoteAddons;
export = useRemoteAddons;
export = useRemoteAddons;

View file

@ -1,2 +1,2 @@
declare const useBoard: () => [Board, ({ start, end }: { start: number, end: number }) => void];
export = useBoard;
export = useBoard;

View file

@ -18,7 +18,7 @@ type Props = {
const Calendar = ({ urlParams }: Props) => {
const calendar = useCalendar(urlParams);
const profile = useProfile();
const [paginationInput] = useSelectableInputs(calendar, profile);
const { toDayMonth } = useCalendarDate(profile);

View file

@ -42,4 +42,4 @@ const Details = ({ selected, items }: Props) => {
);
};
export default Details;
export default Details;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
import Details from './Details';
export default Details;
export default Details;

View file

@ -2,4 +2,4 @@
import Item from './Item';
export default Item;
export default Item;

View file

@ -2,4 +2,4 @@
import List from './List';
export default List;
export default List;

View file

@ -2,4 +2,4 @@
import Placeholder from './Placeholder';
export default Placeholder;
export default Placeholder;

View file

@ -2,4 +2,4 @@
import Cell from './Cell';
export default Cell;
export default Cell;

View file

@ -2,4 +2,4 @@
import Table from './Table';
export default Table;
export default Table;

View file

@ -1,6 +1,6 @@
const useCalendarDate = (profile: Profile) => {
const toMonthYear = (calendarDate: CalendarDate | null): string => {
if (!calendarDate) return ``;
if (!calendarDate) return '';
const date = new Date();
date.setMonth(calendarDate.month - 1);
@ -13,7 +13,7 @@ const useCalendarDate = (profile: Profile) => {
};
const toDayMonth = (calendarDate: CalendarDate | null): string => {
if (!calendarDate) return ``;
if (!calendarDate) return '';
const date = new Date();
date.setDate(calendarDate.day);
@ -31,4 +31,4 @@ const useCalendarDate = (profile: Profile) => {
};
};
export default useCalendarDate;
export default useCalendarDate;

View file

@ -1,2 +1,2 @@
declare const useDiscover: (urlParams: UrlParams, searchParams: URLSearchParams) => [Discover, () => void];
export = useDiscover;
export = useDiscover;

View file

@ -1,6 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
@ -10,7 +11,7 @@ const { Button, Image, useBinaryState } = require('stremio/common');
const CredentialsTextInput = require('./CredentialsTextInput');
const ConsentCheckbox = require('./ConsentCheckbox');
const PasswordResetModal = require('./PasswordResetModal');
const useFacebookToken = require('./useFacebookToken');
const useFacebookLogin = require('./useFacebookLogin');
const styles = require('./styles');
const SIGNUP_FORM = 'signup';
@ -18,8 +19,9 @@ const LOGIN_FORM = 'login';
const Intro = ({ queryParams }) => {
const { core } = useServices();
const { t } = useTranslation();
const routeFocused = useRouteFocused();
const getFacebookToken = useFacebookToken();
const [startFacebookLogin, stopFacebookLogin] = useFacebookLogin();
const emailRef = React.useRef(null);
const passwordRef = React.useRef(null);
const confirmPasswordRef = React.useRef(null);
@ -80,15 +82,17 @@ const Intro = ({ queryParams }) => {
);
const loginWithFacebook = React.useCallback(() => {
openLoaderModal();
getFacebookToken()
.then((accessToken) => {
startFacebookLogin()
.then(({ email, password }) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Authenticate',
args: {
type: 'Facebook',
token: accessToken,
type: 'Login',
email,
password,
facebook: true
}
}
});
@ -98,6 +102,10 @@ const Intro = ({ queryParams }) => {
dispatch({ type: 'error', error: error.message });
});
}, []);
const cancelLoginWithFacebook = React.useCallback(() => {
stopFacebookLogin();
closeLoaderModal();
}, []);
const loginWithEmail = React.useCallback(() => {
if (typeof state.email !== 'string' || state.email.length === 0 || !emailRef.current.validity.valid) {
dispatch({ type: 'error', error: 'Invalid email' });
@ -383,6 +391,9 @@ const Intro = ({ queryParams }) => {
<div className={styles['loader-container']}>
<Icon className={styles['icon']} name={'person'} />
<div className={styles['label']}>Authenticating...</div>
<Button className={styles['button']} onClick={cancelLoginWithFacebook}>
{t('BUTTON_CANCEL')}
</Button>
</div>
</Modal>
:

View file

@ -3,17 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const { useRouteFocused } = require('stremio-router');
const { ModalDialog } = require('stremio/common');
const { ModalDialog, usePlatform } = require('stremio/common');
const CredentialsTextInput = require('../CredentialsTextInput');
const styles = require('./styles');
const PasswordResetModal = ({ email, onCloseRequest }) => {
const routeFocused = useRouteFocused();
const platform = usePlatform();
const [error, setError] = React.useState('');
const emailRef = React.useRef(null);
const goToPasswordReset = React.useCallback(() => {
emailRef.current.value.length > 0 && emailRef.current.validity.valid ?
window.open('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
:
setError('Invalid email');
}, []);

View file

@ -200,7 +200,8 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
gap: 1rem;
padding: 2.5rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
@ -218,7 +219,6 @@
flex: none;
width: 5rem;
height: 5rem;
margin-bottom: 1rem;
color: var(--primary-foreground-color);
animation: 1s linear infinite alternate flash;
}
@ -228,6 +228,27 @@
color: var(--primary-foreground-color);
animation: 1s linear infinite alternate flash;
}
.button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 3.5rem;
width: 100%;
border-radius: 3.5rem;
padding: 0 1rem;
margin-top: 2rem;
font-size: 1.1rem;
font-weight: 700;
color: var(--primary-foreground-color);
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
&:hover {
color: var(--secondary-foreground-color);
background-color: var(--primary-foreground-color);
}
}
}
}

View file

@ -0,0 +1,71 @@
// Copyright (C) 2017-2023 Smart code 203358507
import { useCallback, useEffect, useRef } from 'react';
import hat from 'hat';
import { usePlatform } from 'stremio/common';
const STREMIO_URL = 'https://www.strem.io';
const MAX_TRIES = 25;
const getCredentials = async (state: string) => {
try {
const response = await fetch(`${STREMIO_URL}/login-fb-get-acc/${state}`);
const { user } = await response.json();
return Promise.resolve({
email: user.email,
password: user.fbLoginToken,
});
} catch (e) {
console.error('Failed to get credentials from facebook auth', e);
return Promise.reject(e);
}
};
const useFacebookLogin = () => {
const platform = usePlatform();
const started = useRef(false);
const timeout = useRef<NodeJS.Timeout | null>(null);
const start = useCallback(() => new Promise((resolve, reject) => {
started.current = true;
const state = hat(128);
let tries = 0;
platform.openExternal(`${STREMIO_URL}/login-fb/${state}`);
const waitForCredentials = () => {
if (started.current) {
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
if (tries >= MAX_TRIES)
return reject(new Error('Failed to authenticate with facebook'));
tries++;
getCredentials(state)
.then(resolve)
.catch(waitForCredentials);
}, 1000);
}
};
waitForCredentials();
}), []);
const stop = useCallback(() => {
started.current = false;
timeout.current && clearTimeout(timeout.current);
}, []);
useEffect(() => {
return () => stop();
}, []);
return [
start,
stop,
];
};
module.exports = useFacebookLogin;

View file

@ -1,51 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const useFacebookToken = () => {
const getToken = React.useCallback(() => {
return new Promise((resolve, reject) => {
if (typeof FB === 'undefined') {
reject(new Error('Failed to connect to Facebook'));
return;
}
FB.getLoginStatus((resp) => {
if (resp && resp.authResponse && typeof resp.authResponse.accessToken === 'string') {
resolve(resp.authResponse.accessToken);
return;
}
FB.login((resp) => {
if (!resp || !resp.authResponse || typeof resp.authResponse.accessToken !== 'string') {
reject(new Error('Failed to get token from Facebook'));
return;
}
resolve(resp.authResponse.accessToken);
});
});
});
}, []);
React.useEffect(() => {
window.fbAsyncInit = function() {
FB.init({
appId: '1537119779906825',
status: true,
xfbml: false,
version: 'v2.7'
});
};
const sdkScriptElement = document.createElement('script');
sdkScriptElement.src = 'https://connect.facebook.net/en_US/sdk.js';
sdkScriptElement.async = true;
sdkScriptElement.defer = true;
document.body.appendChild(sdkScriptElement);
return () => {
document.body.removeChild(sdkScriptElement);
};
}, []);
return getToken;
};
module.exports = useFacebookToken;

View file

@ -1,2 +1,2 @@
declare const useLibrary: (model: string, urlParams: UrlParams, searchParams: URLSearchParams) => Library;
export = useLibrary;
export = useLibrary;

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { t } = require('i18next');
const { Button, Image, useProfile, platform, useToast, Popup, useBinaryState } = require('stremio/common');
const { Button, Image, useProfile, usePlatform, useToast, Popup, useBinaryState } = require('stremio/common');
const { useServices } = require('stremio/services');
const { useRouteFocused } = require('stremio-router');
const StreamPlaceholder = require('./StreamPlaceholder');
@ -14,6 +14,7 @@ const styles = require('./styles');
const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => {
const profile = useProfile();
const toast = useToast();
const platform = usePlatform();
const { core } = useServices();
const routeFocused = useRouteFocused();

View file

@ -1,2 +1,2 @@
declare const useMetaDetails: (urlParams: UrlParams) => MetaDetails;
export = useMetaDetails;
export = useMetaDetails;

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { useToast } = require('stremio/common');
const { usePlatform, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
@ -12,6 +12,7 @@ const styles = require('./styles');
const OptionsMenu = ({ className, stream, playbackDevices }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
const toast = useToast();
const [streamingUrl, downloadUrl] = React.useMemo(() => {
return stream !== null ?
@ -48,7 +49,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
}, [streamingUrl, downloadUrl]);
const onDownloadVideoButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
window.open(streamingUrl || downloadUrl);
platform.openExternal(streamingUrl || downloadUrl);
}
}, [streamingUrl, downloadUrl]);
const onExternalDeviceRequested = React.useCallback((deviceId) => {

View file

@ -1,2 +1,2 @@
declare const usePlayer: (urlParams: UrlParams, videoParams: any) => [Player, (time: number, duration: number, device: string) => void, (paused: boolean) => void, () => void, () => void];
export = usePlayer;
export = usePlayer;

View file

@ -1,2 +1,2 @@
declare const useSettings: () => [Settings, (settings: any) => void];
export = useSettings;
export = useSettings;

View file

@ -1,2 +1,2 @@
declare const useSearch: (searchParams: URLSearchParams) => [Search, (range: number) => void];
export = useSearch;
export = useSearch;

View file

@ -7,7 +7,7 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, useStreamingServer, useBinaryState, withCoreSuspender, useToast } = require('stremio/common');
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, usePlatform, useStreamingServer, useBinaryState, withCoreSuspender, useToast } = require('stremio/common');
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
const useDataExport = require('./useDataExport');
@ -25,6 +25,7 @@ const Settings = () => {
const profile = useProfile();
const [dataExport, loadDataExport] = useDataExport();
const streamingServer = useStreamingServer();
const platform = usePlatform();
const toast = useToast();
const {
interfaceLanguageSelect,
@ -90,7 +91,7 @@ const Settings = () => {
}, []);
const toggleTraktOnClick = React.useCallback(() => {
if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') {
window.open(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`);
platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`);
setTraktAuthStarted(true);
} else {
core.transport.dispatch({
@ -102,15 +103,18 @@ const Settings = () => {
}
}, [isTraktAuthenticated, profile.auth]);
const subscribeCalendarOnClick = React.useCallback(() => {
const url = `webcal://www.strem.io/calendar/${profile.auth.user._id}.ics`;
window.open(url);
if (!profile.auth) return;
const protocol = platform.name === 'ios' ? 'webcal' : 'https';
const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`;
platform.openExternal(url);
toast.show({
type: 'success',
title: 'Calendar has been added to your default caldendar app',
title: platform.name === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'),
timeout: 25000
});
//Stremio 4 emits not documented event subscribeCalendar
}, []);
// Stremio 4 emits not documented event subscribeCalendar
}, [profile.auth]);
const exportDataOnClick = React.useCallback(() => {
loadDataExport();
}, []);
@ -181,7 +185,7 @@ const Settings = () => {
}, [isTraktAuthenticated, traktAuthStarted]);
React.useEffect(() => {
if (dataExport.exportUrl !== null && typeof dataExport.exportUrl === 'string') {
window.open(dataExport.exportUrl);
platform.openExternal(dataExport.exportUrl);
}
}, [dataExport.exportUrl]);
React.useLayoutEffect(() => {
@ -261,9 +265,14 @@ const Settings = () => {
</div>
<div className={styles['section-container']}>
<div className={classnames(styles['option-container'], styles['link-container'])}>
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_DATA_EXPORT')} tabIndex={-1} onClick={exportDataOnClick}>
<div className={styles['label']}>{ t('SETTINGS_DATA_EXPORT') }</div>
</Button>
{
profile.auth ?
<Button className={classnames(styles['option-input-container'], styles['link-input-container'])} title={t('SETTINGS_DATA_EXPORT')} tabIndex={-1} onClick={exportDataOnClick}>
<div className={styles['label']}>{ t('SETTINGS_DATA_EXPORT') }</div>
</Button>
:
null
}
</div>
{
profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?

View file

@ -3,11 +3,12 @@
const React = require('react');
const { useTranslation } = require('react-i18next');
const { useServices } = require('stremio/services');
const { CONSTANTS, interfaceLanguages, languageNames, platform } = require('stremio/common');
const { CONSTANTS, usePlatform, interfaceLanguages, languageNames } = require('stremio/common');
const useProfileSettingsInputs = (profile) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
// TODO combine those useMemo in one
const interfaceLanguageSelect = React.useMemo(() => ({
options: interfaceLanguages.map(({ name, codes }) => ({

View file

@ -1,2 +1,2 @@
declare function Core(): Core;
export = Core;
export = Core;

View file

@ -1,2 +1,2 @@
declare function CoreTransport(): CoreTransport;
export = CoreTransport;
export = CoreTransport;

View file

@ -9,4 +9,4 @@ declare global {
}
}
export {};
export {};

View file

@ -25,4 +25,4 @@ interface CoreTransport {
interface Core {
active: boolean,
transport: CoreTransport,
}
}

View file

@ -25,7 +25,7 @@ function DragAndDrop({ core }) {
args: Array.from(new Uint8Array(torrent))
}
});
} catch (error) {
} catch (_error) {
events.emit('error', {
message: 'Failed to process file',
file: {

View file

@ -4,4 +4,4 @@ type ServicesContext = {
chromecast: any,
keyboardShortcuts: any,
dragAndDrop: any,
};
};

View file

@ -1,2 +1,2 @@
declare const useService: () => ServicesContext;
export = useService;
export = useService;

View file

@ -45,6 +45,7 @@ function ShellTransport() {
this.props = {};
// eslint-disable-next-line @typescript-eslint/no-this-alias
const shell = this;
initialize()
.then(() => {

View file

@ -17,4 +17,4 @@ type Addon = {
type AddonsDeepLinks = {
addons: string,
};
};

View file

@ -31,4 +31,4 @@ type LibraryItemDeepLinks = {
metaDetailsStreams: string | null,
player: string | null,
externalPlayer: ExternalPlayerLinks | null,
};
};

View file

@ -29,4 +29,4 @@ type MetaItemDeepLinks = {
metaDetailsVideos: string | null,
metaDetailsStreams: string | null,
player: string | null,
};
};

View file

@ -24,4 +24,4 @@ type SelectableCatalog<T> = {
name: string,
selected: boolean,
deepLinks: T,
};
};

View file

@ -15,4 +15,4 @@ type Stream = {
player: string,
externalPlayer: ExternalPlayerLinks,
},
};
};

View file

@ -14,4 +14,4 @@ type Video = {
episode?: number,
streams: Stream[],
trailerStreams: TrailerStream[],
};
};

15
src/types/global.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
/* eslint-disable no-var */
interface QtTransport {
send: (message: string) => void,
}
interface Qt {
webChannelTransport: QtTransport,
}
declare global {
var qt: Qt | undefined;
}
export { };

View file

@ -1 +1 @@
type Board = CatalogsWithExtra;
type Board = CatalogsWithExtra;

View file

@ -52,4 +52,4 @@ type Calendar = {
selected: CalendarSelected,
monthInfo: CalendarMonthInfo,
items: CalendarItem[],
};
};

View file

@ -8,4 +8,4 @@ type CatalogsWithExtra = {
type: string | null,
extra: [string, string][]
} | null,
};
};

View file

@ -71,4 +71,4 @@ type Ctx = {
profile: Profile,
notifications: Notifications,
searchHistory: SearchHistory,
};
};

View file

@ -23,4 +23,4 @@ type Discover = {
selected: {
request: ResourceRequest,
} | null,
};
};

View file

@ -9,4 +9,4 @@ type InstalledAddons = {
type: string,
}
} | null,
};
};

View file

@ -26,4 +26,4 @@ type Library = {
type: string | null,
}
} | null,
};
};

View file

@ -7,4 +7,4 @@ type LocalSearchItem = {
type LocalSearch = {
items: LocalSearchItem[],
};
};

View file

@ -24,4 +24,4 @@ type MetaDetails = {
content: Loadable<Stream[]>
}[],
title: string | null,
};
};

View file

@ -42,4 +42,4 @@ type Player = {
} | null,
subtitles: Subtitle[],
title: string | null,
};
};

View file

@ -7,4 +7,4 @@ type RemoteAddons = {
selected: {
request: ResourceRequest,
} | null,
};
};

View file

@ -1 +1 @@
type Search = CatalogsWithExtra;
type Search = CatalogsWithExtra;

View file

@ -115,4 +115,4 @@ type StreamingServer = {
torrent: [string, Loadable<Torrent>] | null,
statistics: Loadable<Statistics> | null,
playbackDevices: Loadable<PlaybackDevice[]> | null,
};
};

View file

@ -51,7 +51,7 @@ type BehaviorHints = {
hasScheduledVideos: boolean,
};
type PosterShape = 'square' | 'landscape' | 'poster' | null;
type PosterShape = 'square' | 'landscape' | 'poster' | null;
type Catalog<T, D = any> = {
label?: string,
@ -60,4 +60,4 @@ type Catalog<T, D = any> = {
content: T,
installed?: boolean,
deepLinks?: D,
};
};

View file

@ -2,10 +2,11 @@
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable"],
"jsx": "react",
"baseUrl": "src",
"baseUrl": "./src",
"outDir": "./dist",
"moduleResolution": "node",
"paths": {
"stremio/*": ["src/*"],
"stremio/*": ["*"],
},
"resolveJsonModule": true,
"esModuleInterop": true,
@ -15,6 +16,6 @@
"strict": true,
},
"include": [
"src",
],
"src/",
]
}

View file

@ -194,7 +194,7 @@ module.exports = (env, argv) => ({
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['*']
}),
argv.mode === 'production' &&
argv.mode === 'production' &&
new WorkboxPlugin.GenerateSW({
maximumFileSizeToCacheInBytes: 20000000,
clientsClaim: true,