Compare commits

..

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

568 changed files with 33077 additions and 27647 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": 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
}
]
}
}

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

@ -3,52 +3,26 @@ name: Build
on:
push:
branches:
- development
tags-ignore:
- "**"
pull_request:
branches:
- development
# Allow manual dispatch in GH
workflow_dispatch:
permissions:
contents: write
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@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"
uses: actions/checkout@v2
- name: Install NPM dependencies
run: pnpm install
run: npm ci
- name: Build
run: pnpm build
run: npm run build
- name: Test
run: pnpm test
run: npm 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 }}
run: npm run lint
- name: Deploy to GitHub Pages
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
uses: peaceiris/actions-gh-pages@v4
uses: peaceiris/actions-gh-pages@v3
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 }}
destination_dir: ${{ github.ref_name }}
allow_empty_commit: true

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

@ -9,25 +9,26 @@ jobs:
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
uses: actions/checkout@v2
- name: Install NPM dependencies
run: npm install
- name: Build
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: pnpm 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@2.11.4
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

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 +1,24 @@
# 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"]
# Stremio Node 14.x
FROM stremio/node-base:fermium
# Meta
LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
# Update GitHub remote host key
RUN echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> ~/.ssh/known_hosts
# Create app directory
RUN mkdir -p /var/www/stremio-web
# Install app dependencies
WORKDIR /var/www/stremio-web
COPY . /var/www/stremio-web
RUN npm install
# Bundle app source
WORKDIR /var/www/stremio-web
RUN npm run build
EXPOSE 8080
CMD ["node", "http_server.js"]

View file

@ -1,7 +1,7 @@
# Stremio - Freedom to Stream
[![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)
[![Github Page](https://img.shields.io/website?down_message=offline&label=Page&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B&up_message=online&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/)
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.
@ -10,47 +10,40 @@ 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
* 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-2022 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/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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.

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/icon_x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/icon_x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/intro_background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/stremio_symbol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1,59 +1,40 @@
{
"name": "Stremio Web",
"short_name": "Stremio",
"name": "Stremio Web",
"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": "favicons/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "images/maskable_icon_512x512.png",
"sizes": "512x512",
"src": "images/icon_x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "images/icon_x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "images/maskable_icon_x192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable"
},
{
"src": "images/maskable_icon_196x196.png",
"sizes": "196x196",
"src": "images/maskable_icon_x512.png",
"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"
"purpose": "maskable"
}
],
"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"
}
]
"start_url": "https://web.stremio.com",
"scope": "https://web.stremio.com",
"display": "standalone",
"orientation": "natural",
"theme_color": "#2a2843",
"background_color": "#161523"
}

26078
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

112
package.json Normal file → Executable file
View file

@ -1,85 +1,73 @@
{
"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": "5.0.1",
"@stremio/stremio-core-web": "0.44.21",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"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",
"filter-invalid-dom-props": "2.1.0",
"hat": "0.0.3",
"i18next": "^22.4.3",
"langs": "^2.0.0",
"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"
"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",
"spatial-navigation-polyfill": "https://github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "https://github.com/Stremio/stremio-translations.git#92675658de92113c5888cf5e57003e468e8b8c9c",
"url": "0.11.0",
"use-long-press": "^3.1.5"
},
"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.7.4",
"workbox-webpack-plugin": "^6.5.3"
}
}

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

@ -6,12 +6,9 @@ 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 { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const { ToastProvider, CONSTANTS, withCoreSuspender } = 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 ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
@ -21,7 +18,6 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
const App = () => {
const { i18n } = useTranslation();
const shell = useShell();
const onPathNotMatch = React.useCallback(() => {
return NotFound;
}, []);
@ -39,14 +35,6 @@ const App = () => {
};
}, []);
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 +70,7 @@ const App = () => {
receiverApplicationId: CONSTANTS.CHROMECAST_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
resumeSavedSession: false,
language: null,
androidReceiverCompatible: true
language: null
});
}
};
@ -107,32 +94,6 @@ const App = () => {
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) {
@ -140,11 +101,6 @@ const App = () => {
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
i18n.changeLanguage(args.settings.interfaceLanguage);
}
if (args?.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
break;
}
}
@ -153,10 +109,6 @@ const App = () => {
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({
@ -168,8 +120,7 @@ const App = () => {
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullUserFromAPI',
args: {}
action: 'PullUserFromAPI'
}
});
services.core.transport.dispatch({
@ -192,15 +143,13 @@ const App = () => {
services.core.transport
.getState('ctx')
.then(onCtxState)
.catch(console.error);
.catch((e) => console.error(e));
}
return () => {
if (services.core.active) {
window.removeEventListener('focus', onWindowFocus);
services.core.transport.off('CoreEvent', onCoreEvent);
}
window.removeEventListener('focus', onWindowFocus);
services.core.transport.off('CoreEvent', onCoreEvent);
};
}, [initialized, shell.windowClosed]);
}, [initialized]);
return (
<React.StrictMode>
<ServicesProvider services={services}>
@ -209,28 +158,15 @@ 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']}>
<ServicesToaster />
<DeepLinkHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ToastProvider>
:
<div className={styles['loader-container']} />
}

View file

@ -1,15 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { 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

@ -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,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

@ -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,26 @@
// Copyright (C) 2017-2024 Smart code 203358507
// Copyright (C) 2017-2023 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 +28,7 @@
padding: 0;
box-sizing: border-box;
font-size: 1rem;
line-height: 1.2em;
font-family: inherit;
border: none;
outline: none;
@ -123,7 +41,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 +50,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 +66,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 +85,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 +100,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 +108,7 @@ html {
.loader-container, .error-container {
width: 100%;
height: 100%;
background-color: @color-background-dark2;
}
}
}
@ -270,22 +140,7 @@ html {
@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;
min-width: inherit;
min-height: inherit;
}
}

View file

@ -1,17 +1,15 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
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

@ -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,9 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const ModalDialog = require('stremio/components/ModalDialog');
const ModalDialog = require('stremio/common/ModalDialog');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const { usePlatform } = require('stremio/common/Platform');
const { useServices } = require('stremio/services');
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
const useAddonDetails = require('./useAddonDetails');
@ -30,7 +28,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 +41,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') {
@ -69,10 +64,10 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurable ?
{
className: styles['configure-button'],
label: t('ADDON_CONFIGURE'),
label: 'Configure',
props: {
onClick: (event) => {
platform.openExternal(transportUrl.replace('manifest.json', 'configure'));
window.open(transportUrl.replace('manifest.json', 'configure'));
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'configure',
@ -88,7 +83,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
const toggleButton = addonDetails.localAddon !== null ?
{
className: styles['uninstall-button'],
label: t('ADDON_UNINSTALL'),
label: 'Uninstall',
props: {
onClick: (event) => {
core.transport.dispatch({
@ -109,13 +104,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 +130,24 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
:
null;
return configureButton && toggleButton ? [cancelButton, configureButton, toggleButton] : configureButton ? [cancelButton, configureButton] : toggleButton ? [cancelButton, toggleButton] : [cancelButton];
return toggleButton !== null ? 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;
}, [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,18 +166,17 @@ AddonDetailsModal.propTypes = {
onCloseRequest: PropTypes.func
};
const AddonDetailsModalFallback = ({ onCloseRequest }) => {
const { t } = useTranslation();
return <ModalDialog
const AddonDetailsModalFallback = ({ onCloseRequest }) => (
<ModalDialog
className={styles['addon-details-modal-container']}
title={t('STREMIO_COMMUNITY_ADDON')}
title={'Stremio addon'}
onCloseRequest={onCloseRequest}
>
<div className={styles['addon-details-message-container']}>
{t('ADDON_LOADING_MANIFEST')}
Loading addon manifest
</div>
</ModalDialog>;
};
</ModalDialog>
);
AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes;

View file

@ -0,0 +1,47 @@
// Copyright (C) 2017-2023 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

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

@ -0,0 +1,5 @@
// Copyright (C) 2017-2023 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-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@ -14,6 +14,6 @@
&:global(.disabled) {
pointer-events: none;
opacity: 0.5;
opacity: 0.25;
}
}

View file

@ -1,11 +1,10 @@
// 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];
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 NEXT_VIDEO_POPUP_DURATIONS = [0, 5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000];
const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None';
@ -28,99 +27,21 @@ const TYPE_PRIORITIES = {
other: -Infinity
};
const ICON_FOR_TYPE = new Map([
['movie', 'movies'],
['series', 'series'],
['channel', 'channels'],
['tv', 'tv'],
['movie', 'ic_movies'],
['series', 'ic_series'],
['channel', 'ic_channels'],
['tv', 'ic_tv'],
['book', 'ic_book'],
['game', 'ic_games'],
['music', 'ic_music'],
['adult', 'ic_adult'],
['radio', 'ic_radio'],
['podcast', 'ic_podcast'],
['other', 'movies'],
['other', 'ic_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,
@ -134,10 +55,5 @@ module.exports = {
SHARE_LINK_CATEGORY,
WRITERS_LINK_CATEGORY,
TYPE_PRIORITIES,
ICON_FOR_TYPE,
MIME_SIGNATURES,
SUPPORTED_LOCAL_SUBTITLES,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
PROTOCOL,
ICON_FOR_TYPE
};

View file

@ -0,0 +1,34 @@
// Copyright (C) 2017-2023 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-2023 Smart code 203358507
const Checkbox = require('./Checkbox');
module.exports = Checkbox;

View file

@ -0,0 +1,19 @@
// Copyright (C) 2017-2023 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

@ -1,85 +1,75 @@
// Copyright (C) 2017-2023 Smart code 203358507
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import classnames from 'classnames';
import * as AColorPicker from 'a-color-picker';
import { useTranslation } from 'react-i18next';
import { Button } from 'stremio/components';
import ModalDialog from 'stremio/components/ModalDialog';
import useBinaryState from 'stremio/common/useBinaryState';
import ColorPicker from './ColorPicker';
import styles from './ColorInput.less';
const React = require('react');
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 useBinaryState = require('stremio/common/useBinaryState');
const ColorPicker = require('./ColorPicker');
const styles = require('./styles');
const parseColor = (value: string) => {
const parseColor = (value) => {
const color = AColorPicker.parseColor(value, 'hexcss4');
return typeof color === 'string' ? color : '#ffffffff';
};
type Props = {
className: string,
value: string,
onChange?: (value: string) => void,
onClick?: (event: React.MouseEvent) => void,
};
const ColorInput = ({ className, value, onChange, ...props }: Props) => {
const ColorInput = ({ className, value, dataset, onChange, ...props }) => {
const { t } = useTranslation();
const [modalOpen, openModal, closeModal] = useBinaryState(false);
const [tempValue, setTempValue] = useState(() => {
const [tempValue, setTempValue] = React.useState(() => {
return parseColor(value);
});
const labelButtonStyle = useMemo(() => ({
const labelButtonStyle = React.useMemo(() => ({
backgroundColor: value
}), [value]);
const isTransparent = useMemo(() => {
const isTransparent = React.useMemo(() => {
return parseColor(value).endsWith('00');
}, [value]);
const labelButtonOnClick = useCallback((event: React.MouseEvent) => {
const labelButtonOnClick = React.useCallback((event) => {
if (typeof props.onClick === 'function') {
props.onClick(event);
}
// @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'.
if (!event.nativeEvent.openModalPrevented) {
openModal();
}
}, [props.onClick]);
const modalDialogOnClick = useCallback((event: React.MouseEvent) => {
// @ts-expect-error: Property 'openModalPrevented' does not exist on type 'MouseEvent'.
const modalDialogOnClick = React.useCallback((event) => {
event.nativeEvent.openModalPrevented = true;
}, []);
const modalButtons = useMemo(() => {
const selectButtonOnClick = () => {
const modalButtons = React.useMemo(() => {
const selectButtonOnClick = (event) => {
if (typeof onChange === 'function') {
onChange(tempValue);
onChange({
type: 'change',
value: tempValue,
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
closeModal();
};
return [
{
label: t('SELECT'),
label: 'Select',
props: {
'data-autofocus': true,
onClick: selectButtonOnClick
}
}
];
}, [tempValue, onChange]);
const colorPickerOnInput = useCallback((color: string) => {
setTempValue(parseColor(color));
}, [tempValue, dataset, onChange]);
const colorPickerOnInput = React.useCallback((event) => {
setTempValue(parseColor(event.value));
}, []);
useLayoutEffect(() => {
React.useLayoutEffect(() => {
setTempValue(parseColor(value));
}, [value, modalOpen]);
return (
<Button title={isTransparent ? t('BUTTON_COLOR_TRANSPARENT') : value} {...props} style={labelButtonStyle} className={classnames(className, styles['color-input-container'])} onClick={labelButtonOnClick}>
{
@ -92,7 +82,7 @@ const ColorInput = ({ className, value, onChange, ...props }: Props) => {
}
{
modalOpen ?
<ModalDialog title={t('CHOOSE_COLOR')} buttons={modalButtons} onCloseRequest={closeModal} onClick={modalDialogOnClick}>
<ModalDialog title={'Choose a color:'} buttons={modalButtons} onCloseRequest={closeModal} onClick={modalDialogOnClick}>
<ColorPicker className={styles['color-picker-container']} value={tempValue} onInput={colorPickerOnInput} />
</ModalDialog>
:
@ -102,4 +92,12 @@ const ColorInput = ({ className, value, onChange, ...props }: Props) => {
);
};
export default ColorInput;
ColorInput.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
dataset: PropTypes.object,
onChange: PropTypes.func,
onClick: PropTypes.func
};
module.exports = ColorInput;

View file

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

@ -16,7 +16,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-2023 Smart code 203358507
const ColorInput = require('./ColorInput');
module.exports = ColorInput;

View file

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

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

View file

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

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,
};

View file

@ -1,8 +0,0 @@
import { FileDropProvider, useFileDrop } from './FileDrop';
import onFileDrop from './onFileDrop';
export {
FileDropProvider,
useFileDrop,
onFileDrop,
};

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