mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' into feat/seek-player-action
This commit is contained in:
commit
2b4e616701
104 changed files with 3063 additions and 913 deletions
99
.eslintrc
99
.eslintrc
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
|
|
@ -3,19 +3,28 @@ name: Build
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
tags-ignore:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
- development
|
||||
# Allow manual dispatch in GH
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install NPM dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
|
|
@ -26,14 +35,14 @@ jobs:
|
|||
run: npm run lint
|
||||
# Create recursivelly the destiantion dir with
|
||||
# "--parrents where no error if existing, make parent directories as needed."
|
||||
- run: mkdir -p ./build/${{ github.ref_name }}
|
||||
- run: mkdir -p ./build/${{ github.head_ref || github.ref_name }}
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./build
|
||||
# in stremio, we use `feat/features-name` or `fix/this-bug`
|
||||
# so we need a recursive creation of the destination dir
|
||||
destination_dir: ${{ github.ref_name }}
|
||||
destination_dir: ${{ github.head_ref || github.ref_name }}
|
||||
allow_empty_commit: true
|
||||
|
|
|
|||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
20
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Stremio Node 14.x
|
||||
# Stremio Node 20.x
|
||||
# the node version for running Stremio Web
|
||||
ARG NODE_VERSION=20-alpine
|
||||
FROM node:$NODE_VERSION AS base
|
||||
|
|
|
|||
102
eslint.config.mjs
Normal file
102
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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/semi': 'error',
|
||||
'@stylistic/no-extra-semi': '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',
|
||||
}
|
||||
}
|
||||
];
|
||||
2469
package-lock.json
generated
2469
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"displayName": "Stremio",
|
||||
"version": "5.0.0-beta.8",
|
||||
"version": "5.0.0-beta.13",
|
||||
"author": "Smart Code OOD",
|
||||
"private": true,
|
||||
"license": "gpl-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"start-prod": "webpack serve --mode production",
|
||||
"build": "webpack --mode production",
|
||||
"test": "jest",
|
||||
"lint": "eslint src"
|
||||
|
|
@ -15,16 +16,16 @@
|
|||
"@babel/runtime": "7.16.0",
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@stremio/stremio-colors": "5.0.1",
|
||||
"@stremio/stremio-core-web": "0.47.7",
|
||||
"@stremio/stremio-core-web": "0.47.8",
|
||||
"@stremio/stremio-icons": "5.2.0",
|
||||
"@stremio/stremio-video": "0.0.38",
|
||||
"@stremio/stremio-video": "0.0.46",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
"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",
|
||||
|
|
@ -39,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#57d66ecc8e2df4e73a613dc5e17123ce62ae63f7",
|
||||
"url": "0.11.0",
|
||||
"use-long-press": "^3.1.5"
|
||||
},
|
||||
|
|
@ -49,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",
|
||||
"babel-loader": "8.2.3",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
|
|
@ -56,8 +61,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",
|
||||
|
|
@ -68,6 +74,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",
|
||||
|
|
|
|||
|
|
@ -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']} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -105,7 +107,9 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
|
|||
}
|
||||
}
|
||||
:
|
||||
addonDetails.remoteAddon !== null && addonDetails.remoteAddon.content.type === 'Ready' ?
|
||||
addonDetails.remoteAddon !== null &&
|
||||
addonDetails.remoteAddon.content.type === 'Ready' &&
|
||||
!addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurationRequired ?
|
||||
{
|
||||
|
||||
className: styles['install-button'],
|
||||
|
|
@ -131,7 +135,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
|
|||
}
|
||||
:
|
||||
null;
|
||||
return toggleButton !== null ? configureButton ? [cancelButton, configureButton, toggleButton] : [cancelButton, toggleButton] : [cancelButton];
|
||||
return configureButton && toggleButton ? [cancelButton, configureButton, toggleButton] : configureButton ? [cancelButton, configureButton] : toggleButton ? [cancelButton, toggleButton] : [cancelButton];
|
||||
}, [addonDetails, onCloseRequest]);
|
||||
const modalBackground = React.useMemo(() => {
|
||||
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const classnames = require('classnames');
|
|||
const styles = require('./styles');
|
||||
const { useLongPress } = require('use-long-press');
|
||||
|
||||
const Button = React.forwardRef(({ className, href, disabled, children, onLongPress, ...props }, ref) => {
|
||||
const Button = React.forwardRef(({ className, href, disabled, children, onLongPress, onDoubleClick, ...props }, ref) => {
|
||||
const longPress = useLongPress(onLongPress, { detect: 'pointer' });
|
||||
const onKeyDown = React.useCallback((event) => {
|
||||
if (typeof props.onKeyDown === 'function') {
|
||||
|
|
@ -42,6 +42,7 @@ const Button = React.forwardRef(({ className, href, disabled, children, onLongPr
|
|||
href,
|
||||
onKeyDown,
|
||||
onMouseDown,
|
||||
onDoubleClick,
|
||||
...longPress()
|
||||
},
|
||||
children
|
||||
|
|
@ -58,6 +59,7 @@ Button.propTypes = {
|
|||
onKeyDown: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onLongPress: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = Button;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const EXTERNAL_PLAYERS = [
|
|||
{
|
||||
label: 'EXTERNAL_PLAYER_DISABLED',
|
||||
value: null,
|
||||
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
|
||||
platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'],
|
||||
},
|
||||
{
|
||||
label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING',
|
||||
|
|
@ -54,7 +54,7 @@ const EXTERNAL_PLAYERS = [
|
|||
{
|
||||
label: 'VLC',
|
||||
value: 'vlc',
|
||||
platforms: ['ios', 'android'],
|
||||
platforms: ['ios', 'visionos', 'android'],
|
||||
},
|
||||
{
|
||||
label: 'MPV',
|
||||
|
|
@ -79,15 +79,22 @@ const EXTERNAL_PLAYERS = [
|
|||
{
|
||||
label: 'Outplayer',
|
||||
value: 'outplayer',
|
||||
platforms: ['ios'],
|
||||
platforms: ['ios', 'visionos'],
|
||||
},
|
||||
{
|
||||
label: 'Moonplayer (VisionOS)',
|
||||
value: 'moonplayer',
|
||||
platforms: ['visionos'],
|
||||
},
|
||||
{
|
||||
label: 'M3U Playlist',
|
||||
value: 'm3u',
|
||||
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
|
||||
platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'],
|
||||
},
|
||||
];
|
||||
|
||||
const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'x.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle'];
|
||||
|
||||
module.exports = {
|
||||
CHROMECAST_RECEIVER_APP_ID,
|
||||
SUBTITLES_SIZES,
|
||||
|
|
@ -105,4 +112,5 @@ module.exports = {
|
|||
TYPE_PRIORITIES,
|
||||
ICON_FOR_TYPE,
|
||||
EXTERNAL_PLAYERS,
|
||||
WHITELISTED_HOSTS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,4 +42,4 @@ const Chip = memo(({ label, value, active, onSelect }: Props) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default Chip;
|
||||
export default Chip;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import Chip from './Chip';
|
||||
export default Chip;
|
||||
export default Chip;
|
||||
|
|
|
|||
|
|
@ -53,4 +53,4 @@ const Chips = memo(({ options, selected, onSelect }: Props) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default Chips;
|
||||
export default Chips;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import Chips from './Chips';
|
||||
export default Chips;
|
||||
export default Chips;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ const DelayedRenderer = ({ children, delay }) => {
|
|||
};
|
||||
|
||||
DelayedRenderer.propTypes = {
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
delay: PropTypes.number,
|
||||
};
|
||||
|
||||
module.exports = DelayedRenderer;
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ const Image = ({ className, src, alt, fallbackSrc, renderFallback, ...props }) =
|
|||
typeof renderFallback === 'function' ?
|
||||
renderFallback()
|
||||
:
|
||||
<img {...props} className={className} src={fallbackSrc} alt={alt} />
|
||||
<img {...props} className={className} src={fallbackSrc} alt={alt} loading='lazy'/>
|
||||
:
|
||||
<img {...props} className={className} src={src} alt={alt} onError={onError} />;
|
||||
<img {...props} className={className} src={src} alt={alt} loading='lazy' onError={onError} />;
|
||||
};
|
||||
|
||||
Image.propTypes = {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
|
|||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['modal-dialog-content']}>
|
||||
<div className={styles['body-container']}>
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@
|
|||
.modal-dialog-content {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
.title-container {
|
||||
flex: 1 0 auto;
|
||||
|
|
@ -78,7 +79,7 @@
|
|||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.modal-dialog-content {
|
||||
.body-container {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
overflow-y: auto;
|
||||
|
|
@ -157,9 +158,11 @@
|
|||
z-index: 0;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.buttons-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.modal-dialog-content {
|
||||
.buttons-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
42
src/common/MultiselectMenu/Dropdown/Dropdown.less
Normal file
42
src/common/MultiselectMenu/Dropdown/Dropdown.less
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
.dropdown {
|
||||
background: var(--modal-background-color);
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
box-shadow: var(--outer-glow);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
max-height: calc(3.2rem * 10);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 0.5rem;
|
||||
padding: 0.75rem;
|
||||
color: var(--primary-foreground-color);
|
||||
|
||||
.back-button-icon {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
.dropdown {
|
||||
&.open {
|
||||
max-height: calc(3.2rem * 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/common/MultiselectMenu/Dropdown/Dropdown.tsx
Normal file
54
src/common/MultiselectMenu/Dropdown/Dropdown.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import React from 'react';
|
||||
import Button from 'stremio/common/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import Option from './Option';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import styles from './Dropdown.less';
|
||||
|
||||
type Props = {
|
||||
options: MultiselectMenuOption[];
|
||||
selectedOption?: MultiselectMenuOption | null;
|
||||
menuOpen: boolean | (() => void);
|
||||
level: number;
|
||||
setLevel: (level: number) => void;
|
||||
onSelect: (value: number) => void;
|
||||
};
|
||||
|
||||
const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
setLevel(level - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles['dropdown'], { [styles['open']]: menuOpen })} role={'listbox'}>
|
||||
{
|
||||
level > 0 ?
|
||||
<Button className={styles['back-button']} onClick={onBackButtonClick}>
|
||||
<Icon name={'caret-left'} className={styles['back-button-icon']} />
|
||||
{t('BACK')}
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
{
|
||||
options
|
||||
.filter((option: MultiselectMenuOption) => !option.hidden)
|
||||
.map((option: MultiselectMenuOption, index) => (
|
||||
<Option
|
||||
key={index}
|
||||
option={option}
|
||||
onSelect={onSelect}
|
||||
selectedOption={selectedOption}
|
||||
/>
|
||||
))
|
||||
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
29
src/common/MultiselectMenu/Dropdown/Option/Option.less
Normal file
29
src/common/MultiselectMenu/Dropdown/Option/Option.less
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
.option {
|
||||
font-size: var(--font-size-normal);
|
||||
color: var(--primary-foreground-color);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1rem;
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: none;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: var(--secondary-accent-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
46
src/common/MultiselectMenu/Dropdown/Option/Option.tsx
Normal file
46
src/common/MultiselectMenu/Dropdown/Option/Option.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Button from 'stremio/common/Button';
|
||||
import styles from './Option.less';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
|
||||
type Props = {
|
||||
option: MultiselectMenuOption;
|
||||
selectedOption?: MultiselectMenuOption | null;
|
||||
onSelect: (value: number) => void;
|
||||
};
|
||||
|
||||
const Option = ({ option, selectedOption, onSelect }: Props) => {
|
||||
// consider using option.id === selectedOption?.id instead
|
||||
const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect(option.value);
|
||||
}, [onSelect, option.value]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classNames(styles['option'], { [styles['selected']]: selected })}
|
||||
key={option.id}
|
||||
onClick={handleClick}
|
||||
aria-selected={selected}
|
||||
>
|
||||
<div className={styles['label']}>{ option.label }</div>
|
||||
{
|
||||
selected && !option.level ?
|
||||
<div className={styles['icon']} />
|
||||
: null
|
||||
|
||||
}
|
||||
{
|
||||
option.level ?
|
||||
<Icon name={'caret-right'} className={styles['option-caret']} />
|
||||
: null
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Option;
|
||||
5
src/common/MultiselectMenu/Dropdown/Option/index.ts
Normal file
5
src/common/MultiselectMenu/Dropdown/Option/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import Option from './Option';
|
||||
|
||||
export default Option;
|
||||
5
src/common/MultiselectMenu/Dropdown/index.ts
Normal file
5
src/common/MultiselectMenu/Dropdown/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import Dropdown from './Dropdown';
|
||||
|
||||
export default Dropdown;
|
||||
39
src/common/MultiselectMenu/MultiselectMenu.less
Normal file
39
src/common/MultiselectMenu/MultiselectMenu.less
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
@border-radius: 2.75rem;
|
||||
|
||||
.multiselect-menu {
|
||||
position: relative;
|
||||
min-width: 8.5rem;
|
||||
overflow: visible;
|
||||
border-radius: @border-radius;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.multiselect-button {
|
||||
color: var(--primary-foreground-color);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0 0.5rem;
|
||||
border-radius: @border-radius;
|
||||
|
||||
.icon {
|
||||
width: 1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.6;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
}
|
||||
57
src/common/MultiselectMenu/MultiselectMenu.tsx
Normal file
57
src/common/MultiselectMenu/MultiselectMenu.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import React from 'react';
|
||||
import Button from 'stremio/common/Button';
|
||||
import useBinaryState from 'stremio/common/useBinaryState';
|
||||
import Dropdown from './Dropdown';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import styles from './MultiselectMenu.less';
|
||||
import useOutsideClick from 'stremio/common/useOutsideClick';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
title?: string;
|
||||
options: MultiselectMenuOption[];
|
||||
selectedOption?: MultiselectMenuOption;
|
||||
onSelect: (value: number) => void;
|
||||
};
|
||||
|
||||
const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => {
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const multiselectMenuRef = useOutsideClick(() => closeMenu());
|
||||
const [level, setLevel] = React.useState<number>(0);
|
||||
|
||||
const onOptionSelect = (value: number) => {
|
||||
level ? setLevel(level + 1) : onSelect(value), closeMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
|
||||
<Button
|
||||
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
|
||||
onClick={toggleMenu}
|
||||
tabIndex={0}
|
||||
aria-haspopup='listbox'
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
{title}
|
||||
<Icon name={'caret-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
|
||||
</Button>
|
||||
{
|
||||
menuOpen ?
|
||||
<Dropdown
|
||||
level={level}
|
||||
setLevel={setLevel}
|
||||
options={options}
|
||||
onSelect={onOptionSelect}
|
||||
menuOpen={menuOpen}
|
||||
selectedOption={selectedOption}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiselectMenu;
|
||||
5
src/common/MultiselectMenu/index.ts
Normal file
5
src/common/MultiselectMenu/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import MultiselectMenu from './MultiselectMenu';
|
||||
|
||||
export default MultiselectMenu;
|
||||
9
src/common/MultiselectMenu/types.d.ts
vendored
Normal file
9
src/common/MultiselectMenu/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
type MultiselectMenuOption = {
|
||||
id?: number;
|
||||
label: string;
|
||||
value: number;
|
||||
destination?: string;
|
||||
default?: boolean;
|
||||
hidden?: boolean;
|
||||
level?: MultiselectMenuOption[];
|
||||
};
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
declare const useLocalSearch: () => { items: LocalSearchItem[], search: (query: string) => void };
|
||||
export = useLocalSearch;
|
||||
export = useLocalSearch;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
declare const useSearchHistory: () => { items: SearchHistory, clear: () => void };
|
||||
export = useSearchHistory;
|
||||
export = useSearchHistory;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,17 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick })
|
|||
:
|
||||
null
|
||||
), [icon]);
|
||||
const onDoubleClick = () => {
|
||||
const scrollableElements = document.querySelectorAll('div');
|
||||
|
||||
scrollableElements.forEach((element) => {
|
||||
if (element.scrollTop > 0) {
|
||||
element.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Button className={classnames(className, styles['nav-tab-button-container'], { 'selected': selected })} title={label} tabIndex={-1} href={href} onClick={onClick}>
|
||||
<Button className={classnames(className, styles['nav-tab-button-container'], { 'selected': selected })} title={label} tabIndex={-1} href={href} onClick={onClick} onDoubleClick={onDoubleClick}>
|
||||
{
|
||||
typeof logo === 'string' && logo.length > 0 ?
|
||||
<Image
|
||||
|
|
|
|||
51
src/common/Platform/Platform.tsx
Normal file
51
src/common/Platform/Platform.tsx
Normal 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
|
||||
};
|
||||
31
src/common/Platform/device.ts
Normal file
31
src/common/Platform/device.ts
Normal 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,
|
||||
};
|
||||
5
src/common/Platform/index.ts
Normal file
5
src/common/Platform/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PlatformProvider, usePlatform } from './Platform';
|
||||
export {
|
||||
PlatformProvider,
|
||||
usePlatform,
|
||||
};
|
||||
22
src/common/Platform/useShell.ts
Normal file
22
src/common/Platform/useShell.ts
Normal 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;
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ const MetaPreview = require('./MetaPreview');
|
|||
const MetaRow = require('./MetaRow');
|
||||
const ModalDialog = require('./ModalDialog');
|
||||
const Multiselect = require('./Multiselect');
|
||||
const { default: MultiselectMenu } = require('./MultiselectMenu');
|
||||
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
|
||||
const PaginationInput = require('./PaginationInput');
|
||||
const { PlatformProvider, usePlatform } = require('./Platform');
|
||||
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
|
||||
const Popup = require('./Popup');
|
||||
const SearchBar = require('./SearchBar');
|
||||
|
|
@ -44,7 +46,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 = {
|
||||
|
|
@ -63,9 +64,12 @@ module.exports = {
|
|||
MetaRow,
|
||||
ModalDialog,
|
||||
Multiselect,
|
||||
MultiselectMenu,
|
||||
HorizontalNavBar,
|
||||
VerticalNavBar,
|
||||
PaginationInput,
|
||||
PlatformProvider,
|
||||
usePlatform,
|
||||
PlayIconCircleCentered,
|
||||
Popup,
|
||||
SearchBar,
|
||||
|
|
@ -96,6 +100,5 @@ module.exports = {
|
|||
useStreamingServer,
|
||||
useTorrent,
|
||||
useTranslate,
|
||||
platform,
|
||||
EventModal,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
"name": "български език",
|
||||
"codes": ["bg-BG", "bul"]
|
||||
},
|
||||
{
|
||||
"name": "বাংলা",
|
||||
"codes": ["bn-Bd", "ben"]
|
||||
},
|
||||
{
|
||||
"name": "català",
|
||||
"codes": ["ca-CA", "cat"]
|
||||
|
|
@ -75,6 +79,14 @@
|
|||
"name": "italiano",
|
||||
"codes": ["it-IT", "ita"]
|
||||
},
|
||||
{
|
||||
"name": "日本語 (にほんご)",
|
||||
"codes": ["ja-JP", "jpn"]
|
||||
},
|
||||
{
|
||||
"name": "한국어",
|
||||
"codes": ["ko-KR", "kor"]
|
||||
},
|
||||
{
|
||||
"name": "македонски јазик",
|
||||
"codes": ["mk-MK", "mkd"]
|
||||
|
|
@ -135,6 +147,10 @@
|
|||
"name": "українська мова",
|
||||
"codes": ["uk-UA", "ukr"]
|
||||
},
|
||||
{
|
||||
"name": "Tiếng Việt",
|
||||
"codes": ["vi-VN", "vie"]
|
||||
},
|
||||
{
|
||||
"name": "中文(中华人民共和国)",
|
||||
"codes": ["zh-CN", "zho"]
|
||||
|
|
@ -147,4 +163,4 @@
|
|||
"name": "中文(台灣)",
|
||||
"codes": ["zh-TW", "zho"]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
// this detects ipad properly in safari
|
||||
// while bowser does not
|
||||
function 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 || '');
|
||||
|
||||
const name = iOS() ? 'ios' : (browser?.os?.name || 'unknown').toLowerCase();
|
||||
|
||||
module.exports = {
|
||||
name,
|
||||
isMobile: () => {
|
||||
return name === 'ios' || name === 'android';
|
||||
}
|
||||
};
|
||||
8
src/common/useBinaryState.d.ts
vendored
Normal file
8
src/common/useBinaryState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
declare const useBinaryState: (initialValue?: boolean) => [
|
||||
boolean,
|
||||
() => void,
|
||||
() => void,
|
||||
() => void,
|
||||
];
|
||||
|
||||
export = useBinaryState;
|
||||
2
src/common/useNotifications.d.ts
vendored
2
src/common/useNotifications.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useNotifcations: () => Notifications;
|
||||
export = useNotifcations;
|
||||
export = useNotifcations;
|
||||
|
|
|
|||
27
src/common/useOutsideClick.ts
Normal file
27
src/common/useOutsideClick.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (C) 2017-2024 Smart code 203358507
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useOutsideClick = (callback: () => void) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', handleClickOutside);
|
||||
document.addEventListener('touchend', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleClickOutside);
|
||||
document.removeEventListener('touchend', handleClickOutside);
|
||||
};
|
||||
}, [callback]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useOutsideClick;
|
||||
2
src/common/useProfile.d.ts
vendored
2
src/common/useProfile.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useProfile: () => Profile;
|
||||
export = useProfile;
|
||||
export = useProfile;
|
||||
|
|
|
|||
2
src/common/useStreamingServer.d.ts
vendored
2
src/common/useStreamingServer.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useStreamingServer: () => StreamingServer;
|
||||
export = useStreamingServer;
|
||||
export = useStreamingServer;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const useTranslate = () => {
|
|||
|
||||
const catalogTitle = useCallback(({ addon, id, name, type } = {}, withType = true) => {
|
||||
if (addon && id && name) {
|
||||
const partialKey = `${addon.manifest.id.replaceAll('.', '_')}_${id}`;
|
||||
const partialKey = `${addon.manifest.id.split('.').join('_')}_${id}`;
|
||||
const translatedName = stringWithPrefix(partialKey, 'CATALOG_', name);
|
||||
|
||||
if (type && withType) {
|
||||
|
|
|
|||
9
src/modules.d.ts
vendored
9
src/modules.d.ts
vendored
|
|
@ -1,3 +1,6 @@
|
|||
declare module '*.less';
|
||||
declare module 'stremio/common';
|
||||
declare module 'stremio/common/*';
|
||||
declare module '*.less' {
|
||||
const resource: Record<string, string>;
|
||||
export = resource;
|
||||
}
|
||||
|
||||
declare module 'stremio/common/Button';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
2
src/routes/Addons/useInstalledAddons.d.ts
vendored
2
src/routes/Addons/useInstalledAddons.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useInstalledAddons: (urlParams: UrlParams) => InstalledAddons;
|
||||
export = useInstalledAddons;
|
||||
export = useInstalledAddons;
|
||||
|
|
|
|||
2
src/routes/Addons/useRemoteAddons.d.ts
vendored
2
src/routes/Addons/useRemoteAddons.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useRemoteAddons: (urlParams: UrlParams) => RemoteAddons;
|
||||
export = useRemoteAddons;
|
||||
export = useRemoteAddons;
|
||||
|
|
|
|||
2
src/routes/Board/useBoard.d.ts
vendored
2
src/routes/Board/useBoard.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useBoard: () => [Board, ({ start, end }: { start: number, end: number }) => void];
|
||||
export = useBoard;
|
||||
export = useBoard;
|
||||
|
|
|
|||
2
src/routes/Discover/useDiscover.d.ts
vendored
2
src/routes/Discover/useDiscover.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useDiscover: (urlParams: UrlParams, searchParams: URLSearchParams) => [Discover, () => void];
|
||||
export = useDiscover;
|
||||
export = useDiscover;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
:
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
71
src/routes/Intro/useFacebookLogin.ts
Normal file
71
src/routes/Intro/useFacebookLogin.ts
Normal 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;
|
||||
|
|
@ -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;
|
||||
2
src/routes/Library/useLibrary.d.ts
vendored
2
src/routes/Library/useLibrary.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useLibrary: (model: string, urlParams: UrlParams, searchParams: URLSearchParams) => Library;
|
||||
export = useLibrary;
|
||||
export = useLibrary;
|
||||
|
|
|
|||
|
|
@ -4,15 +4,52 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Button, Image, useProfile, platform, useToast } = require('stremio/common');
|
||||
const { t } = require('i18next');
|
||||
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');
|
||||
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();
|
||||
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
|
||||
const popupLabelOnMouseUp = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented) {
|
||||
if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) {
|
||||
event.preventDefault();
|
||||
toggleMenu();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
const popupLabelOnContextMenu = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [toggleMenu]);
|
||||
const popupLabelOnLongPress = React.useCallback((event) => {
|
||||
if (event.nativeEvent.pointerType !== 'mouse' && !event.nativeEvent.togglePopupPrevented) {
|
||||
toggleMenu();
|
||||
}
|
||||
}, [toggleMenu]);
|
||||
const popupMenuOnPointerDown = React.useCallback((event) => {
|
||||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const popupMenuOnContextMenu = React.useCallback((event) => {
|
||||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const popupMenuOnClick = React.useCallback((event) => {
|
||||
event.nativeEvent.togglePopupPrevented = true;
|
||||
}, []);
|
||||
const popupMenuOnKeyDown = React.useCallback((event) => {
|
||||
event.nativeEvent.buttonClickPrevented = true;
|
||||
}, []);
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
return deepLinks ?
|
||||
|
|
@ -47,6 +84,10 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
null;
|
||||
}, [href, deepLinks]);
|
||||
|
||||
const streamLink = React.useMemo(() => {
|
||||
return deepLinks?.externalPlayer?.download;
|
||||
}, [deepLinks]);
|
||||
|
||||
const markVideoAsWatched = React.useCallback(() => {
|
||||
if (typeof videoId === 'string') {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -74,41 +115,101 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
}
|
||||
}, [props.onClick, profile.settings, markVideoAsWatched]);
|
||||
|
||||
const copyStreamLink = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
if (streamLink) {
|
||||
navigator.clipboard.writeText(streamLink)
|
||||
.then(() => {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: t('PLAYER_COPY_STREAM_SUCCESS'),
|
||||
timeout: 4000
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: t('PLAYER_COPY_STREAM_ERROR'),
|
||||
timeout: 4000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [streamLink]);
|
||||
|
||||
const renderThumbnailFallback = React.useCallback(() => (
|
||||
<Icon className={styles['placeholder-icon']} name={'ic_broken_link'} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={href} download={download} target={target} onClick={onClick}>
|
||||
<div className={styles['info-container']}>
|
||||
const renderLabel = React.useMemo(() => function renderLabel({ className, children, ...props }) {
|
||||
return (
|
||||
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={href} target={target} download={download} onClick={onClick} {...props}>
|
||||
<div className={styles['info-container']}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']} title={name || addonName}>
|
||||
<Image
|
||||
className={styles['thumbnail']}
|
||||
src={thumbnail}
|
||||
alt={' '}
|
||||
renderFallback={renderThumbnailFallback}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<div className={styles['addon-name-container']} title={name || addonName}>
|
||||
<div className={styles['addon-name']}>{name || addonName}</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
progress !== null && !isNaN(progress) && progress > 0 ?
|
||||
<div className={styles['progress-bar-container']}>
|
||||
<div className={styles['progress-bar']} style={{ width: `${progress}%` }} />
|
||||
<div className={styles['progress-bar-background']} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className={styles['description-container']} title={description}>{description}</div>
|
||||
<Icon className={styles['icon']} name={'play'} />
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}, [thumbnail, progress, addonName, name, description, href, target, download, onClick]);
|
||||
|
||||
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||
return (
|
||||
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
|
||||
<Button className={styles['context-menu-option-container']} title={t('CTX_PLAY')}>
|
||||
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
||||
</Button>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']} title={name || addonName}>
|
||||
<Image
|
||||
className={styles['thumbnail']}
|
||||
src={thumbnail}
|
||||
alt={' '}
|
||||
renderFallback={renderThumbnailFallback}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<div className={styles['addon-name-container']} title={name || addonName}>
|
||||
<div className={styles['addon-name']}>{name || addonName}</div>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
progress !== null && !isNaN(progress) && progress > 0 ?
|
||||
<div className={styles['progress-bar-container']}>
|
||||
<div className={styles['progress-bar']} style={{ width: `${progress}%` }} />
|
||||
<div className={styles['progress-bar-background']} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
streamLink &&
|
||||
<Button className={styles['context-menu-option-container']} title={t('CTX_COPY_STREAM_LINK')} onClick={copyStreamLink}>
|
||||
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_STREAM_LINK')}</div>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div className={styles['description-container']} title={description}>{description}</div>
|
||||
<Icon className={styles['icon']} name={'play'} />
|
||||
</Button>
|
||||
);
|
||||
}, [copyStreamLink, onClick]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!routeFocused) {
|
||||
closeMenu();
|
||||
}
|
||||
}, [routeFocused]);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
className={className}
|
||||
onMouseUp={popupLabelOnMouseUp}
|
||||
onLongPress={popupLabelOnLongPress}
|
||||
onContextMenu={popupLabelOnContextMenu}
|
||||
open={menuOpen}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderLabel}
|
||||
renderMenu={renderMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
@import (reference) '~stremio/common/screen-sizes.less';
|
||||
|
||||
:import('~stremio/common/Popup/styles.less') {
|
||||
context-menu-container: menu-container;
|
||||
menu-direction-top-left: menu-direction-top-left;
|
||||
menu-direction-bottom-left: menu-direction-bottom-left;
|
||||
menu-direction-top-right: menu-direction-top-right;
|
||||
menu-direction-bottom-right: menu-direction-bottom-right;
|
||||
}
|
||||
|
||||
.stream-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -103,6 +111,33 @@
|
|||
color: var(--primary-foreground-color);
|
||||
background-color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
z-index: 2;
|
||||
|
||||
.context-menu-content {
|
||||
--spatial-navigation-contain: contain;
|
||||
|
||||
.context-menu-option-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
.context-menu-option-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color:var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @small) {
|
||||
|
|
@ -125,6 +160,28 @@
|
|||
.addon-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
&.menu-direction-top-left,
|
||||
&.menu-direction-bottom-left {
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-right,
|
||||
&.menu-direction-bottom-right {
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
&.menu-direction-top-left,
|
||||
&.menu-direction-top-right {
|
||||
bottom: 90%;
|
||||
}
|
||||
|
||||
&.menu-direction-bottom-left,
|
||||
&.menu-direction-bottom-right {
|
||||
top: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,17 @@ const StreamsList = ({ className, video, ...props }) => {
|
|||
setSelectedAddon(event.value);
|
||||
}, []);
|
||||
const backButtonOnClick = React.useCallback(() => {
|
||||
window.history.back();
|
||||
}, []);
|
||||
if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') {
|
||||
window.location.replace(video.deepLinks.metaDetailsVideos + (
|
||||
typeof video.season === 'number' ?
|
||||
`?${new URLSearchParams({'season': video.season})}`
|
||||
:
|
||||
null
|
||||
));
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}, [video]);
|
||||
const countLoadingAddons = React.useMemo(() => {
|
||||
return props.streams.filter((stream) => stream.content.type === 'Loading').length;
|
||||
}, [props.streams]);
|
||||
|
|
@ -78,6 +87,30 @@ const StreamsList = ({ className, video, ...props }) => {
|
|||
}, [streamsByAddon, selectedAddon]);
|
||||
return (
|
||||
<div className={classnames(className, styles['streams-list-container'])}>
|
||||
<div className={styles['select-choices-wrapper']}>
|
||||
{
|
||||
video ?
|
||||
<React.Fragment>
|
||||
<Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||
</Button>
|
||||
<div className={styles['episode-title']}>
|
||||
{`S${video?.season}E${video?.episode} ${(video?.title)}`}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
Object.keys(streamsByAddon).length > 1 ?
|
||||
<Multiselect
|
||||
{...selectableOptions}
|
||||
className={styles['select-input-container']}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
props.streams.length === 0 ?
|
||||
<div className={styles['message-container']}>
|
||||
|
|
@ -109,30 +142,6 @@ const StreamsList = ({ className, video, ...props }) => {
|
|||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['select-choices-wrapper']}>
|
||||
{
|
||||
video ?
|
||||
<React.Fragment>
|
||||
<Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}>
|
||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||
</Button>
|
||||
<div className={styles['episode-title']}>
|
||||
{`S${video?.season}E${video?.episode} ${(video?.title)}`}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
Object.keys(streamsByAddon).length > 1 ?
|
||||
<Multiselect
|
||||
{...selectableOptions}
|
||||
className={styles['select-input-container']}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className={styles['streams-container']}>
|
||||
{filteredStreams.map((stream, index) => (
|
||||
<Stream
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@
|
|||
|
||||
.streams-container {
|
||||
margin-top: 0;
|
||||
overflow: visible;
|
||||
scrollbar-color: @color-surface-light5-20 transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const { t } = require('i18next');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Button, Multiselect } = require('stremio/common');
|
||||
const { Button } = require('stremio/common');
|
||||
const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder');
|
||||
const styles = require('./styles');
|
||||
const { MultiselectMenu } = require('stremio/common');
|
||||
|
||||
const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
||||
const options = React.useMemo(() => {
|
||||
|
|
@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
|||
label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')
|
||||
}));
|
||||
}, [seasons]);
|
||||
const selected = React.useMemo(() => {
|
||||
return [String(season)];
|
||||
const selectedSeason = React.useMemo(() => {
|
||||
return { label: String(season), value: String(season) };
|
||||
}, [season]);
|
||||
const prevNextButtonOnClick = React.useCallback((event) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
|
|
@ -35,8 +36,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
|||
});
|
||||
}
|
||||
}, [season, seasons, onSelect]);
|
||||
const seasonOnSelect = React.useCallback((event) => {
|
||||
const value = parseFloat(event.value);
|
||||
const seasonOnSelect = React.useCallback((value) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
type: 'select',
|
||||
|
|
@ -61,12 +61,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
|||
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||
<div className={styles['label']}>Prev</div>
|
||||
</Button>
|
||||
<Multiselect
|
||||
<MultiselectMenu
|
||||
className={styles['seasons-popup-label-container']}
|
||||
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
||||
direction={'bottom-left'}
|
||||
options={options}
|
||||
selected={selected}
|
||||
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
||||
selectedOption={selectedSeason}
|
||||
onSelect={seasonOnSelect}
|
||||
/>
|
||||
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>
|
||||
|
|
|
|||
|
|
@ -56,17 +56,14 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
}
|
||||
});
|
||||
}, [id, released, watched]);
|
||||
const href = React.useMemo(() => {
|
||||
return deepLinks ?
|
||||
typeof deepLinks.player === 'string' ?
|
||||
deepLinks.player
|
||||
:
|
||||
typeof deepLinks.metaDetailsStreams === 'string' ?
|
||||
deepLinks.metaDetailsStreams
|
||||
:
|
||||
null
|
||||
:
|
||||
null;
|
||||
const videoButtonOnClick = React.useCallback(() => {
|
||||
if (deepLinks) {
|
||||
if (typeof deepLinks.player === 'string') {
|
||||
window.location = deepLinks.player;
|
||||
} else if (typeof deepLinks.metaDetailsStreams === 'string') {
|
||||
window.location.replace(deepLinks.metaDetailsStreams);
|
||||
}
|
||||
}
|
||||
}, [deepLinks]);
|
||||
const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ...props }) {
|
||||
return (
|
||||
|
|
@ -119,7 +116,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
}
|
||||
<div className={styles['upcoming-watched-container']}>
|
||||
{
|
||||
upcoming ?
|
||||
upcoming && !watched ?
|
||||
<div className={styles['upcoming-container']}>
|
||||
<div className={styles['flag-label']}>Upcoming</div>
|
||||
</div>
|
||||
|
|
@ -171,7 +168,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
watched={watched}
|
||||
progress={progress}
|
||||
scheduled={scheduled}
|
||||
href={href}
|
||||
onClick={videoButtonOnClick}
|
||||
{...props}
|
||||
onMouseUp={popupLabelOnMouseUp}
|
||||
onLongPress={popupLabelOnLongPress}
|
||||
|
|
|
|||
|
|
@ -74,5 +74,9 @@
|
|||
@media only screen and (max-width: @minimum) {
|
||||
.videos-list-container {
|
||||
overflow: visible;
|
||||
|
||||
.videos-container {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/routes/MetaDetails/useMetaDetails.d.ts
vendored
2
src/routes/MetaDetails/useMetaDetails.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useMetaDetails: (urlParams: UrlParams) => MetaDetails;
|
||||
export = useMetaDetails;
|
||||
export = useMetaDetails;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
2
src/routes/Player/usePlayer.d.ts
vendored
2
src/routes/Player/usePlayer.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const usePlayer: (urlParams: UrlParams) => [Player, (videoParams: { hash: string | null, size: number | null, filename: string | null }) => void, (time: number, duration: number, device: string) => void, (time: number, duration: number, device: string) => void, (paused: boolean) => void, () => void, () => void,];
|
||||
export = usePlayer;
|
||||
export = usePlayer;
|
||||
|
|
|
|||
2
src/routes/Player/useSettings.d.ts
vendored
2
src/routes/Player/useSettings.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useSettings: () => [Settings, (settings: any) => void];
|
||||
export = useSettings;
|
||||
export = useSettings;
|
||||
|
|
|
|||
2
src/routes/Search/useSearch.d.ts
vendored
2
src/routes/Search/useSearch.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare const useSearch: (searchParams: URLSearchParams) => [Search, (range: number) => void];
|
||||
export = useSearch;
|
||||
export = useSearch;
|
||||
|
|
|
|||
|
|
@ -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' ?
|
||||
|
|
|
|||
|
|
@ -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 }) => ({
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => {
|
|||
options: [
|
||||
{
|
||||
label: t('SETTINGS_DISABLED'),
|
||||
value: null,
|
||||
value: '',
|
||||
},
|
||||
...streamingServer.networkInfo.content.availableInterfaces.map((address) => ({
|
||||
label: address,
|
||||
|
|
|
|||
2
src/services/Core/Core.d.ts
vendored
2
src/services/Core/Core.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare function Core(): Core;
|
||||
export = Core;
|
||||
export = Core;
|
||||
|
|
|
|||
2
src/services/Core/CoreTransport.d.ts
vendored
2
src/services/Core/CoreTransport.d.ts
vendored
|
|
@ -1,2 +1,2 @@
|
|||
declare function CoreTransport(): CoreTransport;
|
||||
export = CoreTransport;
|
||||
export = CoreTransport;
|
||||
|
|
|
|||
2
src/services/Core/globals.d.ts
vendored
2
src/services/Core/globals.d.ts
vendored
|
|
@ -9,4 +9,4 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
|
|
|||
2
src/services/Core/types.d.ts
vendored
2
src/services/Core/types.d.ts
vendored
|
|
@ -25,4 +25,4 @@ interface CoreTransport {
|
|||
interface Core {
|
||||
active: boolean,
|
||||
transport: CoreTransport,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
2
src/services/ServicesContext/types.d.ts
vendored
2
src/services/ServicesContext/types.d.ts
vendored
|
|
@ -4,4 +4,4 @@ type ServicesContext = {
|
|||
chromecast: any,
|
||||
keyboardShortcuts: any,
|
||||
dragAndDrop: any,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
declare const useService: () => ServicesContext;
|
||||
export = useService;
|
||||
export = useService;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function ShellTransport() {
|
|||
|
||||
this.props = {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const shell = this;
|
||||
initialize()
|
||||
.then(() => {
|
||||
|
|
|
|||
2
src/types/Addon.d.ts
vendored
2
src/types/Addon.d.ts
vendored
|
|
@ -17,4 +17,4 @@ type Addon = {
|
|||
|
||||
type AddonsDeepLinks = {
|
||||
addons: string,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/LibraryItem.d.ts
vendored
2
src/types/LibraryItem.d.ts
vendored
|
|
@ -31,4 +31,4 @@ type LibraryItemDeepLinks = {
|
|||
metaDetailsStreams: string | null,
|
||||
player: string | null,
|
||||
externalPlayer: ExternalPlayerLinks | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
4
src/types/MetaItem.d.ts
vendored
4
src/types/MetaItem.d.ts
vendored
|
|
@ -23,10 +23,10 @@ type MetaItemPreview = {
|
|||
|
||||
type MetaItem = MetaItemPreview & {
|
||||
videos: Video[],
|
||||
}
|
||||
};
|
||||
|
||||
type MetaItemDeepLinks = {
|
||||
metaDetailsVideos: string | null,
|
||||
metaDetailsStreams: string | null,
|
||||
player: string | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/Selectable.d.ts
vendored
2
src/types/Selectable.d.ts
vendored
|
|
@ -24,4 +24,4 @@ type SelectableCatalog<T> = {
|
|||
name: string,
|
||||
selected: boolean,
|
||||
deepLinks: T,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/Stream.d.ts
vendored
2
src/types/Stream.d.ts
vendored
|
|
@ -15,4 +15,4 @@ type Stream = {
|
|||
player: string,
|
||||
externalPlayer: ExternalPlayerLinks,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/Video.d.ts
vendored
2
src/types/Video.d.ts
vendored
|
|
@ -14,4 +14,4 @@ type Video = {
|
|||
episode?: number,
|
||||
streams: Stream[],
|
||||
trailerStreams: TrailerStream[],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
15
src/types/global.d.ts
vendored
Normal file
15
src/types/global.d.ts
vendored
Normal 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 { };
|
||||
2
src/types/models/Board.d.ts
vendored
2
src/types/models/Board.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
type Board = CatalogsWithExtra;
|
||||
type Board = CatalogsWithExtra;
|
||||
|
|
|
|||
2
src/types/models/CatalogsWithExtra.d.ts
vendored
2
src/types/models/CatalogsWithExtra.d.ts
vendored
|
|
@ -8,4 +8,4 @@ type CatalogsWithExtra = {
|
|||
type: string | null,
|
||||
extra: [string, string][]
|
||||
} | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
4
src/types/models/Ctx.d.ts
vendored
4
src/types/models/Ctx.d.ts
vendored
|
|
@ -56,7 +56,7 @@ type NotificationItem = {
|
|||
metaId: string,
|
||||
videoId: string,
|
||||
videoReleased: string,
|
||||
}
|
||||
};
|
||||
|
||||
type SearchHistoryItem = {
|
||||
query: string,
|
||||
|
|
@ -71,4 +71,4 @@ type Ctx = {
|
|||
profile: Profile,
|
||||
notifications: Notifications,
|
||||
searchHistory: SearchHistory,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/Discover.d.ts
vendored
2
src/types/models/Discover.d.ts
vendored
|
|
@ -23,4 +23,4 @@ type Discover = {
|
|||
selected: {
|
||||
request: ResourceRequest,
|
||||
} | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/InstalledAddons.d.ts
vendored
2
src/types/models/InstalledAddons.d.ts
vendored
|
|
@ -9,4 +9,4 @@ type InstalledAddons = {
|
|||
type: string,
|
||||
}
|
||||
} | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/Library.d.ts
vendored
2
src/types/models/Library.d.ts
vendored
|
|
@ -26,4 +26,4 @@ type Library = {
|
|||
type: string | null,
|
||||
}
|
||||
} | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/LocalSearch.d.ts
vendored
2
src/types/models/LocalSearch.d.ts
vendored
|
|
@ -7,4 +7,4 @@ type LocalSearchItem = {
|
|||
|
||||
type LocalSearch = {
|
||||
items: LocalSearchItem[],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/MetaDetails.d.ts
vendored
2
src/types/models/MetaDetails.d.ts
vendored
|
|
@ -24,4 +24,4 @@ type MetaDetails = {
|
|||
content: Loadable<Stream[]>
|
||||
}[],
|
||||
title: string | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/Player.d.ts
vendored
2
src/types/models/Player.d.ts
vendored
|
|
@ -42,4 +42,4 @@ type Player = {
|
|||
} | null,
|
||||
subtitles: Subtitle[],
|
||||
title: string | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/RemoteAddons.d.ts
vendored
2
src/types/models/RemoteAddons.d.ts
vendored
|
|
@ -7,4 +7,4 @@ type RemoteAddons = {
|
|||
selected: {
|
||||
request: ResourceRequest,
|
||||
} | null,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
2
src/types/models/Search.d.ts
vendored
2
src/types/models/Search.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
type Search = CatalogsWithExtra;
|
||||
type Search = CatalogsWithExtra;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue