Merge branch 'development' into feat/captions-shortkey

This commit is contained in:
Neeraj TK 2025-01-14 00:55:20 +05:30 committed by GitHub
commit ea5d05c31d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
345 changed files with 11654 additions and 7042 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
}
]
}
}

82
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Bug report
description: Report a bug in Stremio-Web
title: "[Bug]: "
labels:
- bug
body:
- type: dropdown
id: stremio_web_version
attributes:
label: "Stremio-Web Version"
description: "Select the version of the Stremio-Web app you are using"
options:
- /development branch
- web.stremio.com
- web.strem.io
validations:
required: true
- type: dropdown
id: browser
attributes:
label: "Browser"
description: "Which browser are you using?"
options:
- Chrome
- Brave
- Firefox
- Arc
- Opera
- Safari
- Edge
validations:
required: true
- type: dropdown
id: platform
attributes:
label: "Platform / Device type"
description: "Which platform / device type are you using?"
options:
- Windows
- Linux
- MacOS
- Android Web
- Android PWA
- iOS Web
- iOS PWA
validations:
required: true
- type: textarea
id: what_happened
attributes:
label: "What Happened?"
description: "Describe the issue you encountered"
placeholder: "Explain what you were doing, what you expected to happen, and what actually happened."
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs"
description: "Paste any relevant logs here (optional)"
render: shell
- type: textarea
id: notes
attributes:
label: "Notes"
description: "Any additional information (optional)"
- type: checkboxes
id: code_of_conduct
attributes:
label: "Code of Conduct"
description: "Please confirm you have read and agree to the Code of Conduct"
options:
- label: "I agree"
validations:
required: true

View file

@ -0,0 +1,42 @@
name: Feature request
description: Suggest a new feature or enhancement for Stremio-Web
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: "Thank you for your interest in improving Stremio-Web! Please provide as much detail as possible."
- type: textarea
id: feature_description
attributes:
label: "Feature Description"
description: "Describe the feature you would like to see implemented. What problem does it solve, or what functionality does it add?"
placeholder: "Describe your idea in detail..."
validations:
required: true
- type: textarea
id: proposed_solution
attributes:
label: "Proposed Solution"
description: "If you have any thoughts on how this could be implemented or approached, share them here."
placeholder: "Suggest possible approaches or solutions..."
- type: textarea
id: additional_context
attributes:
label: "Additional Context or Screenshots"
description: "Add any other context, screenshots, or references that may help us understand the request."
placeholder: "Any extra info that might help..."
- type: checkboxes
id: code_of_conduct
attributes:
label: "Code of Conduct"
description: "Please confirm you have read and agree to the Code of Conduct"
options:
- label: "I agree"
validations:
required: true

View file

@ -21,6 +21,10 @@ jobs:
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
@ -33,7 +37,7 @@ jobs:
# "--parrents where no error if existing, make parent directories as needed."
- 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 }}

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20

37
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,37 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, level of experience, education, nationality or race.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language.
- Being respectful of differing viewpoints and experiences.
- Accepting constructive criticism.
- Focusing on what is best for the community.
- Showing empathy towards other community members.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances.
- Trolling, insulting/derogatory comments, and personal or political attacks.
- Public or private harassment.
- Publishing others private information, such as a physical or electronic address, without explicit permission.
- Other conduct which could reasonably be considered inappropriate in a professional setting.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, pull requests, and other contributions that do not align with this Code of Conduct, as well as to temporarily or permanently ban any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all `stremio-web` spaces, and also applies when an individual is officially representing the project or its community in public spaces.
## Enforcement
Pls be nice or we will ban you `:)`

View file

@ -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
View 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',
}
}
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

11799
package-lock.json generated

File diff suppressed because it is too large Load diff

103
package.json Executable file → Normal file
View file

@ -1,77 +1,84 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.8",
"version": "5.0.0-beta.16",
"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"
},
"dependencies": {
"@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-icons": "5.2.0",
"@stremio/stremio-video": "0.0.38",
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.48.4",
"@stremio/stremio-icons": "5.4.1",
"@stremio/stremio-video": "0.0.48",
"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",
"i18next": "^22.4.3",
"langs": "^2.0.0",
"classnames": "2.5.1",
"eventemitter3": "5.0.1",
"filter-invalid-dom-props": "3.0.1",
"hat": "^0.0.3",
"i18next": "^24.0.5",
"langs": "github:Stremio/nodejs-langs",
"lodash.debounce": "4.0.8",
"lodash.intersection": "4.4.0",
"lodash.isequal": "4.5.0",
"lodash.throttle": "4.1.1",
"magnet-uri": "6.2.0",
"prop-types": "15.7.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-focus-lock": "2.9.1",
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"prop-types": "15.8.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-focus-lock": "2.13.2",
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd",
"url": "0.11.0",
"use-long-press": "^3.1.5"
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
"devDependencies": {
"@babel/core": "7.16.0",
"@babel/plugin-proposal-class-properties": "7.16.0",
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0",
"@types/react": "^18.2.9",
"babel-loader": "8.2.3",
"@babel/core": "7.26.0",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@eslint/js": "^9.16.0",
"@stylistic/eslint-plugin": "^2.11.0",
"@stylistic/eslint-plugin-jsx": "^2.11.0",
"@types/hat": "^0.0.4",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"babel-loader": "9.2.1",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1",
"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",
"html-webpack-plugin": "5.5.0",
"jest": "27.3.1",
"less": "4.1.2",
"less-loader": "10.2.0",
"mini-css-extract-plugin": "2.4.3",
"postcss-loader": "6.2.0",
"readdirp": "3.6.0",
"terser-webpack-plugin": "5.2.4",
"copy-webpack-plugin": "12.0.2",
"css-loader": "6.11.0",
"cssnano": "7.0.6",
"cssnano-preset-advanced": "7.0.6",
"eslint": "^9.16.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.13.0",
"html-webpack-plugin": "5.6.3",
"jest": "29.7.0",
"less": "4.2.1",
"less-loader": "12.2.0",
"mini-css-extract-plugin": "2.9.2",
"postcss-loader": "8.1.1",
"readdirp": "4.0.2",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",
"typescript": "^5.4.2",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "^4.7.4",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",
"webpack": "5.97.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "^5.1.0",
"webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^6.5.3"
"workbox-webpack-plugin": "^7.3.0"
}
}

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,7 +4,7 @@ const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Button, Image } = require('stremio/common');
const { Image, Button } = require('stremio/components');
const styles = require('./styles');
const ErrorDialog = ({ className }) => {

View file

@ -23,6 +23,10 @@ const routerViewsConfig = [
...routesRegexp.library,
component: routes.Library
},
{
...routesRegexp.calendar,
component: routes.Calendar
},
{
...routesRegexp.continuewatching,
component: routes.Library

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -13,6 +13,20 @@
@import (once, less) '~stremio-router/styles.css';
}
// iOS pads the bottom inset more than needed, so we deduce the actual inset size when using the webapp
@calculated-bottom-safe-inset: ~"min(env(safe-area-inset-bottom, 0rem), max(1rem, calc(100lvh - 100svh - env(safe-area-inset-top, 0rem))))";
@html-width: ~"calc(max(100svw, 100dvw))";
@html-height: ~"calc(max(100svh, 100dvh))";
@safe-area-inset-top: env(safe-area-inset-top, 0rem);
@safe-area-inset-right: env(safe-area-inset-right, 0rem);
@safe-area-inset-bottom: env(safe-area-inset-bottom, 0rem);
@safe-area-inset-left: env(safe-area-inset-left, 0rem);
@top-overlay-size: 5.25rem;
@bottom-overlay-size: 0rem;
@overlap-size: 3rem;
@transparency-grandient-pad: 6rem;
:root {
--landscape-shape-ratio: 0.5625;
--poster-shape-ratio: 1.464;
@ -38,8 +52,17 @@
--quaternary-accent-color: rgba(18, 69, 166, 1);
--overlay-color: rgba(255, 255, 255, 0.05);
--modal-background-color: rgba(15, 13, 32, 1);
--outer-glow: 0px 0px 30px rgba(123, 91, 245, 0.37);
--outer-glow: 0px 0px 15px rgba(123, 91, 245, 0.37);
--border-radius: 0.75rem;
--calculated-bottom-safe-inset: @calculated-bottom-safe-inset;
--top-overlay-size: @top-overlay-size;
--bottom-overlay-size: @bottom-overlay-size;
--overlap-size: @overlap-size;
--transparency-grandient-pad: @transparency-grandient-pad;
--safe-area-inset-top: @safe-area-inset-top;
--safe-area-inset-right: @safe-area-inset-right;
--safe-area-inset-bottom: @safe-area-inset-bottom;
--safe-area-inset-left: @safe-area-inset-left;
}
* {
@ -85,12 +108,16 @@ svg {
}
html {
width: 100%;
height: 100%;
width: @html-width;
height: @html-height;
min-width: 640px;
min-height: 480px;
font-family: 'PlusJakartaSans', 'sans-serif';
overflow: auto;
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
body {
width: 100%;
@ -105,13 +132,13 @@ html {
.toasts-container {
position: absolute;
top: calc(1.2 * var(--horizontal-nav-bar-size));
right: 0;
bottom: calc(1.2 * var(--horizontal-nav-bar-size));
top: calc(1.2 * var(--horizontal-nav-bar-size) + var(--safe-area-inset-top));
right: var(--safe-area-inset-right);
bottom: calc(1.2 * var(--horizontal-nav-bar-size) + var(--calculated-bottom-safe-inset, 0rem));
left: auto;
z-index: 1;
padding: 0 calc(0.5 * var(--horizontal-nav-bar-size));
overflow-y: auto;
overflow: visible;
scrollbar-width: none;
pointer-events: none;
@ -192,4 +219,10 @@ html {
}
}
}
}
@media only screen and (max-width: @minimum) {
:root {
--bottom-overlay-size: 6rem;
}
}

View file

@ -1,63 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
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 longPress = useLongPress(onLongPress, { detect: 'pointer' });
const onKeyDown = React.useCallback((event) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (event.key === 'Enter') {
event.preventDefault();
if (!event.nativeEvent.buttonClickPrevented) {
event.currentTarget.click();
}
}
}, [props.onKeyDown]);
const onMouseDown = React.useCallback((event) => {
if (typeof props.onMouseDown === 'function') {
props.onMouseDown(event);
}
if (!event.nativeEvent.buttonBlurPrevented) {
event.preventDefault();
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}, [props.onMouseDown]);
return React.createElement(
typeof href === 'string' && href.length > 0 ? 'a' : 'div',
{
tabIndex: 0,
...props,
ref,
className: classnames(className, styles['button-container'], { 'disabled': disabled }),
href,
onKeyDown,
onMouseDown,
...longPress()
},
children
);
});
Button.displayName = 'Button';
Button.propTypes = {
className: PropTypes.string,
href: PropTypes.string,
disabled: PropTypes.bool,
children: PropTypes.node,
onKeyDown: PropTypes.func,
onMouseDown: PropTypes.func,
onLongPress: PropTypes.func,
};
module.exports = Button;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Button = require('./Button');
module.exports = Button;

View file

@ -1,6 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const DEFAULT_STREAMING_SERVER_URL = 'http://127.0.0.1:11470/';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['PlusJakartaSans', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [3000, 5000, 10000, 15000, 20000, 30000];
@ -44,7 +45,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 +55,7 @@ const EXTERNAL_PLAYERS = [
{
label: 'VLC',
value: 'vlc',
platforms: ['ios', 'android'],
platforms: ['ios', 'visionos', 'android'],
},
{
label: 'MPV',
@ -79,17 +80,25 @@ 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,
DEFAULT_STREAMING_SERVER_URL,
SUBTITLES_SIZES,
SUBTITLES_FONTS,
SEEK_TIME_DURATIONS,
@ -105,4 +114,5 @@ module.exports = {
TYPE_PRIORITIES,
ICON_FOR_TYPE,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
};

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Checkbox = require('./Checkbox');
module.exports = Checkbox;

View file

@ -1,56 +0,0 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { memo, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import Chip from './Chip';
import styles from './Chips.less';
type Option = {
label: string,
value: string,
};
type Props = {
options: Option[],
selected: string[],
onSelect: (value: string) => {},
};
const SCROLL_THRESHOLD = 1;
const Chips = memo(({ options, selected, onSelect }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [scrollPosition, setScrollPosition] = useState('left');
useEffect(() => {
const onScroll = ({ target }: Event) => {
const { scrollLeft, scrollWidth, offsetWidth} = target as HTMLDivElement;
const position =
(scrollLeft - SCROLL_THRESHOLD) <= 0 ? 'left' :
(scrollLeft + offsetWidth + SCROLL_THRESHOLD) >= scrollWidth ? 'right' :
'center';
setScrollPosition(position);
};
ref.current?.addEventListener('scroll', onScroll);
return () => ref.current?.removeEventListener('scroll', onScroll);
}, []);
return (
<div ref={ref} className={classNames(styles['chips'], [styles[scrollPosition]])}>
{
options.map(({ label, value }) => (
<Chip
key={value}
label={label}
value={value}
active={selected.includes(value)}
onSelect={onSelect}
/>
))
}
</div>
);
});
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

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Image = require('./Image');
module.exports = Image;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const MainNavBars = require('./MainNavBars');
module.exports = MainNavBars;

View file

@ -1,44 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => {
const prevNextButtonOnClick = React.useCallback((event) => {
if (typeof onSelect === 'function') {
onSelect({
type: 'change-page',
value: event.currentTarget.dataset.value,
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [dataset, onSelect]);
return (
<div {...props} className={classnames(className, styles['pagination-input-container'])} >
<Button className={styles['prev-button-container']} title={'Previous page'} data-value={'prev'} onClick={prevNextButtonOnClick}>
<Icon className={styles['icon']} name={'chevron-back'} />
</Button>
<div className={styles['label-container']} title={label}>
<div className={styles['label']}>{label}</div>
</div>
<Button className={styles['next-button-container']} title={'Next page'} data-value={'next'} onClick={prevNextButtonOnClick}>
<Icon className={styles['icon']} name={'chevron-forward'} />
</Button>
</div>
);
};
PaginationInput.propTypes = {
className: PropTypes.string,
label: PropTypes.string,
dataset: PropTypes.object,
onSelect: PropTypes.func
};
module.exports = PaginationInput;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const PaginationInput = require('./PaginationInput');
module.exports = PaginationInput;

View file

@ -1,42 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.pagination-input-container {
display: flex;
flex-direction: row;
border-radius: var(--border-radius);
.prev-button-container, .next-button-container {
flex: none;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--overlay-color);
.icon {
display: block;
color: var(--primary-foreground-color);
}
}
.label-container {
flex: 1;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--overlay-color);
.label {
flex: none;
min-width: 1.2rem;
max-width: 3rem;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
font-weight: 500;
color: var(--primary-foreground-color);
}
}
}

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,43 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const styles = require('./styles');
const TextInput = React.forwardRef((props, ref) => {
const onKeyDown = React.useCallback((event) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (event.key === 'Enter' && !event.nativeEvent.submitPrevented && typeof props.onSubmit === 'function') {
props.onSubmit(event);
}
}, [props.onKeyDown, props.onSubmit]);
return (
<input
size={1}
autoCorrect={'off'}
autoCapitalize={'off'}
autoComplete={'off'}
spellCheck={false}
tabIndex={0}
{...props}
ref={ref}
className={classnames(props.className, styles['text-input'], { 'disabled': props.disabled })}
onKeyDown={onKeyDown}
/>
);
});
TextInput.displayName = 'TextInput';
TextInput.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool,
onKeyDown: PropTypes.func,
onSubmit: PropTypes.func
};
module.exports = TextInput;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const TextInput = require('./TextInput');
module.exports = TextInput;

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

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const { Button } = require('stremio/components');
const styles = require('./styles');
const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => {

View file

@ -10,14 +10,8 @@ const TooltipItem = React.memo(({ className, active, label, position, margin, pa
const [style, setStyle] = React.useState(null);
const onTransitionEnd = React.useCallback(() => {
if (!active) {
setStyle(null);
}
}, [active]);
React.useEffect(() => {
if (!ref.current) return setStyle(null);
if (!ref.current || !active) return setStyle(null);
const tooltipBounds = ref.current.getBoundingClientRect();
const parentBounds = parent.getBoundingClientRect();
@ -47,7 +41,7 @@ const TooltipItem = React.memo(({ className, active, label, position, margin, pa
}, [active, position, margin, parent, label]);
return (
<div ref={ref} className={classNames(className, styles['tooltip-item'], { 'active': active })} style={style} onTransitionEnd={onTransitionEnd}>
<div ref={ref} className={classNames(className, styles['tooltip-item'], { 'active': active })} style={style}>
{ label }
</div>
);

View file

@ -19,4 +19,36 @@
opacity: 1;
transform: translateY(0);
}
}
:global(.animation-slide-up) {
:local {
animation-name: slide-up;
}
animation-timing-function: ease-out;
animation-duration: 0.1s;
}
@keyframes slide-up {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0%);
}
}
.slide-left-enter {
transform: translateX(100%);
}
.slide-left-active {
transform: translateX(0%);
transition: transform 0.3s cubic-bezier(0.32, 0, 0.67, 0);
}
.slide-left-exit {
transform: translateX(100%);
}

View file

@ -1,29 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
const AddonDetailsModal = require('./AddonDetailsModal');
const Button = require('./Button');
const Checkbox = require('./Checkbox');
const { default: Chips } = require('./Chips');
const ColorInput = require('./ColorInput');
const ContinueWatchingItem = require('./ContinueWatchingItem');
const DelayedRenderer = require('./DelayedRenderer');
const Image = require('./Image');
const LibItem = require('./LibItem');
const MainNavBars = require('./MainNavBars');
const MetaItem = require('./MetaItem');
const MetaPreview = require('./MetaPreview');
const MetaRow = require('./MetaRow');
const ModalDialog = require('./ModalDialog');
const Multiselect = require('./Multiselect');
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
const PaginationInput = require('./PaginationInput');
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
const Popup = require('./Popup');
const SearchBar = require('./SearchBar');
const StreamingServerWarning = require('./StreamingServerWarning');
const SharePrompt = require('./SharePrompt');
const Slider = require('./Slider');
const TextInput = require('./TextInput');
const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const comparatorWithPriorities = require('./comparatorWithPriorities');
@ -32,6 +9,7 @@ const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
const interfaceLanguages = require('./interfaceLanguages.json');
const languageNames = require('./languageNames.json');
const languages = require('./languages');
const routesRegexp = require('./routesRegexp');
const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
@ -44,35 +22,10 @@ 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 = {
AddonDetailsModal,
Button,
Checkbox,
Chips,
ColorInput,
ContinueWatchingItem,
DelayedRenderer,
Image,
LibItem,
MainNavBars,
MetaItem,
MetaPreview,
MetaRow,
ModalDialog,
Multiselect,
HorizontalNavBar,
VerticalNavBar,
PaginationInput,
PlayIconCircleCentered,
Popup,
SearchBar,
StreamingServerWarning,
SharePrompt,
Slider,
TextInput,
PlatformProvider,
usePlatform,
ToastProvider,
useToast,
TooltipProvider,
@ -84,6 +37,7 @@ module.exports = {
getVisibleChildrenRange,
interfaceLanguages,
languageNames,
languages,
routesRegexp,
useAnimationFrame,
useBinaryState,
@ -96,6 +50,4 @@ module.exports = {
useStreamingServer,
useTorrent,
useTranslate,
platform,
EventModal,
};

View file

@ -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"]
}
]
]

25
src/common/languages.ts Normal file
View file

@ -0,0 +1,25 @@
import langs from 'langs';
const all = langs.all().map((lang) => ({
...lang,
code: lang['2'],
label: lang.local,
alpha2: lang['1'],
alpha3: [lang['2'], lang['2B'], lang['2T'], lang['3']],
locale: lang['locale'],
}));
const find = (code: string) => {
return all.find(({ alpha2, alpha3, locale }) => [alpha2, ...alpha3, locale].includes(code));
};
const label = (code: string) => {
const language = find(code);
return language?.label ?? code;
};
export {
all,
find,
label,
};

View file

@ -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';
}
};

View file

@ -17,6 +17,10 @@ const routesRegexp = {
regexp: /^\/library(?:\/([^/]*))?$/,
urlParamsNames: ['type']
},
calendar: {
regexp: /^\/calendar(?:\/([^/]*)\/([^/]*))?$/,
urlParamsNames: ['year', 'month']
},
continuewatching: {
regexp: /^\/continuewatching(?:\/([^/]*))?$/,
urlParamsNames: ['type']

8
src/common/useBinaryState.d.ts vendored Normal file
View file

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

View file

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

View 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;

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;

View file

@ -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) {

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Image = require('stremio/common/Image');
const { default: Image } = require('stremio/components/Image');
const styles = require('./styles');
const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => {

View file

@ -2,8 +2,9 @@
const React = require('react');
const PropTypes = require('prop-types');
const ModalDialog = require('stremio/common/ModalDialog');
const ModalDialog = require('stremio/components/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;

View file

@ -2,10 +2,6 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
:import('~stremio/common/ModalDialog/styles.less') {
label: label;
}
.addon-details-modal-container {
.addon-details-container, .addon-details-message-container {
width: 40rem;

View file

@ -0,0 +1,107 @@
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
.bottom-sheet {
z-index: 99;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
.backdrop {
z-index: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--primary-background-color);
opacity: 0.8;
transition: opacity 0.1s ease-out;
cursor: pointer;
}
.container {
z-index: 1;
position: absolute;
bottom: 0;
max-height: calc(100% - var(--horizontal-nav-bar-size));
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
padding-bottom: 1rem;
border-radius: 2rem 2rem 0 0;
background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
overflow: hidden;
&:not(.dragging) {
transition: transform 0.1s ease-out;
}
.heading {
position: relative;
.handle {
position: relative;
height: 2.5rem;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
&::after {
content: "";
height: 0.3rem;
width: 3rem;
border-radius: 1rem;
background-color: var(--primary-foreground-color);
opacity: 0.3;
}
}
.title {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
padding-left: 1.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-foreground-color);
}
}
.content {
position: relative;
overflow-y: auto;
}
}
}
@media only screen and (min-width: @small) and (orientation: portait) {
.bottom-sheet {
display: none;
}
}
@media only screen and (min-width: @xsmall) and (orientation: landscape) {
.bottom-sheet {
display: none;
}
}
@media only screen and (orientation: landscape) {
.bottom-sheet {
.container {
max-width: 90%;
}
}
}

View file

@ -0,0 +1,87 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import classNames from 'classnames';
import useBinaryState from 'stremio/common/useBinaryState';
import styles from './BottomSheet.less';
const CLOSE_THRESHOLD = 100;
type Props = {
children: JSX.Element,
title: string,
show: boolean,
onClose: () => void,
};
const BottomSheet = ({ children, title, show, onClose }: Props) => {
const containerRef = useRef<HTMLDivElement>(null);
const [startOffset, setStartOffset] = useState(0);
const [offset, setOffset] = useState(0);
const [opened, open, close] = useBinaryState();
const containerStyle = useMemo(() => ({
transform: `translateY(${offset}px)`
}), [offset]);
const containerHeight = () => containerRef.current?.offsetHeight ?? 0;
const onCloseRequest = () => setOffset(containerHeight());
const onTouchStart = ({ touches }: React.TouchEvent<HTMLDivElement>) => {
const { clientY } = touches[0];
setStartOffset(clientY);
};
const onTouchMove = useCallback(({ touches }: React.TouchEvent<HTMLDivElement>) => {
const { clientY } = touches[0];
setOffset(Math.max(0, clientY - startOffset));
}, [startOffset]);
const onTouchEnd = () => {
setOffset((offset) => offset > CLOSE_THRESHOLD ? containerHeight() : 0);
setStartOffset(0);
};
const onTransitionEnd = useCallback(() => {
(offset === containerHeight()) && close();
}, [offset]);
useEffect(() => {
setOffset(0);
show ? open() : close();
}, [show]);
useEffect(() => {
!opened && onClose();
}, [opened]);
return opened && createPortal((
<div className={styles['bottom-sheet']}>
<div className={styles['backdrop']} onClick={onCloseRequest} />
<div
ref={containerRef}
className={classNames(styles['container'], { [styles['dragging']]: startOffset }, 'animation-slide-up')}
style={containerStyle}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTransitionEnd={onTransitionEnd}
>
<div className={styles['heading']}>
<div className={styles['handle']} />
<div className={styles['title']}>
{title}
</div>
</div>
<div className={styles['content']} onClick={onCloseRequest}>
{children}
</div>
</div>
</div>
), document.body);
};
export default BottomSheet;

View file

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

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';

View file

@ -0,0 +1,71 @@
// Copyright (C) 2017-2023 Smart code 203358507
import { createElement, forwardRef, useCallback } from 'react';
import classNames from 'classnames';
import { LongPressEventType, useLongPress } from 'use-long-press';
import styles from './Button.less';
type Props = {
className?: string,
href?: string,
title?: string,
disabled?: boolean,
tabIndex?: number,
children: React.ReactNode,
onKeyDown?: (event: React.KeyboardEvent) => void,
onMouseDown?: (event: React.MouseEvent) => void,
onLongPress?: () => void,
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
onDoubleClick?: () => void,
};
const Button = forwardRef(({ className, href, disabled, children, onLongPress, onDoubleClick, ...props }: Props, ref) => {
const longPress = useLongPress(onLongPress!, { detect: LongPressEventType.Pointer });
const onKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (event.key === 'Enter') {
event.preventDefault();
// @ts-expect-error: Property 'buttonClickPrevented' does not exist on type 'KeyboardEvent'.
if (!event.nativeEvent.buttonClickPrevented) {
event.currentTarget.click();
}
}
}, [props.onKeyDown]);
const onMouseDown = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (typeof props.onMouseDown === 'function') {
props.onMouseDown(event);
}
// @ts-expect-error: Property 'buttonBlurPrevented' does not exist on type 'MouseEvent'.
if (!event.nativeEvent.buttonBlurPrevented) {
event.preventDefault();
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
}, [props.onMouseDown]);
return createElement(
typeof href === 'string' && href.length > 0 ? 'a' : 'div',
{
tabIndex: 0,
...props,
ref,
className: classNames(className, styles['button-container'], { 'disabled': disabled }),
href,
onKeyDown,
onMouseDown,
onDoubleClick,
...longPress()
},
children
);
});
export default Button;

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
import Button from './Button';
export default Button;

View file

@ -16,18 +16,19 @@
text-transform: capitalize;
padding: 0 1.75rem;
border-radius: @height;
-webkit-tap-highlight-color: transparent;
background-color: transparent;
user-select: none;
overflow: hidden;
opacity: 0.6;
&:hover {
background-color: var(--overlay-color);
transition: background-color 0.1s ease-out;
opacity: 1;
}
&.active {
font-weight: 700;
opacity: 1;
background-color: var(--quaternary-accent-color);
transition: background-color 0.1s ease-in;
}

View file

@ -2,7 +2,7 @@
import React, { MouseEvent, memo, useCallback, useEffect, useRef } from 'react';
import classNames from 'classnames';
import Button from 'stremio/common/Button';
import { Button } from 'stremio/components';
import styles from './Chip.less';
type Props = {
@ -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

@ -0,0 +1,10 @@
// Copyright (C) 2017-2024 Smart code 203358507
.chips {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
}

View file

@ -0,0 +1,37 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { memo } from 'react';
import { HorizontalScroll } from 'stremio/components';
import Chip from './Chip';
import styles from './Chips.less';
type Option = {
label: string,
value: string,
};
type Props = {
options: Option[],
selected: string[],
onSelect: (value: string) => {},
};
const Chips = memo(({ options, selected, onSelect }: Props) => {
return (
<HorizontalScroll className={styles['chips']}>
{
options.map(({ label, value }) => (
<Chip
key={value}
label={label}
value={value}
active={selected.includes(value)}
onSelect={onSelect}
/>
))
}
</HorizontalScroll>
);
});
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

@ -5,8 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const AColorPicker = require('a-color-picker');
const { useTranslation } = require('react-i18next');
const Button = require('stremio/common/Button');
const ModalDialog = require('stremio/common/ModalDialog');
const { Button, ModalDialog } = require('stremio/components');
const useBinaryState = require('stremio/common/useBinaryState');
const ColorPicker = require('./ColorPicker');
const styles = require('./styles');

View file

@ -17,7 +17,6 @@
align-items: center;
justify-content: center;
padding: 0 0.5rem;
border: thin solid @color-surface-light5-20;
pointer-events: none;
.transparent-label {

View file

@ -3,7 +3,7 @@
const React = require('react');
const PropTypes = require('prop-types');
const { useServices } = require('stremio/services');
const LibItem = require('stremio/common/LibItem');
const LibItem = require('stremio/components/LibItem');
const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...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

@ -2,8 +2,7 @@
const React = require('react');
const { useTranslation } = require('react-i18next');
const Button = require('stremio/common/Button');
const ModalDialog = require('stremio/common/ModalDialog');
const { Button, ModalDialog } = require('stremio/components');
const useEvents = require('./useEvents');
const styles = require('./styles');
const { default: Icon } = require('@stremio/stremio-icons/react');

View file

@ -2,7 +2,7 @@
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/ModalDialog/styles.less') {
:import('~stremio/components/ModalDialog/styles.less') {
modal-dialog-content: modal-dialog-content;
modal-dialog-container: modal-dialog-container;
}
@ -19,26 +19,30 @@
flex-direction: column;
align-items: center;
overflow: visible;
position: relative;
.modal-dialog-content {
.body-container {
overflow-y: visible;
}
.image {
width: 100%;
height: 100%;
margin-top: -10rem;
position: absolute;
top: -10rem;
left: 50%;
transform: translateX(-50%);
object-fit: cover;
width: 30rem;
height: 30rem;
}
.info-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2.5rem;
padding: 1rem 4rem;
margin-top: -7rem;
padding: 10rem 4rem 0;
.title-container {
display: flex;
flex-direction: column;
@ -50,7 +54,7 @@
text-align: center;
padding: 0 6rem;
}
.label {
color: var(--primary-foreground-color);
font-size: 1rem;
@ -58,7 +62,7 @@
opacity: 0.5;
}
}
.addon-container {
display: flex;
align-items: center;
@ -76,19 +80,19 @@
color: var(--primary-foreground-color);
}
}
.action-button {
background-color: var(--primary-foreground-color);
border: 2px solid var(--primary-foreground-color);
padding: 0.8rem 2rem;
border-radius: 2rem;
.button-label {
color: var(--primary-accent-color);
font-size: 1rem;
font-weight: 700;
}
&:hover {
background-color: transparent;
}
@ -96,15 +100,34 @@
}
}
}
}
@media only screen and (max-width: @minimum) {
@media (orientation: landscape) and (max-height: @minimum) {
.event-modal {
.modal-dialog-container {
.modal-dialog-content {
.image {
height: 125%;
width: 125%;
overflow-y: auto;
.body-container {
overflow-y: auto;
}
.image {
display: none;
}
.info-container {
padding: 1rem 4rem 0;
}
}
}
}
}
@media only screen and (max-width: @minimum) {
.event-modal {
.modal-dialog-container {
.modal-dialog-content {
.info-container {
.title-container {
.title {
@ -120,4 +143,4 @@
}
}
}
}
}

View file

@ -2,13 +2,8 @@
@mask-width: 10%;
.chips {
.horizontal-scroll {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
overflow-x: auto;
&.left {
@ -22,4 +17,4 @@
&.center {
mask-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) @mask-width, rgba(0, 0, 0, 1) calc(100% - @mask-width), rgba(0, 0, 0, 0) 100%);
}
}
}

View file

@ -0,0 +1,40 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useRef, useEffect, useState } from 'react';
import classNames from 'classnames';
import styles from './HorizontalScroll.less';
const SCROLL_THRESHOLD = 1;
type Props = {
className: string,
children: React.ReactNode,
};
const HorizontalScroll = ({ className, children }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [scrollPosition, setScrollPosition] = useState('left');
useEffect(() => {
const onScroll = ({ target }: Event) => {
const { scrollLeft, scrollWidth, offsetWidth } = target as HTMLDivElement;
setScrollPosition(() => (
(scrollLeft - SCROLL_THRESHOLD) <= 0 ? 'left' :
(scrollLeft + offsetWidth + SCROLL_THRESHOLD) >= scrollWidth ? 'right' :
'center'
));
};
ref.current?.addEventListener('scroll', onScroll);
return () => ref.current?.removeEventListener('scroll', onScroll);
}, []);
return (
<div ref={ref} className={classNames(styles['horizontal-scroll'], className, [styles[scrollPosition]])}>
{children}
</div>
);
};
export default HorizontalScroll;

View file

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

View file

@ -1,36 +1,37 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
import React, { useCallback, useLayoutEffect, useState } from 'react';
const Image = ({ className, src, alt, fallbackSrc, renderFallback, ...props }) => {
const [broken, setBroken] = React.useState(false);
const onError = React.useCallback((event) => {
type Props = {
className: string,
src: string,
alt: string,
fallbackSrc: string,
renderFallback: () => void,
onError: (event: React.SyntheticEvent<HTMLImageElement>) => void,
};
const Image = ({ className, src, alt, fallbackSrc, renderFallback, ...props }: Props) => {
const [broken, setBroken] = useState(false);
const onError = useCallback((event: React.SyntheticEvent<HTMLImageElement>) => {
if (typeof props.onError === 'function') {
props.onError(event);
}
setBroken(true);
}, [props.onError]);
React.useLayoutEffect(() => {
useLayoutEffect(() => {
setBroken(false);
}, [src]);
return (broken || typeof src !== 'string' || src.length === 0) && (typeof renderFallback === 'function' || typeof fallbackSrc === 'string') ?
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 = {
className: PropTypes.string,
src: PropTypes.string,
alt: PropTypes.string,
fallbackSrc: PropTypes.string,
renderFallback: PropTypes.func,
onError: PropTypes.func
};
module.exports = Image;
export default Image;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2023 Smart code 203358507
import Image from './Image';
export default Image;

View file

@ -3,7 +3,7 @@
const React = require('react');
const { useServices } = require('stremio/services');
const PropTypes = require('prop-types');
const MetaItem = require('stremio/common/MetaItem');
const MetaItem = require('stremio/components/MetaItem');
const { t } = require('i18next');
const LibItem = ({ _id, removable, notifications, watched, ...props }) => {

View file

@ -1,10 +1,15 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
.main-nav-bars-container {
position: relative;
z-index: 0;
overflow: clip;
margin-left: env(safe-area-inset-left, 0px);
margin-right: env(safe-area-inset-right, 0px);
width: calc(100% - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
height: 100%;
.horizontal-nav-bar {
position: absolute;
@ -17,18 +22,20 @@
.vertical-nav-bar {
position: absolute;
top: var(--horizontal-nav-bar-size);
bottom: 0;
bottom: var(--calculated-bottom-safe-inset);
left: 0;
z-index: 1;
}
.nav-content-container {
position: absolute;
top: var(--horizontal-nav-bar-size);
padding-top: calc(var(--horizontal-nav-bar-size) + env(safe-area-inset-top, 0px));
top: 0;
right: 0;
bottom: 0;
left: var(--vertical-nav-bar-size);
z-index: 0;
overflow: scroll;
}
}
@ -36,7 +43,7 @@
.main-nav-bars-container {
.nav-content-container {
left: 0;
bottom: var(--vertical-nav-bar-size);
padding-bottom: var(--vertical-nav-bar-size);
}
.vertical-nav-bar {

View file

@ -1,20 +1,27 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { VerticalNavBar, HorizontalNavBar } = require('stremio/common/NavBar');
const styles = require('./styles');
import React, { memo } from 'react';
import classnames from 'classnames';
import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar';
import styles from './MainNavBars.less';
const TABS = [
{ id: 'board', label: 'Board', icon: 'home', href: '#/' },
{ id: 'discover', label: 'Discover', icon: 'discover', href: '#/discover' },
{ id: 'library', label: 'Library', icon: 'library', href: '#/library' },
{ id: 'calendar', label: 'Calendar', icon: 'calendar', href: '#/calendar' },
{ id: 'addons', label: 'ADDONS', icon: 'addons', href: '#/addons' },
{ id: 'settings', label: 'SETTINGS', icon: 'settings', href: '#/settings' },
];
const MainNavBars = React.memo(({ className, route, query, children }) => {
type Props = {
className: string,
route?: string,
query?: string,
children?: React.ReactNode,
};
const MainNavBars = memo(({ className, route, query, children }: Props) => {
return (
<div className={classnames(className, styles['main-nav-bars-container'])}>
<HorizontalNavBar
@ -23,7 +30,6 @@ const MainNavBars = React.memo(({ className, route, query, children }) => {
query={query}
backButton={false}
searchBar={true}
addonsButton={true}
fullscreenButton={true}
navMenu={true}
/>
@ -37,13 +43,5 @@ const MainNavBars = React.memo(({ className, route, query, children }) => {
);
});
MainNavBars.displayName = 'MainNavBars';
export default MainNavBars;
MainNavBars.propTypes = {
className: PropTypes.string,
route: PropTypes.string,
query: PropTypes.string,
children: PropTypes.node
};
module.exports = MainNavBars;

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
import MainNavBars from './MainNavBars';
export default MainNavBars;

View file

@ -6,9 +6,9 @@ const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const filterInvalidDOMProps = require('filter-invalid-dom-props').default;
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
const Multiselect = require('stremio/components/Multiselect');
const useBinaryState = require('stremio/common/useBinaryState');
const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS');
const styles = require('./styles');

View file

@ -1,29 +1,23 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2024 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/Popup/styles.less') {
:import('~stremio/components/Popup/styles.less') {
popup-menu-container: menu-container;
}
:import('~stremio/common/Multiselect/styles.less') {
:import('~stremio/components/Multiselect/styles.less') {
multiselect-menu-container: menu-container;
multiselect-option-container: option-container;
multiselect-option-label: label;
}
:import('~stremio/common/PlayIconCircleCentered/styles.less') {
play-icon-circle-centered-background: background;
play-icon-circle-centered-icon: icon;
}
@play-icon-size: 4rem;
.meta-item-container {
padding: 1rem;
overflow: visible;
-webkit-tap-highlight-color: transparent;
&:hover, &:focus, &:global(.active), &:global(.selected) {
outline-style: none;
@ -166,6 +160,7 @@
object-position: center;
object-fit: cover;
opacity: 0.9;
overflow-clip-margin: unset;
}
.placeholder-icon {

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const { Button } = require('stremio/components');
const styles = require('./styles');
const { Tooltip } = require('stremio/common/Tooltips');

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 Button = require('stremio/common/Button');
const { Button } = require('stremio/components');
const styles = require('./styles');
const MetaLinks = ({ className, label, links }) => {

View file

@ -6,10 +6,10 @@ const classnames = require('classnames');
const UrlUtils = require('url');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image');
const ModalDialog = require('stremio/common/ModalDialog');
const SharePrompt = require('stremio/common/SharePrompt');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
const ModalDialog = require('stremio/components/ModalDialog');
const SharePrompt = require('stremio/components/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
const routesRegexp = require('stremio/common/routesRegexp');
const useBinaryState = require('stremio/common/useBinaryState');

Some files were not shown because too many files have changed in this diff Show more