Merge branch 'development' into pr/420

This commit is contained in:
kKaskak 2024-01-27 19:58:56 +02:00
commit 1a861968b1
199 changed files with 7010 additions and 16588 deletions

View file

@ -3,7 +3,7 @@ name: Build
on: on:
push: push:
branches: branches:
- '*' - '**'
jobs: jobs:
build: build:

1
.gitignore vendored
View file

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

View file

@ -1,24 +1,31 @@
# Stremio Node 14.x # Stremio Node 14.x
FROM stremio/node-base:fermium # the node version for running Stremio Web
ARG NODE_VERSION=20-alpine
# Meta FROM node:$NODE_VERSION AS base
LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
# Meta
# Update GitHub remote host key LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
RUN echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> ~/.ssh/known_hosts
RUN mkdir -p /var/www/stremio-web
# Create app directory WORKDIR /var/www/stremio-web
RUN mkdir -p /var/www/stremio-web
# Install app dependencies
# Install app dependencies FROM base AS prebuild
WORKDIR /var/www/stremio-web
COPY . /var/www/stremio-web RUN apk update && apk upgrade && \
RUN npm install apk add --no-cache git
WORKDIR /var/www/stremio-web
# Bundle app source COPY . .
WORKDIR /var/www/stremio-web RUN npm install
RUN npm run build
RUN npm run build
# Bundle app source
EXPOSE 8080 FROM base AS final
CMD ["node", "http_server.js"]
WORKDIR /var/www/stremio-web
COPY . .
COPY --from=prebuild /var/www/stremio-web/node_modules ./node_modules
COPY --from=prebuild /var/www/stremio-web/build ./build
EXPOSE 8080
CMD ["node", "http_server.js"]

View file

@ -1,7 +1,7 @@
# Stremio - Freedom to Stream # Stremio - Freedom to Stream
![Build](https://github.com/stremio/stremio-web/workflows/Build/badge.svg?branch=development) ![Build](https://github.com/stremio/stremio-web/workflows/Build/badge.svg?branch=development)
[![Github Page](https://img.shields.io/website?down_message=offline&label=Page&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B&up_message=online&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/) [![Github Page](https://img.shields.io/website?down_message=offline&label=Page&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OC44NTQgMEMyMS44MzkgMCAwIDIyIDAgNDkuMjE3YzAgMjEuNzU2IDEzLjk5MyA0MC4xNzIgMzMuNDA1IDQ2LjY5IDIuNDI3LjQ5IDMuMzE2LTEuMDU5IDMuMzE2LTIuMzYyIDAtMS4xNDEtLjA4LTUuMDUyLS4wOC05LjEyNy0xMy41OSAyLjkzNC0xNi40Mi01Ljg2Ny0xNi40Mi01Ljg2Ny0yLjE4NC01LjcwNC01LjQyLTcuMTctNS40Mi03LjE3LTQuNDQ4LTMuMDE1LjMyNC0zLjAxNS4zMjQtMy4wMTUgNC45MzQuMzI2IDcuNTIzIDUuMDUyIDcuNTIzIDUuMDUyIDQuMzY3IDcuNDk2IDExLjQwNCA1LjM3OCAxNC4yMzUgNC4wNzQuNDA0LTMuMTc4IDEuNjk5LTUuMzc4IDMuMDc0LTYuNi0xMC44MzktMS4xNDEtMjIuMjQzLTUuMzc4LTIyLjI0My0yNC4yODMgMC01LjM3OCAxLjk0LTkuNzc4IDUuMDE0LTEzLjItLjQ4NS0xLjIyMi0yLjE4NC02LjI3NS40ODYtMTMuMDM4IDAgMCA0LjEyNS0xLjMwNCAxMy40MjYgNS4wNTJhNDYuOTcgNDYuOTcgMCAwIDEgMTIuMjE0LTEuNjNjNC4xMjUgMCA4LjMzLjU3MSAxMi4yMTMgMS42MyA5LjMwMi02LjM1NiAxMy40MjctNS4wNTIgMTMuNDI3LTUuMDUyIDIuNjcgNi43NjMuOTcgMTEuODE2LjQ4NSAxMy4wMzggMy4xNTUgMy40MjIgNS4wMTUgNy44MjIgNS4wMTUgMTMuMiAwIDE4LjkwNS0xMS40MDQgMjMuMDYtMjIuMzI0IDI0LjI4MyAxLjc4IDEuNTQ4IDMuMzE2IDQuNDgxIDMuMzE2IDkuMTI2IDAgNi42LS4wOCAxMS44OTctLjA4IDEzLjUyNiAwIDEuMzA0Ljg5IDIuODUzIDMuMzE2IDIuMzY0IDE5LjQxMi02LjUyIDMzLjQwNS0yNC45MzUgMzMuNDA1LTQ2LjY5MUM5Ny43MDcgMjIgNzUuNzg4IDAgNDguODU0IDB6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B&up_message=online&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/development)
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. 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.
@ -46,4 +46,4 @@ npm run build
## License ## License
Stremio is copyright 2017-2022 Smart code and available under GPLv2 license. See the [LICENSE](/LICENSE.md) file in the project for more information. Stremio is copyright 2017-2023 Smart code and available under GPLv2 license. See the [LICENSE](/LICENSE.md) file in the project for more information.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
fonts/PlusJakartaSans.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

15
images/background_1.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 1.5 KiB

13
images/background_2.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 96 KiB

BIN
images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/maskable_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,40 +0,0 @@
{
"short_name": "Stremio",
"name": "Stremio Web",
"description": "Freedom To Stream",
"icons": [
{
"src": "favicons/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"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_x512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": "https://web.stremio.com",
"scope": "https://web.stremio.com",
"display": "standalone",
"orientation": "natural",
"theme_color": "#2a2843",
"background_color": "#161523"
}

16108
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "stremio", "name": "stremio",
"displayName": "Stremio", "displayName": "Stremio",
"version": "5.0.0", "version": "5.0.0-beta.4",
"author": "Smart Code OOD", "author": "Smart Code OOD",
"private": true, "private": true,
"license": "gpl-2.0", "license": "gpl-2.0",
@ -15,9 +15,9 @@
"@babel/runtime": "7.16.0", "@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3", "@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1", "@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.20", "@stremio/stremio-core-web": "0.46.0",
"@stremio/stremio-icons": "4.0.0", "@stremio/stremio-icons": "5.2.0",
"@stremio/stremio-video": "0.0.24", "@stremio/stremio-video": "0.0.30",
"a-color-picker": "1.2.1", "a-color-picker": "1.2.1",
"bowser": "2.11.0", "bowser": "2.11.0",
"buffer": "6.0.3", "buffer": "6.0.3",
@ -38,8 +38,8 @@
"react-focus-lock": "2.9.1", "react-focus-lock": "2.9.1",
"react-i18next": "^12.1.1", "react-i18next": "^12.1.1",
"react-is": "18.2.0", "react-is": "18.2.0",
"spatial-navigation-polyfill": "https://github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "https://github.com/Stremio/stremio-translations.git#92675658de92113c5888cf5e57003e468e8b8c9c", "stremio-translations": "github:Stremio/stremio-translations#12b1307f95249496960d2a257b371db5700721e6",
"url": "0.11.0", "url": "0.11.0",
"use-long-press": "^3.1.5" "use-long-press": "^3.1.5"
}, },
@ -49,6 +49,7 @@
"@babel/plugin-proposal-object-rest-spread": "7.16.0", "@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0", "@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0", "@babel/preset-react": "7.16.0",
"@types/react": "^18.2.9",
"babel-loader": "8.2.3", "babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1", "copy-webpack-plugin": "9.0.1",
@ -67,7 +68,8 @@
"terser-webpack-plugin": "5.2.4", "terser-webpack-plugin": "5.2.4",
"webpack": "5.61.0", "webpack": "5.61.0",
"webpack-cli": "4.9.1", "webpack-cli": "4.9.1",
"webpack-dev-server": "4.7.4", "webpack-dev-server": "^4.7.4",
"webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^6.5.3" "workbox-webpack-plugin": "^6.5.3"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
screenshots/board_wide.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 947 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router'); const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes'); const { NotFound } = require('stremio/routes');
const { ToastProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster'); const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler'); const DeepLinkHandler = require('./DeepLinkHandler');
const DefaultSettingsHandler = require('./DefaultSettingsHandler'); const DefaultSettingsHandler = require('./DefaultSettingsHandler');
@ -130,6 +130,12 @@ const App = () => {
action: 'SyncLibraryWithAPI' action: 'SyncLibraryWithAPI'
} }
}); });
services.core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullNotifications'
}
});
}; };
if (services.core.active) { if (services.core.active) {
onWindowFocus(); onWindowFocus();
@ -141,8 +147,10 @@ const App = () => {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
return () => { return () => {
window.removeEventListener('focus', onWindowFocus); if (services.core.active) {
services.core.transport.off('CoreEvent', onCoreEvent); window.removeEventListener('focus', onWindowFocus);
services.core.transport.off('CoreEvent', onCoreEvent);
}
}; };
}, [initialized]); }, [initialized]);
return ( return (
@ -154,14 +162,15 @@ const App = () => {
<ErrorDialog className={styles['error-container']} /> <ErrorDialog className={styles['error-container']} />
: :
<ToastProvider className={styles['toasts-container']}> <ToastProvider className={styles['toasts-container']}>
<ServicesToaster /> <TooltipProvider className={styles['tooltip-container']}>
<DeepLinkHandler /> <ServicesToaster />
<DefaultSettingsHandler /> <DeepLinkHandler />
<RouterWithProtectedRoutes <RouterWithProtectedRoutes
className={styles['router']} className={styles['router']}
viewsConfig={routerViewsConfig} viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch} onPathNotMatch={onPathNotMatch}
/> />
</TooltipProvider>
</ToastProvider> </ToastProvider>
: :
<div className={styles['loader-container']} /> <div className={styles['loader-container']} />

View file

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

View file

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

View file

@ -1,9 +1,13 @@
// Copyright (C) 2017-2023 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/common/screen-sizes.less';
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@font-face {
font-family: 'PlusJakartaSans';
src: url('/fonts/PlusJakartaSans.ttf') format('truetype');
}
:global { :global {
@import (once, less) '~stremio/common/animations.less'; @import (once, less) '~stremio/common/animations.less';
@import (once, less) '~stremio-router/styles.css'; @import (once, less) '~stremio-router/styles.css';
@ -13,14 +17,28 @@
--landscape-shape-ratio: 0.5625; --landscape-shape-ratio: 0.5625;
--poster-shape-ratio: 1.464; --poster-shape-ratio: 1.464;
--scroll-bar-size: 6px; --scroll-bar-size: 6px;
--horizontal-nav-bar-size: 4rem; --horizontal-nav-bar-size: 5.5rem;
--vertical-nav-bar-size: 5.2rem; --vertical-nav-bar-size: 6rem;
--focus-outline-size: 2px; --focus-outline-size: 2px;
--color-facebook: #4267b2; --color-facebook: #1877F1;
--color-twitter: #1DA1F2; --color-x: #000000;
--color-reddit: #FF4500;
--color-imdb: #f5c518;
--color-trakt: #ED2224;
--color-placeholder: #60606080; --color-placeholder: #60606080;
--color-placeholder-text: @color-surface-50; --color-placeholder-text: @color-surface-50;
--color-placeholder-background: @color-surface-dark5-20; --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);
--overlay-color: rgba(255, 255, 255, 0.05);
--modal-background-color: rgba(15, 13, 32, 1);
--outer-glow: 0px 0px 30px rgba(123, 91, 245, 0.37);
--border-radius: 0.75rem;
} }
* { * {
@ -28,7 +46,6 @@
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-size: 1rem; font-size: 1rem;
line-height: 1.2em;
font-family: inherit; font-family: inherit;
border: none; border: none;
outline: none; outline: none;
@ -41,7 +58,7 @@
overflow: hidden; overflow: hidden;
word-break: break-word; word-break: break-word;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: @color-secondaryvariant2-light1 @color-background-dark2; scrollbar-color: var(--overlay-color) transparent;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
@ -50,15 +67,16 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: @color-secondaryvariant2-light1; border-radius: var(--scroll-bar-size);
background-color: var(--overlay-color);
&:hover { &:hover {
background-color: @color-secondaryvariant2-light2; background-color: var(--primary-accent-color);
} }
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: @color-background-dark2; background-color: transparent;
} }
svg { svg {
@ -70,12 +88,13 @@ html {
height: 100%; height: 100%;
min-width: 640px; min-width: 640px;
min-height: 480px; min-height: 480px;
font-family: 'Roboto', 'sans-serif'; font-family: 'PlusJakartaSans', 'sans-serif';
overflow: auto; overflow: auto;
body { body {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%);
:global(#app) { :global(#app) {
position: relative; position: relative;
@ -100,6 +119,24 @@ 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;
&:global(.active) {
transition-delay: 0.25s;
}
}
.router { .router {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -108,7 +145,6 @@ html {
.loader-container, .error-container { .loader-container, .error-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: @color-background-dark2;
} }
} }
} }

View file

@ -3,13 +3,13 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const styles = require('./styles'); const styles = require('./styles');
const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => { const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => {
const renderLogoFallback = React.useCallback(() => ( const renderLogoFallback = React.useCallback(() => (
<Icon className={styles['icon']} icon={'ic_addons'} /> <Icon className={styles['icon']} name={'addons'} />
), []); ), []);
return ( return (
<div className={classnames(className, styles['addon-details-container'])}> <div className={classnames(className, styles['addon-details-container'])}>

View file

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

View file

@ -10,38 +10,37 @@
.addon-details-container, .addon-details-message-container { .addon-details-container, .addon-details-message-container {
width: 40rem; width: 40rem;
max-width: 100%; max-width: 100%;
} color: var(--primary-foreground-color);
.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 { .cancel-button {
background-color: transparent; background-color: transparent;
opacity: 0.3;
&:hover { &:hover {
background-color: @color-surface-light3; outline: var(--focus-outline-size) solid var(--primary-foreground-color);
opacity: 1;
} }
.label { &:focus {
color: @color-surface-dark2; outline-color: var(--primary-foreground-color);
}
.cancel-button-label {
color: var(--primary-foreground-color);
} }
} }
.uninstall-button { .uninstall-button {
background-color: @color-accent2; background-color: var(--overlay-color);
&:hover { &:hover {
background-color: @color-accent2-light2; outline: var(--focus-outline-size) solid var(--overlay-color);
background-color: transparent;
}
&:focus {
outline-color: var(--primary-foreground-color);
} }
} }
} }

View file

@ -14,6 +14,6 @@
&:global(.disabled) { &:global(.disabled) {
pointer-events: none; pointer-events: none;
opacity: 0.25; opacity: 0.5;
} }
} }

View file

@ -2,9 +2,9 @@
const CHROMECAST_RECEIVER_APP_ID = '1634F54B'; const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250]; const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace']; const SUBTITLES_FONTS = ['PlusJakartaSans', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000]; 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]; 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 CATALOG_PREVIEW_SIZE = 10; const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100; const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None'; const NONE_EXTRA_VALUE = 'None';
@ -27,19 +27,62 @@ const TYPE_PRIORITIES = {
other: -Infinity other: -Infinity
}; };
const ICON_FOR_TYPE = new Map([ const ICON_FOR_TYPE = new Map([
['movie', 'ic_movies'], ['movie', 'movies'],
['series', 'ic_series'], ['series', 'series'],
['channel', 'ic_channels'], ['channel', 'channels'],
['tv', 'ic_tv'], ['tv', 'tv'],
['book', 'ic_book'], ['book', 'ic_book'],
['game', 'ic_games'], ['game', 'ic_games'],
['music', 'ic_music'], ['music', 'ic_music'],
['adult', 'ic_adult'], ['adult', 'ic_adult'],
['radio', 'ic_radio'], ['radio', 'ic_radio'],
['podcast', 'ic_podcast'], ['podcast', 'ic_podcast'],
['other', 'ic_movies'], ['other', 'movies'],
]); ]);
const EXTERNAL_PLAYERS = [
{
label: 'EXTERNAL_PLAYER_DISABLED',
value: null,
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
},
{
label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING',
value: 'choose',
platforms: ['android'],
},
{
label: 'VLC',
value: 'vlc',
platforms: ['ios', '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'],
},
];
module.exports = { module.exports = {
CHROMECAST_RECEIVER_APP_ID, CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES, SUBTITLES_SIZES,
@ -55,5 +98,6 @@ module.exports = {
SHARE_LINK_CATEGORY, SHARE_LINK_CATEGORY,
WRITERS_LINK_CATEGORY, WRITERS_LINK_CATEGORY,
TYPE_PRIORITIES, TYPE_PRIORITIES,
ICON_FOR_TYPE ICON_FOR_TYPE,
EXTERNAL_PLAYERS,
}; };

View file

@ -3,21 +3,13 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const styles = require('./styles'); const styles = require('./styles');
const Checkbox = React.forwardRef(({ className, checked, children, ...props }, ref) => { const Checkbox = React.forwardRef(({ className, checked, children, ...props }, ref) => {
return ( return (
<Button {...props} ref={ref} className={classnames(className, styles['checkbox-container'], { 'checked': checked })}> <Button {...props} ref={ref} className={classnames(className, styles['checkbox-container'], { 'checked': checked })}>
{ <div className={styles['toggle']} />
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} {children}
</Button> </Button>
); );

View file

@ -2,18 +2,43 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@height: 1.7rem;
@width: 3.2rem;
@thumb-margin: 0.5rem;
@thumb-size: calc(@height - @thumb-margin);
.checkbox-container { .checkbox-container {
&:global(.checked) { position: relative;
.icon {
fill: @color-surface-light5; .toggle {
background-color: @color-primaryvariant1; position: relative;
width: @width;
height: @height;
border-radius: @height;
background-color: var(--overlay-color);
transition: background-color 0.1s ease-in-out;
&::before {
content: "";
position: absolute;
height: @thumb-size;
width: @thumb-size;
top: calc(@thumb-margin / 2);
left: calc(@thumb-margin / 2);
border-radius: 50%;
background-color: var(--primary-foreground-color);
transition: transform 0.1s ease-in-out;
} }
} }
.icon { &:global(.checked) {
display: block; .toggle {
width: 1rem; background-color: var(--secondary-accent-color);
height: 1rem;
fill: @color-surface-light5; &::before {
transform: translateX(calc(@width - @thumb-size - @thumb-margin));
}
}
} }
} }

View file

@ -0,0 +1,72 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const { useServices } = require('stremio/services');
const useNotifications = require('stremio/common/useNotifications');
const LibItem = require('stremio/common/LibItem');
const ContinueWatchingItem = ({ _id, deepLinks, ...props }) => {
const { core } = useServices();
const notifications = useNotifications();
const newVideos = React.useMemo(() => {
const count = notifications.items?.[_id]?.length ?? 0;
return Math.min(Math.max(count, 0), 99);
}, [_id, notifications.items]);
const onClick = React.useCallback(() => {
if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
window.location = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams;
}
}, [deepLinks]);
const onPlayClick = React.useCallback((event) => {
event.stopPropagation();
if (deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos) {
window.location = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos;
}
}, [deepLinks]);
const onDismissClick = React.useCallback((event) => {
event.stopPropagation();
if (typeof _id === 'string') {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RewindLibraryItem',
args: _id
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DismissNotificationItem',
args: _id
}
});
}
}, [_id]);
return (
<LibItem
{...props}
posterChangeCursor={true}
newVideos={newVideos}
onClick={onClick}
onPlayClick={onPlayClick}
onDismissClick={onDismissClick}
/>
);
};
ContinueWatchingItem.propTypes = {
_id: PropTypes.string,
deepLinks: PropTypes.shape({
metaDetailsVideos: PropTypes.string,
metaDetailsStreams: PropTypes.string,
player: PropTypes.string
}),
};
module.exports = ContinueWatchingItem;

View file

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

View file

@ -0,0 +1,90 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const Button = require('stremio/common/Button');
const ModalDialog = require('stremio/common/ModalDialog');
const useEvents = require('./useEvents');
const styles = require('./styles');
const { default: Icon } = require('@stremio/stremio-icons/react');
const EventModal = () => {
const { t } = useTranslation();
const { events, pullEvents, dismissEvent } = useEvents();
const modal = React.useMemo(() => {
return events?.modal?.type === 'Ready' ?
events.modal.content
:
null;
}, [events]);
const onClose = React.useCallback(() => {
modal?.id && dismissEvent(modal.id);
}, [modal]);
React.useEffect(() => {
pullEvents();
}, []);
return (
modal !== null ?
<ModalDialog className={styles['event-modal']} onCloseRequest={onClose}>
{
modal.imageUrl ?
<img className={styles['image']} src={modal.imageUrl} />
:
null
}
<div className={styles['info-container']}>
<div className={styles['title-container']}>
{
modal.title ?
<div className={styles['title']}>{modal.title}</div>
:
null
}
{
modal.message ?
<div className={styles['label']}>{modal.message}</div>
:
null
}
</div>
{
modal?.addon?.name ?
<div className={styles['addon-container']}>
<Icon className={styles['icon']} name={'addons'} />
<div className={styles['name']}>
{ modal.addon.name }
</div>
</div>
:
null
}
{
modal?.addon?.manifestUrl ?
<Button className={styles['action-button']} href={`#/addons?addon=${encodeURIComponent(modal.addon.manifestUrl)}`} onClick={onClose}>
<div className={styles['button-label']}>
{ t('INSTALL_ADDON') }
</div>
</Button>
:
modal.externalUrl ?
<Button className={styles['action-button']} href={modal.externalUrl} target={'_blank'}>
<div className={styles['button-label']}>
{ t('LEARN_MORE') }
</div>
</Button>
:
null
}
</div>
</ModalDialog>
:
null
);
};
module.exports = EventModal;

View file

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

View file

@ -0,0 +1,119 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/ModalDialog/styles.less') {
modal-dialog-content: modal-dialog-content;
modal-dialog-container: modal-dialog-container;
}
.event-modal {
backdrop-filter: blur(10px);
.modal-dialog-container {
overflow: visible;
max-width: 45rem;
.modal-dialog-content {
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
.image {
width: 100%;
height: 100%;
margin-top: -10rem;
}
.info-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2.5rem;
padding: 1rem 4rem;
margin-top: -7rem;
.title-container {
display: flex;
flex-direction: column;
gap: 1rem;
.title {
color: var(--primary-foreground-color);
font-size: 1.325rem;
text-align: center;
padding: 0 6rem;
}
.label {
color: var(--primary-foreground-color);
font-size: 1rem;
text-align: center;
opacity: 0.5;
}
}
.addon-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 0.5rem;
.icon {
height: 2rem;
width: 2rem;
color: var(--primary-accent-color);
}
.name {
color: var(--primary-foreground-color);
}
}
.action-button {
background-color: var(--primary-foreground-color);
border: 2px solid var(--primary-foreground-color);
padding: 0.8rem 2rem;
border-radius: 2rem;
.button-label {
color: var(--primary-accent-color);
font-size: 1rem;
font-weight: 700;
}
&:hover {
background-color: transparent;
}
}
}
}
}
@media only screen and (max-width: @minimum) {
.modal-dialog-container {
.modal-dialog-content {
.image {
height: 125%;
width: 125%;
}
.info-container {
.title-container {
.title {
padding: 0rem;
font-size: 1rem;
}
.label {
font-size: 0.875rem;
}
}
}
}
}
}
}

View file

@ -0,0 +1,36 @@
// Copyright (C) 2017-2023 Smart code 203358507
const useModelState = require('stremio/common/useModelState');
const { useServices } = require('stremio/services');
const map = (ctx) => ({
...ctx.events,
});
const useEvents = () => {
const { core } = useServices();
const pullEvents = () => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'GetEvents',
},
});
};
const dismissEvent = (id) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DismissEvent',
args: id,
},
});
};
const events = useModelState({ model: 'ctx', map });
return { events, pullEvents, dismissEvent };
};
module.exports = useEvents;

View file

@ -68,6 +68,13 @@ const LibItem = ({ _id, removable, ...props }) => {
args: _id args: _id
} }
}); });
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DismissNotificationItem',
args: _id
}
});
} }
break; break;

View file

@ -7,11 +7,11 @@ const { VerticalNavBar, HorizontalNavBar } = require('stremio/common/NavBar');
const styles = require('./styles'); const styles = require('./styles');
const TABS = [ const TABS = [
{ id: 'board', label: 'Board', icon: 'ic_board', href: '#/' }, { id: 'board', label: 'Board', icon: 'home', href: '#/' },
{ id: 'discover', label: 'Discover', icon: 'ic_discover', href: '#/discover' }, { id: 'discover', label: 'Discover', icon: 'discover', href: '#/discover' },
{ id: 'library', label: 'Library', icon: 'ic_library', href: '#/library' }, { id: 'library', label: 'Library', icon: 'library', href: '#/library' },
{ id: 'settings', label: 'SETTINGS', icon: 'ic_settings', href: '#/settings' }, { id: 'addons', label: 'ADDONS', icon: 'addons', href: '#/addons' },
{ id: 'addons', label: 'ADDONS', icon: 'ic_addons', href: '#/addons' } { id: 'settings', label: 'SETTINGS', icon: 'settings', href: '#/settings' },
]; ];
const MainNavBars = React.memo(({ className, route, query, children }) => { const MainNavBars = React.memo(({ className, route, query, children }) => {

View file

@ -3,17 +3,18 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const filterInvalidDOMProps = require('filter-invalid-dom-props').default; const filterInvalidDOMProps = require('filter-invalid-dom-props').default;
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect'); const Multiselect = require('stremio/common/Multiselect');
const PlayIconCircleCentered = require('stremio/common/PlayIconCircleCentered');
const useBinaryState = require('stremio/common/useBinaryState'); const useBinaryState = require('stremio/common/useBinaryState');
const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS'); const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS');
const styles = require('./styles'); const styles = require('./styles');
const MetaItem = React.memo(({ className, type, name, poster, posterShape, playIcon, progress, options, deepLinks, dataset, optionOnSelect, ...props }) => { const MetaItem = React.memo(({ className, type, name, poster, posterShape, posterChangeCursor, progress, newVideos, options, deepLinks, dataset, optionOnSelect, onDismissClick, onPlayClick, watched, ...props }) => {
const { t } = useTranslation();
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false); const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const href = React.useMemo(() => { const href = React.useMemo(() => {
return deepLinks ? return deepLinks ?
@ -56,15 +57,32 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
const renderPosterFallback = React.useCallback(() => ( const renderPosterFallback = React.useCallback(() => (
<Icon <Icon
className={styles['placeholder-icon']} className={styles['placeholder-icon']}
icon={ICON_FOR_TYPE.has(type) ? ICON_FOR_TYPE.get(type) : ICON_FOR_TYPE.get('other')} name={ICON_FOR_TYPE.has(type) ? ICON_FOR_TYPE.get(type) : ICON_FOR_TYPE.get('other')}
/> />
), [type]); ), [type]);
const renderMenuLabelContent = React.useCallback(() => ( const renderMenuLabelContent = React.useCallback(() => (
<Icon className={styles['icon']} icon={'ic_more'} /> <Icon className={styles['icon']} name={'more-vertical'} />
), []); ), []);
return ( return (
<Button title={name} href={href} {...filterInvalidDOMProps(props)} className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} onClick={metaItemOnClick}> <Button title={name} href={href} {...filterInvalidDOMProps(props)} className={classnames(className, styles['meta-item-container'], styles['poster-shape-poster'], styles[`poster-shape-${posterShape}`], { 'active': menuOpen })} onClick={metaItemOnClick}>
<div className={styles['poster-container']}> <div className={classnames(styles['poster-container'], { 'poster-change-cursor': posterChangeCursor })}>
{
onDismissClick ?
<div title={t('LIBRARY_RESUME_DISMISS')} className={styles['dismiss-icon-layer']} onClick={onDismissClick}>
<Icon className={styles['dismiss-icon']} name={'close'} />
<div className={styles['dismiss-icon-backdrop']} />
</div>
:
null
}
{
watched ?
<div className={styles['watched-icon-layer']}>
<Icon className={styles['watched-icon']} name={'checkmark'} />
</div>
:
null
}
<div className={styles['poster-image-layer']}> <div className={styles['poster-image-layer']}>
<Image <Image
className={styles['poster-image']} className={styles['poster-image']}
@ -74,9 +92,11 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
/> />
</div> </div>
{ {
playIcon ? onPlayClick ?
<div className={styles['play-icon-layer']}> <div title={t('CONTINUE_WATCHING')} className={styles['play-icon-layer']} onClick={onPlayClick}>
<PlayIconCircleCentered className={styles['play-icon']} /> <Icon className={styles['play-icon']} name={'play'} />
<div className={styles['play-icon-outer']} />
<div className={styles['play-icon-background']} />
</div> </div>
: :
null null
@ -85,6 +105,22 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
progress > 0 ? progress > 0 ?
<div className={styles['progress-bar-layer']}> <div className={styles['progress-bar-layer']}>
<div className={styles['progress-bar']} style={{ width: `${Math.max(0, Math.min(1, progress)) * 100}%` }} /> <div className={styles['progress-bar']} style={{ width: `${Math.max(0, Math.min(1, progress)) * 100}%` }} />
<div className={styles['progress-bar-background']} />
</div>
:
null
}
{
newVideos > 0 ?
<div className={styles['new-videos']}>
<div className={styles['layer']} />
<div className={styles['layer']} />
<div className={styles['layer']}>
<Icon className={styles['icon']} name={'add'} />
<div className={styles['label']}>
{newVideos}
</div>
</div>
</div> </div>
: :
null null
@ -127,8 +163,9 @@ MetaItem.propTypes = {
name: PropTypes.string, name: PropTypes.string,
poster: PropTypes.string, poster: PropTypes.string,
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']), posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']),
playIcon: PropTypes.bool, posterChangeCursor: PropTypes.bool,
progress: PropTypes.number, progress: PropTypes.number,
newVideos: PropTypes.number,
options: PropTypes.array, options: PropTypes.array,
deepLinks: PropTypes.shape({ deepLinks: PropTypes.shape({
metaDetailsVideos: PropTypes.string, metaDetailsVideos: PropTypes.string,
@ -137,7 +174,10 @@ MetaItem.propTypes = {
}), }),
dataset: PropTypes.object, dataset: PropTypes.object,
optionOnSelect: PropTypes.func, optionOnSelect: PropTypes.func,
onClick: PropTypes.func onDismissClick: PropTypes.func,
onPlayClick: PropTypes.func,
onClick: PropTypes.func,
watched: PropTypes.bool
}; };
module.exports = MetaItem; module.exports = MetaItem;

View file

@ -18,14 +18,44 @@
play-icon-circle-centered-icon: icon; play-icon-circle-centered-icon: icon;
} }
@play-icon-size: 4rem;
.meta-item-container { .meta-item-container {
padding: 1rem; padding: 1rem;
overflow: visible; overflow: visible;
&:hover, &:focus, &:global(.active), &:global(.selected) { &:hover, &:focus, &:global(.active), &:global(.selected) {
outline-style: none; outline-style: none;
background-color: @color-background-light3;
transition: background-color 100ms ease-out; transition: background-color 100ms ease-out;
.poster-container {
box-shadow: 0 0 0 0.2rem var(--primary-foreground-color);
.dismiss-icon-layer {
opacity: 1;
}
.poster-image-layer {
transform: scale(1.05);
}
.play-icon-layer {
.play-icon-outer {
color: transparent;
}
.play-icon-background {
background-color: var(--secondary-accent-color);
opacity: 1;
}
}
}
.title-bar-container {
.menu-label-container {
opacity: 1;
}
}
} }
&.poster-shape-poster { &.poster-shape-poster {
@ -49,7 +79,71 @@
.poster-container { .poster-container {
position: relative; position: relative;
z-index: 0; z-index: 0;
background-color: @color-background; background-color: var(--overlay-color);
border-radius: var(--border-radius);
&:global(.poster-change-cursor) {
.poster-image-layer {
&:hover {
cursor: zoom-in;
}
}
}
.dismiss-icon-layer {
z-index: -2;
position: absolute;
top: 0.5rem;
left: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
height: 1.5rem;
width: 1.5rem;
border-radius: 100%;
opacity: 0;
transition: opacity 0.1s ease-in;
.dismiss-icon {
z-index: 1;
position: relative;
height: 1.25rem;
width: 1.25rem;
color: var(--primary-foreground-color);
opacity: 0.8;
}
.dismiss-icon-backdrop {
z-index: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--primary-background-color);
opacity: 0.6;
}
}
.watched-icon-layer {
position: absolute;
top: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
width: 1.5rem;
height: 1.5rem;
background-color: var(--primary-accent-color);
border-radius: 50%;
margin: 0.5rem;
.watched-icon {
width: 0.75rem;
height: 0.75rem;
color: var(--primary-foreground-color);
}
}
.poster-image-layer { .poster-image-layer {
position: absolute; position: absolute;
@ -62,6 +156,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transform 0.1s ease-out;
.poster-image { .poster-image {
flex: none; flex: none;
@ -76,46 +171,138 @@
flex: none; flex: none;
width: 80%; width: 80%;
height: 50%; height: 50%;
fill: @color-background-light3-90; color: var(--primary-foreground-color);
opacity: 0.2;
} }
} }
.play-icon-layer { .play-icon-layer {
position: absolute;
top: 30%;
right: 0;
bottom: 30%;
left: 0;
z-index: -2; z-index: -2;
overflow: visible; position: absolute;
top: 50%;
left: 50%;
margin-top: calc(@play-icon-size / -2);
margin-left: calc(@play-icon-size / -2);
display: flex;
align-items: center;
justify-content: center;
height: @play-icon-size;
width: @play-icon-size;
transition: transform 0.1s ease-out;
.play-icon { .play-icon {
display: block; z-index: 2;
width: 100%; position: relative;
height: 100%; height: 2.25rem;
filter: drop-shadow(0 0 0.5rem @color-background); width: 2.25rem;
color: var(--primary-foreground-color);
}
.play-icon-circle-centered-background { .play-icon-outer {
fill: @color-accent4-90; z-index: 1;
} position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
color: var(--primary-foreground-color);
box-shadow: 0 0 0 0.15rem currentColor inset;
border-radius: 100%;
transition: color 0.1s ease-in;
}
.play-icon-circle-centered-icon { .play-icon-background {
fill: @color-surface-light5-90; z-index: 0;
} position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--primary-background-color);
border-radius: 100%;
opacity: 0.4;
transition: all 0.1s ease-in;
}
&:hover {
transform: scale(1.1);
} }
} }
.progress-bar-layer { .progress-bar-layer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: -1; z-index: -1;
background-color: @color-background-light2; position: absolute;
bottom: 1rem;
left: 1rem;
right: 1rem;
height: 0.45rem;
border-radius: 0.45rem;
overflow: hidden;
.progress-bar { .progress-bar {
height: 0.4rem; position: relative;
background-color: @color-primaryvariant1; height: 100%;
background-color: var(--primary-foreground-color);
}
.progress-bar-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--primary-foreground-color);
opacity: 0.3;
}
}
.new-videos {
z-index: -1;
position: absolute;
top: 0;
right: 0;
overflow: visible;
.layer {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
height: 1.25rem;
width: 2.25rem;
border-radius: 0.25rem;
&:nth-child(1) {
top: 0.5rem;
right: 0.5rem;
background-color: var(--primary-foreground-color);
opacity: 0.4;
}
&:nth-child(2) {
top: 0.75rem;
right: 0.75rem;
background-color: var(--primary-foreground-color);
opacity: 0.6;
}
&:nth-child(3) {
top: 1rem;
right: 1rem;
background-color: var(--primary-foreground-color);
}
.icon {
height: 0.8rem;
width: 0.8rem;
color: var(--primary-accent-color);
}
.label {
font-size: 0.8rem;
font-weight: 600;
color: var(--primary-accent-color);
}
} }
} }
} }
@ -124,52 +311,60 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: 2.8rem; height: 4rem;
overflow: visible; overflow: visible;
.title-label { .title-label {
flex: 1; flex: 1;
max-height: 2.4em; max-height: 2.4em;
padding-left: 0.5rem; padding-left: 1.5rem;
color: @color-surface-light5-90; font-weight: 600;
text-align: center;
color: var(--primary-foreground-color);
&:only-child { &:only-child {
padding-right: 0.5rem; padding: 0 0.5rem;
} }
} }
.menu-label-container { .menu-label-container {
z-index: 1;
flex: none; flex: none;
width: 1.5rem; width: 1.5rem;
height: 2.8rem; height: 4rem;
padding: 1rem 0; padding: 1rem 0;
background-color: transparent; background-color: transparent;
opacity: 0;
transform: translateX(1rem);
transition: opacity 0.1s ease-out;
.icon { .icon {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: @color-surface-light1-90; color: var(--primary-foreground-color);
opacity: 0.6;
} }
.popup-menu-container { .popup-menu-container {
width: auto; width: auto;
.multiselect-menu-container { .multiselect-menu-container {
min-width: 6rem; min-width: 8rem;
max-width: 12rem; max-width: 14rem;
.multiselect-option-container { .multiselect-option-container {
padding: 0.5rem; padding: 1rem 1.5rem;
background-color: @color-surface-light5;
&:hover, &:focus { &:hover, &:focus {
outline: none; outline: none;
background-color: @color-surface-light2; background-color: var(--overlay-color);
} }
.multiselect-option-label { .multiselect-option-label {
color: @color-background-dark5-90; font-weight: 500;
color: var(--primary-foreground-color);
opacity: 0.8;
} }
} }
} }

View file

@ -3,23 +3,30 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const styles = require('./styles'); const styles = require('./styles');
const { Tooltip } = require('stremio/common/Tooltips');
const ActionButton = ({ className, icon, label, ...props }) => { const ActionButton = ({ className, icon, label, tooltip, ...props }) => {
return ( return (
<Button title={label} {...props} className={classnames(className, styles['action-button-container'])}> <Button title={tooltip ? '' : label} {...props} className={classnames(className, styles['action-button-container'], { 'wide': typeof label === 'string' && !tooltip })}>
{
tooltip === true ?
<Tooltip label={label} position={'top'} />
:
null
}
{ {
typeof icon === 'string' && icon.length > 0 ? typeof icon === 'string' && icon.length > 0 ?
<div className={styles['icon-container']}> <div className={styles['icon-container']}>
<Icon className={styles['icon']} icon={icon} /> <Icon className={styles['icon']} name={icon} />
</div> </div>
: :
null null
} }
{ {
typeof label === 'string' && label.length > 0 ? !tooltip && typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}> <div className={styles['label-container']}>
<div className={styles['label']}>{label}</div> <div className={styles['label']}>{label}</div>
</div> </div>
@ -33,7 +40,8 @@ const ActionButton = ({ className, icon, label, ...props }) => {
ActionButton.propTypes = { ActionButton.propTypes = {
className: PropTypes.string, className: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
label: PropTypes.string label: PropTypes.string,
tooltip: PropTypes.bool
}; };
module.exports = ActionButton; module.exports = ActionButton;

View file

@ -5,33 +5,34 @@
.action-button-container { .action-button-container {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: center; justify-content: center;
background-color: @color-surface-light5-20; align-items: center;
gap: 1rem;
border-radius: 100%;
background-color: var(--overlay-color);
backdrop-filter: blur(5px);
transition: background-color 0.1s ease-out;
&:hover, &:focus { &:hover, &:focus {
background-color: @color-accent3; outline: var(--focus-outline-size) solid var(--primary-foreground-color);
background-color: transparent;
} }
.icon-container { .icon-container {
flex: 0 0 50%; flex: none;
align-self: stretch;
padding-top: 15%;
&:only-child {
padding: 5% 0;
}
.icon { .icon {
display: block; display: block;
width: 100%; height: 1.75rem;
height: 100%; width: 1.75rem;
fill: @color-surface-light5-90; color: var(--primary-foreground-color);
opacity: 0.9;
} }
} }
.label-container { .label-container {
flex: 0 0 50%; flex: none;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -39,11 +40,13 @@
.label { .label {
flex: 1; flex: 1;
font-size: 1rem;
font-weight: 500; font-weight: 500;
max-height: 2.4em; max-height: 2.4em;
padding: 0 0.2rem; padding: 0 0.2rem;
text-align: center; text-align: center;
color: @color-surface-light5-90; color: var(--primary-foreground-color);
opacity: 0.9;
} }
} }
} }
@ -54,21 +57,8 @@
padding: 0 1rem; padding: 0 1rem;
.icon-container { .icon-container {
flex: none;
align-self: center;
height: 2rem; height: 2rem;
width: 2rem; width: 2rem;
padding-top: 0;
margin-right: 0.5rem;
&:only-child {
padding: 0;
margin-right: 0;
}
}
.label-container {
flex: 1;
} }
} }
} }

View file

@ -4,10 +4,12 @@
.meta-links-container { .meta-links-container {
.label-container { .label-container {
margin-bottom: 0.2rem; margin-bottom: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
font-weight: 500; font-size: 0.95rem;
color: @color-surface-dark3-90; font-weight: 700;
color: var(--primary-foreground-color);
opacity: 0.3;
} }
.links-container { .links-container {
@ -19,15 +21,18 @@
flex-grow: 0; flex-grow: 0;
flex-shrink: 1; flex-shrink: 1;
flex-basis: auto; flex-basis: auto;
margin-right: 0.5rem; margin-right: 0.75rem;
margin-bottom: 0.2rem; margin-bottom: 0.75rem;
padding: 0.3rem 0.5rem; padding: 0.4rem 1.25rem;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 2rem; border-radius: 2rem;
border: var(--focus-outline-size) solid transparent; border: var(--focus-outline-size) solid transparent;
color: @color-surface-light2-90; font-size: 1rem;
background-color: @color-surface-light5-20; font-weight: 500;
color: var(--primary-foreground-color);
background-color: var(--overlay-color);
backdrop-filter: blur(5px);
&:hover, &:focus { &:hover, &:focus {
background-color: @color-surface-light5-30; background-color: @color-surface-light5-30;

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const UrlUtils = require('url'); const UrlUtils = require('url');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const ModalDialog = require('stremio/common/ModalDialog'); const ModalDialog = require('stremio/common/ModalDialog');
@ -95,8 +95,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
return trailerStreams[0].deepLinks.player; return trailerStreams[0].deepLinks.player;
}, [trailerStreams]); }, [trailerStreams]);
const renderLogoFallback = React.useCallback(() => ( const renderLogoFallback = React.useCallback(() => (
<div className={styles['logo-placeholder']}>{!compact ? name : null}</div> <div className={styles['logo-placeholder']}>{name}</div>
), [compact, name]); ), [name]);
return ( return (
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })}> <div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })}>
{ {
@ -147,8 +147,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
target={'_blank'} target={'_blank'}
{...(compact ? { tabIndex: -1 } : null)} {...(compact ? { tabIndex: -1 } : null)}
> >
<Icon className={styles['icon']} icon={'ic_imdbnoframe'} />
<div className={styles['label']}>{linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}</div> <div className={styles['label']}>{linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}</div>
<Icon className={styles['icon']} name={'imdb'} />
</Button> </Button>
: :
null null
@ -157,17 +157,11 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
: :
null null
} }
{
compact && typeof name === 'string' && name.length > 0 ?
<div className={styles['name-container']}>
{name}
</div>
:
null
}
{ {
compact && typeof description === 'string' && description.length > 0 ? compact && typeof description === 'string' && description.length > 0 ?
<div className={styles['description-container']}>{description}</div> <div className={styles['description-container']}>
{description}
</div>
: :
null null
} }
@ -187,14 +181,26 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
/> />
)) ))
} }
{
!compact && typeof description === 'string' && description.length > 0 ?
<div className={styles['description-container']}>
<div className={styles['label-container']}>
{t('SUMMARY')}
</div>
{description}
</div>
:
null
}
</div> </div>
<div className={styles['action-buttons-container']}> <div className={styles['action-buttons-container']}>
{ {
typeof toggleInLibrary === 'function' ? typeof toggleInLibrary === 'function' ?
<ActionButton <ActionButton
className={styles['action-button']} className={styles['action-button']}
icon={inLibrary ? 'ic_removelib' : 'ic_addlib'} icon={inLibrary ? 'remove-from-library' : 'add-to-library'}
label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')} label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')}
tooltip={compact}
tabIndex={compact ? -1 : 0} tabIndex={compact ? -1 : 0}
onClick={toggleInLibrary} onClick={toggleInLibrary}
/> />
@ -205,10 +211,11 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
typeof trailerHref === 'string' ? typeof trailerHref === 'string' ?
<ActionButton <ActionButton
className={styles['action-button']} className={styles['action-button']}
icon={'ic_movies'} icon={'trailer'}
label={t('TRAILER')} label={t('TRAILER')}
tabIndex={compact ? -1 : 0} tabIndex={compact ? -1 : 0}
href={trailerHref} href={trailerHref}
tooltip={compact}
/> />
: :
null null
@ -216,8 +223,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
{ {
typeof showHref === 'string' && compact ? typeof showHref === 'string' && compact ?
<ActionButton <ActionButton
className={styles['action-button']} className={classnames(styles['action-button'], styles['show-button'])}
icon={'ic_play'} icon={'play'}
label={t('SHOW')} label={t('SHOW')}
tabIndex={compact ? -1 : 0} tabIndex={compact ? -1 : 0}
href={showHref} href={showHref}
@ -230,8 +237,9 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<React.Fragment> <React.Fragment>
<ActionButton <ActionButton
className={styles['action-button']} className={styles['action-button']}
icon={'ic_share'} icon={'share'}
label={t('CTX_SHARE')} label={t('CTX_SHARE')}
tooltip={true}
tabIndex={compact ? -1 : 0} tabIndex={compact ? -1 : 0}
onClick={openShareModal} onClick={openShareModal}
/> />

View file

@ -14,9 +14,9 @@ const MetaPreviewPlaceholder = ({ className }) => {
<div className={styles['duration-container']} /> <div className={styles['duration-container']} />
<div className={styles['release-info-container']} /> <div className={styles['release-info-container']} />
</div> </div>
<div className={styles['description-container']}> <div className={styles['genres-container']}>
<div className={styles['description-label-container']} /> <div className={styles['genres-header-container']} />
<div className={styles['description-label-container']} /> <div className={styles['genre-label-container']} />
</div> </div>
<div className={styles['genres-container']}> <div className={styles['genres-container']}>
<div className={styles['genres-header-container']} /> <div className={styles['genres-header-container']} />
@ -27,20 +27,7 @@ const MetaPreviewPlaceholder = ({ className }) => {
<div className={styles['genre-label-container']} /> <div className={styles['genre-label-container']} />
</div> </div>
</div> </div>
<div className={styles['action-buttons-container']}> <div className={styles['action-buttons-container']} />
<div className={styles['action-button-container']}>
<div className={styles['action-button-icon']} />
<div className={styles['action-button-label']} />
</div>
<div className={styles['action-button-container']}>
<div className={styles['action-button-icon']} />
<div className={styles['action-button-label']} />
</div>
<div className={styles['action-button-container']}>
<div className={styles['action-button-icon']} />
<div className={styles['action-button-label']} />
</div>
</div>
</div> </div>
); );
}; };

View file

@ -3,7 +3,6 @@
.meta-preview-placeholder-container { .meta-preview-placeholder-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2rem;
.meta-info-container { .meta-info-container {
flex: 1; flex: 1;
@ -13,6 +12,7 @@
width: 20rem; width: 20rem;
height: 8rem; height: 8rem;
max-width: 100%; max-width: 100%;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
@ -26,37 +26,18 @@
flex-basis: 5rem; flex-basis: 5rem;
height: 1.4rem; height: 1.4rem;
margin-right: 1rem; margin-right: 1rem;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
.release-info-container { .release-info-container {
flex-basis: 5rem; flex-basis: 5rem;
height: 1.4rem; height: 1.4rem;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
} }
.description-container {
margin: 1rem 0;
.description-label-container {
background-color: var(--color-placeholder-background);
&:nth-child(1) {
width: 20rem;
height: 1.4rem;
max-width: 80%;
}
&:nth-child(2) {
width: 26rem;
height: 4.6rem;
max-width: 80%;
margin-top: 1rem;
}
}
}
.genres-container { .genres-container {
margin: 1rem 0; margin: 1rem 0;
@ -64,6 +45,7 @@
width: 6.5rem; width: 6.5rem;
height: 1.6rem; height: 1.6rem;
max-width: 100%; max-width: 100%;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
@ -72,6 +54,7 @@
height: 1.2rem; height: 1.2rem;
max-width: 100%; max-width: 100%;
margin-top: 0.2rem; margin-top: 0.2rem;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
} }
@ -81,32 +64,9 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
max-height: 6rem; height: 4rem;
margin-bottom: 1rem;
.action-button-container { border-radius: 4rem;
flex: none; background-color: var(--color-placeholder-background);
width: 6rem;
height: 6rem;
margin-right: 2rem;
background-color: var(--color-placeholder-background);
&:last-child {
margin-right: 0;
}
.action-button-icon {
width: 2rem;
height: 2rem;
margin: 1rem 2rem 0 2rem;
background-color: var(--color-placeholder-background);
}
.action-button-label {
width: 4rem;
height: 1.2rem;
margin: 0.9rem 1rem;
background-color: var(--color-placeholder-background);
}
}
} }
} }

View file

@ -13,12 +13,11 @@
.meta-info-container { .meta-info-container {
.logo, .logo-placeholder { .logo, .logo-placeholder {
width: 100%; width: 100%;
height: 8rem; height: 6rem;
background-color: @color-surface-dark5-10;
} }
.runtime-release-info-container { .runtime-release-info-container {
justify-content: space-evenly; justify-content: space-between;
.runtime-label, .release-info-label { .runtime-label, .release-info-label {
margin: 1rem 0.4rem; margin: 1rem 0.4rem;
@ -31,8 +30,7 @@
} }
.action-buttons-container { .action-buttons-container {
justify-content: space-evenly; justify-content: space-between;
padding: 0;
.action-button:not(:last-child) { .action-button:not(:last-child) {
margin-right: 0; margin-right: 0;
@ -48,32 +46,20 @@
left: -10px; left: -10px;
z-index: -1; z-index: -1;
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
background: @color-background-dark2-60;
content: "";
}
.background-image { .background-image {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
opacity: 0.9; filter: blur(10px);
filter: blur(5px); opacity: 0.3;
} }
} }
.meta-info-container { .meta-info-container {
flex: 1; flex: 1;
align-self: stretch; align-self: stretch;
padding: 0 2rem;
overflow-y: auto; overflow-y: auto;
&:not(:hover) { &:not(:hover) {
@ -87,11 +73,11 @@
.logo, .logo-placeholder { .logo, .logo-placeholder {
display: block; display: block;
max-width: 100%; max-width: 100%;
margin: 2rem 0; margin-bottom: 2rem;
} }
.logo { .logo {
height: 8rem; height: 9rem;
object-fit: contain; object-fit: contain;
object-position: center; object-position: center;
} }
@ -110,10 +96,10 @@
.runtime-label, .release-info-label { .runtime-label, .release-info-label {
flex: 0 1 auto; flex: 0 1 auto;
margin-right: 2rem; margin-right: 3rem;
margin-bottom: 0.5rem; font-size: 1.25rem;
font-size: 1.4rem; font-weight: 600;
color: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
.imdb-button-container { .imdb-button-container {
@ -121,55 +107,47 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 0.5rem;
padding: 0.3rem 1rem;
border-radius: 2.5rem; border-radius: 2.5rem;
border: var(--focus-outline-size) solid transparent;
background-color: @color-surface-light5-20;
&:hover, &:focus {
background-color: @color-surface-light5-30;
}
&:focus { &:focus {
outline: none; outline: none;
border: var(--focus-outline-size) solid @color-surface-light5; border: var(--focus-outline-size) solid @color-surface-light5;
} }
.label {
flex: 0 1 auto;
margin-right: 1rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-foreground-color);
}
.icon { .icon {
flex: none; flex: none;
width: 3rem; width: 3rem;
height: 1.1rem; height: 3rem;
margin-right: 1rem; color: var(--color-imdb);
fill: @color-surface-90;
}
.label {
flex: 0 1 auto;
max-height: 1.2em;
font-size: 1.6rem;
font-weight: 500;
color: @color-surface-light5-90;
} }
} }
} }
.name-container {
margin-top: 1rem;
font-size: 1.5rem;
color: @color-surface-light5-90;
}
.description-container { .description-container {
max-height: 6em;
margin-top: 1rem; margin-top: 1rem;
font-size: 1.1rem; font-size: 1rem;
line-height: 1.5em; font-weight: 400;
color: @color-surface-light5-90; line-height: 2em;
color: var(--primary-foreground-color);
.label-container {
text-transform: uppercase;
font-size: 0.95rem;
font-weight: 700;
color: var(--primary-foreground-color);
opacity: 0.3;
}
} }
.meta-links { .meta-links {
margin-top: 1rem; margin-top: 1.5rem;
} }
} }
@ -178,18 +156,54 @@
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-end;
max-height: 15rem;
flex-wrap: wrap; flex-wrap: wrap;
max-height: 10rem; padding-top: 3.5rem;
padding: 0 2rem; overflow: visible;
.label {
position: absolute;
top: -3rem;
left: 0;
opacity: 0;
transition: opacity 0.3s ease;
text-align: center;
color: var(--primary-foreground-color);
overflow: visible;
}
&:not(:last-child) {
margin-right: 1rem;
}
&:hover {
.label {
opacity: 0.7;
}
}
.action-button { .action-button {
flex: none; flex: none;
width: 6rem; width: 4rem;
height: 6rem; height: 4rem;
margin: 2rem 0; margin-bottom: 1rem;
&:global(.wide) {
width: auto;
padding: 0 2rem;
border-radius: 4rem;
}
&:not(:last-child) { &:not(:last-child) {
margin-right: 2rem; margin-right: 1rem;
}
&.show-button {
&:hover, &:focus {
background-color: var( --secondary-accent-color);
outline: none;
}
} }
} }
} }
@ -202,34 +216,29 @@
@media only screen and (max-width: @minimum) { @media only screen and (max-width: @minimum) {
.meta-preview-container { .meta-preview-container {
.meta-info-container { .meta-info-container {
padding: 0 1.5rem;
.logo { .logo {
margin: 1em 0; margin: 2rem auto;
}
}
.action-buttons-container {
flex-wrap: nowrap;
padding: 0 1.5rem;
overflow-x: visible;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
} }
.action-button { .runtime-release-info-container {
width: auto; justify-content: space-between;
height: 4rem;
max-width: 60%;
margin: 1rem 0;
&:not(:last-child) { .runtime-label, .release-info-label {
margin-right: 1rem; margin: 0;
} }
} }
} }
.action-buttons-container {
flex-shrink: 0;
margin-top: 3rem;
overflow: visible;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
} }
.share-prompt { .share-prompt {

View file

@ -4,39 +4,47 @@ const React = require('react');
const ReactIs = require('react-is'); const ReactIs = require('react-is');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Icon = require('@stremio/stremio-icons/dom');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const CONSTANTS = require('stremio/common/CONSTANTS'); const CONSTANTS = require('stremio/common/CONSTANTS');
const useTranslate = require('stremio/common/useTranslate');
const MetaRowPlaceholder = require('./MetaRowPlaceholder'); const MetaRowPlaceholder = require('./MetaRowPlaceholder');
const styles = require('./styles'); const styles = require('./styles');
const MetaRow = ({ className, title, message, items, itemComponent, deepLinks }) => { const MetaRow = ({ className, title, catalog, message, itemComponent }) => {
const { t } = useTranslation(); const t = useTranslate();
const catalogTitle = React.useMemo(() => {
return title ?? t.catalogTitle(catalog);
}, [title, catalog, t.catalogTitle]);
const items = React.useMemo(() => {
return catalog?.items ?? catalog?.content?.content;
}, [catalog]);
const href = React.useMemo(() => {
return catalog?.deepLinks?.discover ?? catalog?.deepLinks?.library;
}, [catalog]);
return ( return (
<div className={classnames(className, styles['meta-row-container'])}> <div className={classnames(className, styles['meta-row-container'])}>
{ <div className={styles['header-container']}>
(typeof title === 'string' && title.length > 0) || (deepLinks && (typeof deepLinks.discover === 'string' || typeof deepLinks.library === 'string')) ? {
<div className={styles['header-container']}> typeof catalogTitle === 'string' && catalogTitle.length > 0 ?
{ <div className={styles['title-container']} title={catalogTitle}>{catalogTitle}</div>
typeof title === 'string' && title.length > 0 ? :
<div className={styles['title-container']} title={title}>{title}</div> null
: }
null {
} href ?
{ <Button className={styles['see-all-container']} title={t.string('BUTTON_SEE_ALL')} href={href} tabIndex={-1}>
deepLinks && (typeof deepLinks.discover === 'string' || typeof deepLinks.library === 'string') ? <div className={styles['label']}>{ t.string('BUTTON_SEE_ALL') }</div>
<Button className={styles['see-all-container']} title={t('BUTTON_SEE_ALL')} href={deepLinks.discover || deepLinks.library} tabIndex={-1}> <Icon className={styles['icon']} name={'chevron-forward'} />
<div className={styles['label']}>{ t('BUTTON_SEE_ALL') }</div> </Button>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} /> :
</Button> null
: }
null </div>
}
</div>
:
null
}
{ {
typeof message === 'string' && message.length > 0 ? typeof message === 'string' && message.length > 0 ?
<div className={styles['message-container']} title={message}>{message}</div> <div className={styles['message-container']} title={message}>{message}</div>
@ -69,14 +77,33 @@ MetaRow.propTypes = {
className: PropTypes.string, className: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({ catalog: PropTypes.shape({
posterShape: PropTypes.string id: PropTypes.string,
})), name: PropTypes.string,
type: PropTypes.string,
addon: PropTypes.shape({
manifest: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
}),
}),
content: PropTypes.shape({
content: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.shape({
posterShape: PropTypes.string,
})),
]),
}),
items: PropTypes.arrayOf(PropTypes.shape({
posterShape: PropTypes.string,
})),
deepLinks: PropTypes.shape({
discover: PropTypes.string,
library: PropTypes.string,
}),
}),
itemComponent: PropTypes.elementType, itemComponent: PropTypes.elementType,
deepLinks: PropTypes.shape({
discover: PropTypes.string,
library: PropTypes.string
})
}; };
module.exports = MetaRow; module.exports = MetaRow;

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const CONSTANTS = require('stremio/common/CONSTANTS'); const CONSTANTS = require('stremio/common/CONSTANTS');
const styles = require('./styles'); const styles = require('./styles');
@ -21,7 +21,7 @@ const MetaRowPlaceholder = ({ className, title, deepLinks }) => {
deepLinks && typeof deepLinks.discover === 'string' ? deepLinks && typeof deepLinks.discover === 'string' ?
<Button className={styles['see-all-container']} title={t('BUTTON_SEE_ALL')} href={deepLinks.discover} tabIndex={-1}> <Button className={styles['see-all-container']} title={t('BUTTON_SEE_ALL')} href={deepLinks.discover} tabIndex={-1}>
<div className={styles['label']}>{ t('BUTTON_SEE_ALL') }</div> <div className={styles['label']}>{ t('BUTTON_SEE_ALL') }</div>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} /> <Icon className={styles['icon']} name={'chevron-forward'} />
</Button> </Button>
: :
null null

View file

@ -9,12 +9,13 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 0 1rem; padding: 0 1rem;
margin-bottom: 1rem; margin-bottom: 0.5rem;
.title-container { .title-container {
flex: 1; flex: 1;
max-height: 2.4em; max-height: 2.4em;
font-size: 1.8rem; font-size: 1.6rem;
font-weight: 500;
color: var(--color-placeholder-text); color: var(--color-placeholder-text);
&:empty { &:empty {
@ -39,17 +40,16 @@
.label { .label {
flex: 0 1 auto; flex: 0 1 auto;
max-height: 1.2em; max-height: 1.2em;
font-size: 1.3rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
text-transform: uppercase;
color: var(--color-placeholder-text); color: var(--color-placeholder-text);
} }
.icon { .icon {
flex: none; flex: none;
height: 1.3rem; height: 1rem;
margin-left: 0.5rem; margin-left: 0.5rem;
fill: var(--color-placeholder-text); color: var(--color-placeholder-text);
} }
} }
} }
@ -68,6 +68,7 @@
} }
.poster-container { .poster-container {
border-radius: var(--border-radius);
padding-bottom: calc(100% * var(--poster-shape-ratio)); padding-bottom: calc(100% * var(--poster-shape-ratio));
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
@ -76,12 +77,14 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center;
height: 2.8rem; height: 2.8rem;
.title-label { .title-label {
flex: none; flex: none;
width: 60%; width: 60%;
height: 1.2rem; height: 1.2rem;
border-radius: var(--border-radius);
background-color: var(--color-placeholder-background); background-color: var(--color-placeholder-background);
} }
} }

View file

@ -12,13 +12,14 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 0 1rem; padding: 0 1rem;
margin-bottom: 1rem; margin-bottom: 0.25rem;
.title-container { .title-container {
flex: 1; flex: 1;
max-height: 2.4em; max-height: 2.4em;
font-size: 1.8rem; font-size: 1.6rem;
color: @color-secondaryvariant2-light1-90; font-weight: 500;
color: var(--primary-foreground-color);
} }
.see-all-container { .see-all-container {
@ -27,46 +28,40 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
max-width: 12rem; max-width: 12rem;
padding: 0.2rem; height: 2.5rem;
padding: 0 0.5rem 0 1rem;
border-radius: 2.5rem;
opacity: 0.6;
&:focus { &:hover, &:focus {
outline: none; outline: none;
background-color: @color-background-light3; background-color: var(--overlay-color);
} opacity: 1;
&:hover {
.label {
color: @color-secondaryvariant2-light2-90;
}
.icon {
fill: @color-secondaryvariant2-light2-90;
}
} }
.label { .label {
flex: 0 1 auto; flex: 0 1 auto;
max-height: 1.2em; max-height: 1.2em;
font-size: 1.3rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
text-transform: uppercase; color: var(--primary-foreground-color);
color: @color-secondaryvariant2-light1-90;
} }
.icon { .icon {
flex: none; flex: none;
height: 1.3rem; height: 1.5rem;
margin-left: 0.5rem; margin-left: 0.5rem;
fill: @color-secondaryvariant2-light1-90; color: var(--primary-foreground-color);
} }
} }
} }
.message-container { .message-container {
max-height: 3.6em; max-height: 3.6em;
padding: 0 1rem; padding: 0 0.5rem;
font-size: 1.3rem; font-size: 1.3rem;
color: @color-secondaryvariant2-light1-90; color: var(--primary-foreground-color);
opacity: 0.6;
} }
.meta-items-container { .meta-items-container {
@ -76,10 +71,6 @@
overflow: visible; overflow: visible;
.meta-item { .meta-item {
&:not(:first-child) {
margin-left: 0.5rem;
}
&.poster-shape-poster { &.poster-shape-poster {
flex: calc(1 / var(--poster-shape-ratio)); flex: calc(1 / var(--poster-shape-ratio));
} }

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useRouteFocused, useModalsContainer } = require('stremio-router'); const { useRouteFocused, useModalsContainer } = require('stremio-router');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { Modal } = require('stremio-router'); const { Modal } = require('stremio-router');
const styles = require('./styles'); const styles = require('./styles');
@ -60,7 +60,7 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
<Modal ref={modalContainerRef} {...props} className={classnames(className, styles['modal-container'])} onMouseDown={onModalContainerMouseDown}> <Modal ref={modalContainerRef} {...props} className={classnames(className, styles['modal-container'])} onMouseDown={onModalContainerMouseDown}>
<div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}> <div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}>
<Button className={styles['close-button-container']} title={'Close'} onClick={closeButtonOnClick}> <Button className={styles['close-button-container']} title={'Close'} onClick={closeButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_x'} /> <Icon className={styles['icon']} name={'close'} />
</Button> </Button>
{ {
typeof title === 'string' && title.length > 0 ? typeof title === 'string' && title.length > 0 ?
@ -78,7 +78,7 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
<Button title={label} {...props} key={index} className={classnames(className, styles['action-button'])}> <Button title={label} {...props} key={index} className={classnames(className, styles['action-button'])}>
{ {
typeof icon === 'string' && icon.length > 0 ? typeof icon === 'string' && icon.length > 0 ?
<Icon className={styles['icon']} icon={icon} /> <Icon className={styles['icon']} name={icon} />
: :
null null
} }

View file

@ -10,54 +10,61 @@
background-color: @color-background-dark5-40; background-color: @color-background-dark5-40;
.modal-dialog-container { .modal-dialog-container {
position: relative;
flex: none; flex: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 80%; max-width: 80%;
max-height: 80%; max-height: 80%;
background-color: @color-surface-light5; padding: 0 2rem;
border-radius: var(--border-radius);
background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
.close-button-container { .close-button-container {
flex: none; position: absolute;
align-self: flex-end; top: 0.5rem;
width: 2rem; right: 0.5rem;
height: 2rem; width: 3rem;
margin: 0.2rem 0.2rem 0 0; height: 3rem;
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--border-radius);
.icon { .icon {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: @color-surface-dark1-90; color: var(--primary-foreground-color);
opacity: 0.4;
} }
&:hover, &:focus { &:hover, &:focus {
.icon { .icon {
fill: @color-surface-light1-90; opacity: 1;
color: var(--primary-foreground-color);
} }
} }
&:focus { &:focus {
outline-color: @color-background-dark5; outline-color: var(--primary-foreground-color);
} }
} }
.title-container { .title-container {
flex: 1 0 auto; flex: 1 0 auto;
max-height: 2.4em; display: flex;
margin: 0 2rem; align-items: center;
height: 4.5rem;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 500; font-weight: 500;
color: @color-background-dark5-90; color: var(--primary-foreground-color);
} }
.modal-dialog-content { .modal-dialog-content {
flex: 1; flex: 1;
align-self: stretch; align-self: stretch;
margin: 1.5rem 1rem 0;
padding: 0 1rem;
overflow-y: auto; overflow-y: auto;
padding: 2rem 0;
&:last-child { &:last-child {
margin-bottom: 2rem; margin-bottom: 2rem;
@ -67,13 +74,12 @@
.buttons-container { .buttons-container {
flex: none; flex: none;
align-self: stretch; align-self: stretch;
margin: 2rem 2rem 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
&:last-child { &:last-child {
margin-bottom: 2rem; margin: 2rem 0;
} }
} }
} }
@ -85,15 +91,18 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 3.5rem;
border-radius: 3.5rem;
padding: 1.2rem; padding: 1.2rem;
background-color: @color-accent3; background-color: var(--secondary-accent-color);
&:hover { &:hover {
background-color: @color-accent3-light1; background-color: transparent;
outline: var(--focus-outline-size) solid var(--secondary-accent-color);
} }
&:focus { &:focus {
outline-color: @color-background-dark5; outline-color: var(--primary-foreground-color);
} }
&:not(:last-child) { &:not(:last-child) {
@ -105,7 +114,7 @@
width: 1.2rem; width: 1.2rem;
height: 1.2rem; height: 1.2rem;
margin-right: .5rem; margin-right: .5rem;
fill: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
.label { .label {
@ -114,9 +123,9 @@
flex-basis: auto; flex-basis: auto;
max-height: 3.6em; max-height: 3.6em;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 500; font-weight: 700;
text-align: center; text-align: center;
color: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
} }
@ -127,35 +136,20 @@
width: 90%; width: 90%;
max-width: initial; max-width: initial;
z-index: 0; z-index: 0;
padding: 0 1.5rem;
.close-button-container {
position: absolute;
top: 0;
right: 0;
margin: 0.75rem 0.75rem 0 0;
padding: 0.25rem;
}
.title-container {
max-height: 4.8em;
margin: 1rem 3rem 1rem 1.5rem;
}
.modal-dialog-content {
margin: 0 0.5rem;
padding: 0 0.5rem;
&:last-child {
margin-bottom: 1rem;
}
}
.buttons-container { .buttons-container {
margin: 1rem 1rem 0; flex-direction: column;
gap: 1rem;
}
}
&:last-child { .action-button {
margin-bottom: 1rem; width: 100%;
}
.label {
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
} }

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Popup = require('stremio/common/Popup'); const Popup = require('stremio/common/Popup');
const ModalDialog = require('stremio/common/ModalDialog'); const ModalDialog = require('stremio/common/ModalDialog');
@ -15,7 +15,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, ren
const options = React.useMemo(() => { const options = React.useMemo(() => {
return Array.isArray(props.options) ? return Array.isArray(props.options) ?
props.options.filter((option) => { props.options.filter((option) => {
return option && typeof option.value === 'string'; return option && (typeof option.value === 'string' || option.value === null);
}) })
: :
[]; [];
@ -23,7 +23,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, ren
const selected = React.useMemo(() => { const selected = React.useMemo(() => {
return Array.isArray(props.selected) ? return Array.isArray(props.selected) ?
props.selected.filter((value) => { props.selected.filter((value) => {
return typeof value === 'string'; return typeof value === 'string' || value === null;
}) })
: :
[]; [];
@ -104,7 +104,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, ren
title title
} }
</div> </div>
<Icon className={styles['icon']} icon={'ic_arrow_thin_down'} /> <Icon className={styles['icon']} name={'caret-down'} />
</React.Fragment> </React.Fragment>
} }
{children} {children}
@ -161,7 +161,7 @@ Multiselect.propTypes = {
direction: PropTypes.any, direction: PropTypes.any,
title: PropTypes.string, title: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({ options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired, value: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
label: PropTypes.string label: PropTypes.string
})), })),

View file

@ -11,16 +11,14 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 1rem; height: 2.75rem;
background-color: @color-background; padding: 0 1.5rem;
border-radius: 2.75rem;
background-color: var(--overlay-color);
&:global(.active) { &:global(.active) {
.label {
color: @color-surface-light5-90;
}
.icon { .icon {
fill: @color-surface-light5-90; transform: rotate(180deg);
} }
} }
@ -28,7 +26,7 @@
flex: 1; flex: 1;
max-height: 2.4em; max-height: 2.4em;
font-weight: 500; font-weight: 500;
color: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
} }
.icon { .icon {
@ -36,7 +34,8 @@
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
margin-left: 1rem; margin-left: 1rem;
fill: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
opacity: 0.4;
} }
.popup-menu-container { .popup-menu-container {
@ -51,7 +50,6 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
background-color: @color-background;
&:global(.selected) { &:global(.selected) {
.icon { .icon {
@ -60,13 +58,13 @@
} }
&:hover, &:focus { &:hover, &:focus {
background-color: @color-background-light2; background-color: var(--overlay-color);
} }
.label { .label {
flex: 1; flex: 1;
max-height: 4.8em; max-height: 4.8em;
color: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
.icon { .icon {
@ -76,7 +74,8 @@
height: 0.5rem; height: 0.5rem;
border-radius: 100%; border-radius: 100%;
margin-left: 1rem; margin-left: 1rem;
background-color: @color-accent3-90; background-color: var(--secondary-accent-color);
opacity: 1;
} }
} }

View file

@ -3,10 +3,11 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const useFullscreen = require('stremio/common/useFullscreen'); const useFullscreen = require('stremio/common/useFullscreen');
const usePWA = require('stremio/common/usePWA');
const SearchBar = require('./SearchBar'); const SearchBar = require('./SearchBar');
const NavMenu = require('./NavMenu'); const NavMenu = require('./NavMenu');
const styles = require('./styles'); const styles = require('./styles');
@ -17,9 +18,10 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
window.history.back(); window.history.back();
}, []); }, []);
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
const [isIOSPWA] = usePWA();
const renderNavMenuLabel = React.useCallback(({ ref, className, onClick, children, }) => ( const renderNavMenuLabel = React.useCallback(({ ref, className, onClick, children, }) => (
<Button ref={ref} className={classnames(className, styles['button-container'], styles['menu-button-container'])} tabIndex={-1} onClick={onClick}> <Button ref={ref} className={classnames(className, styles['button-container'], styles['menu-button-container'])} tabIndex={-1} onClick={onClick}>
<Icon className={styles['icon']} icon={'ic_more'} /> <Icon className={styles['icon']} name={'person-outline'} />
{children} {children}
</Button> </Button>
), []); ), []);
@ -28,7 +30,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
{ {
backButton ? backButton ?
<Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}> <Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_back_ios'} /> <Icon className={styles['icon']} name={'chevron-back'} />
</Button> </Button>
: :
<div className={styles['logo-container']}> <div className={styles['logo-container']}>
@ -45,36 +47,36 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
: :
null null
} }
<div className={styles['spacing']} />
{ {
searchBar ? searchBar && route !== 'addons' ?
<SearchBar className={styles['search-bar']} query={query} active={route === 'search'} /> <SearchBar className={styles['search-bar']} query={query} active={route === 'search'} />
: :
null null
} }
<div className={styles['spacing']} /> <div className={styles['buttons-container']}>
{ {
addonsButton ? addonsButton ?
<Button className={styles['button-container']} href={'#/addons'} title={t('ADDONS')} tabIndex={-1}> <Button className={styles['button-container']} href={'#/addons'} title={t('ADDONS')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_addons'} /> <Icon className={styles['icon']} name={'addons-outline'} />
</Button> </Button>
: :
null null
} }
{ {
fullscreenButton ? !isIOSPWA && fullscreenButton ?
<Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}> <Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} /> <Icon className={styles['icon']} name={fullscreen ? 'minimize' : 'maximize'} />
</Button> </Button>
: :
null null
} }
{ {
navMenu ? navMenu ?
<NavMenu renderLabel={renderNavMenuLabel} /> <NavMenu renderLabel={renderNavMenuLabel} />
: :
null null
} }
</div>
</nav> </nav>
); );
}); });

View file

@ -7,6 +7,7 @@ const { useRouteFocused } = require('stremio-router');
const Popup = require('stremio/common/Popup'); const Popup = require('stremio/common/Popup');
const useBinaryState = require('stremio/common/useBinaryState'); const useBinaryState = require('stremio/common/useBinaryState');
const NavMenuContent = require('./NavMenuContent'); const NavMenuContent = require('./NavMenuContent');
const styles = require('./styles.less');
const NavMenu = (props) => { const NavMenu = (props) => {
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
@ -42,6 +43,7 @@ const NavMenu = (props) => {
onCloseRequest={closeMenu} onCloseRequest={closeMenu}
renderLabel={renderLabel} renderLabel={renderLabel}
renderMenu={renderMenu} renderMenu={renderMenu}
className={styles['nav-menu-popup-label']}
/> />
); );
}; };

View file

@ -4,11 +4,12 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const useFullscreen = require('stremio/common/useFullscreen'); const useFullscreen = require('stremio/common/useFullscreen');
const useProfile = require('stremio/common/useProfile'); const useProfile = require('stremio/common/useProfile');
const usePWA = require('stremio/common/usePWA');
const useTorrent = require('stremio/common/useTorrent'); const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const styles = require('./styles'); const styles = require('./styles');
@ -19,6 +20,7 @@ const NavMenuContent = ({ onClick }) => {
const profile = useProfile(); const profile = useProfile();
const { createTorrentFromMagnet } = useTorrent(); const { createTorrentFromMagnet } = useTorrent();
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
const [isIOSPWA, isAndroidPWA] = usePWA();
const logoutButtonOnClick = React.useCallback(() => { const logoutButtonOnClick = React.useCallback(() => {
core.transport.dispatch({ core.transport.dispatch({
action: 'Ctx', action: 'Ctx',
@ -50,34 +52,41 @@ const NavMenuContent = ({ onClick }) => {
`url('${require('/images/default_avatar.png')}')` `url('${require('/images/default_avatar.png')}')`
}} }}
/> />
<div className={styles['email-container']}> <div className={styles['user-info-details']}>
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div> <div className={styles['email-container']}>
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div>
</div>
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={profile.auth === null ? '#/intro' : null} onClick={profile.auth !== null ? logoutButtonOnClick : null}>
<div className={styles['logout-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
</Button>
</div> </div>
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={profile.auth === null ? '#/intro' : null} onClick={profile.auth !== null ? logoutButtonOnClick : null}>
<div className={styles['logout-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
</Button>
</div>
<div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} />
<div className={styles['nav-menu-option-label']}>{fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')}</div>
</Button>
</div> </div>
{
!isIOSPWA && !isAndroidPWA ?
<div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} name={fullscreen ? 'minimize' : 'maximize'} />
<div className={styles['nav-menu-option-label']}>{fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')}</div>
</Button>
</div>
:
null
}
<div className={styles['nav-menu-section']}> <div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={ t('SETTINGS') } href={'#/settings'}> <Button className={styles['nav-menu-option-container']} title={ t('SETTINGS') } href={'#/settings'}>
<Icon className={styles['icon']} icon={'ic_settings'} /> <Icon className={styles['icon']} name={'settings'} />
<div className={styles['nav-menu-option-label']}>{ t('SETTINGS') }</div> <div className={styles['nav-menu-option-label']}>{ t('SETTINGS') }</div>
</Button> </Button>
<Button className={styles['nav-menu-option-container']} title={ t('ADDONS') } href={'#/addons'}> <Button className={styles['nav-menu-option-container']} title={ t('ADDONS') } href={'#/addons'}>
<Icon className={styles['icon']} icon={'ic_addons'} /> <Icon className={styles['icon']} name={'addons-outline'} />
<div className={styles['nav-menu-option-label']}>{ t('ADDONS') }</div> <div className={styles['nav-menu-option-label']}>{ t('ADDONS') }</div>
</Button> </Button>
<Button className={styles['nav-menu-option-container']} title={ t('PLAY_URL_MAGNET_LINK') } onClick={onPlayMagnetLinkClick}> <Button className={styles['nav-menu-option-container']} title={ t('PLAY_URL_MAGNET_LINK') } onClick={onPlayMagnetLinkClick}>
<Icon className={styles['icon']} icon={'ic_magnet'} /> <Icon className={styles['icon']} name={'magnet-link'} />
<div className={styles['nav-menu-option-label']}>{ t('PLAY_URL_MAGNET_LINK') }</div> <div className={styles['nav-menu-option-label']}>{ t('PLAY_URL_MAGNET_LINK') }</div>
</Button> </Button>
<Button className={styles['nav-menu-option-container']} title={ t('HELP_FEEDBACK') } href={'https://stremio.zendesk.com/'} target={'_blank'}> <Button className={styles['nav-menu-option-container']} title={ t('HELP_FEEDBACK') } href={'https://stremio.zendesk.com/'} target={'_blank'}>
<Icon className={styles['icon']} icon={'ic_help'} /> <Icon className={styles['icon']} name={'help'} />
<div className={styles['nav-menu-option-label']}>{ t('HELP_FEEDBACK') }</div> <div className={styles['nav-menu-option-label']}>{ t('HELP_FEEDBACK') }</div>
</Button> </Button>
</div> </div>
@ -88,9 +97,14 @@ const NavMenuContent = ({ onClick }) => {
<Button className={styles['nav-menu-option-container']} title={ t('PRIVACY_POLICY') } href={'https://www.stremio.com/privacy'} target={'_blank'}> <Button className={styles['nav-menu-option-container']} title={ t('PRIVACY_POLICY') } href={'https://www.stremio.com/privacy'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>{ t('PRIVACY_POLICY') }</div> <div className={styles['nav-menu-option-label']}>{ t('PRIVACY_POLICY') }</div>
</Button> </Button>
<Button className={styles['nav-menu-option-container']} title={ t('ABOUT_STREMIO') } href={'https://www.stremio.com/'} target={'_blank'}> {
<div className={styles['nav-menu-option-label']}>{ t('ABOUT_STREMIO') }</div> profile.auth !== null ?
</Button> <Button className={styles['nav-menu-option-container']} title={ t('USER_PANEL') } href={'https://www.stremio.com/acc-settings'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>{ t('USER_PANEL') }</div>
</Button>
:
null
}
</div> </div>
</div> </div>
); );

View file

@ -3,101 +3,104 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less'; @import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/common/Popup/styles.less') {
popup-menu-container: menu-container;
}
.nav-menu-popup-label {
.popup-menu-container {
margin-top: 1rem;
}
}
.nav-menu-container { .nav-menu-container {
width: 20rem; width: 22rem;
max-height: calc(100vh - var(--horizontal-nav-bar-size)); max-height: calc(100vh - var(--horizontal-nav-bar-size));
overflow-y: auto; overflow-y: auto;
background-color: @color-background-dark1; border-radius: var(--border-radius);
background-color: var(--modal-background-color);
.user-info-container { .user-info-container {
display: grid; display: flex;
height: 7rem; padding: 1.5rem 1rem;
grid-template-columns: 7rem 1fr;
grid-template-rows: 50% 50%;
grid-template-areas:
"avatar-area email-area"
"avatar-area logout-button-area";
.avatar-container { .avatar-container {
grid-area: avatar-area; flex: none;
padding: 1rem; height: 4rem;
width: 4rem;
border-radius: 50%; border-radius: 50%;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-origin: content-box; background-origin: content-box;
background-clip: content-box; background-clip: content-box;
background-color: var(--primary-foreground-color);
opacity: 0.9; opacity: 0.9;
} }
.email-container { .user-info-details {
grid-area: email-area; flex: auto;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; justify-content: center;
padding: 1rem 1rem 0 0; margin-left: 1rem;
.email-label { .email-container {
flex: 1; flex: none;
max-height: 2.4em; margin-bottom: 0.5rem;
color: @color-surface-light5-90;
}
}
.logout-button-container { .email-label {
grid-area: logout-button-area; flex: 1;
display: flex; color: var(--primary-foreground-color);
flex-direction: row;
align-items: center;
padding: 0 1rem 1rem 0;
&:hover, &:focus {
outline: none;
.logout-label {
color: @color-surface-light5-90;
text-decoration: underline;
} }
} }
.logout-label { .logout-button-container {
flex: 1; flex: none;
max-height: 2.4em;
color: @color-surface-light3-90; &:hover, &:focus {
outline: none;
.logout-label {
text-decoration: underline;
}
}
.logout-label {
flex: 1;
font-size: 0.9rem;
font-weight: 500;
color: var(--primary-foreground-color);
}
} }
} }
} }
.nav-menu-section { .nav-menu-section {
border-top: thin solid @color-surface-light5-20; border-top: thin solid var(--overlay-color);
.nav-menu-option-container { .nav-menu-option-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: 4rem; height: 4rem;
padding: 0 1.5rem;
&:hover { &:hover {
background-color: @color-background-light2; background-color: var(--overlay-color);
} }
.icon { .icon {
flex: none; flex: none;
width: 1.4rem; width: 2rem;
height: 1.4rem; height: 2rem;
margin: 1.3rem; margin-right: 1rem;
fill: @color-secondaryvariant2-light1-90; color: var(--primary-foreground-color);
opacity: 0.3;
} }
.nav-menu-option-label { .nav-menu-option-label {
flex: 1; flex: 1;
max-height: 2.4em; color: var(--primary-foreground-color);
padding-right: 1.3rem;
color: @color-surface-light5-90;
&:only-child {
padding-left: 1.3rem;
}
} }
} }
} }

View file

@ -3,44 +3,100 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput'); const TextInput = require('stremio/common/TextInput');
const useTorrent = require('stremio/common/useTorrent'); const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useSearchHistory = require('./useSearchHistory');
const useLocalSearch = require('./useLocalSearch');
const styles = require('./styles'); const styles = require('./styles');
const useBinaryState = require('stremio/common/useBinaryState');
const SearchBar = ({ className, query, active }) => { const SearchBar = React.memo(({ className, query, active }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
const searchHistory = useSearchHistory();
const localSearch = useLocalSearch();
const { createTorrentFromMagnet } = useTorrent(); const { createTorrentFromMagnet } = useTorrent();
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(false);
const [currentQuery, setCurrentQuery] = React.useState(query || '');
const searchInputRef = React.useRef(null); const searchInputRef = React.useRef(null);
const containerRef = React.useRef(null);
const searchBarOnClick = React.useCallback(() => { const searchBarOnClick = React.useCallback(() => {
if (!active) { if (!active) {
window.location = '#/search'; window.location = '#/search';
} }
}, [active]); }, [active]);
const searchHistoryOnClose = React.useCallback((event) => {
if (historyOpen && containerRef.current && !containerRef.current.contains(event.target)) {
closeHistory();
}
}, [historyOpen]);
React.useEffect(() => {
document.addEventListener('mousedown', searchHistoryOnClose);
return () => {
document.removeEventListener('mousedown', searchHistoryOnClose);
};
}, [searchHistoryOnClose]);
const queryInputOnChange = React.useCallback(() => { const queryInputOnChange = React.useCallback(() => {
const value = searchInputRef.current.value;
setCurrentQuery(value);
openHistory();
try { try {
createTorrentFromMagnet(searchInputRef.current.value); createTorrentFromMagnet(value);
// eslint-disable-next-line no-empty } catch (error) {
} catch { } console.error('Failed to create torrent from magnet:', error);
}, []); }
const queryInputOnSubmit = React.useCallback(() => { }, [createTorrentFromMagnet]);
if (searchInputRef.current !== null) {
const queryParams = new URLSearchParams([['search', searchInputRef.current.value]]); const queryInputOnSubmit = React.useCallback((event) => {
window.location = `#/search?${queryParams.toString()}`; event.preventDefault();
const searchValue = `/search?search=${event.target.value}`;
setCurrentQuery(searchValue);
if (searchInputRef.current && searchValue) {
window.location.hash = searchValue;
closeHistory();
} }
}, []); }, []);
const queryInputClear = React.useCallback(() => {
searchInputRef.current.value = '';
setCurrentQuery('');
window.location.hash = '/search';
}, []);
const updateLocalSearchDebounced = React.useCallback(debounce((query) => {
localSearch.search(query);
}, 250), []);
React.useEffect(() => {
updateLocalSearchDebounced(currentQuery);
}, [currentQuery]);
React.useEffect(() => { React.useEffect(() => {
if (routeFocused && active) { if (routeFocused && active) {
searchInputRef.current.focus(); searchInputRef.current.focus();
} }
}, [routeFocused, active, query]); }, [routeFocused, active]);
React.useEffect(() => {
return () => {
updateLocalSearchDebounced.cancel();
};
}, []);
return ( return (
<label className={classnames(className, styles['search-bar-container'], { 'active': active })} onClick={searchBarOnClick}> <div className={classnames(className, styles['search-bar-container'], { 'active': active })} onClick={searchBarOnClick} ref={containerRef}>
{ {
active ? active ?
<TextInput <TextInput
@ -53,18 +109,72 @@ const SearchBar = ({ className, query, active }) => {
tabIndex={-1} tabIndex={-1}
onChange={queryInputOnChange} onChange={queryInputOnChange}
onSubmit={queryInputOnSubmit} onSubmit={queryInputOnSubmit}
onClick={openHistory}
/> />
: :
<div className={styles['search-input']}> <div className={styles['search-input']}>
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div> <div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div> </div>
} }
<Button className={styles['submit-button-container']} tabIndex={-1} onClick={queryInputOnSubmit}> {
<Icon className={styles['icon']} icon={'ic_search_link'} /> currentQuery.length > 0 ?
</Button> <Button className={styles['submit-button-container']} onClick={queryInputClear}>
</label> <Icon className={styles['icon']} name={'close'} />
</Button>
:
<Button className={styles['submit-button-container']}>
<Icon className={styles['icon']} name={'search'} />
</Button>
}
{
historyOpen && (searchHistory?.items?.length || localSearch?.items?.length) ?
<div className={styles['menu-container']}>
{
searchHistory?.items?.length > 0 ?
<div className={styles['items']}>
<div className={styles['title']}>
<div className={styles['label']}>{ t('STREMIO_TV_SEARCH_HISTORY_TITLE') }</div>
<button className={styles['search-history-clear']} onClick={searchHistory.clear}>
{ t('CLEAR_HISTORY') }
</button>
</div>
{
searchHistory.items.slice(0, 8).map(({ query, deepLinks }, index) => (
<Button key={index} className={styles['item']} href={deepLinks.search} onClick={closeHistory}>
{query}
</Button>
))
}
</div>
:
null
}
{
localSearch?.items?.length ?
<div className={styles['items']}>
<div className={styles['title']}>
<div className={styles['label']}>{ t('Recommendations') }</div>
</div>
{
localSearch.items.map(({ query, deepLinks }, index) => (
<Button key={index} className={styles['item']} href={deepLinks.search} onClick={closeHistory}>
{query}
</Button>
))
}
</div>
:
null
}
</div>
:
null
}
</div>
); );
}; });
SearchBar.displayName = 'SearchBar';
SearchBar.propTypes = { SearchBar.propTypes = {
className: PropTypes.string, className: PropTypes.string,
@ -80,7 +190,7 @@ const SearchBarFallback = ({ className }) => {
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div> <div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div> </div>
<Button className={styles['submit-button-container']} tabIndex={-1}> <Button className={styles['submit-button-container']} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_search_link'} /> <Icon className={styles['icon']} name={'search'} />
</Button> </Button>
</label> </label>
); );

View file

@ -3,16 +3,14 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.search-bar-container { .search-bar-container {
--search-bar-size: calc(var(--horizontal-nav-bar-size) - 1.2rem); --search-bar-size: 3.25rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: var(--search-bar-size); height: var(--search-bar-size);
border-radius: var(--search-bar-size); border-radius: var(--search-bar-size);
background-color: @color-background-light2; background-color: var(--overlay-color);
position: relative;
&:hover { overflow: visible;
background-color: @color-background-light3;
}
.search-input { .search-input {
flex: 1; flex: 1;
@ -20,15 +18,16 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 0 0.5rem 0 1.5rem; padding: 0 0.5rem 0 2rem;
font-weight: 500; font-weight: 500;
color: @color-secondaryvariant1-light1; color: var(--primary-foreground-color);
cursor: text; cursor: text;
&::placeholder, .placeholder-label { &::placeholder, .placeholder-label {
max-height: 1.2em; max-height: 1.2em;
opacity: 1; opacity: 1;
color: @color-secondaryvariant1-light1-90; color: var(--primary-foreground-color);
opacity: 0.6;
} }
} }
@ -38,20 +37,81 @@
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: var(--search-bar-size);
height: var(--search-bar-size); height: var(--search-bar-size);
padding: 0 1.5rem;
&:hover {
.icon {
fill: @color-secondaryvariant2-light1-90;
}
}
.icon { .icon {
flex: none; flex: none;
width: 1.7rem; width: 1.7rem;
height: 1.7rem; height: 1.7rem;
fill: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
opacity: 0.6;
}
}
.menu-container {
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: auto;
z-index: 10;
padding: 1rem;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
gap: 1.5rem;
background-color: var(--modal-background-color);
border-radius: var(--border-radius);
.label {
font-size: 0.9rem;
color: var(--primary-foreground-color);
}
.title {
display: flex;
justify-content: space-between;
width: 100%;
opacity: 0.8;
padding-bottom: 1rem;
.search-history-clear {
cursor: pointer;
color: var(--primary-foreground-color);
font-size: 0.9rem;
&:hover {
opacity: 0.6;
}
}
}
.items {
width: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
.item {
width: 90%;
color: var(--primary-foreground-color);
text-align: left;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
width: 100%;
cursor: pointer;
z-index: 10;
&:hover {
background-color: var(--secondary-background-color);
}
}
} }
} }
} }

View file

@ -0,0 +1,2 @@
declare const useLocalSearch: () => { items: LocalSearchItem[], search: (query: string) => void };
export = useLocalSearch;

View file

@ -0,0 +1,38 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useServices } = require('stremio/services');
const useModelState = require('stremio/common/useModelState');
const useLocalSearch = () => {
const { core } = useServices();
const action = React.useMemo(() => ({
action: 'Load',
args: {
model: 'LocalSearch',
}
}), []);
const { items } = useModelState({ model: 'local_search', action });
const search = React.useCallback((query) => {
core.transport.dispatch({
action: 'Search',
args: {
action: 'Search',
args: {
searchQuery: query,
maxResults: 5
}
},
});
}, []);
return {
items,
search,
};
};
module.exports = useLocalSearch;

View file

@ -0,0 +1,2 @@
declare const useSearchHistory: () => { items: SearchHistory, clear: () => void };
export = useSearchHistory;

View file

@ -0,0 +1,26 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const useModelState = require('stremio/common/useModelState');
const { useServices } = require('stremio/services');
const useSearchHistory = () => {
const { core } = useServices();
const { searchHistory: items } = useModelState({ model: 'ctx' });
const clear = React.useCallback(() => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'ClearSearchHistory',
},
});
}, []);
return {
items,
clear,
};
};
module.exports = useSearchHistory;

View file

@ -7,9 +7,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between;
height: var(--horizontal-nav-bar-size); height: var(--horizontal-nav-bar-size);
padding-right: 1rem; padding-right: 1rem;
background-color: @color-background; background-color: transparent;
overflow: visible; overflow: visible;
.logo-container { .logo-container {
@ -30,8 +31,8 @@
} }
} }
.spacing { .back-button-container {
flex: 1 0 0; margin-left: 1rem;
} }
.title { .title {
@ -44,19 +45,18 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
color: @color-secondaryvariant2-light1-90; color: @color-secondaryvariant2-light1-90;
&+.spacing {
display: none;
}
} }
.search-bar { .search-bar {
flex: 2 0 9.5rem; height: 3.25rem;
max-width: 30rem; width: 30rem;
}
&+.spacing { .buttons-container {
max-width: 11rem; display: flex;
} flex-direction: row;
gap: 0.5rem;
overflow: visible;
} }
.button-container { .button-container {
@ -64,31 +64,27 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: var(--horizontal-nav-bar-size); width: 3.5rem;
height: var(--horizontal-nav-bar-size); height: 3.5rem;
border-radius: 0.75rem;
&.back-button-container { opacity: 0.4;
width: var(--vertical-nav-bar-size);
height: var(--horizontal-nav-bar-size);
}
&:hover {
background-color: @color-background-light2;
}
&:global(.active) {
background-color: @color-background-light3;
.icon {
fill: @color-surface-light5-90;
}
}
.icon { .icon {
flex: none; flex: none;
width: 1.7rem; width: 2rem;
height: 1.7rem; height: 2rem;
fill: @color-secondaryvariant2-light1-90; color: var(--primary-foreground-color);
opacity: 0.6;
}
&:hover, &:global(.active) {
background-color: var(--overlay-color);
opacity: 1;
.icon {
color: var(--primary-foreground-color);
opacity: 0.8;
}
} }
} }
} }
@ -102,16 +98,12 @@
width: var(--horizontal-nav-bar-size); width: var(--horizontal-nav-bar-size);
} }
.search-bar { .button-container {
margin: 0 0.5rem; margin: 0 1rem;
}
.spacing { &:not(.back-button-container):not(.menu-button-container) {
display: none; display: none;
} }
.button-container:not(.back-button-container):not(.menu-button-container) {
display: none;
} }
} }
} }

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const Image = require('stremio/common/Image'); const Image = require('stremio/common/Image');
const styles = require('./styles'); const styles = require('./styles');
@ -11,7 +11,7 @@ const styles = require('./styles');
const NavTabButton = ({ className, logo, icon, label, href, selected, onClick }) => { const NavTabButton = ({ className, logo, icon, label, href, selected, onClick }) => {
const renderLogoFallback = React.useCallback(() => ( const renderLogoFallback = React.useCallback(() => (
typeof icon === 'string' && icon.length > 0 ? typeof icon === 'string' && icon.length > 0 ?
<Icon className={styles['icon']} icon={icon} /> <Icon className={styles['icon']} name={icon} />
: :
null null
), [icon]); ), [icon]);
@ -27,7 +27,7 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick })
/> />
: :
typeof icon === 'string' && icon.length > 0 ? typeof icon === 'string' && icon.length > 0 ?
<Icon className={styles['icon']} icon={icon} /> <Icon className={styles['icon']} name={selected ? icon : `${icon}-outline`} />
: :
null null
} }

View file

@ -1,50 +1,71 @@
// Copyright (C) 2017-2023 Smart code 203358507 // Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
.nav-tab-button-container { .nav-tab-button-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: @color-background-dark1; background-color: transparent;
border-radius: 0.75rem;
&:hover { &:hover {
background-color: @color-background-light2; background-color: var(--overlay-color);
.label {
opacity: 0.6;
}
} }
&:global(.selected) { &:global(.selected) {
background-color: @color-secondaryvariant1-dark5;
&:hover {
background-color: @color-secondaryvariant1-dark4;
}
.icon { .icon {
fill: @color-surface-light5-90; opacity: 1;
} color: var(--primary-accent-color);
.label {
color: @color-surface-light5-90;
} }
} }
.icon, .logo { .icon, .logo {
flex: none; flex: none;
width: 1.7rem; width: 2.2rem;
height: 1.7rem; height: 2.2rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
fill: @color-secondary-light5-90; }
.icon {
color: var(--primary-foreground-color);
opacity: 0.2;
} }
.label { .label {
flex: none; flex: none;
position: relative;
max-width: 100%;
max-height: 2.4em; max-height: 2.4em;
padding: 0 0.2rem; padding: 0 0.5rem;
font-size: 0.9rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
letter-spacing: 0.01rem; letter-spacing: 0.01rem;
text-align: center; text-align: center;
color: @color-secondaryvariant1-90; white-space: nowrap;
text-overflow: ellipsis;
color: var(--primary-foreground-color);
opacity: 0;
overflow: hidden;
}
}
@media only screen and (max-width: @minimum) {
.nav-tab-button-container {
.label {
opacity: 0.2;
}
&:global(.selected) {
.label {
opacity: 0.6;
}
}
} }
} }

View file

@ -4,8 +4,12 @@
@import (reference) '~stremio/common/screen-sizes.less'; @import (reference) '~stremio/common/screen-sizes.less';
.vertical-nav-bar-container { .vertical-nav-bar-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
width: var(--vertical-nav-bar-size); width: var(--vertical-nav-bar-size);
background-color: @color-background-dark1; background-color: transparent;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
@ -14,8 +18,8 @@
} }
.nav-tab-button { .nav-tab-button {
width: var(--vertical-nav-bar-size); width: calc(var(--vertical-nav-bar-size) - 1.5rem);
height: var(--vertical-nav-bar-size); height: calc(var(--vertical-nav-bar-size) - 1.5rem);
&:first-child { &:first-child {
margin-top: 1rem; margin-top: 1rem;
@ -29,11 +33,12 @@
@media only screen and (max-width: @minimum) { @media only screen and (max-width: @minimum) {
.vertical-nav-bar-container { .vertical-nav-bar-container {
display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
gap: 0;
height: var(--vertical-nav-bar-size); height: var(--vertical-nav-bar-size);
width: 100%; width: 100%;
padding: 0 1rem;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const styles = require('./styles'); const styles = require('./styles');
@ -22,13 +22,13 @@ const PaginationInput = ({ className, label, dataset, onSelect, ...props }) => {
return ( return (
<div {...props} className={classnames(className, styles['pagination-input-container'])} > <div {...props} className={classnames(className, styles['pagination-input-container'])} >
<Button className={styles['prev-button-container']} title={'Previous page'} data-value={'prev'} onClick={prevNextButtonOnClick}> <Button className={styles['prev-button-container']} title={'Previous page'} data-value={'prev'} onClick={prevNextButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_arrow_left'} /> <Icon className={styles['icon']} name={'chevron-back'} />
</Button> </Button>
<div className={styles['label-container']} title={label}> <div className={styles['label-container']} title={label}>
<div className={styles['label']}>{label}</div> <div className={styles['label']}>{label}</div>
</div> </div>
<Button className={styles['next-button-container']} title={'Next page'} data-value={'next'} onClick={prevNextButtonOnClick}> <Button className={styles['next-button-container']} title={'Next page'} data-value={'next'} onClick={prevNextButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_arrow_right'} /> <Icon className={styles['icon']} name={'chevron-forward'} />
</Button> </Button>
</div> </div>
); );

View file

@ -5,17 +5,18 @@
.pagination-input-container { .pagination-input-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-radius: var(--border-radius);
.prev-button-container, .next-button-container { .prev-button-container, .next-button-container {
flex: none; flex: none;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: @color-background; background-color: var(--overlay-color);
.icon { .icon {
display: block; display: block;
fill: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
} }
} }
@ -25,7 +26,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: @color-background-dark1; background-color: var(--overlay-color);
.label { .label {
flex: none; flex: none;
@ -35,7 +36,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
color: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
} }
} }
} }

View file

@ -13,10 +13,11 @@
.menu-container { .menu-container {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
overflow: visible; overflow: hidden;
visibility: hidden; visibility: hidden;
box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, border-radius: var(--border-radius);
0 1.1rem 0.85rem @color-background-dark5-20; background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
cursor: auto; cursor: auto;
&.menu-direction-top-left { &.menu-direction-top-left {

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const TextInput = require('stremio/common/TextInput'); const TextInput = require('stremio/common/TextInput');
const SearchBarPlaceholder = require('./SearchBarPlaceholder'); const SearchBarPlaceholder = require('./SearchBarPlaceholder');
const styles = require('./styles'); const styles = require('./styles');
@ -18,7 +18,7 @@ const SearchBar = ({ className, title, value, onChange }) => {
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
<Icon className={styles['icon']} icon={'ic_search'} /> <Icon className={styles['icon']} name={'search'} />
</label> </label>
); );
}; };

View file

@ -3,14 +3,14 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const styles = require('./styles'); const styles = require('./styles');
const SearchBarPlaceholder = ({ className, title }) => { const SearchBarPlaceholder = ({ className, title }) => {
return ( return (
<div className={classnames(className, styles['search-bar-container'])}> <div className={classnames(className, styles['search-bar-container'])}>
<div className={styles['search-input']}>{title}</div> <div className={styles['search-input']}>{title}</div>
<Icon className={styles['icon']} icon={'ic_search'} /> <Icon className={styles['icon']} name={'search'} />
</div> </div>
); );
}; };

View file

@ -22,6 +22,6 @@
flex: none; flex: none;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
fill: var(--color-placeholder-background); color: var(--color-placeholder-background);
} }
} }

View file

@ -6,31 +6,26 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
height: 3.5rem; height: 3rem;
padding: 0 1rem; padding: 0 1.5rem;
border-radius: 3.5rem; border-radius: 3rem;
border: var(--focus-outline-size) solid transparent; border: var(--focus-outline-size) solid transparent;
background-color: @color-background; background-color: var(--overlay-color);
cursor: text; cursor: text;
&:hover, &:focus-within {
background-color: @color-background-light1;
}
&:focus-within { &:focus-within {
border: var(--focus-outline-size) solid @color-surface-light5; border: var(--focus-outline-size) solid var(--primary-foreground-color);
} }
.search-input { .search-input {
flex: 1; flex: 1;
margin-right: 1rem; margin-right: 1rem;
font-size: 1.1rem; font-size: 1rem;
color: @color-surface-light5; color: var(--primary-foreground-color);
&::placeholder { &::placeholder {
max-height: 1.2em; color: var(--primary-foreground-color);
opacity: 1; opacity: 0.6;
color: @color-secondaryvariant1-light1-90;
} }
} }
@ -38,6 +33,7 @@
flex: none; flex: none;
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
fill: @color-secondaryvariant1-90; color: var(--primary-foreground-color);
opacity: 0.6;
} }
} }

View file

@ -4,9 +4,10 @@ const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { useTranslation } = require('react-i18next'); const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router'); const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services'); const { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput'); const TextInput = require('stremio/common/TextInput');
const styles = require('./styles'); const styles = require('./styles');
@ -14,6 +15,7 @@ const styles = require('./styles');
const SharePrompt = ({ className, url }) => { const SharePrompt = ({ className, url }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { core } = useServices(); const { core } = useServices();
const toast = useToast();
const inputRef = React.useRef(null); const inputRef = React.useRef(null);
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
const selectInputContent = React.useCallback(() => { const selectInputContent = React.useCallback(() => {
@ -25,6 +27,11 @@ const SharePrompt = ({ className, url }) => {
if (inputRef.current !== null) { if (inputRef.current !== null) {
inputRef.current.select(); inputRef.current.select();
document.execCommand('copy'); document.execCommand('copy');
toast.show({
type: 'success',
title: 'Copied to clipboard',
timeout: 3000,
});
} }
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@ -44,12 +51,13 @@ const SharePrompt = ({ className, url }) => {
<div className={classnames(className, styles['share-prompt-container'])}> <div className={classnames(className, styles['share-prompt-container'])}>
<div className={styles['buttons-container']}> <div className={styles['buttons-container']}>
<Button className={classnames(styles['button-container'], styles['facebook-button'])} title={'Facebook'} href={`https://www.facebook.com/sharer/sharer.php?u=${url}`} target={'_blank'}> <Button className={classnames(styles['button-container'], styles['facebook-button'])} title={'Facebook'} href={`https://www.facebook.com/sharer/sharer.php?u=${url}`} target={'_blank'}>
<Icon className={styles['icon']} icon={'ic_facebook'} /> <Icon className={styles['icon']} name={'facebook'} />
<div className={styles['label']}>Facebook</div>
</Button> </Button>
<Button className={classnames(styles['button-container'], styles['twitter-button'])} title={'Twitter'} href={`https://twitter.com/home?status=${url}`} target={'_blank'}> <Button className={classnames(styles['button-container'], styles['x-button'])} title={'X (Twitter)'} href={`https://twitter.com/intent/tweet?text=${url}`} target={'_blank'}>
<Icon className={styles['icon']} icon={'ic_twitter'} /> <Icon className={styles['icon']} name={'x'} />
<div className={styles['label']}>Twitter</div> </Button>
<Button className={classnames(styles['button-container'], styles['reddit-button'])} title={'Reddit'} href={`https://www.reddit.com/submit?url=${url}`} target={'_blank'}>
<Icon className={styles['icon']} name={'reddit'} />
</Button> </Button>
</div> </div>
<div className={styles['url-container']}> <div className={styles['url-container']}>
@ -63,7 +71,7 @@ const SharePrompt = ({ className, url }) => {
tabIndex={-1} tabIndex={-1}
/> />
<Button className={styles['copy-button']} title={'Copy to clipboard'} onClick={copyToClipboard}> <Button className={styles['copy-button']} title={'Copy to clipboard'} onClick={copyToClipboard}>
<Icon className={styles['icon']} icon={'ic_link'} /> <Icon className={styles['icon']} name={'link'} />
<div className={styles['label']}>{ t('COPY') }</div> <div className={styles['label']}>{ t('COPY') }</div>
</Button> </Button>
</div> </div>

View file

@ -21,26 +21,16 @@
.icon { .icon {
flex: none; flex: none;
height: 1.2rem; height: 1.5rem;
margin-right: 1rem; color: var(--primary-foreground-color);
fill: @color-surface-light5-90;
}
.label {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
max-height: 2.4em;
font-size: 1.1rem;
font-weight: 500;
text-align: center;
color: @color-surface-light5-90;
} }
} }
.facebook-button, .twitter-button { .facebook-button, .x-button, .reddit-button {
border-radius: var(--border-radius);
&:focus { &:focus {
outline-color: @color-background-dark5; outline-color: var(--primary-foreground-color);
} }
} }
@ -48,8 +38,12 @@
background-color: var(--color-facebook); background-color: var(--color-facebook);
} }
.twitter-button { .x-button {
background-color: var(--color-twitter); background-color: var(--color-x);
}
.reddit-button {
background-color: var(--color-reddit);
} }
} }
@ -57,7 +51,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: 1rem; margin-top: 1rem;
background-color: @color-surface-light2; border-radius: var(--border-radius);
background-color: var(--overlay-color);
.url-text-input { .url-text-input {
flex: 1; flex: 1;
@ -65,7 +60,7 @@
padding: 1rem; padding: 1rem;
font-size: 1.1rem; font-size: 1.1rem;
text-align: center; text-align: center;
color: @color-background-dark5; color: var(--primary-foreground-color);
} }
.copy-button { .copy-button {
@ -77,14 +72,16 @@
justify-content: center; justify-content: center;
width: 8rem; width: 8rem;
padding: 1rem; padding: 1rem;
background-color: @color-accent3; border-radius: 0 var(--border-radius) var(--border-radius) 0;
background-color: var(--secondary-accent-color);
&:hover { &:hover {
background-color: @color-accent3-light1; outline: var(--focus-outline-size) solid var(--secondary-accent-color);
background-color: transparent;
} }
&:focus { &:focus {
outline-color: @color-background-dark5; outline-color: var(--primary-foreground-color);
} }
.icon { .icon {
@ -92,7 +89,7 @@
width: 1.2rem; width: 1.2rem;
height: 1.2rem; height: 1.2rem;
margin-right: 0.5rem; margin-right: 0.5rem;
fill: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
.label { .label {
@ -101,7 +98,7 @@
flex-basis: auto; flex-basis: auto;
max-height: 2.4em; max-height: 2.4em;
font-size: 1.1rem; font-size: 1.1rem;
color: @color-surface-light5-90; color: var(--primary-foreground-color);
} }
} }
} }

View file

@ -109,9 +109,7 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
<div className={styles['track-after']} style={{ width: `calc(100% * ${thumbPosition})` }} /> <div className={styles['track-after']} style={{ width: `calc(100% * ${thumbPosition})` }} />
</div> </div>
<div className={styles['layer']}> <div className={styles['layer']}>
<svg className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} viewBox={'0 0 10 10'}> <div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
<circle cx={'5'} cy={'5'} r={'5'} />
</svg>
</div> </div>
</div> </div>
); );

View file

@ -16,26 +16,9 @@ html.active-slider-within {
overflow: visible; overflow: visible;
cursor: pointer; cursor: pointer;
&:hover, &:global(.active) {
.track-after {
background-color: @color-primary-light5;
}
}
&:global(.disabled) { &:global(.disabled) {
pointer-events: none; pointer-events: none;
opacity: 0.5;
.track {
background-color: @color-surface-dark5;
}
.track-after {
background-color: @color-surface;
}
.thumb {
fill: @color-surface;
}
} }
.layer { .layer {
@ -55,21 +38,24 @@ html.active-slider-within {
z-index: 0; z-index: 0;
flex: 1; flex: 1;
height: var(--track-size); height: var(--track-size);
background-color: @color-surface-light5-20; border-radius: var(--track-size);
background-color: var(--overlay-color);
} }
.track-before { .track-before {
z-index: 1; z-index: 1;
flex: none; flex: none;
height: var(--track-size); height: var(--track-size);
background-color: @color-surface-light5-10; border-radius: var(--track-size);
background-color: var(--overlay-color);
} }
.track-after { .track-after {
z-index: 2; z-index: 2;
flex: none; flex: none;
height: var(--track-size); height: var(--track-size);
background-color: @color-primary-light3; border-radius: var(--track-size);
background-color: var(--primary-foreground-color);
} }
.thumb { .thumb {
@ -78,6 +64,7 @@ html.active-slider-within {
width: var(--thumb-size); width: var(--thumb-size);
height: var(--thumb-size); height: var(--thumb-size);
transform: translateX(-50%); transform: translateX(-50%);
fill: @color-surface-light5; background-color: var(--primary-foreground-color);
border-radius: 100%;
} }
} }

View file

@ -3,7 +3,7 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom'); const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button'); const Button = require('stremio/common/Button');
const styles = require('./styles'); const styles = require('./styles');
@ -16,8 +16,8 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
}, [props.type]); }, [props.type]);
const icon = React.useMemo(() => { const icon = React.useMemo(() => {
return typeof props.icon === 'string' ? props.icon : return typeof props.icon === 'string' ? props.icon :
type === 'success' ? 'ic_check' : type === 'success' ? 'checkmark' :
type === 'error' ? 'ic_warning' : type === 'error' ? 'warning' :
null; null;
}, [type, props.icon]); }, [type, props.icon]);
const toastOnClick = React.useCallback((event) => { const toastOnClick = React.useCallback((event) => {
@ -54,7 +54,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
{ {
typeof icon === 'string' && icon.length > 0 ? typeof icon === 'string' && icon.length > 0 ?
<div className={styles['icon-container']}> <div className={styles['icon-container']}>
<Icon className={styles['icon']} icon={icon} /> <Icon className={styles['icon']} name={icon} />
</div> </div>
: :
null null
@ -74,7 +74,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
} }
</div> </div>
<Button className={styles['close-button-container']} title={'Close'} tabIndex={-1} onClick={closeButtonOnClick}> <Button className={styles['close-button-container']} title={'Close'} tabIndex={-1} onClick={closeButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_x'} /> <Icon className={styles['icon']} name={'close'} />
</Button> </Button>
</Button> </Button>
); );

View file

@ -18,7 +18,7 @@
background-color: @color-accent3; background-color: @color-accent3;
.icon { .icon {
fill: @color-surface-light5-90; color: @color-surface-light5-90;
} }
} }
} }
@ -28,7 +28,7 @@
background-color: @color-accent2; background-color: @color-accent2;
.icon { .icon {
fill: @color-surface-light5-90; color: @color-surface-light5-90;
} }
} }
} }
@ -43,7 +43,7 @@
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: @color-background-dark5-90; color: @color-background-dark5-90;
} }
} }

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