Compare commits
No commits in common. "development" and "v5.0.0-alpha25" have entirely different histories.
developmen
...
v5.0.0-alp
99
.eslintrc
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
|
||||
42
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -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
|
||||
8
.github/dependabot.yml
vendored
|
|
@ -1,8 +0,0 @@
|
|||
version: 2
|
||||
# Check for outdated actions
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
# Check for updates every Monday
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
66
.github/workflows/auto_assign.yml
vendored
|
|
@ -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
|
||||
});
|
||||
}
|
||||
42
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
53
.github/workflows/pages_cleanup.yml
vendored
|
|
@ -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
|
||||
21
.github/workflows/release.yml
vendored
|
|
@ -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
|
|
@ -3,4 +3,3 @@
|
|||
/yarn.lock
|
||||
/npm-debug.log
|
||||
.DS_Store
|
||||
.prettierignore
|
||||
1
.nvmrc
|
|
@ -1 +0,0 @@
|
|||
20
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 `:)`
|
||||
65
Dockerfile
|
|
@ -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"]
|
||||
|
|
|
|||
27
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# Stremio - Freedom to Stream
|
||||
|
||||
[](https://github.com/Stremio/stremio-web/actions/workflows/build.yml)
|
||||
[](https://stremio.github.io/stremio-web/development)
|
||||

|
||||
[](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
|
||||
|
||||

|
||||

|
||||
|
||||
### Discover
|
||||
|
||||

|
||||

|
||||
|
||||
### Meta Details
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 652 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
|
@ -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
|
After Width: | Height: | Size: 31 KiB |
BIN
favicons/icon-96.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
fonts/Roboto-Bold.ttf
Normal file
BIN
fonts/Roboto-BoldItalic.ttf
Normal file
BIN
fonts/Roboto-Light.ttf
Normal file
BIN
fonts/Roboto-LightItalic.ttf
Normal file
BIN
fonts/Roboto-Medium.ttf
Normal file
BIN
fonts/Roboto-MediumItalic.ttf
Normal file
BIN
fonts/Roboto-Regular.ttf
Normal file
BIN
fonts/Roboto-RegularItalic.ttf
Normal file
BIN
images/anonymous.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
images/default_avatar.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
images/empty.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
images/icon_x192.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/icon_x512.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/intro_background.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
images/maskable_icon_x192.png
Normal file
|
After Width: | Height: | Size: 5 KiB |
BIN
images/maskable_icon_x512.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
images/stremio_symbol.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -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
112
package.json
Normal file → Executable 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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10076
pnpm-lock.yaml
BIN
screenshots/board.png
Normal file
|
After Width: | Height: | Size: 976 KiB |
BIN
screenshots/discover.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/metadetails.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -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']} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import ShortcutsModal from './ShortcutsModal';
|
||||
export default ShortcutsModal;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import UpdaterBanner from './UpdaterBanner';
|
||||
export default UpdaterBanner;
|
||||
|
|
@ -23,10 +23,6 @@ const routerViewsConfig = [
|
|||
...routesRegexp.library,
|
||||
component: routes.Library
|
||||
},
|
||||
{
|
||||
...routesRegexp.calendar,
|
||||
component: routes.Calendar
|
||||
},
|
||||
{
|
||||
...routesRegexp.continuewatching,
|
||||
component: routes.Library
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
47
src/common/AddonDetailsModal/styles.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/common/Button/Button.js
Normal 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;
|
||||
5
src/common/Button/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Button = require('./Button');
|
||||
|
||||
module.exports = Button;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
34
src/common/Checkbox/Checkbox.js
Normal 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;
|
||||
5
src/common/Checkbox/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const Checkbox = require('./Checkbox');
|
||||
|
||||
module.exports = Checkbox;
|
||||
19
src/common/Checkbox/styles.less
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 () => {
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
6
src/common/ColorInput/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const ColorInput = require('./ColorInput');
|
||||
|
||||
module.exports = ColorInput;
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ const DelayedRenderer = ({ children, delay }) => {
|
|||
};
|
||||
|
||||
DelayedRenderer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
delay: PropTypes.number,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
module.exports = DelayedRenderer;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { FileDropProvider, useFileDrop } from './FileDrop';
|
||||
import onFileDrop from './onFileDrop';
|
||||
|
||||
export {
|
||||
FileDropProvider,
|
||||
useFileDrop,
|
||||
onFileDrop,
|
||||
};
|
||||