Compare commits

..

No commits in common. "development" and "v5.0.0-alpha10" have entirely different histories.

662 changed files with 17229 additions and 32541 deletions

99
.eslintrc Normal file
View file

@ -0,0 +1,99 @@
{
"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": 9,
"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
}
]
}
}

View file

@ -1,82 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,8 +0,0 @@
version: 2
# Check for outdated actions
updates:
- package-ecosystem: "github-actions"
directory: "/"
# Check for updates every Monday
schedule:
interval: "weekly"

View file

@ -1,66 +0,0 @@
name: PR and Issue Workflow
on:
pull_request:
types: [opened, reopened]
issues:
types: [opened]
jobs:
auto-assign-and-label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
# Auto assign PR to author
- name: Auto Assign PR to Author
if: github.event.pull_request.head.repo.fork == false && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request;
if (pr) {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
assignees: [pr.user.login]
});
console.log(`Assigned PR #${pr.number} to author @${pr.user.login}`);
}
# Dynamic labeling based on PR/Issue title
- name: Label PRs and Issues
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prTitle = context.payload.pull_request ? context.payload.pull_request.title : context.payload.issue.title;
const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number;
const isIssue = context.payload.issue !== undefined;
const labelMappings = [
{ pattern: /^feat(ure)?/i, label: 'feature' },
{ pattern: /^fix/i, label: 'bug' },
{ pattern: /^refactor/i, label: 'refactor' },
{ pattern: /^chore/i, label: 'chore' },
{ pattern: /^docs?/i, label: 'documentation' },
{ pattern: /^perf(ormance)?/i, label: 'performance' },
{ pattern: /^test/i, label: 'testing' }
];
let labelsToAdd = [];
for (const mapping of labelMappings) {
if (mapping.pattern.test(prTitle)) {
labelsToAdd.push(mapping.label);
}
}
if (labelsToAdd.length > 0) {
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToAdd
});
}

View file

@ -1,54 +1,19 @@
name: Build
on:
push:
branches:
- development
tags-ignore:
- "**"
pull_request:
branches:
- development
# Allow manual dispatch in GH
workflow_dispatch:
permissions:
contents: write
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install NPM dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Test
run: pnpm test
- name: Lint
run: pnpm lint
# Create recursively the destination dir with
# "--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.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.head_ref || github.ref_name }}
allow_empty_commit: true
- name: Checkout
uses: actions/checkout@v2
- name: Install NPM dependencies
run: npm install
- name: Build
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v2
with:
name: stremio-web
path: build

View file

@ -1,53 +0,0 @@
name: GitHub Pages Cleanup
on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
permissions:
contents: write
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: gh-pages
fetch-depth: 0
- name: Delete directories that don't have existing branch
run: |
branches=( $(git branch -r | grep origin | grep -v HEAD | sed 's|origin/||') )
declare -p branches
find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*' | while read -r dir; do
path="${dir#./}"
if [[ " ${branches[*]} " =~ " $path " ]]; then
continue
fi
keep_parent=false
for branch in "${branches[@]}"; do
if [[ "$branch" == "$path/"* ]]; then
keep_parent=true
break
fi
done
if ! $keep_parent; then
echo "Deleting $dir"
rm -rf "$dir"
fi
done
- name: Commit and push
run: |
git config --global user.name 'GitHub Pages Cleanup'
git config --global user.email 'actions@stremio.com'
git add -A
git diff --cached --quiet || git commit -m "cleanup"
git push origin gh-pages

View file

@ -8,26 +8,25 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: pnpm build
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.11.4
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip
asset_name: stremio-web.zip
tag: ${{ github.ref }}
overwrite: true
- name: Checkout
uses: actions/checkout@v2
- name: Install NPM dependencies
run: npm install
- name: Build
run: npm run build
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@v1-release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip
asset_name: stremio-web.zip
tag: ${{ github.ref }}
overwrite: true
- name: Upload build artifact to Netlify
run: |
curl -H "Content-Type: application/zip" \
-H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \
--data-binary "@stremio-web.zip" \
https://api.netlify.com/api/v1/sites/stremio-development.netlify.com/deploys

16
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Test
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install NPM dependencies
run: npm install
- name: Test
run: npm test
- name: Lint
run: npm run lint

1
.gitignore vendored
View file

@ -3,4 +3,3 @@
/yarn.lock
/npm-debug.log
.DS_Store
.prettierignore

1
.nvmrc
View file

@ -1 +0,0 @@
20

View file

@ -1,26 +0,0 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"9EWRZ4QP3J.com.stremio.one"
],
"appID": "9EWRZ4QP3J.com.stremio.one",
"paths": [
"*"
]
}
]
},
"activitycontinuation": {
"apps": [
"9EWRZ4QP3J.com.stremio.one"
]
},
"webcredentials": {
"apps": [
"9EWRZ4QP3J.com.stremio.one"
]
}
}

View file

@ -1,44 +0,0 @@
# Code of Conduct
## Our Pledge
We as contributors and maintainers want to make contributing to our project and community a nice experience for everyone.
## Our Standards
Examples of positive behavior:
- Using welcoming language.
- Being respectful.
- Accepting constructive criticism.
Examples of bad behavior:
- Use of sexualized language.
- Trolling, insulting comments, and personal or political attacks.
- Public or private harassment.
- Publishing others private information, such as a physical or electronic address, without explicit permission.
- Submitting entirely generated by AI PRs with agents such as Devin, Claude Code, Cursor Agent etc.
- Submitting PRs which in majority contain only AI generated code (including docs & comments) and do not solve an actual issue.
- Spamming issues because of no ETAs on issues.
## Our Responsibilities
Project maintainers are responsible for enforcing this code of conduct. They can remove or edit comments, code, and other contributions that don't follow these rules. They can also ban users who behave inappropriately.
## Suggestions for newbies
- Contributors are welcomed to use AI models as "help" in solving issues, but you must always double check the code that you're submitting.
- Refrain from excessive comments generated by AI.
- Refrain from docs generated entirely by AI.
- Always check what files you are committing and submitting to the PR when you are using any agent for help or an AI model.
- If you don't know how to tackle a problem and AI can't help you, please just ask or look in Stack Overlflow, Google, Medium etc.
- Learning how to code is fun and easier when using AI, but sometimes it might be just too much ... what are you going to learn, if AI does everything for you and you don't know what the code you are submitting actually does?!
## Scope
This Code of Conduct applies everywhere in `stremio-web` repository, and also applies when an individual is officially representing the project or its community in other spaces.
## Enforcement
Pls be nice or we will ban you `:)`

View file

@ -1,41 +0,0 @@
# Stremio Node 20.x
# the node version for running Stremio Web
ARG NODE_VERSION=20-alpine
FROM node:$NODE_VERSION AS base
# Setup pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN apk add --no-cache git
# Meta
LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
RUN mkdir -p /var/www/stremio-web
WORKDIR /var/www/stremio-web
# Setup app
FROM base AS app
COPY package.json pnpm-lock.yaml /var/www/stremio-web
RUN pnpm i --frozen-lockfile
COPY . /var/www/stremio-web
RUN pnpm build
# Setup server
FROM base AS server
RUN pnpm i express@4
# Finalize
FROM base
COPY http_server.js /var/www/stremio-web
COPY --from=server /var/www/stremio-web/node_modules /var/www/stremio-web/node_modules
COPY --from=app /var/www/stremio-web/build /var/www/stremio-web/build
EXPOSE 8080
CMD ["node", "http_server.js"]

View file

@ -1,7 +1,7 @@
# Stremio - Freedom to Stream
# Stremio - The media center you need
[![Build](https://github.com/Stremio/stremio-web/actions/workflows/build.yml/badge.svg)](https://github.com/Stremio/stremio-web/actions/workflows/build.yml)
[![Github Page](https://img.shields.io/website?label=Page&logo=github&up_message=online&down_message=offline&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/development)
![Build](https://github.com/stremio/stremio-web/workflows/Build/badge.svg?branch=development)
[![Netlify](https://api.netlify.com/api/v1/badges/ac26d7ae-d08b-4cc4-a14d-a83ba7c3e8ca/deploy-status)](https://stremio-development.netlify.app)
Stremio is a modern media center that's a one-stop solution for your video entertainment. You discover, watch and organize video content from easy to install addons.
@ -9,48 +9,41 @@ Stremio is a modern media center that's a one-stop solution for your video enter
### Prerequisites
* Node.js 12 or higher
* [pnpm](https://pnpm.io/installation) 10 or higher
* Node.js 10 or higher
* npm 6 or higher
### Install dependencies
```bash
pnpm install
npm install
```
### Start development server
```bash
pnpm start
npm start
```
### Production build
```bash
pnpm run build
```
### Run with Docker
```bash
docker build -t stremio-web .
docker run -p 8080:8080 stremio-web
npm run build
```
## Screenshots
### Board
![Board](/assets/screenshots/board.png)
![Board](/screenshots/board.png)
### Discover
![Discover](/assets/screenshots/discover.png)
![Discover](/screenshots/discover.png)
### Meta Details
![Meta Details](/assets/screenshots/metadetails.png)
![Meta Details](/screenshots/metadetails.png)
## License
Stremio is copyright 2017-2023 Smart code and available under GPLv2 license. See the [LICENSE](/LICENSE.md) file in the project for more information.
Stremio is copyright 2017-2020 Smart code and available under GPLv2 license. See the [LICENSE](/LICENSE.md) file in the project for more information.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

View file

@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="968" height="565" viewBox="0 0 968 565">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_1144" data-name="Rectangle 1144" width="968" height="565" transform="translate(0 262)" fill="#fff" stroke="#707070" stroke-width="1"/>
</clipPath>
</defs>
<g id="Mask_Group_31" data-name="Mask Group 31" transform="translate(0 -262)" clip-path="url(#clip-path)">
<g id="Group_2309" data-name="Group 2309">
<path id="Path_983" data-name="Path 983" d="M410.951-49.5c337,24.76,699.788,308.381,792,500.579S897.064,762.814,577.9,762.814,0,593.971,0,385.694,73.955-74.26,410.951-49.5Z" transform="translate(-301.147 411.907)" fill="#362565" opacity="0.8"/>
<path id="Path_979" data-name="Path 979" d="M360.91-73.97c324,27.3,638,301.633,720.932,474.48S806.748,680.86,519.716,680.86,0,529.016,0,341.708,36.91-101.27,360.91-73.97Z" transform="translate(-231.91 594.67)" fill="rgba(123,91,245,0.83)" opacity="0.8"/>
<path id="Path_984" data-name="Path 984" d="M262.171-10C444.7-10,659.821,73.865,660.993,203.729S513.025,402.667,330.5,402.667,0,313.6,0,203.729,79.643-10,262.171-10Z" transform="translate(-69 681.267)" fill="#5126ed"/>
<path id="Path_980" data-name="Path 980" d="M262.171-10C444.7-10,659.821,66.535,660.993,185.049S513.025,366.6,330.5,366.6,0,285.317,0,185.049,79.643-10,262.171-10Z" transform="translate(-69 762.333)" fill="#4516fc"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="718" height="356" viewBox="0 0 718 356">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_1144" data-name="Rectangle 1144" width="718" height="356" transform="translate(602 -8)" fill="#fff" stroke="#707070" stroke-width="1"/>
</clipPath>
</defs>
<g id="Mask_Group_31" data-name="Mask Group 31" transform="translate(-602 8)" clip-path="url(#clip-path)">
<g id="Group_2308" data-name="Group 2308" transform="translate(-49.883 86.23)">
<path id="Path_982" data-name="Path 982" d="M264.138,0C470.016,0,780.486,131.36,775.97,319.553S578.654,535.889,372.776,535.889,0,418.717,0,274.178,58.26,0,264.138,0Z" transform="translate(1521.635 173.714) rotate(180)" fill="rgba(137,91,245,0.64)" opacity="0.52"/>
<path id="Path_981" data-name="Path 981" d="M177.9,0C301.753,0,447.725,59.059,448.52,150.512s-100.4,140.1-224.26,140.1S0,227.885,0,150.512,54.042,0,177.9,0Z" transform="translate(1366.094 26.124) rotate(180)" fill="#4722d2"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -1,102 +0,0 @@
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/function-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',
}
}
];

BIN
favicons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
fonts/Roboto-Bold.ttf Normal file

Binary file not shown.

BIN
fonts/Roboto-BoldItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Roboto-Light.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/Roboto-Medium.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/Roboto-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

View file

@ -1,23 +0,0 @@
#!/usr/bin/env node
// Copyright (C) 2017-2023 Smart code 203358507
const INDEX_CACHE = 7200;
const ASSETS_CACHE = 2629744;
const HTTP_PORT = 8080;
const express = require('express');
const path = require('path');
const build_path = path.resolve(__dirname, 'build');
const index_path = path.join(build_path, 'index.html');
express().use(express.static(build_path, {
setHeaders: (res, path) => {
if (path === index_path) res.set('cache-control', `public, max-age: ${INDEX_CACHE}`);
else res.set('cache-control', `public, max-age: ${ASSETS_CACHE}`);
}
})).all('*', (_req, res) => {
// TODO: better 404 page
res.status(404).send('<h1>404! Page not found</h1>');
}).listen(HTTP_PORT, () => console.info(`Server listening on port: ${HTTP_PORT}`));

BIN
images/anonymous.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
images/default_avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
images/intro_background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
images/stremio_symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1,59 +0,0 @@
{
"name": "Stremio Web",
"short_name": "Stremio",
"description": "Freedom To Stream",
"background_color": "#161523",
"theme_color": "#2a2843",
"orientation": "any",
"display": "standalone",
"display_override": ["standalone"],
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "favicons/icon_256x256.ico",
"sizes": "256x256",
"type": "image/vnd.microsoft.icon"
},
{
"src": "images/maskable_icon_512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "images/maskable_icon_196x196.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "images/icon_512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "images/icon_196x196.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "any"
}
],
"screenshots": [
{
"src": "screenshots/board_wide.webp",
"sizes": "1440x900",
"type": "image/webp",
"form_factor": "wide",
"label": "Homescreen of Stremio"
},
{
"src": "screenshots/board_narrow.webp",
"sizes": "414x896",
"type": "image/webp",
"form_factor": "narrow",
"label": "Homescreen of Stremio"
}
]
}

10055
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

106
package.json Normal file → Executable file
View file

@ -1,85 +1,61 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.31",
"version": "5.0.0",
"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",
"scan-translations": "pnpx jest ./tests/i18nScan.test.js"
"lint": "eslint src"
},
"dependencies": {
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.53.0",
"@stremio/stremio-icons": "5.8.0",
"@stremio/stremio-video": "0.0.70",
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "4.0.1",
"@stremio/stremio-core-web": "0.26.0",
"@stremio/stremio-icons": "3.0.5",
"@stremio/stremio-video": "0.0.14",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
"classnames": "2.5.1",
"eventemitter3": "5.0.1",
"fast-equals": "^6.0.0",
"filter-invalid-dom-props": "3.0.1",
"hat": "^0.0.3",
"i18next": "^24.0.5",
"langs": "github:Stremio/nodejs-langs",
"classnames": "2.3.1",
"eventemitter3": "4.0.7",
"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.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#72a0decf9c636efc3171ee7238a0037ccefc36c1",
"url": "0.11.4",
"use-long-press": "^3.2.0"
"pako": "1.0.11",
"prop-types": "15.7.2",
"react": "16.12.0",
"react-dom": "16.12.0",
"react-focus-lock": "2.2.1",
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#40204ad9942fe786794c62f99ea5ab2b52b24096"
},
"devDependencies": {
"@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": "^5.4.0",
"@stylistic/eslint-plugin-jsx": "^4.4.1",
"@types/hat": "^0.0.4",
"@types/lodash.throttle": "^4.1.9",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"babel-loader": "9.2.1",
"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",
"recast": "0.23.11",
"terser-webpack-plugin": "5.3.10",
"thread-loader": "^4.0.4",
"ts-loader": "^9.5.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",
"webpack": "5.97.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "^5.1.0",
"workbox-webpack-plugin": "^7.3.0"
"@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",
"babel-loader": "8.2.3",
"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",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "4.4.0"
}
}

File diff suppressed because it is too large Load diff

BIN
screenshots/board.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

BIN
screenshots/discover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
screenshots/metadetails.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -1,52 +1,33 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { Core, Shell, Chromecast, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
const { default: UpdaterBanner } = require('./UpdaterBanner');
const { default: ShortcutsModal } = require('./ShortcutsModal');
const { ToastProvider, sanitizeLocationPath, CONSTANTS } = require('stremio/common');
const CoreEventsToaster = require('./CoreEventsToaster');
const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router));
window.core_imports = {
app_version: process.env.VERSION,
shell_version: null,
sanitize_location_path: sanitizeLocationPath
};
const App = () => {
const { i18n } = useTranslation();
const shell = useShell();
const onPathNotMatch = React.useCallback(() => {
return NotFound;
}, []);
const services = React.useMemo(() => {
const core = new Core({
appVersion: process.env.VERSION,
shellVersion: null
});
return {
core,
shell: new Shell(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts(),
dragAndDrop: new DragAndDrop({ core })
};
}, []);
const services = React.useMemo(() => ({
core: new Core(),
shell: new Shell(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts()
}), []);
const [initialized, setInitialized] = React.useState(false);
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
const onShortcut = React.useCallback((name) => {
if (name === 'shortcuts') {
toggleShortcutModal();
}
}, [toggleShortcutModal]);
React.useEffect(() => {
let prevPath = window.location.hash.slice(1);
const onLocationHashChange = () => {
@ -82,8 +63,7 @@ const App = () => {
receiverApplicationId: CONSTANTS.CHROMECAST_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
resumeSavedSession: false,
language: null,
androidReceiverCompatible: true
language: null
});
}
};
@ -94,113 +74,17 @@ const App = () => {
services.shell.start();
services.chromecast.start();
services.keyboardShortcuts.start();
services.dragAndDrop.start();
window.services = services;
return () => {
services.core.stop();
services.shell.stop();
services.chromecast.stop();
services.keyboardShortcuts.stop();
services.dragAndDrop.stop();
services.core.off('stateChanged', onCoreStateChanged);
services.shell.off('stateChanged', onShellStateChanged);
services.chromecast.off('stateChanged', onChromecastStateChange);
};
}, []);
// Handle shell events
React.useEffect(() => {
const onOpenMedia = (data) => {
try {
const { protocol, hostname, pathname, searchParams } = new URL(data);
if (protocol === CONSTANTS.PROTOCOL) {
if (hostname.length) {
const transportUrl = `https://${hostname}${pathname}`;
window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`;
} else {
window.location.href = `#${pathname}?${searchParams.toString()}`;
}
}
} catch (e) {
console.error('Failed to open media:', e);
}
};
shell.on('open-media', onOpenMedia);
return () => {
shell.off('open-media', onOpenMedia);
};
}, []);
React.useEffect(() => {
const onCoreEvent = ({ event, args }) => {
switch (event) {
case 'SettingsUpdated': {
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
i18n.changeLanguage(args.settings.interfaceLanguage);
}
if (args?.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
break;
}
}
};
const onCtxState = (state) => {
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
}
if (state?.profile?.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
};
const onWindowFocus = () => {
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullAddonsFromAPI'
}
});
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullUserFromAPI',
args: {}
}
});
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'SyncLibraryWithAPI'
}
});
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullNotifications'
}
});
};
if (services.core.active) {
onWindowFocus();
window.addEventListener('focus', onWindowFocus);
services.core.transport.on('CoreEvent', onCoreEvent);
services.core.transport
.getState('ctx')
.then(onCtxState)
.catch(console.error);
}
return () => {
if (services.core.active) {
window.removeEventListener('focus', onWindowFocus);
services.core.transport.off('CoreEvent', onCoreEvent);
}
};
}, [initialized, shell.windowClosed]);
return (
<React.StrictMode>
<ServicesProvider services={services}>
@ -209,28 +93,14 @@ const App = () => {
services.core.error instanceof Error ?
<ErrorDialog className={styles['error-container']} />
:
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-container']}>
<ShortcutsProvider onShortcut={onShortcut}>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ShortcutsProvider>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<CoreEventsToaster />
<Router
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ToastProvider>
:
<div className={styles['loader-container']} />
}

View file

@ -0,0 +1,29 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const { useServices } = require('stremio/services');
const { useToast } = require('stremio/common');
const CoreEventsToaster = () => {
const { core } = useServices();
const toast = useToast();
React.useEffect(() => {
const onCoreEvent = ({ event, args }) => {
if (event === 'Error') {
toast.show({
type: 'error',
title: args.source.event,
message: args.error.message,
timeout: 4000
});
}
};
core.transport.on('CoreEvent', onCoreEvent);
return () => {
core.transport.off('CoreEvent', onCoreEvent);
};
}, []);
return null;
};
module.exports = CoreEventsToaster;

View file

@ -1,22 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { withCoreSuspender, useStreamingServer } = require('stremio/common');
const DeepLinkHandler = () => {
const streamingServer = useStreamingServer();
React.useEffect(() => {
if (streamingServer.torrent !== null) {
const [, { type, content }] = streamingServer.torrent;
if (type === 'Ready') {
const [, deepLinks] = content;
if (typeof deepLinks.metaDetailsVideos === 'string') {
window.location = deepLinks.metaDetailsVideos;
}
}
}
}, [streamingServer.torrent]);
return null;
};
module.exports = withCoreSuspender(DeepLinkHandler);

View file

@ -1,15 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Image, Button } = require('stremio/components');
const { Button, Image } = require('stremio/common');
const styles = require('./styles');
const ErrorDialog = ({ className }) => {
const { t } = useTranslation();
const [dataCleared, setDataCleared] = React.useState(false);
const reload = React.useCallback(() => {
window.location.reload();
@ -22,22 +19,16 @@ const ErrorDialog = ({ className }) => {
<div className={classnames(className, styles['error-container'])}>
<Image
className={styles['error-image']}
src={require('/assets/images/empty.png')}
src={require('/images/empty.png')}
alt={' '}
/>
<div className={styles['error-message']}>
{ t('GENERIC_ERROR_MESSAGE') }
</div>
<div className={styles['error-message']}>Something went wrong!</div>
<div className={styles['buttons-container']}>
<Button className={styles['button-container']} title={t('TRY_AGAIN')} onClick={reload}>
<div className={styles['label']}>
{ t('TRY_AGAIN') }
</div>
<Button className={styles['button-container']} title={'Try again'} onClick={reload}>
<div className={styles['label']}>Try again</div>
</Button>
<Button className={styles['button-container']} disabled={dataCleared} title={t('CLEAR_DATA')} onClick={clearData}>
<div className={styles['label']}>
{ t('CLEAR_DATA') }
</div>
<Button className={styles['button-container']} disabled={dataCleared} title={'Clear data'} onClick={clearData}>
<div className={styles['label']}>Clear data</div>
</Button>
</div>
</div>

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const ErrorDialog = require('./ErrorDialog');

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -7,12 +7,12 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
.error-image {
flex: none;
width: 12rem;
height: 12rem;
margin-bottom: 1rem;
object-fit: contain;
object-position: center;
opacity: 0.9;
@ -24,7 +24,7 @@
font-size: 2rem;
max-height: 3.6em;
text-align: center;
color: var(--primary-foreground-color);
color: @color-surface-light5-90;
}
.buttons-container {
@ -36,8 +36,6 @@
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin-top: 1rem;
.button-container {
flex-grow: 0;
@ -47,23 +45,18 @@
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 2.5rem;
margin: 2rem 1rem 0;
padding: 0 1rem;
min-width: 8rem;
height: 3.5rem;
border-radius: 3.5rem;
background-color: var(--overlay-color);
height: 3rem;
background-color: @color-accent3;
&:hover {
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
background-color: transparent;
}
&:active {
outline: none;
background-color: @color-accent3-light1;
}
&:global(.disabled) {
opacity: 0.3;
background-color: @color-surface-dark5;
}
.label {
@ -74,7 +67,7 @@
font-size: 1.1rem;
font-weight: 500;
text-align: center;
color: var(--primary-foreground-color);
color: @color-surface-light5-90;
}
}
}

View file

@ -1,63 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { deepEqual } = require('fast-equals');
const { withCoreSuspender, useProfile, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const SearchParamsHandler = () => {
const { core } = useServices();
const profile = useProfile();
const toast = useToast();
const [searchParams, setSearchParams] = React.useState({});
const onLocationChange = () => {
const { origin, hash, search } = window.location;
const { searchParams } = new URL(`${origin}${hash.replace('#', '')}${search}`);
setSearchParams((previousSearchParams) => {
const currentSearchParams = Object.fromEntries(searchParams.entries());
return deepEqual(previousSearchParams, currentSearchParams) ? previousSearchParams : currentSearchParams;
});
};
React.useEffect(() => {
const { streamingServerUrl } = searchParams;
if (streamingServerUrl) {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
streamingServerUrl,
},
},
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddServerUrl',
args: streamingServerUrl,
},
});
toast.show({
type: 'success',
title: `Using streaming server at ${streamingServerUrl}`,
timeout: 4000,
});
}
}, [searchParams]);
React.useEffect(() => {
onLocationChange();
window.addEventListener('hashchange', onLocationChange);
return () => window.removeEventListener('hashchange', onLocationChange);
}, []);
return null;
};
module.exports = withCoreSuspender(SearchParamsHandler);

View file

@ -1,81 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useServices } = require('stremio/services');
const { useToast } = require('stremio/common');
const ServicesToaster = () => {
const { core, dragAndDrop } = useServices();
const toast = useToast();
React.useEffect(() => {
const onCoreEvent = ({ event, args }) => {
switch (event) {
case 'Error': {
if (args.source.event === 'UserPulledFromAPI' && args.source.args.uid === null) {
break;
}
if (args.source.event === 'LibrarySyncWithAPIPlanned' && args.source.args.uid === null) {
break;
}
if (args.error.type === 'Other' && args.error.code === 3 && args.source.event === 'AddonInstalled' && args.source.args.transport_url.startsWith('https://www.strem.io/trakt/addon')) {
break;
}
toast.show({
type: 'error',
title: args.source.event,
message: args.error.message,
timeout: 4000,
dataset: {
type: 'CoreEvent'
}
});
break;
}
case 'TorrentParsed': {
toast.show({
type: 'success',
title: 'Torrent file parsed',
timeout: 4000
});
break;
}
case 'MagnetParsed': {
toast.show({
type: 'success',
title: 'Magnet link parsed',
timeout: 4000
});
break;
}
case 'PlayingOnDevice': {
toast.show({
type: 'success',
title: `Stream opened in ${args.device}`,
timeout: 4000
});
break;
}
}
};
const onDragAndDropError = (error) => {
toast.show({
type: 'error',
title: error.message,
message: error.file?.name,
timeout: 4000
});
};
core.transport.on('CoreEvent', onCoreEvent);
dragAndDrop.on('error', onDragAndDropError);
return () => {
core.transport.off('CoreEvent', onCoreEvent);
dragAndDrop.off('error', onDragAndDropError);
};
}, []);
return null;
};
module.exports = ServicesToaster;

View file

@ -1,59 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import Icon from '@stremio/stremio-icons/react';
import { useShortcuts } from 'stremio/common';
import { Button, ShortcutsGroup } from 'stremio/components';
import styles from './styles.less';
type Props = {
onClose: () => void,
};
const ShortcutsModal = ({ onClose }: Props) => {
const { t } = useTranslation();
const { grouped } = useShortcuts();
useEffect(() => {
const onKeyDown = ({ key }: KeyboardEvent) => {
key === 'Escape' && onClose();
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, []);
return createPortal((
<div className={styles['shortcuts-modal']}>
<div className={styles['backdrop']} onClick={onClose} />
<div className={styles['container']}>
<div className={styles['header']}>
<div className={styles['title']}>
{t('SETTINGS_NAV_SHORTCUTS')}
</div>
<Button className={styles['close-button']} title={t('BUTTON_CLOSE')} onClick={onClose}>
<Icon className={styles['icon']} name={'close'} />
</Button>
</div>
<div className={styles['content']}>
{
grouped.map(({ name, label, shortcuts }) => (
<ShortcutsGroup
key={name}
label={label}
shortcuts={shortcuts}
/>
))
}
</div>
</div>
</div>
), document.body);
};
export default ShortcutsModal;

View file

@ -1,2 +0,0 @@
import ShortcutsModal from './ShortcutsModal';
export default ShortcutsModal;

View file

@ -1,91 +0,0 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.shortcuts-modal {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
.backdrop {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: @color-background-dark5-40;
cursor: pointer;
}
.container {
position: relative;
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 80%;
max-width: 80%;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
overflow-y: auto;
.header {
flex: none;
display: flex;
justify-content: space-between;
align-items: center;
height: 5rem;
padding-left: 2.5rem;
padding-right: 1rem;
.title {
position: relative;
font-size: 1.5rem;
font-weight: 500;
color: var(--primary-foreground-color);
}
.close-button {
position: relative;
width: 3rem;
height: 3rem;
padding: 0.5rem;
border-radius: var(--border-radius);
z-index: 2;
.icon {
display: block;
width: 100%;
height: 100%;
color: var(--primary-foreground-color);
opacity: 0.4;
}
&:hover, &:focus {
.icon {
opacity: 1;
color: var(--primary-foreground-color);
}
}
&:focus {
outline-color: var(--primary-foreground-color);
}
}
}
.content {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 3rem;
padding: 0 2.5rem;
padding-bottom: 2rem;
overflow-y: auto;
}
}
}

View file

@ -1,46 +0,0 @@
.updater-banner {
height: 4rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0 1rem;
font-size: 1rem;
font-weight: bold;
color: var(--primary-foreground-color);
background-color: var(--primary-accent-color);
.button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: var(--border-radius);
color: var(--primary-background-color);
background-color: var(--primary-foreground-color);
transition: all 0.1s ease-out;
&:hover {
color: var(--primary-foreground-color);
background-color: transparent;
box-shadow: inset 0 0 0 0.15rem var(--primary-foreground-color);
}
}
.close {
position: absolute;
right: 0;
height: 4rem;
width: 4rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.icon {
height: 2rem;
}
}
}

View file

@ -1,50 +0,0 @@
import React, { useEffect } from 'react';
import Icon from '@stremio/stremio-icons/react';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { useBinaryState, useShell } from 'stremio/common';
import { Button, Transition } from 'stremio/components';
import styles from './UpdaterBanner.less';
type Props = {
className: string,
};
const UpdaterBanner = ({ className }: Props) => {
const { t } = useTranslation();
const { shell } = useServices();
const shellTransport = useShell();
const [visible, show, hide] = useBinaryState(false);
const onInstallClick = () => {
shellTransport.send('autoupdater-notif-clicked');
};
useEffect(() => {
shell.transport && shell.transport.on('autoupdater-show-notif', show);
return () => {
shell.transport && shell.transport.off('autoupdater-show-notif', show);
};
}, []);
return (
<div className={className}>
<Transition when={visible} name={'slide-up'}>
<div className={styles['updater-banner']}>
<div className={styles['label']}>
{ t('UPDATER_TITLE') }
</div>
<Button className={styles['button']} onClick={onInstallClick}>
{ t('UPDATER_INSTALL_BUTTON') }
</Button>
<Button className={styles['close']} onClick={hide}>
<Icon className={styles['icon']} name={'close'} />
</Button>
</div>
</Transition>
</div>
);
};
export default UpdaterBanner;

View file

@ -1,2 +0,0 @@
import UpdaterBanner from './UpdaterBanner';
export default UpdaterBanner;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const App = require('./App');

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const routes = require('stremio/routes');
const { routesRegexp } = require('stremio/common');
@ -23,10 +23,6 @@ const routerViewsConfig = [
...routesRegexp.library,
component: routes.Library
},
{
...routesRegexp.calendar,
component: routes.Calendar
},
{
...routesRegexp.continuewatching,
component: routes.Library

View file

@ -1,109 +1,25 @@
// Copyright (C) 2017-2024 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
@import (inline, once, css) '~stremio/common/roboto.css';
@import (reference) '~stremio/common/screen-sizes.less';
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@font-face {
font-family: 'PlusJakartaSans';
src: url('/assets/fonts/PlusJakartaSans.ttf') format('truetype');
}
:global {
@import (once, less) '~stremio/common/animations.less';
@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(var(--viewport-height-diff) - env(safe-area-inset-top, 0rem))))";
// Viewport sizes
@viewport-width: ~"100vw";
@viewport-height: ~"100vh";
// HTML sizes
@html-width: ~"calc(max(var(--small-viewport-width), var(--dynamic-viewport-width)))";
@html-height: ~"calc(max(var(--small-viewport-height), var(--dynamic-viewport-height)))";
@html-standalone-width: ~"calc(max(100%, var(--large-viewport-width)))";
@html-standalone-height: ~"calc(max(100%, var(--large-viewport-height)))";
// Safe area insets
@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-gradient-pad: 6rem;
:root {
--landscape-shape-ratio: 0.5625;
--poster-shape-ratio: 1.464;
--scroll-bar-size: 6px;
--horizontal-nav-bar-size: 5.5rem;
--vertical-nav-bar-size: 6rem;
--horizontal-nav-bar-size: 4rem;
--vertical-nav-bar-size: 5.2rem;
--focus-outline-size: 2px;
--color-facebook: #1877F1;
--color-x: #000000;
--color-reddit: #FF4500;
--color-imdb: #f5c518;
--color-trakt: rgb(255, 255, 255);
--color-facebook: #4267b2;
--color-twitter: #1DA1F2;
--color-placeholder: #60606080;
--color-placeholder-text: @color-surface-50;
--color-placeholder-background: @color-surface-dark5-20;
--primary-background-color: rgba(12, 11, 17, 1);
--secondary-background-color: rgba(26, 23, 62, 1);
--primary-foreground-color: rgba(255, 255, 255, 0.9);
--secondary-foreground-color: rgb(12, 11, 17, 1);
--primary-accent-color: rgb(123, 91, 245);
--secondary-accent-color: rgba(34, 179, 101, 1);
--tertiary-accent-color: rgba(246, 199, 0, 1);
--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 15px rgba(123, 91, 245, 0.37);
--warning-accent-color: rgba(255, 165, 0, 1);
--danger-accent-color: rgba(220, 38, 38, 1);
--border-radius: 0.75rem;
--top-overlay-size: @top-overlay-size;
--bottom-overlay-size: @bottom-overlay-size;
--overlap-size: @overlap-size;
--transparency-gradient-pad: @transparency-gradient-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;
--dynamic-viewport-width: @viewport-width;
--dynamic-viewport-height: @viewport-height;
--large-viewport-width: @viewport-width;
--large-viewport-height: @viewport-height;
--small-viewport-width: @viewport-width;
--small-viewport-height: @viewport-height;
--viewport-height-diff: calc(100vh - 100vh);
@supports (height: 100dvh) {
--dynamic-viewport-width: 100dvw;
--dynamic-viewport-height: 100dvh;
}
@supports (height: 100lvh) {
--large-viewport-width: 100lvw;
--large-viewport-height: 100lvh;
}
@supports (height: 100svh) {
--small-viewport-width: 100svw;
--small-viewport-height: 100svh;
}
@supports (height: 100lvh) and (height: 100svh) {
--viewport-height-diff: calc(100lvh - 100svh);
}
@media (display-mode: standalone) {
--safe-area-inset-bottom: @calculated-bottom-safe-inset;
}
}
* {
@ -111,6 +27,7 @@
padding: 0;
box-sizing: border-box;
font-size: 1rem;
line-height: 1.2em;
font-family: inherit;
border: none;
outline: none;
@ -123,7 +40,7 @@
overflow: hidden;
word-break: break-word;
scrollbar-width: thin;
scrollbar-color: var(--overlay-color) transparent;
scrollbar-color: @color-secondaryvariant2-light1 @color-background-dark2;
}
::-webkit-scrollbar {
@ -132,16 +49,15 @@
}
::-webkit-scrollbar-thumb {
border-radius: var(--scroll-bar-size);
background-color: var(--overlay-color);
background-color: @color-secondaryvariant2-light1;
&:hover {
background-color: var(--primary-accent-color);
background-color: @color-secondaryvariant2-light2;
}
}
::-webkit-scrollbar-track {
background-color: transparent;
background-color: @color-background-dark2;
}
svg {
@ -149,26 +65,16 @@ svg {
}
html {
width: @html-width;
height: @html-height;
font-family: 'PlusJakartaSans', 'Arial', 'Helvetica', 'sans-serif';
width: 100%;
height: 100%;
min-width: 640px;
min-height: 480px;
font-family: 'Roboto', 'sans-serif';
overflow: auto;
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
background-color: var(--primary-background-color);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {
width: @html-standalone-width;
height: @html-standalone-height;
}
body {
width: 100%;
height: 100%;
background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%);
-webkit-font-smoothing: antialiased;
:global(#app) {
position: relative;
@ -178,13 +84,13 @@ html {
.toasts-container {
position: absolute;
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(--safe-area-inset-bottom, 0rem));
top: calc(1.2 * var(--horizontal-nav-bar-size));
right: 0;
bottom: calc(1.2 * var(--horizontal-nav-bar-size));
left: auto;
z-index: 1;
padding: 0 calc(0.5 * var(--horizontal-nav-bar-size));
overflow: visible;
overflow-y: auto;
scrollbar-width: none;
pointer-events: none;
@ -193,44 +99,6 @@ html {
}
}
.tooltip-container {
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.5rem;
font-size: 1rem;
color: var(--primary-foreground-color);
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
transition: opacity 0.1s ease-out;
}
.file-drop-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: 1rem;
border: 0.5rem dashed transparent;
pointer-events: none;
transition: border-color 0.25s ease-out;
&:global(.active) {
border-color: var(--primary-accent-color);
}
}
.updater-banner-container {
z-index: 1;
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.router {
width: 100%;
height: 100%;
@ -239,6 +107,7 @@ html {
.loader-container, .error-container {
width: 100%;
height: 100%;
background-color: @color-background-dark2;
}
}
}
@ -266,26 +135,4 @@ html {
html {
font-size: 14px;
}
}
@media only screen and (max-width: @xsmall) {
html {
body {
:global(#app) {
.toasts-container {
padding: 0 1rem;
}
.tooltip-container {
display: none;
}
}
}
}
}
@media only screen and (max-width: @minimum) {
:root {
--bottom-overlay-size: 6rem;
}
}

View file

@ -1,29 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { Intro } = require('stremio/routes');
const { useProfile } = require('stremio/common');
const withProtectedRoutes = (Component) => {
return function withProtectedRoutes(props) {
const profile = useProfile();
const previousAuthRef = React.useRef(profile.auth);
React.useEffect(() => {
if (previousAuthRef.current !== null && profile.auth === null) {
window.location = '#/intro';
}
previousAuthRef.current = profile.auth;
}, [profile]);
const onRouteChange = React.useCallback((routeConfig) => {
if (profile.auth !== null && routeConfig.component === Intro) {
window.location.replace('#/');
return true;
}
}, [profile]);
return (
<Component {...props} onRouteChange={onRouteChange} />
);
};
};
module.exports = withProtectedRoutes;

View file

@ -1,17 +1,15 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 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');
const { default: Image } = require('stremio/components/Image');
const Icon = require('@stremio/stremio-icons/dom');
const Image = require('stremio/common/Image');
const styles = require('./styles');
const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => {
const { t } = useTranslation();
const renderLogoFallback = React.useCallback(() => (
<Icon className={styles['icon']} name={'addons'} />
<Icon className={styles['icon']} icon={'ic_addons'} />
), []);
return (
<div className={classnames(className, styles['addon-details-container'])}>
@ -26,7 +24,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
<span className={styles['name']}>{typeof name === 'string' && name.length > 0 ? name : id}</span>
{
typeof version === 'string' && version.length > 0 ?
<span className={styles['version']}>{t('ADDON_VERSION_SHORT', {version})}</span>
<span className={styles['version']}>v. {version}</span>
:
null
}
@ -43,7 +41,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
{
typeof transportUrl === 'string' && transportUrl.length > 0 ?
<div className={styles['section-container']}>
<span className={styles['section-header']}>{`${t('URL')}:`}</span>
<span className={styles['section-header']}>URL: </span>
<span className={classnames(styles['section-label'], styles['transport-url-label'])}>{transportUrl}</span>
</div>
:
@ -52,7 +50,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
{
Array.isArray(types) && types.length > 0 ?
<div className={styles['section-container']}>
<span className={styles['section-header']}>{`${t('ADDON_SUPPORTED_TYPES')}:`} </span>
<span className={styles['section-header']}>Supported types: </span>
<span className={styles['section-label']}>
{
types.length === 1 ?
@ -68,7 +66,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types,
{
!official ?
<div className={styles['section-container']}>
<div className={classnames(styles['section-label'], styles['disclaimer-label'])}>{t('ADDON_DISCLAIMER')}</div>
<div className={classnames(styles['section-label'], styles['disclaimer-label'])}>Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.</div>
</div>
:
null

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const AddonDetails = require('./AddonDetails');

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -14,6 +14,7 @@
height: 5rem;
margin-right: 1.5rem;
padding: 0.5rem;
background-color: @color-background-dark5;
}
.logo {
@ -22,7 +23,7 @@
}
.icon {
color: var(--primary-foreground-color);
fill: @color-secondaryvariant1-light3;
}
.name-container {
@ -40,7 +41,7 @@
flex-basis: auto;
margin-right: 0.5rem;
font-size: 1.6rem;
color: var(--primary-foreground-color);
color: @color-background-dark5-90;
}
.version {
@ -48,7 +49,7 @@
flex-shrink: 1;
flex-basis: auto;
margin-top: 0.5rem;
color: var(--primary-foreground-color);
color: @color-background-dark5-60;
}
}
}
@ -58,13 +59,13 @@
.section-header {
font-size: 1.1rem;
color: var(--primary-foreground-color);
color: @color-background-dark5-90;
}
.section-label {
font-size: 1.1rem;
font-weight: 300;
color: var(--primary-foreground-color);
color: @color-background-dark5-90;
&.transport-url-label {
user-select: text;

View file

@ -1,11 +1,8 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const ModalDialog = require('stremio/components/ModalDialog');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const { usePlatform } = require('stremio/common/Platform');
const ModalDialog = require('stremio/common/ModalDialog');
const { useServices } = require('stremio/services');
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
const useAddonDetails = require('./useAddonDetails');
@ -30,7 +27,6 @@ function withRemoteAndLocalAddon(AddonDetails) {
id={addon.manifest.id}
name={addon.manifest.name}
version={addon.manifest.version}
background={addon.manifest.background}
logo={addon.manifest.logo}
description={addon.manifest.description}
types={addon.manifest.types}
@ -44,14 +40,12 @@ function withRemoteAndLocalAddon(AddonDetails) {
}
const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
const addonDetails = useAddonDetails(transportUrl);
const modalButtons = React.useMemo(() => {
const cancelButton = {
className: styles['cancel-button'],
label: t('BUTTON_CANCEL'),
label: 'Cancel',
props: {
onClick: (event) => {
if (typeof onCloseRequest === 'function') {
@ -64,31 +58,10 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
}
};
const configureButton = addonDetails.remoteAddon !== null &&
addonDetails.remoteAddon.content.type === 'Ready' &&
addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurable ?
{
className: styles['configure-button'],
label: t('ADDON_CONFIGURE'),
props: {
onClick: (event) => {
platform.openExternal(transportUrl.replace('manifest.json', 'configure'));
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'configure',
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}
}
}
:
null;
const toggleButton = addonDetails.localAddon !== null ?
{
className: styles['uninstall-button'],
label: t('ADDON_UNINSTALL'),
label: 'Uninstall',
props: {
onClick: (event) => {
core.transport.dispatch({
@ -109,13 +82,11 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
}
:
addonDetails.remoteAddon !== null &&
addonDetails.remoteAddon.content.type === 'Ready' &&
!addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurationRequired ?
addonDetails.remoteAddon !== null && addonDetails.remoteAddon.content.type === 'Ready' ?
{
className: styles['install-button'],
label: t('ADDON_INSTALL'),
label: 'Install',
props: {
onClick: (event) => {
core.transport.dispatch({
@ -137,27 +108,24 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
:
null;
return configureButton && toggleButton ? [cancelButton, configureButton, toggleButton] : configureButton ? [cancelButton, configureButton] : toggleButton ? [cancelButton, toggleButton] : [cancelButton];
return toggleButton !== null ? [cancelButton, toggleButton] : [cancelButton];
}, [addonDetails, onCloseRequest]);
const modalBackground = React.useMemo(() => {
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
}, [addonDetails.remoteAddon]);
return (
<ModalDialog className={styles['addon-details-modal-container']} title={t('STREMIO_COMMUNITY_ADDON')} buttons={modalButtons} background={modalBackground} onCloseRequest={onCloseRequest}>
<ModalDialog className={styles['addon-details-modal-container']} title={'Stremio addon'} buttons={modalButtons} onCloseRequest={onCloseRequest}>
{
addonDetails.selected === null ?
<div className={styles['addon-details-message-container']}>
{t('ADDON_LOADING_MANIFEST')}
Loading addon manifest
</div>
:
addonDetails.remoteAddon === null || addonDetails.remoteAddon.content.type === 'Loading' ?
<div className={styles['addon-details-message-container']}>
{t('ADDON_LOADING_MANIFEST_FROM', { origin: addonDetails.selected.transportUrl})}
Loading addon manifest from {addonDetails.selected.transportUrl}
</div>
:
addonDetails.remoteAddon.content.type === 'Err' && addonDetails.localAddon === null ?
<div className={styles['addon-details-message-container']}>
{t('ADDON_LOADING_MANIFEST_FAILED', {origin: addonDetails.selected.transportUrl})}
Failed to get addon manifest from {addonDetails.selected.transportUrl}
<div>{addonDetails.remoteAddon.content.content.message}</div>
</div>
:
@ -176,19 +144,4 @@ AddonDetailsModal.propTypes = {
onCloseRequest: PropTypes.func
};
const AddonDetailsModalFallback = ({ onCloseRequest }) => {
const { t } = useTranslation();
return <ModalDialog
className={styles['addon-details-modal-container']}
title={t('STREMIO_COMMUNITY_ADDON')}
onCloseRequest={onCloseRequest}
>
<div className={styles['addon-details-message-container']}>
{t('ADDON_LOADING_MANIFEST')}
</div>
</ModalDialog>;
};
AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes;
module.exports = withCoreSuspender(AddonDetailsModal, AddonDetailsModalFallback);
module.exports = AddonDetailsModal;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const AddonDetailsModal = require('./AddonDetailsModal');

View file

@ -0,0 +1,47 @@
// Copyright (C) 2017-2020 Smart code 203358507
@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;
max-width: 100%;
}
.install-button, .uninstall-button, .cancel-button {
.label {
font-size: 1.2rem;
font-weight: 500;
}
}
.uninstall-button, .cancel-button {
&:focus {
outline-color: @color-background-dark5;
}
}
.cancel-button {
background-color: transparent;
&:hover {
background-color: @color-surface-light3;
}
.label {
color: @color-surface-dark2;
}
}
.uninstall-button {
background-color: @color-accent2;
&:hover {
background-color: @color-accent2-light2;
}
}
}

View file

@ -1,8 +1,14 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const useModelState = require('stremio/common/useModelState');
const init = () => ({
selected: null,
localAddon: null,
remoteAddon: null
});
const useAddonDetails = (transportUrl) => {
const action = React.useMemo(() => {
if (typeof transportUrl === 'string') {
@ -21,7 +27,7 @@ const useAddonDetails = (transportUrl) => {
};
}
}, [transportUrl]);
return useModelState({ model: 'addon_details', action });
return useModelState({ model: 'addon_details', action, init });
};
module.exports = useAddonDetails;

View file

@ -0,0 +1,56 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const styles = require('./styles');
const Button = React.forwardRef(({ className, href, disabled, children, ...props }, ref) => {
const onKeyDown = React.useCallback((event) => {
if (typeof props.onKeyDown === 'function') {
props.onKeyDown(event);
}
if (event.key === 'Enter' && !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
},
children
);
});
Button.displayName = 'Button';
Button.propTypes = {
className: PropTypes.string,
href: PropTypes.string,
disabled: PropTypes.bool,
children: PropTypes.node,
onKeyDown: PropTypes.func,
onMouseDown: PropTypes.func
};
module.exports = Button;

View file

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

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2024 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -14,6 +14,5 @@
&:global(.disabled) {
pointer-events: none;
opacity: 0.5;
}
}

View file

@ -1,11 +1,9 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 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];
const NEXT_VIDEO_POPUP_DURATIONS = [0, 5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000, 55000, 60000, 65000, 70000, 75000, 80000, 85000, 90000];
const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000];
const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None';
@ -13,7 +11,6 @@ const SKIP_EXTRA_NAME = 'skip';
const META_LINK_CATEGORY = 'meta';
const IMDB_LINK_CATEGORY = 'imdb';
const SHARE_LINK_CATEGORY = 'share';
const WRITERS_LINK_CATEGORY = 'Writers';
const TYPE_PRIORITIES = {
movie: 10,
series: 9,
@ -27,104 +24,12 @@ const TYPE_PRIORITIES = {
adult: 1,
other: -Infinity
};
const ICON_FOR_TYPE = new Map([
['movie', 'movies'],
['series', 'series'],
['channel', 'channels'],
['tv', 'tv'],
['book', 'ic_book'],
['game', 'ic_games'],
['music', 'ic_music'],
['adult', 'ic_adult'],
['radio', 'ic_radio'],
['podcast', 'ic_podcast'],
['other', 'movies'],
]);
const MIME_SIGNATURES = {
'application/x-subrip': ['310D0A', '310A'],
'text/vtt': ['574542565454'],
};
const SUPPORTED_LOCAL_SUBTITLES = [
'application/x-subrip',
'text/vtt',
];
const EXTERNAL_PLAYERS = [
{
label: 'EXTERNAL_PLAYER_DISABLED',
value: null,
platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'],
},
{
label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING',
value: 'choose',
platforms: ['android'],
},
{
label: 'VLC',
value: 'vlc',
platforms: ['ios', 'visionos', 'android'],
},
{
label: 'MPV',
value: 'mpv',
platforms: ['macos'],
},
{
label: 'IINA',
value: 'iina',
platforms: ['macos'],
},
{
label: 'MX Player',
value: 'mxplayer',
platforms: ['android'],
},
{
label: 'Just Player',
value: 'justplayer',
platforms: ['android'],
},
{
label: 'Outplayer',
value: 'outplayer',
platforms: ['ios', 'visionos'],
},
{
label: 'Moonplayer (VisionOS)',
value: 'moonplayer',
platforms: ['visionos'],
},
{
label: 'Infuse',
value: 'infuse',
platforms: ['ios', 'visionos', 'macos'],
},
{
label: 'Vidhub',
value: 'vidhub',
platforms: ['ios'],
},
{
label: 'M3U Playlist',
value: 'm3u',
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'];
const PROTOCOL = 'stremio:';
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
DEFAULT_STREAMING_SERVER_URL,
SUBTITLES_SIZES,
SUBTITLES_FONTS,
SEEK_TIME_DURATIONS,
NEXT_VIDEO_POPUP_DURATIONS,
CATALOG_PREVIEW_SIZE,
CATALOG_PAGE_SIZE,
NONE_EXTRA_VALUE,
@ -132,12 +37,5 @@ module.exports = {
META_LINK_CATEGORY,
IMDB_LINK_CATEGORY,
SHARE_LINK_CATEGORY,
WRITERS_LINK_CATEGORY,
TYPE_PRIORITIES,
ICON_FOR_TYPE,
MIME_SIGNATURES,
SUPPORTED_LOCAL_SUBTITLES,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
PROTOCOL,
TYPE_PRIORITIES
};

View file

@ -0,0 +1,34 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const Checkbox = React.forwardRef(({ className, checked, children, ...props }, ref) => {
return (
<Button {...props} ref={ref} className={classnames(className, styles['checkbox-container'], { 'checked': checked })}>
{
checked ?
<svg className={styles['icon']} viewBox={'0 0 100 100'}>
<Icon x={'10'} y={'10'} width={'80'} height={'80'} icon={'ic_check'} />
</svg>
:
<Icon className={styles['icon']} icon={'ic_box_empty'} />
}
{children}
</Button>
);
});
Checkbox.displayName = 'Checkbox';
Checkbox.propTypes = {
className: PropTypes.string,
checked: PropTypes.bool,
children: PropTypes.node
};
module.exports = Checkbox;

View file

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

View file

@ -0,0 +1,19 @@
// Copyright (C) 2017-2020 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.checkbox-container {
&:global(.checked) {
.icon {
fill: @color-surface-light5;
background-color: @color-primaryvariant1;
}
}
.icon {
display: block;
width: 1rem;
height: 1rem;
fill: @color-surface-light5;
}
}

View file

@ -0,0 +1,101 @@
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const AColorPicker = require('a-color-picker');
const Button = require('stremio/common/Button');
const ModalDialog = require('stremio/common/ModalDialog');
const useBinaryState = require('stremio/common/useBinaryState');
const ColorPicker = require('./ColorPicker');
const styles = require('./styles');
const parseColor = (value) => {
const color = AColorPicker.parseColor(value, 'hexcss4');
return typeof color === 'string' ? color : '#ffffffff';
};
const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
const [modalOpen, openModal, closeModal] = useBinaryState(false);
const [tempValue, setTempValue] = React.useState(() => {
return parseColor(value);
});
const labelButtonStyle = React.useMemo(() => ({
backgroundColor: value
}), [value]);
const isTransparent = React.useMemo(() => {
return parseColor(value).endsWith('00');
}, [value]);
const labelButtonOnClick = React.useCallback((event) => {
if (typeof props.onClick === 'function') {
props.onClick(event);
}
if (!event.nativeEvent.openModalPrevented) {
openModal();
}
}, [props.onClick]);
const modalDialogOnClick = React.useCallback((event) => {
event.nativeEvent.openModalPrevented = true;
}, []);
const modalButtons = React.useMemo(() => {
const selectButtonOnClick = (event) => {
if (typeof onChange === 'function') {
onChange({
type: 'change',
value: tempValue,
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
closeModal();
};
return [
{
label: 'Select',
props: {
'data-autofocus': true,
onClick: selectButtonOnClick
}
}
];
}, [tempValue, dataset, onChange]);
const colorPickerOnInput = React.useCallback((event) => {
setTempValue(parseColor(event.value));
}, []);
React.useLayoutEffect(() => {
setTempValue(parseColor(value));
}, [value, modalOpen]);
return (
<Button title={isTransparent ? 'Transparent' : value} {...props} style={labelButtonStyle} className={classnames(className, styles['color-input-container'])} onClick={labelButtonOnClick}>
{
isTransparent ?
<div className={styles['transparent-label-container']}>
<div className={styles['transparent-label']}>Transparent</div>
</div>
:
null
}
{
modalOpen ?
<ModalDialog title={'Choose a color:'} buttons={modalButtons} onCloseRequest={closeModal} onClick={modalDialogOnClick}>
<ColorPicker className={styles['color-picker-container']} value={tempValue} onInput={colorPickerOnInput} />
</ModalDialog>
:
null
}
</Button>
);
};
ColorInput.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
dataset: PropTypes.object,
onChange: PropTypes.func,
onClick: PropTypes.func
};
module.exports = ColorInput;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
@ -21,7 +21,7 @@ const ColorPicker = ({ className, value, onInput }) => {
showRGB: false,
showAlpha: true
});
const pickerClipboard = pickerElementRef.current.querySelector('.a-color-picker-clipboard');
const pickerClipboard = pickerElementRef.current.querySelector('.a-color-picker-clipbaord');
if (pickerClipboard instanceof HTMLElement) {
pickerClipboard.tabIndex = -1;
}
@ -29,7 +29,10 @@ const ColorPicker = ({ className, value, onInput }) => {
React.useLayoutEffect(() => {
if (typeof onInput === 'function') {
pickerRef.current.on('change', (picker, value) => {
onInput(parseColor(value));
onInput({
type: 'input',
value: parseColor(value)
});
});
}
return () => {

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
const ColorPicker = require('./ColorPicker');

View file

@ -1,8 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
.color-picker-container {
overflow: visible;
text-align: center;
* {
overflow: visible;
@ -16,7 +15,7 @@
box-shadow: 0 0 .2rem var(--color-surfacedark);
}
:global(.a-color-picker-clipboard) {
:global(.a-color-picker-clipbaord) {
pointer-events: none;
}
}

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2020 Smart code 203358507
const ColorInput = require('./ColorInput');
module.exports = ColorInput;

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
// Copyright (C) 2017-2020 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -17,6 +17,7 @@
align-items: center;
justify-content: center;
padding: 0 0.5rem;
border: thin solid @color-surface-light5-20;
pointer-events: none;
.transparent-label {

View file

@ -1,79 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useServices } = require('stremio/services');
const CoreSuspenderContext = React.createContext(null);
CoreSuspenderContext.displayName = 'CoreSuspenderContext';
function wrapPromise(promise) {
let status = 'pending';
let result;
const suspender = promise.then(
(resp) => {
status = 'success';
result = resp;
},
(error) => {
status = 'error';
result = error;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
}
};
}
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();
const parentSuspender = useCoreSuspender();
const [render, setRender] = React.useState(parentSuspender === null);
const statesRef = React.useRef({});
const streamsRef = React.useRef({});
const getState = React.useCallback((model) => {
if (!statesRef.current[model]) {
statesRef.current[model] = wrapPromise(core.transport.getState(model));
}
return statesRef.current[model].read();
}, []);
const decodeStream = React.useCallback((stream) => {
if (!streamsRef.current[stream]) {
streamsRef.current[stream] = wrapPromise(core.transport.decodeStream(stream));
}
return streamsRef.current[stream].read();
}, []);
const suspender = React.useMemo(() => ({ getState, decodeStream }), []);
React.useLayoutEffect(() => {
if (!render) {
setRender(true);
}
}, []);
return render ?
<React.Suspense fallback={<Fallback {...props} />}>
<CoreSuspenderContext.Provider value={suspender}>
<Component {...props} />
</CoreSuspenderContext.Provider>
</React.Suspense>
:
null;
};
};
module.exports = { withCoreSuspender, useCoreSuspender };

View file

@ -1,91 +0,0 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { isFileType } from './utils';
export type FileType = string;
export type FileDropListener = (filename: string, buffer: ArrayBuffer) => void;
type FileDropContext = {
on: (type: FileType, listener: FileDropListener) => void,
off: (type: FileType, listener: FileDropListener) => void,
};
const FileDropContext = createContext({} as FileDropContext);
type Props = {
className: string,
children: JSX.Element,
};
const FileDropProvider = ({ className, children }: Props) => {
const [listeners, setListeners] = useState<[FileType, FileDropListener][]>([]);
const [active, setActive] = useState(false);
const onDragOver = (event: DragEvent) => {
event.preventDefault();
setActive(true);
};
const onDragLeave = () => {
setActive(false);
};
const onDrop = useCallback((event: DragEvent) => {
event.preventDefault();
const { dataTransfer } = event;
if (dataTransfer && dataTransfer?.files.length > 0) {
const file = dataTransfer.files[0];
file
.arrayBuffer()
.then((buffer) => {
listeners
.filter(([type]) => file.type ? type === file.type : isFileType(buffer, type))
.forEach(([, listener]) => listener(file.name, buffer));
});
}
setActive(false);
}, [listeners]);
const on = (type: FileType, listener: FileDropListener) => {
setListeners((listeners) => {
return [...listeners, [type, listener]];
});
};
const off = (type: FileType, listener: FileDropListener) => {
setListeners((listeners) => {
return listeners.filter(([key, value]) => key !== type && value !== listener);
});
};
useEffect(() => {
window.addEventListener('dragover', onDragOver);
window.addEventListener('dragleave', onDragLeave);
window.addEventListener('drop', onDrop);
return () => {
window.removeEventListener('dragover', onDragOver);
window.removeEventListener('dragleave', onDragLeave);
window.removeEventListener('drop', onDrop);
};
}, [onDrop]);
return (
<FileDropContext.Provider value={{ on, off }}>
{ children }
<div className={classNames(className, { 'active': active })} />
</FileDropContext.Provider>
);
};
const useFileDrop = () => {
return useContext(FileDropContext);
};
export {
FileDropProvider,
useFileDrop,
};

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