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:
push:
branches:
- '*'
- '**'
jobs:
build:

1
.gitignore vendored
View file

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

View file

@ -1,24 +1,31 @@
# Stremio Node 14.x
FROM stremio/node-base:fermium
# Meta
LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
# Update GitHub remote host key
RUN echo "github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> ~/.ssh/known_hosts
# Create app directory
RUN mkdir -p /var/www/stremio-web
# Install app dependencies
WORKDIR /var/www/stremio-web
COPY . /var/www/stremio-web
RUN npm install
# Bundle app source
WORKDIR /var/www/stremio-web
RUN npm run build
EXPOSE 8080
CMD ["node", "http_server.js"]
# Stremio Node 14.x
# the node version for running Stremio Web
ARG NODE_VERSION=20-alpine
FROM node:$NODE_VERSION AS base
# Meta
LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0"
RUN mkdir -p /var/www/stremio-web
WORKDIR /var/www/stremio-web
# Install app dependencies
FROM base AS prebuild
RUN apk update && apk upgrade && \
apk add --no-cache git
WORKDIR /var/www/stremio-web
COPY . .
RUN npm install
RUN npm run build
# Bundle app source
FROM base AS final
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
![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.
@ -46,4 +46,4 @@ npm run build
## 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",
"displayName": "Stremio",
"version": "5.0.0",
"version": "5.0.0-beta.4",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -15,9 +15,9 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.20",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"@stremio/stremio-core-web": "0.46.0",
"@stremio/stremio-icons": "5.2.0",
"@stremio/stremio-video": "0.0.30",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -38,8 +38,8 @@
"react-focus-lock": "2.9.1",
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"spatial-navigation-polyfill": "https://github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "https://github.com/Stremio/stremio-translations.git#92675658de92113c5888cf5e57003e468e8b8c9c",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#12b1307f95249496960d2a257b371db5700721e6",
"url": "0.11.0",
"use-long-press": "^3.1.5"
},
@ -49,6 +49,7 @@
"@babel/plugin-proposal-object-rest-spread": "7.16.0",
"@babel/preset-env": "7.16.0",
"@babel/preset-react": "7.16.0",
"@types/react": "^18.2.9",
"babel-loader": "8.2.3",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "9.0.1",
@ -67,7 +68,8 @@
"terser-webpack-plugin": "5.2.4",
"webpack": "5.61.0",
"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"
}
}

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

View file

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

View file

@ -7,12 +7,12 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
.error-image {
flex: none;
width: 12rem;
height: 12rem;
margin-bottom: 1rem;
object-fit: contain;
object-position: center;
opacity: 0.9;
@ -24,7 +24,7 @@
font-size: 2rem;
max-height: 3.6em;
text-align: center;
color: @color-surface-light5-90;
color: var(--primary-foreground-color);
}
.buttons-container {
@ -36,6 +36,8 @@
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin-top: 1rem;
.button-container {
flex-grow: 0;
@ -45,18 +47,23 @@
flex-direction: row;
align-items: center;
justify-content: center;
margin: 2rem 1rem 0;
padding: 0 1rem;
padding: 0 2.5rem;
min-width: 8rem;
height: 3rem;
background-color: @color-accent3;
height: 3.5rem;
border-radius: 3.5rem;
background-color: var(--overlay-color);
&:hover {
background-color: @color-accent3-light1;
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
background-color: transparent;
}
&:active {
outline: none;
}
&:global(.disabled) {
background-color: @color-surface-dark5;
opacity: 0.3;
}
.label {
@ -67,7 +74,7 @@
font-size: 1.1rem;
font-weight: 500;
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
@import (inline, once, css) '~stremio/common/roboto.css';
@import (reference) '~stremio/common/screen-sizes.less';
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@font-face {
font-family: 'PlusJakartaSans';
src: url('/fonts/PlusJakartaSans.ttf') format('truetype');
}
:global {
@import (once, less) '~stremio/common/animations.less';
@import (once, less) '~stremio-router/styles.css';
@ -13,14 +17,28 @@
--landscape-shape-ratio: 0.5625;
--poster-shape-ratio: 1.464;
--scroll-bar-size: 6px;
--horizontal-nav-bar-size: 4rem;
--vertical-nav-bar-size: 5.2rem;
--horizontal-nav-bar-size: 5.5rem;
--vertical-nav-bar-size: 6rem;
--focus-outline-size: 2px;
--color-facebook: #4267b2;
--color-twitter: #1DA1F2;
--color-facebook: #1877F1;
--color-x: #000000;
--color-reddit: #FF4500;
--color-imdb: #f5c518;
--color-trakt: #ED2224;
--color-placeholder: #60606080;
--color-placeholder-text: @color-surface-50;
--color-placeholder-background: @color-surface-dark5-20;
--primary-background-color: rgba(12, 11, 17, 1);
--secondary-background-color: rgba(26, 23, 62, 1);
--primary-foreground-color: rgba(255, 255, 255, 0.9);
--secondary-foreground-color: rgb(12, 11, 17, 1);
--primary-accent-color: rgb(123, 91, 245);
--secondary-accent-color: rgba(34, 179, 101, 1);
--tertiary-accent-color: rgba(246, 199, 0, 1);
--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;
box-sizing: border-box;
font-size: 1rem;
line-height: 1.2em;
font-family: inherit;
border: none;
outline: none;
@ -41,7 +58,7 @@
overflow: hidden;
word-break: break-word;
scrollbar-width: thin;
scrollbar-color: @color-secondaryvariant2-light1 @color-background-dark2;
scrollbar-color: var(--overlay-color) transparent;
}
::-webkit-scrollbar {
@ -50,15 +67,16 @@
}
::-webkit-scrollbar-thumb {
background-color: @color-secondaryvariant2-light1;
border-radius: var(--scroll-bar-size);
background-color: var(--overlay-color);
&:hover {
background-color: @color-secondaryvariant2-light2;
background-color: var(--primary-accent-color);
}
}
::-webkit-scrollbar-track {
background-color: @color-background-dark2;
background-color: transparent;
}
svg {
@ -70,12 +88,13 @@ html {
height: 100%;
min-width: 640px;
min-height: 480px;
font-family: 'Roboto', 'sans-serif';
font-family: 'PlusJakartaSans', 'sans-serif';
overflow: auto;
body {
width: 100%;
height: 100%;
background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%);
:global(#app) {
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 {
width: 100%;
height: 100%;
@ -108,7 +145,6 @@ html {
.loader-container, .error-container {
width: 100%;
height: 100%;
background-color: @color-background-dark2;
}
}
}

View file

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

View file

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

View file

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

View file

@ -2,9 +2,9 @@
const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
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 SEEK_TIME_DURATIONS = [5000, 10000, 15000, 20000, 25000, 30000];
const NEXT_VIDEO_POPUP_DURATIONS = [0, 5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000];
const SUBTITLES_FONTS = ['PlusJakartaSans', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [3000, 5000, 10000, 15000, 20000, 30000];
const NEXT_VIDEO_POPUP_DURATIONS = [0, 5000, 10000, 15000, 20000, 25000, 30000, 35000, 40000, 45000, 50000, 55000, 60000, 65000, 70000, 75000, 80000, 85000, 90000];
const CATALOG_PREVIEW_SIZE = 10;
const CATALOG_PAGE_SIZE = 100;
const NONE_EXTRA_VALUE = 'None';
@ -27,19 +27,62 @@ const TYPE_PRIORITIES = {
other: -Infinity
};
const ICON_FOR_TYPE = new Map([
['movie', 'ic_movies'],
['series', 'ic_series'],
['channel', 'ic_channels'],
['tv', 'ic_tv'],
['movie', 'movies'],
['series', 'series'],
['channel', 'channels'],
['tv', 'tv'],
['book', 'ic_book'],
['game', 'ic_games'],
['music', 'ic_music'],
['adult', 'ic_adult'],
['radio', 'ic_radio'],
['podcast', 'ic_podcast'],
['other', '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 = {
CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES,
@ -55,5 +98,6 @@ module.exports = {
SHARE_LINK_CATEGORY,
WRITERS_LINK_CATEGORY,
TYPE_PRIORITIES,
ICON_FOR_TYPE
ICON_FOR_TYPE,
EXTERNAL_PLAYERS,
};

View file

@ -3,21 +3,13 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const Checkbox = React.forwardRef(({ className, checked, children, ...props }, ref) => {
return (
<Button {...props} ref={ref} className={classnames(className, styles['checkbox-container'], { 'checked': checked })}>
{
checked ?
<svg className={styles['icon']} viewBox={'0 0 100 100'}>
<Icon x={'10'} y={'10'} width={'80'} height={'80'} icon={'ic_check'} />
</svg>
:
<Icon className={styles['icon']} icon={'ic_box_empty'} />
}
<div className={styles['toggle']} />
{children}
</Button>
);

View file

@ -2,18 +2,43 @@
@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 {
&:global(.checked) {
.icon {
fill: @color-surface-light5;
background-color: @color-primaryvariant1;
position: relative;
.toggle {
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 {
display: block;
width: 1rem;
height: 1rem;
fill: @color-surface-light5;
&:global(.checked) {
.toggle {
background-color: var(--secondary-accent-color);
&::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
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'DismissNotificationItem',
args: _id
}
});
}
break;

View file

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

View file

@ -3,17 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
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 Image = require('stremio/common/Image');
const Multiselect = require('stremio/common/Multiselect');
const PlayIconCircleCentered = require('stremio/common/PlayIconCircleCentered');
const useBinaryState = require('stremio/common/useBinaryState');
const { ICON_FOR_TYPE } = require('stremio/common/CONSTANTS');
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 href = React.useMemo(() => {
return deepLinks ?
@ -56,15 +57,32 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
const renderPosterFallback = React.useCallback(() => (
<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]);
const renderMenuLabelContent = React.useCallback(() => (
<Icon className={styles['icon']} icon={'ic_more'} />
<Icon className={styles['icon']} name={'more-vertical'} />
), []);
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}>
<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']}>
<Image
className={styles['poster-image']}
@ -74,9 +92,11 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
/>
</div>
{
playIcon ?
<div className={styles['play-icon-layer']}>
<PlayIconCircleCentered className={styles['play-icon']} />
onPlayClick ?
<div title={t('CONTINUE_WATCHING')} className={styles['play-icon-layer']} onClick={onPlayClick}>
<Icon className={styles['play-icon']} name={'play'} />
<div className={styles['play-icon-outer']} />
<div className={styles['play-icon-background']} />
</div>
:
null
@ -85,6 +105,22 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, playI
progress > 0 ?
<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-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>
:
null
@ -127,8 +163,9 @@ MetaItem.propTypes = {
name: PropTypes.string,
poster: PropTypes.string,
posterShape: PropTypes.oneOf(['poster', 'landscape', 'square']),
playIcon: PropTypes.bool,
posterChangeCursor: PropTypes.bool,
progress: PropTypes.number,
newVideos: PropTypes.number,
options: PropTypes.array,
deepLinks: PropTypes.shape({
metaDetailsVideos: PropTypes.string,
@ -137,7 +174,10 @@ MetaItem.propTypes = {
}),
dataset: PropTypes.object,
optionOnSelect: PropTypes.func,
onClick: PropTypes.func
onDismissClick: PropTypes.func,
onPlayClick: PropTypes.func,
onClick: PropTypes.func,
watched: PropTypes.bool
};
module.exports = MetaItem;

View file

@ -18,14 +18,44 @@
play-icon-circle-centered-icon: icon;
}
@play-icon-size: 4rem;
.meta-item-container {
padding: 1rem;
overflow: visible;
&:hover, &:focus, &:global(.active), &:global(.selected) {
outline-style: none;
background-color: @color-background-light3;
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 {
@ -49,7 +79,71 @@
.poster-container {
position: relative;
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 {
position: absolute;
@ -62,6 +156,7 @@
flex-direction: row;
align-items: center;
justify-content: center;
transition: transform 0.1s ease-out;
.poster-image {
flex: none;
@ -76,46 +171,138 @@
flex: none;
width: 80%;
height: 50%;
fill: @color-background-light3-90;
color: var(--primary-foreground-color);
opacity: 0.2;
}
}
.play-icon-layer {
position: absolute;
top: 30%;
right: 0;
bottom: 30%;
left: 0;
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 {
display: block;
width: 100%;
height: 100%;
filter: drop-shadow(0 0 0.5rem @color-background);
z-index: 2;
position: relative;
height: 2.25rem;
width: 2.25rem;
color: var(--primary-foreground-color);
}
.play-icon-circle-centered-background {
fill: @color-accent4-90;
}
.play-icon-outer {
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 {
fill: @color-surface-light5-90;
}
.play-icon-background {
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 {
position: absolute;
right: 0;
bottom: 0;
left: 0;
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 {
height: 0.4rem;
background-color: @color-primaryvariant1;
position: relative;
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;
flex-direction: row;
align-items: center;
height: 2.8rem;
height: 4rem;
overflow: visible;
.title-label {
flex: 1;
max-height: 2.4em;
padding-left: 0.5rem;
color: @color-surface-light5-90;
padding-left: 1.5rem;
font-weight: 600;
text-align: center;
color: var(--primary-foreground-color);
&:only-child {
padding-right: 0.5rem;
padding: 0 0.5rem;
}
}
.menu-label-container {
z-index: 1;
flex: none;
width: 1.5rem;
height: 2.8rem;
height: 4rem;
padding: 1rem 0;
background-color: transparent;
opacity: 0;
transform: translateX(1rem);
transition: opacity 0.1s ease-out;
.icon {
display: block;
width: 100%;
height: 100%;
fill: @color-surface-light1-90;
color: var(--primary-foreground-color);
opacity: 0.6;
}
.popup-menu-container {
width: auto;
.multiselect-menu-container {
min-width: 6rem;
max-width: 12rem;
min-width: 8rem;
max-width: 14rem;
.multiselect-option-container {
padding: 0.5rem;
background-color: @color-surface-light5;
padding: 1rem 1.5rem;
&:hover, &:focus {
outline: none;
background-color: @color-surface-light2;
background-color: var(--overlay-color);
}
.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 PropTypes = require('prop-types');
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 styles = require('./styles');
const { Tooltip } = require('stremio/common/Tooltips');
const ActionButton = ({ className, icon, label, ...props }) => {
const ActionButton = ({ className, icon, label, tooltip, ...props }) => {
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 ?
<div className={styles['icon-container']}>
<Icon className={styles['icon']} icon={icon} />
<Icon className={styles['icon']} name={icon} />
</div>
:
null
}
{
typeof label === 'string' && label.length > 0 ?
!tooltip && typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}>
<div className={styles['label']}>{label}</div>
</div>
@ -33,7 +40,8 @@ const ActionButton = ({ className, icon, label, ...props }) => {
ActionButton.propTypes = {
className: PropTypes.string,
icon: PropTypes.string,
label: PropTypes.string
label: PropTypes.string,
tooltip: PropTypes.bool
};
module.exports = ActionButton;

View file

@ -5,33 +5,34 @@
.action-button-container {
display: flex;
flex-direction: column;
flex-direction: row;
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 {
background-color: @color-accent3;
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
background-color: transparent;
}
.icon-container {
flex: 0 0 50%;
align-self: stretch;
padding-top: 15%;
&:only-child {
padding: 5% 0;
}
flex: none;
.icon {
display: block;
width: 100%;
height: 100%;
fill: @color-surface-light5-90;
height: 1.75rem;
width: 1.75rem;
color: var(--primary-foreground-color);
opacity: 0.9;
}
}
.label-container {
flex: 0 0 50%;
flex: none;
align-self: stretch;
display: flex;
flex-direction: row;
@ -39,11 +40,13 @@
.label {
flex: 1;
font-size: 1rem;
font-weight: 500;
max-height: 2.4em;
padding: 0 0.2rem;
text-align: center;
color: @color-surface-light5-90;
color: var(--primary-foreground-color);
opacity: 0.9;
}
}
}
@ -54,21 +57,8 @@
padding: 0 1rem;
.icon-container {
flex: none;
align-self: center;
height: 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 {
.label-container {
margin-bottom: 0.2rem;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 500;
color: @color-surface-dark3-90;
font-size: 0.95rem;
font-weight: 700;
color: var(--primary-foreground-color);
opacity: 0.3;
}
.links-container {
@ -19,15 +21,18 @@
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
margin-right: 0.5rem;
margin-bottom: 0.2rem;
padding: 0.3rem 0.5rem;
margin-right: 0.75rem;
margin-bottom: 0.75rem;
padding: 0.4rem 1.25rem;
white-space: nowrap;
text-overflow: ellipsis;
border-radius: 2rem;
border: var(--focus-outline-size) solid transparent;
color: @color-surface-light2-90;
background-color: @color-surface-light5-20;
font-size: 1rem;
font-weight: 500;
color: var(--primary-foreground-color);
background-color: var(--overlay-color);
backdrop-filter: blur(5px);
&:hover, &:focus {
background-color: @color-surface-light5-30;

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const UrlUtils = require('url');
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 Image = require('stremio/common/Image');
const ModalDialog = require('stremio/common/ModalDialog');
@ -95,8 +95,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
return trailerStreams[0].deepLinks.player;
}, [trailerStreams]);
const renderLogoFallback = React.useCallback(() => (
<div className={styles['logo-placeholder']}>{!compact ? name : null}</div>
), [compact, name]);
<div className={styles['logo-placeholder']}>{name}</div>
), [name]);
return (
<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'}
{...(compact ? { tabIndex: -1 } : null)}
>
<Icon className={styles['icon']} icon={'ic_imdbnoframe'} />
<div className={styles['label']}>{linksGroups.get(CONSTANTS.IMDB_LINK_CATEGORY).label}</div>
<Icon className={styles['icon']} name={'imdb'} />
</Button>
:
null
@ -157,17 +157,11 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
:
null
}
{
compact && typeof name === 'string' && name.length > 0 ?
<div className={styles['name-container']}>
{name}
</div>
:
null
}
{
compact && typeof description === 'string' && description.length > 0 ?
<div className={styles['description-container']}>{description}</div>
<div className={styles['description-container']}>
{description}
</div>
:
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 className={styles['action-buttons-container']}>
{
typeof toggleInLibrary === 'function' ?
<ActionButton
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')}
tooltip={compact}
tabIndex={compact ? -1 : 0}
onClick={toggleInLibrary}
/>
@ -205,10 +211,11 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
typeof trailerHref === 'string' ?
<ActionButton
className={styles['action-button']}
icon={'ic_movies'}
icon={'trailer'}
label={t('TRAILER')}
tabIndex={compact ? -1 : 0}
href={trailerHref}
tooltip={compact}
/>
:
null
@ -216,8 +223,8 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
{
typeof showHref === 'string' && compact ?
<ActionButton
className={styles['action-button']}
icon={'ic_play'}
className={classnames(styles['action-button'], styles['show-button'])}
icon={'play'}
label={t('SHOW')}
tabIndex={compact ? -1 : 0}
href={showHref}
@ -230,8 +237,9 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<React.Fragment>
<ActionButton
className={styles['action-button']}
icon={'ic_share'}
icon={'share'}
label={t('CTX_SHARE')}
tooltip={true}
tabIndex={compact ? -1 : 0}
onClick={openShareModal}
/>

View file

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

View file

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

View file

@ -13,12 +13,11 @@
.meta-info-container {
.logo, .logo-placeholder {
width: 100%;
height: 8rem;
background-color: @color-surface-dark5-10;
height: 6rem;
}
.runtime-release-info-container {
justify-content: space-evenly;
justify-content: space-between;
.runtime-label, .release-info-label {
margin: 1rem 0.4rem;
@ -31,8 +30,7 @@
}
.action-buttons-container {
justify-content: space-evenly;
padding: 0;
justify-content: space-between;
.action-button:not(:last-child) {
margin-right: 0;
@ -48,32 +46,20 @@
left: -10px;
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 {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
opacity: 0.9;
filter: blur(5px);
filter: blur(10px);
opacity: 0.3;
}
}
.meta-info-container {
flex: 1;
align-self: stretch;
padding: 0 2rem;
overflow-y: auto;
&:not(:hover) {
@ -87,11 +73,11 @@
.logo, .logo-placeholder {
display: block;
max-width: 100%;
margin: 2rem 0;
margin-bottom: 2rem;
}
.logo {
height: 8rem;
height: 9rem;
object-fit: contain;
object-position: center;
}
@ -110,10 +96,10 @@
.runtime-label, .release-info-label {
flex: 0 1 auto;
margin-right: 2rem;
margin-bottom: 0.5rem;
font-size: 1.4rem;
color: @color-surface-light5-90;
margin-right: 3rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-foreground-color);
}
.imdb-button-container {
@ -121,55 +107,47 @@
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 0.5rem;
padding: 0.3rem 1rem;
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 {
outline: none;
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 {
flex: none;
width: 3rem;
height: 1.1rem;
margin-right: 1rem;
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;
height: 3rem;
color: var(--color-imdb);
}
}
}
.name-container {
margin-top: 1rem;
font-size: 1.5rem;
color: @color-surface-light5-90;
}
.description-container {
max-height: 6em;
margin-top: 1rem;
font-size: 1.1rem;
line-height: 1.5em;
color: @color-surface-light5-90;
font-size: 1rem;
font-weight: 400;
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 {
margin-top: 1rem;
margin-top: 1.5rem;
}
}
@ -178,18 +156,54 @@
align-self: stretch;
display: flex;
flex-direction: row;
align-items: flex-end;
max-height: 15rem;
flex-wrap: wrap;
max-height: 10rem;
padding: 0 2rem;
padding-top: 3.5rem;
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 {
flex: none;
width: 6rem;
height: 6rem;
margin: 2rem 0;
width: 4rem;
height: 4rem;
margin-bottom: 1rem;
&:global(.wide) {
width: auto;
padding: 0 2rem;
border-radius: 4rem;
}
&: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) {
.meta-preview-container {
.meta-info-container {
padding: 0 1.5rem;
.logo {
margin: 1em 0;
}
}
.action-buttons-container {
flex-wrap: nowrap;
padding: 0 1.5rem;
overflow-x: visible;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
margin: 2rem auto;
}
.action-button {
width: auto;
height: 4rem;
max-width: 60%;
margin: 1rem 0;
.runtime-release-info-container {
justify-content: space-between;
&:not(:last-child) {
margin-right: 1rem;
.runtime-label, .release-info-label {
margin: 0;
}
}
}
.action-buttons-container {
flex-shrink: 0;
margin-top: 3rem;
overflow: visible;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
.share-prompt {

View file

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

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom');
const { default: Icon } = require('@stremio/stremio-icons/react');
const Button = require('stremio/common/Button');
const CONSTANTS = require('stremio/common/CONSTANTS');
const styles = require('./styles');
@ -21,7 +21,7 @@ const MetaRowPlaceholder = ({ className, title, deepLinks }) => {
deepLinks && typeof deepLinks.discover === 'string' ?
<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>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
<Icon className={styles['icon']} name={'chevron-forward'} />
</Button>
:
null

View file

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

View file

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

View file

@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useRouteFocused, useModalsContainer } = require('stremio-router');
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 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}>
<div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}>
<Button className={styles['close-button-container']} title={'Close'} onClick={closeButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_x'} />
<Icon className={styles['icon']} name={'close'} />
</Button>
{
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'])}>
{
typeof icon === 'string' && icon.length > 0 ?
<Icon className={styles['icon']} icon={icon} />
<Icon className={styles['icon']} name={icon} />
:
null
}

View file

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

View file

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

View file

@ -11,16 +11,14 @@
display: flex;
flex-direction: row;
align-items: center;
padding: 0 1rem;
background-color: @color-background;
height: 2.75rem;
padding: 0 1.5rem;
border-radius: 2.75rem;
background-color: var(--overlay-color);
&:global(.active) {
.label {
color: @color-surface-light5-90;
}
.icon {
fill: @color-surface-light5-90;
transform: rotate(180deg);
}
}
@ -28,7 +26,7 @@
flex: 1;
max-height: 2.4em;
font-weight: 500;
color: @color-secondaryvariant1-90;
color: var(--primary-foreground-color);
}
.icon {
@ -36,7 +34,8 @@
width: 1rem;
height: 1rem;
margin-left: 1rem;
fill: @color-secondaryvariant1-90;
color: var(--primary-foreground-color);
opacity: 0.4;
}
.popup-menu-container {
@ -51,7 +50,6 @@
flex-direction: row;
align-items: center;
padding: 1rem;
background-color: @color-background;
&:global(.selected) {
.icon {
@ -60,13 +58,13 @@
}
&:hover, &:focus {
background-color: @color-background-light2;
background-color: var(--overlay-color);
}
.label {
flex: 1;
max-height: 4.8em;
color: @color-surface-light5-90;
color: var(--primary-foreground-color);
}
.icon {
@ -76,7 +74,8 @@
height: 0.5rem;
border-radius: 100%;
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 PropTypes = require('prop-types');
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 Image = require('stremio/common/Image');
const useFullscreen = require('stremio/common/useFullscreen');
const usePWA = require('stremio/common/usePWA');
const SearchBar = require('./SearchBar');
const NavMenu = require('./NavMenu');
const styles = require('./styles');
@ -17,9 +18,10 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
window.history.back();
}, []);
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
const [isIOSPWA] = usePWA();
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}>
<Icon className={styles['icon']} icon={'ic_more'} />
<Icon className={styles['icon']} name={'person-outline'} />
{children}
</Button>
), []);
@ -28,7 +30,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
{
backButton ?
<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>
:
<div className={styles['logo-container']}>
@ -45,36 +47,36 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
:
null
}
<div className={styles['spacing']} />
{
searchBar ?
searchBar && route !== 'addons' ?
<SearchBar className={styles['search-bar']} query={query} active={route === 'search'} />
:
null
}
<div className={styles['spacing']} />
{
addonsButton ?
<Button className={styles['button-container']} href={'#/addons'} title={t('ADDONS')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_addons'} />
</Button>
:
null
}
{
fullscreenButton ?
<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'} />
</Button>
:
null
}
{
navMenu ?
<NavMenu renderLabel={renderNavMenuLabel} />
:
null
}
<div className={styles['buttons-container']}>
{
addonsButton ?
<Button className={styles['button-container']} href={'#/addons'} title={t('ADDONS')} tabIndex={-1}>
<Icon className={styles['icon']} name={'addons-outline'} />
</Button>
:
null
}
{
!isIOSPWA && fullscreenButton ?
<Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} name={fullscreen ? 'minimize' : 'maximize'} />
</Button>
:
null
}
{
navMenu ?
<NavMenu renderLabel={renderNavMenuLabel} />
:
null
}
</div>
</nav>
);
});

View file

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

View file

@ -4,11 +4,12 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
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 Button = require('stremio/common/Button');
const useFullscreen = require('stremio/common/useFullscreen');
const useProfile = require('stremio/common/useProfile');
const usePWA = require('stremio/common/usePWA');
const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const styles = require('./styles');
@ -19,6 +20,7 @@ const NavMenuContent = ({ onClick }) => {
const profile = useProfile();
const { createTorrentFromMagnet } = useTorrent();
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
const [isIOSPWA, isAndroidPWA] = usePWA();
const logoutButtonOnClick = React.useCallback(() => {
core.transport.dispatch({
action: 'Ctx',
@ -50,34 +52,41 @@ const NavMenuContent = ({ onClick }) => {
`url('${require('/images/default_avatar.png')}')`
}}
/>
<div className={styles['email-container']}>
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div>
<div className={styles['user-info-details']}>
<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>
<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>
{
!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']}>
<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>
</Button>
<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>
</Button>
<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>
</Button>
<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>
</Button>
</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'}>
<div className={styles['nav-menu-option-label']}>{ t('PRIVACY_POLICY') }</div>
</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>
</Button>
{
profile.auth !== null ?
<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>
);

View file

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

View file

@ -3,44 +3,100 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
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 Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput');
const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useSearchHistory = require('./useSearchHistory');
const useLocalSearch = require('./useLocalSearch');
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 routeFocused = useRouteFocused();
const searchHistory = useSearchHistory();
const localSearch = useLocalSearch();
const { createTorrentFromMagnet } = useTorrent();
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(false);
const [currentQuery, setCurrentQuery] = React.useState(query || '');
const searchInputRef = React.useRef(null);
const containerRef = React.useRef(null);
const searchBarOnClick = React.useCallback(() => {
if (!active) {
window.location = '#/search';
}
}, [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 value = searchInputRef.current.value;
setCurrentQuery(value);
openHistory();
try {
createTorrentFromMagnet(searchInputRef.current.value);
// eslint-disable-next-line no-empty
} catch { }
}, []);
const queryInputOnSubmit = React.useCallback(() => {
if (searchInputRef.current !== null) {
const queryParams = new URLSearchParams([['search', searchInputRef.current.value]]);
window.location = `#/search?${queryParams.toString()}`;
createTorrentFromMagnet(value);
} catch (error) {
console.error('Failed to create torrent from magnet:', error);
}
}, [createTorrentFromMagnet]);
const queryInputOnSubmit = React.useCallback((event) => {
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(() => {
if (routeFocused && active) {
searchInputRef.current.focus();
}
}, [routeFocused, active, query]);
}, [routeFocused, active]);
React.useEffect(() => {
return () => {
updateLocalSearchDebounced.cancel();
};
}, []);
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 ?
<TextInput
@ -53,18 +109,72 @@ const SearchBar = ({ className, query, active }) => {
tabIndex={-1}
onChange={queryInputOnChange}
onSubmit={queryInputOnSubmit}
onClick={openHistory}
/>
:
<div className={styles['search-input']}>
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div>
}
<Button className={styles['submit-button-container']} tabIndex={-1} onClick={queryInputOnSubmit}>
<Icon className={styles['icon']} icon={'ic_search_link'} />
</Button>
</label>
{
currentQuery.length > 0 ?
<Button className={styles['submit-button-container']} onClick={queryInputClear}>
<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 = {
className: PropTypes.string,
@ -80,7 +190,7 @@ const SearchBarFallback = ({ className }) => {
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div>
<Button className={styles['submit-button-container']} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_search_link'} />
<Icon className={styles['icon']} name={'search'} />
</Button>
</label>
);

View file

@ -3,16 +3,14 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.search-bar-container {
--search-bar-size: calc(var(--horizontal-nav-bar-size) - 1.2rem);
--search-bar-size: 3.25rem;
display: flex;
flex-direction: row;
height: var(--search-bar-size);
border-radius: var(--search-bar-size);
background-color: @color-background-light2;
&:hover {
background-color: @color-background-light3;
}
background-color: var(--overlay-color);
position: relative;
overflow: visible;
.search-input {
flex: 1;
@ -20,15 +18,16 @@
display: flex;
flex-direction: row;
align-items: center;
padding: 0 0.5rem 0 1.5rem;
padding: 0 0.5rem 0 2rem;
font-weight: 500;
color: @color-secondaryvariant1-light1;
color: var(--primary-foreground-color);
cursor: text;
&::placeholder, .placeholder-label {
max-height: 1.2em;
opacity: 1;
color: @color-secondaryvariant1-light1-90;
color: var(--primary-foreground-color);
opacity: 0.6;
}
}
@ -38,20 +37,81 @@
flex-direction: row;
justify-content: center;
align-items: center;
width: var(--search-bar-size);
height: var(--search-bar-size);
&:hover {
.icon {
fill: @color-secondaryvariant2-light1-90;
}
}
padding: 0 1.5rem;
.icon {
flex: none;
width: 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;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: var(--horizontal-nav-bar-size);
padding-right: 1rem;
background-color: @color-background;
background-color: transparent;
overflow: visible;
.logo-container {
@ -30,8 +31,8 @@
}
}
.spacing {
flex: 1 0 0;
.back-button-container {
margin-left: 1rem;
}
.title {
@ -44,19 +45,18 @@
white-space: nowrap;
text-overflow: ellipsis;
color: @color-secondaryvariant2-light1-90;
&+.spacing {
display: none;
}
}
.search-bar {
flex: 2 0 9.5rem;
max-width: 30rem;
height: 3.25rem;
width: 30rem;
}
&+.spacing {
max-width: 11rem;
}
.buttons-container {
display: flex;
flex-direction: row;
gap: 0.5rem;
overflow: visible;
}
.button-container {
@ -64,31 +64,27 @@
display: flex;
align-items: center;
justify-content: center;
width: var(--horizontal-nav-bar-size);
height: var(--horizontal-nav-bar-size);
&.back-button-container {
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;
}
}
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
opacity: 0.4;
.icon {
flex: none;
width: 1.7rem;
height: 1.7rem;
fill: @color-secondaryvariant2-light1-90;
width: 2rem;
height: 2rem;
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);
}
.search-bar {
margin: 0 0.5rem;
}
.button-container {
margin: 0 1rem;
.spacing {
display: none;
}
.button-container:not(.back-button-container):not(.menu-button-container) {
display: none;
&:not(.back-button-container):not(.menu-button-container) {
display: none;
}
}
}
}

View file

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

View file

@ -1,50 +1,71 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
@import (reference) '~stremio/common/screen-sizes.less';
.nav-tab-button-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: @color-background-dark1;
background-color: transparent;
border-radius: 0.75rem;
&:hover {
background-color: @color-background-light2;
background-color: var(--overlay-color);
.label {
opacity: 0.6;
}
}
&:global(.selected) {
background-color: @color-secondaryvariant1-dark5;
&:hover {
background-color: @color-secondaryvariant1-dark4;
}
.icon {
fill: @color-surface-light5-90;
}
.label {
color: @color-surface-light5-90;
opacity: 1;
color: var(--primary-accent-color);
}
}
.icon, .logo {
flex: none;
width: 1.7rem;
height: 1.7rem;
width: 2.2rem;
height: 2.2rem;
margin-bottom: 0.5rem;
fill: @color-secondary-light5-90;
}
.icon {
color: var(--primary-foreground-color);
opacity: 0.2;
}
.label {
flex: none;
position: relative;
max-width: 100%;
max-height: 2.4em;
padding: 0 0.2rem;
font-size: 0.9rem;
padding: 0 0.5rem;
font-size: 0.8rem;
font-weight: 500;
letter-spacing: 0.01rem;
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';
.vertical-nav-bar-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
width: var(--vertical-nav-bar-size);
background-color: @color-background-dark1;
background-color: transparent;
overflow-y: auto;
scrollbar-width: none;
@ -14,8 +18,8 @@
}
.nav-tab-button {
width: var(--vertical-nav-bar-size);
height: var(--vertical-nav-bar-size);
width: calc(var(--vertical-nav-bar-size) - 1.5rem);
height: calc(var(--vertical-nav-bar-size) - 1.5rem);
&:first-child {
margin-top: 1rem;
@ -29,11 +33,12 @@
@media only screen and (max-width: @minimum) {
.vertical-nav-bar-container {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0;
height: var(--vertical-nav-bar-size);
width: 100%;
padding: 0 1rem;
overflow-y: hidden;
overflow-x: auto;

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
const React = require('react');
const PropTypes = require('prop-types');
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 SearchBarPlaceholder = require('./SearchBarPlaceholder');
const styles = require('./styles');
@ -18,7 +18,7 @@ const SearchBar = ({ className, title, value, onChange }) => {
value={value}
onChange={onChange}
/>
<Icon className={styles['icon']} icon={'ic_search'} />
<Icon className={styles['icon']} name={'search'} />
</label>
);
};

View file

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

View file

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

View file

@ -6,31 +6,26 @@
display: flex;
flex-direction: row;
align-items: center;
height: 3.5rem;
padding: 0 1rem;
border-radius: 3.5rem;
height: 3rem;
padding: 0 1.5rem;
border-radius: 3rem;
border: var(--focus-outline-size) solid transparent;
background-color: @color-background;
background-color: var(--overlay-color);
cursor: text;
&:hover, &:focus-within {
background-color: @color-background-light1;
}
&:focus-within {
border: var(--focus-outline-size) solid @color-surface-light5;
border: var(--focus-outline-size) solid var(--primary-foreground-color);
}
.search-input {
flex: 1;
margin-right: 1rem;
font-size: 1.1rem;
color: @color-surface-light5;
font-size: 1rem;
color: var(--primary-foreground-color);
&::placeholder {
max-height: 1.2em;
opacity: 1;
color: @color-secondaryvariant1-light1-90;
color: var(--primary-foreground-color);
opacity: 0.6;
}
}
@ -38,6 +33,7 @@
flex: none;
width: 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 classnames = require('classnames');
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 { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput');
const styles = require('./styles');
@ -14,6 +15,7 @@ const styles = require('./styles');
const SharePrompt = ({ className, url }) => {
const { t } = useTranslation();
const { core } = useServices();
const toast = useToast();
const inputRef = React.useRef(null);
const routeFocused = useRouteFocused();
const selectInputContent = React.useCallback(() => {
@ -25,6 +27,11 @@ const SharePrompt = ({ className, url }) => {
if (inputRef.current !== null) {
inputRef.current.select();
document.execCommand('copy');
toast.show({
type: 'success',
title: 'Copied to clipboard',
timeout: 3000,
});
}
}, []);
React.useEffect(() => {
@ -44,12 +51,13 @@ const SharePrompt = ({ className, url }) => {
<div className={classnames(className, styles['share-prompt-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'}>
<Icon className={styles['icon']} icon={'ic_facebook'} />
<div className={styles['label']}>Facebook</div>
<Icon className={styles['icon']} name={'facebook'} />
</Button>
<Button className={classnames(styles['button-container'], styles['twitter-button'])} title={'Twitter'} href={`https://twitter.com/home?status=${url}`} target={'_blank'}>
<Icon className={styles['icon']} icon={'ic_twitter'} />
<div className={styles['label']}>Twitter</div>
<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']} name={'x'} />
</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>
</div>
<div className={styles['url-container']}>
@ -63,7 +71,7 @@ const SharePrompt = ({ className, url }) => {
tabIndex={-1}
/>
<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>
</Button>
</div>

View file

@ -21,26 +21,16 @@
.icon {
flex: none;
height: 1.2rem;
margin-right: 1rem;
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;
height: 1.5rem;
color: var(--primary-foreground-color);
}
}
.facebook-button, .twitter-button {
.facebook-button, .x-button, .reddit-button {
border-radius: var(--border-radius);
&:focus {
outline-color: @color-background-dark5;
outline-color: var(--primary-foreground-color);
}
}
@ -48,8 +38,12 @@
background-color: var(--color-facebook);
}
.twitter-button {
background-color: var(--color-twitter);
.x-button {
background-color: var(--color-x);
}
.reddit-button {
background-color: var(--color-reddit);
}
}
@ -57,7 +51,8 @@
display: flex;
flex-direction: row;
margin-top: 1rem;
background-color: @color-surface-light2;
border-radius: var(--border-radius);
background-color: var(--overlay-color);
.url-text-input {
flex: 1;
@ -65,7 +60,7 @@
padding: 1rem;
font-size: 1.1rem;
text-align: center;
color: @color-background-dark5;
color: var(--primary-foreground-color);
}
.copy-button {
@ -77,14 +72,16 @@
justify-content: center;
width: 8rem;
padding: 1rem;
background-color: @color-accent3;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
background-color: var(--secondary-accent-color);
&:hover {
background-color: @color-accent3-light1;
outline: var(--focus-outline-size) solid var(--secondary-accent-color);
background-color: transparent;
}
&:focus {
outline-color: @color-background-dark5;
outline-color: var(--primary-foreground-color);
}
.icon {
@ -92,7 +89,7 @@
width: 1.2rem;
height: 1.2rem;
margin-right: 0.5rem;
fill: @color-surface-light5-90;
color: var(--primary-foreground-color);
}
.label {
@ -101,7 +98,7 @@
flex-basis: auto;
max-height: 2.4em;
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>
<div className={styles['layer']}>
<svg className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} viewBox={'0 0 10 10'}>
<circle cx={'5'} cy={'5'} r={'5'} />
</svg>
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
</div>
</div>
);

View file

@ -16,26 +16,9 @@ html.active-slider-within {
overflow: visible;
cursor: pointer;
&:hover, &:global(.active) {
.track-after {
background-color: @color-primary-light5;
}
}
&:global(.disabled) {
pointer-events: none;
.track {
background-color: @color-surface-dark5;
}
.track-after {
background-color: @color-surface;
}
.thumb {
fill: @color-surface;
}
opacity: 0.5;
}
.layer {
@ -55,21 +38,24 @@ html.active-slider-within {
z-index: 0;
flex: 1;
height: var(--track-size);
background-color: @color-surface-light5-20;
border-radius: var(--track-size);
background-color: var(--overlay-color);
}
.track-before {
z-index: 1;
flex: none;
height: var(--track-size);
background-color: @color-surface-light5-10;
border-radius: var(--track-size);
background-color: var(--overlay-color);
}
.track-after {
z-index: 2;
flex: none;
height: var(--track-size);
background-color: @color-primary-light3;
border-radius: var(--track-size);
background-color: var(--primary-foreground-color);
}
.thumb {
@ -78,6 +64,7 @@ html.active-slider-within {
width: var(--thumb-size);
height: var(--thumb-size);
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 PropTypes = require('prop-types');
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 styles = require('./styles');
@ -16,8 +16,8 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
}, [props.type]);
const icon = React.useMemo(() => {
return typeof props.icon === 'string' ? props.icon :
type === 'success' ? 'ic_check' :
type === 'error' ? 'ic_warning' :
type === 'success' ? 'checkmark' :
type === 'error' ? 'warning' :
null;
}, [type, props.icon]);
const toastOnClick = React.useCallback((event) => {
@ -54,7 +54,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
{
typeof icon === 'string' && icon.length > 0 ?
<div className={styles['icon-container']}>
<Icon className={styles['icon']} icon={icon} />
<Icon className={styles['icon']} name={icon} />
</div>
:
null
@ -74,7 +74,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) =>
}
</div>
<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>
);

View file

@ -18,7 +18,7 @@
background-color: @color-accent3;
.icon {
fill: @color-surface-light5-90;
color: @color-surface-light5-90;
}
}
}
@ -28,7 +28,7 @@
background-color: @color-accent2;
.icon {
fill: @color-surface-light5-90;
color: @color-surface-light5-90;
}
}
}
@ -43,7 +43,7 @@
display: block;
width: 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