diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d728715a2..6782e9d8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: version: 10 run_install: false - name: Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: "pnpm" diff --git a/.github/workflows/pages_cleanup.yml b/.github/workflows/pages_cleanup.yml index 58ba548a7..2147b25d6 100644 --- a/.github/workflows/pages_cleanup.yml +++ b/.github/workflows/pages_cleanup.yml @@ -9,7 +9,7 @@ permissions: contents: write jobs: - build: + cleanup: runs-on: ubuntu-latest steps: - name: Checkout @@ -18,13 +18,30 @@ jobs: ref: gh-pages fetch-depth: 0 - - name: Delete directories older than 1 year + - name: Delete directories that don't have existing branch run: | - for dir in $(find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*'); do - if ! git log -1 --since="1 year ago" -- "$dir" | grep -q .; then - echo "Deleting $dir" - rm -rf "$dir" - fi + branches=( $(git branch -r | grep origin | grep -v HEAD | sed 's|origin/||') ) + declare -p branches + + find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*' | while read -r dir; do + path="${dir#./}" + + if [[ " ${branches[*]} " =~ " $path " ]]; then + continue + fi + + keep_parent=false + for branch in "${branches[@]}"; do + if [[ "$branch" == "$path/"* ]]; then + keep_parent=true + break + fi + done + + if ! $keep_parent; then + echo "Deleting $dir" + rm -rf "$dir" + fi done - name: Commit and push diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 921fba0a7..ae01c05f5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -29,10 +29,10 @@ Project maintainers are responsible for enforcing this code of conduct. They can ## Suggestions for newbies - Contributors are welcomed to use AI models as "help" in solving issues, but you must always double check the code that you're submitting. -- Refrain from excesive comments generated by AI. +- Refrain from excessive comments generated by AI. - Refrain from docs generated entirely by AI. - Always check what files you are committing and submitting to the PR when you are using any agent for help or an AI model. -- If you don't how to tackle a problem and AI can't help you, please just ask or look in Stack Overlflow, Google, Medium etc. +- If you don't know how to tackle a problem and AI can't help you, please just ask or look in Stack Overlflow, Google, Medium etc. - Learning how to code is fun and easier when using AI, but sometimes it might be just too much ... what are you going to learn, if AI does everything for you and you don't know what the code you are submitting actually does?! ## Scope diff --git a/Dockerfile b/Dockerfile index a0a3c597a..c9aa2cb23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,29 +3,39 @@ ARG NODE_VERSION=20-alpine FROM node:$NODE_VERSION AS base +# Setup pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable +RUN apk add --no-cache git + # Meta LABEL Description="Stremio Web" Vendor="Smart Code OOD" Version="1.0.0" RUN mkdir -p /var/www/stremio-web WORKDIR /var/www/stremio-web -# Install app dependencies -FROM base AS prebuild +# Setup app +FROM base AS app -RUN apk update && apk upgrade && \ - apk add --no-cache git -WORKDIR /var/www/stremio-web -COPY . . -RUN npm install -RUN npm run build +COPY package.json pnpm-lock.yaml /var/www/stremio-web +RUN pnpm i --frozen-lockfile -# Bundle app source -FROM base AS final +COPY . /var/www/stremio-web +RUN pnpm build -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 +# Setup server +FROM base AS server + +RUN pnpm i express@4 + +# Finalize +FROM base + +COPY http_server.js /var/www/stremio-web +COPY --from=server /var/www/stremio-web/node_modules /var/www/stremio-web/node_modules +COPY --from=app /var/www/stremio-web/build /var/www/stremio-web/build EXPOSE 8080 CMD ["node", "http_server.js"] diff --git a/README.md b/README.md index f84c3bb0c..b7cd999dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stremio - Freedom to Stream -![Build](https://github.com/stremio/stremio-web/workflows/Build/badge.svg?branch=development) +[![Build](https://github.com/Stremio/stremio-web/actions/workflows/build.yml/badge.svg)](https://github.com/Stremio/stremio-web/actions/workflows/build.yml) [![Github Page](https://img.shields.io/website?label=Page&logo=github&up_message=online&down_message=offline&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/development) 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. diff --git a/package.json b/package.json index c824ad411..284696840 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.49.4", + "@stremio/stremio-core-web": "0.50.0", "@stremio/stremio-icons": "5.7.1", - "@stremio/stremio-video": "0.0.62", + "@stremio/stremio-video": "0.0.64", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -41,7 +41,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", + "stremio-translations": "github:Stremio/stremio-translations#01aaa201e419782b26b9f2cbe4430795021426e5", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 121d0dbea..3f0659a55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,14 +18,14 @@ importers: specifier: 5.2.0 version: 5.2.0 '@stremio/stremio-core-web': - specifier: 0.49.4 - version: 0.49.4 + specifier: 0.50.0 + version: 0.50.0 '@stremio/stremio-icons': specifier: 5.7.1 version: 5.7.1 '@stremio/stremio-video': - specifier: 0.0.62 - version: 0.0.62 + specifier: 0.0.64 + version: 0.0.64 a-color-picker: specifier: 1.2.1 version: 1.2.1 @@ -90,8 +90,8 @@ importers: specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6 version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6 stremio-translations: - specifier: github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824 - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824 + specifier: github:Stremio/stremio-translations#01aaa201e419782b26b9f2cbe4430795021426e5 + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5 url: specifier: 0.11.4 version: 0.11.4 @@ -1302,14 +1302,14 @@ packages: '@stremio/stremio-colors@5.2.0': resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==} - '@stremio/stremio-core-web@0.49.4': - resolution: {integrity: sha512-K9LJGKXs8juV3pZOHH6thWTwOShAhjFt9bLL6K1VlORAe6AiieZ2uRp9wdOwFmPX+UgzWLIOd0r2aFXJ4OsJCw==} + '@stremio/stremio-core-web@0.50.0': + resolution: {integrity: sha512-SRE9nStgYNbhjJAw7mXfmM0wdnSLS4GMSJsSMTXvoGxnUgd+yisJUkN/9Sughe4t2IU7Uct8QWpdx9zFdlil+g==} '@stremio/stremio-icons@5.7.1': resolution: {integrity: sha512-Z96p36LLX3G+ewMnFKmNZVsO/AtcHA33WQ3wGOYFubxiYADPRAkcLVU5rHIfiGSC9IUaUVhxQWTPVB9ScY4Q5Q==} - '@stremio/stremio-video@0.0.62': - resolution: {integrity: sha512-lzm1sWLVN9Z3qr8mZm3MRLw7S9v7QYIe6swWava3Ao4sjhVc8jVlwzF5un0LcHxY8hQe0OjK7TDCvPRbw+spBQ==} + '@stremio/stremio-video@0.0.64': + resolution: {integrity: sha512-29w/lwU8BB6ai8LUyCnpRc2F9kPf7cpys40NCobt70MqBP/UqvYISsrnD/ijoBwvtpKdZ6ptv5h9BbDj6rrerw==} '@stylistic/eslint-plugin-jsx@4.4.1': resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==} @@ -4527,9 +4527,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824} - version: 1.44.12 + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5} + version: 1.44.13 string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -6561,13 +6561,13 @@ snapshots: '@stremio/stremio-colors@5.2.0': {} - '@stremio/stremio-core-web@0.49.4': + '@stremio/stremio-core-web@0.50.0': dependencies: '@babel/runtime': 7.24.1 '@stremio/stremio-icons@5.7.1': {} - '@stremio/stremio-video@0.0.62': + '@stremio/stremio-video@0.0.64': dependencies: buffer: 6.0.3 color: 4.2.3 @@ -10283,7 +10283,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/01aaa201e419782b26b9f2cbe4430795021426e5: {} string-length@4.0.2: dependencies: diff --git a/screenshots/board.png b/screenshots/board.png index 1fa83799a..8a54886f7 100644 Binary files a/screenshots/board.png and b/screenshots/board.png differ diff --git a/screenshots/discover.png b/screenshots/discover.png index eab8b5a2e..53f03a3e1 100644 Binary files a/screenshots/discover.png and b/screenshots/discover.png differ diff --git a/screenshots/metadetails.png b/screenshots/metadetails.png index 9aec4a302..ff0abe6f8 100644 Binary files a/screenshots/metadetails.png and b/screenshots/metadetails.png differ diff --git a/src/App/App.js b/src/App/App.js index 3e816be9f..7a1383dc4 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,11 +6,12 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common'); +const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); const { default: UpdaterBanner } = require('./UpdaterBanner'); +const { default: ShortcutsModal } = require('./ShortcutsModal'); const ErrorDialog = require('./ErrorDialog'); const withProtectedRoutes = require('./withProtectedRoutes'); const routerViewsConfig = require('./routerViewsConfig'); @@ -38,6 +39,14 @@ const App = () => { }; }, []); const [initialized, setInitialized] = React.useState(false); + const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false); + + const onShortcut = React.useCallback((name) => { + if (name === 'shortcuts') { + toggleShortcutModal(); + } + }, [toggleShortcutModal]); + React.useEffect(() => { let prevPath = window.location.hash.slice(1); const onLocationHashChange = () => { @@ -159,7 +168,8 @@ const App = () => { services.core.transport.dispatch({ action: 'Ctx', args: { - action: 'PullUserFromAPI' + action: 'PullUserFromAPI', + args: {} } }); services.core.transport.dispatch({ @@ -203,15 +213,20 @@ const App = () => { - - - - - + + { + shortcutModalOpen && + } + + + + + + diff --git a/src/App/ShortcutsModal/ShortcutsModal.tsx b/src/App/ShortcutsModal/ShortcutsModal.tsx new file mode 100644 index 000000000..5fec24837 --- /dev/null +++ b/src/App/ShortcutsModal/ShortcutsModal.tsx @@ -0,0 +1,59 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import Icon from '@stremio/stremio-icons/react'; +import { useShortcuts } from 'stremio/common'; +import { Button, ShortcutsGroup } from 'stremio/components'; +import styles from './styles.less'; + +type Props = { + onClose: () => void, +}; + +const ShortcutsModal = ({ onClose }: Props) => { + const { t } = useTranslation(); + const { grouped } = useShortcuts(); + + useEffect(() => { + const onKeyDown = ({ key }: KeyboardEvent) => { + key === 'Escape' && onClose(); + }; + + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); + }, []); + + return createPortal(( +
+
+ +
+
+
+ {t('SETTINGS_NAV_SHORTCUTS')} +
+ + +
+ +
+ { + grouped.map(({ name, label, shortcuts }) => ( + + )) + } +
+
+
+ ), document.body); +}; + +export default ShortcutsModal; diff --git a/src/App/ShortcutsModal/index.ts b/src/App/ShortcutsModal/index.ts new file mode 100644 index 000000000..5a7549fac --- /dev/null +++ b/src/App/ShortcutsModal/index.ts @@ -0,0 +1,2 @@ +import ShortcutsModal from './ShortcutsModal'; +export default ShortcutsModal; diff --git a/src/App/ShortcutsModal/styles.less b/src/App/ShortcutsModal/styles.less new file mode 100644 index 000000000..ebbc19c62 --- /dev/null +++ b/src/App/ShortcutsModal/styles.less @@ -0,0 +1,91 @@ +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.shortcuts-modal { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + + .backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: @color-background-dark5-40; + cursor: pointer; + } + + .container { + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 80%; + max-width: 80%; + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + box-shadow: var(--outer-glow); + overflow-y: auto; + + .header { + flex: none; + display: flex; + justify-content: space-between; + align-items: center; + height: 5rem; + padding-left: 2.5rem; + padding-right: 1rem; + + .title { + position: relative; + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + } + + .close-button { + position: relative; + width: 3rem; + height: 3rem; + padding: 0.5rem; + border-radius: var(--border-radius); + z-index: 2; + + .icon { + display: block; + width: 100%; + height: 100%; + color: var(--primary-foreground-color); + opacity: 0.4; + } + + &:hover, &:focus { + .icon { + opacity: 1; + color: var(--primary-foreground-color); + } + } + + &:focus { + outline-color: var(--primary-foreground-color); + } + } + } + + .content { + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 3rem; + padding: 0 2.5rem; + padding-bottom: 2rem; + overflow-y: auto; + } + } +} \ No newline at end of file diff --git a/src/App/styles.less b/src/App/styles.less index 373b46900..adce0ae5c 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -35,7 +35,7 @@ @top-overlay-size: 5.25rem; @bottom-overlay-size: 0rem; @overlap-size: 3rem; -@transparency-grandient-pad: 6rem; +@transparency-gradient-pad: 6rem; :root { --landscape-shape-ratio: 0.5625; @@ -69,7 +69,7 @@ --top-overlay-size: @top-overlay-size; --bottom-overlay-size: @bottom-overlay-size; --overlap-size: @overlap-size; - --transparency-grandient-pad: @transparency-grandient-pad; + --transparency-gradient-pad: @transparency-gradient-pad; --safe-area-inset-top: @safe-area-inset-top; --safe-area-inset-right: @safe-area-inset-right; --safe-area-inset-bottom: @safe-area-inset-bottom; diff --git a/src/common/FileDrop/FileDrop.tsx b/src/common/FileDrop/FileDrop.tsx index aae4e146b..2993991e7 100644 --- a/src/common/FileDrop/FileDrop.tsx +++ b/src/common/FileDrop/FileDrop.tsx @@ -42,7 +42,7 @@ const FileDropProvider = ({ className, children }: Props) => { .then((buffer) => { listeners .filter(([type]) => file.type ? type === file.type : isFileType(buffer, type)) - .forEach(([, listerner]) => listerner(file.name, buffer)); + .forEach(([, listener]) => listener(file.name, buffer)); }); } diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx new file mode 100644 index 000000000..532e9a409 --- /dev/null +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -0,0 +1,54 @@ +import React, { createContext, useCallback, useContext, useEffect } from 'react'; +import shortcuts from './shortcuts.json'; + +const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat(); + +export type ShortcutName = string; +export type ShortcutListener = () => void; + +interface ShortcutsContext { + grouped: ShortcutGroup[], +} + +const ShortcutsContext = createContext({} as ShortcutsContext); + +type Props = { + children: JSX.Element, + onShortcut: (name: ShortcutName) => void, +}; + +const ShortcutsProvider = ({ children, onShortcut }: Props) => { + const onKeyDown = useCallback(({ ctrlKey, shiftKey, key }: KeyboardEvent) => { + SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => { + const modifers = (keys.includes('Ctrl') ? ctrlKey : true) + && (keys.includes('Shift') ? shiftKey : true); + + if (modifers && keys.includes(key.toUpperCase())) { + onShortcut(name as ShortcutName); + } + })); + }, [onShortcut]); + + useEffect(() => { + document.addEventListener('keydown', onKeyDown); + + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); + + return ( + + {children} + + ); +}; + +const useShortcuts = () => { + return useContext(ShortcutsContext); +}; + +export { + ShortcutsProvider, + useShortcuts +}; diff --git a/src/common/Shortcuts/index.ts b/src/common/Shortcuts/index.ts new file mode 100644 index 000000000..f7fa38a18 --- /dev/null +++ b/src/common/Shortcuts/index.ts @@ -0,0 +1,5 @@ +import { ShortcutsProvider, useShortcuts } from './Shortcuts'; +export { + ShortcutsProvider, + useShortcuts, +}; diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json new file mode 100644 index 000000000..766288fb0 --- /dev/null +++ b/src/common/Shortcuts/shortcuts.json @@ -0,0 +1,89 @@ +[ + { + "name": "general", + "label": "SETTINGS_NAV_GENERAL", + "shortcuts": [ + { + "name": "navigateTabs", + "label": "SETTINGS_SHORTCUT_NAVIGATE_MENUS", + "combos": [["1", "2", "3", "4", "5", "6"]] + }, + { + "name": "navigateSearch", + "label": "SETTINGS_SHORTCUT_GO_TO_SEARCH", + "combos": [["0"]] + }, + { + "name": "fullscreen", + "label": "SETTINGS_SHORTCUT_FULLSCREEN", + "combos": [["F"]] + }, + { + "name": "exit", + "label": "SETTINGS_SHORTCUT_EXIT_BACK", + "combos": [["Escape"]] + }, + { + "name": "shortcuts", + "label": "SETTINGS_SHORTCUT_SHORTCUTS", + "combos": [["Ctrl", "/"]] + } + ] + }, + { + "name": "player", + "label": "SETTINGS_NAV_PLAYER", + "shortcuts": [ + { + "name": "playPause", + "label": "SETTINGS_SHORTCUT_PLAY_PAUSE", + "combos": [["Space"]] + }, + { + "name": "seekForward", + "label": "SETTINGS_SHORTCUT_SEEK_FORWARD", + "combos": [["ArrowRight"], ["Shift", "ArrowRight"]] + }, + { + "name": "seekBackward", + "label": "SETTINGS_SHORTCUT_SEEK_BACKWARD", + "combos": [["ArrowLeft"], ["Shift", "ArrowLeft"]] + }, + { + "name": "volumeUp", + "label": "SETTINGS_SHORTCUT_VOLUME_UP", + "combos": [["ArrowUp"]] + }, + { + "name": "volumeDown", + "label": "SETTINGS_SHORTCUT_VOLUME_DOWN", + "combos": [["ArrowDown"]] + }, + { + "name": "subtitlesSize", + "label": "SETTINGS_SHORTCUT_SUBTITLES_SIZE", + "combos": [["-"], ["="]] + }, + { + "name": "subtitlesDelay", + "label": "SETTINGS_SHORTCUT_SUBTITLES_DELAY", + "combos": [["G"], ["H"]] + }, + { + "name": "subtitlesMenu", + "label": "SETTINGS_SHORTCUT_MENU_SUBTITLES", + "combos": [["S"]] + }, + { + "name": "audioMenu", + "label": "SETTINGS_SHORTCUT_MENU_AUDIO", + "combos": [["A"]] + }, + { + "name": "infoMenu", + "label": "SETTINGS_SHORTCUT_MENU_INFO", + "combos": [["I"]] + } + ] + } +] \ No newline at end of file diff --git a/src/common/Shortcuts/types.d.ts b/src/common/Shortcuts/types.d.ts new file mode 100644 index 000000000..e4180616d --- /dev/null +++ b/src/common/Shortcuts/types.d.ts @@ -0,0 +1,11 @@ +type Shortcut = { + name: string, + label: string, + combos: string[][], +}; + +type ShortcutGroup = { + name: string, + label: string, + shortcuts: Shortcut[], +}; diff --git a/src/common/index.js b/src/common/index.js index 25df5c158..0b9cb252f 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -4,6 +4,7 @@ const { FileDropProvider, onFileDrop } = require('./FileDrop'); const { PlatformProvider, usePlatform } = require('./Platform'); const { ToastProvider, useToast } = require('./Toast'); const { TooltipProvider, Tooltip } = require('./Tooltips'); +const { ShortcutsProvider, useShortcuts } = require('./Shortcuts'); const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); @@ -35,6 +36,8 @@ module.exports = { onFileDrop, PlatformProvider, usePlatform, + ShortcutsProvider, + useShortcuts, ToastProvider, useToast, TooltipProvider, diff --git a/src/common/interfaceLanguages.json b/src/common/interfaceLanguages.json index d4eaa7f60..ce3504691 100644 --- a/src/common/interfaceLanguages.json +++ b/src/common/interfaceLanguages.json @@ -51,6 +51,10 @@ "name": "فارسی", "codes": ["fa-IR", "fas"] }, + { + "name": "Suomi", + "codes": ["fi-FI", "fin"] + }, { "name": "Français", "codes": ["fr-FR", "fre"] @@ -119,13 +123,17 @@ "name": "português", "codes": ["pt-PT", "por"] }, + { + "name": "Română", + "codes": ["ro-RO", "ron"] + }, { "name": "русский язык", "codes": ["ru-RU", "rus"] }, { - "name": "Svenska", - "codes": ["sv-SE", "swe"] + "name": "Slovenčina", + "codes": ["sk-SK", "slk"] }, { "name": "slovenski jezik", @@ -135,6 +143,10 @@ "name": "српски језик", "codes": ["sr-RS", "srp"] }, + { + "name": "Svenska", + "codes": ["sv-SE", "swe"] + }, { "name": "తెలుగు", "codes": ["te-IN", "tel"] diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 69cdcd494..8a1692254 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -10,11 +10,15 @@ const useFullscreen = () => { const [fullscreen, setFullscreen] = useState(false); - const requestFullscreen = useCallback(() => { + const requestFullscreen = useCallback(async () => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: true }); } else { - document.documentElement.requestFullscreen(); + try { + await document.documentElement.requestFullscreen(); + } catch (err) { + console.error('Error enabling fullscreen', err); + } } }, []); diff --git a/src/common/useNotifications.d.ts b/src/common/useNotifications.d.ts index e3cae5b81..bfe63d67b 100644 --- a/src/common/useNotifications.d.ts +++ b/src/common/useNotifications.d.ts @@ -1,2 +1,2 @@ -declare const useNotifcations: () => Notifications; -export = useNotifcations; +declare const useNotifications: () => Notifications; +export = useNotifications; diff --git a/src/components/BottomSheet/BottomSheet.less b/src/components/BottomSheet/BottomSheet.less index f7e3315e1..7fcc6e508 100644 --- a/src/components/BottomSheet/BottomSheet.less +++ b/src/components/BottomSheet/BottomSheet.less @@ -86,7 +86,7 @@ } } -@media only screen and (min-width: @small) and (orientation: portait) { +@media only screen and (min-width: @small) and (orientation: portrait) { .bottom-sheet { display: none; } diff --git a/src/components/ColorInput/ColorPicker/ColorPicker.js b/src/components/ColorInput/ColorPicker/ColorPicker.js index 823ea55c8..3b66fcdf4 100644 --- a/src/components/ColorInput/ColorPicker/ColorPicker.js +++ b/src/components/ColorInput/ColorPicker/ColorPicker.js @@ -21,7 +21,7 @@ const ColorPicker = ({ className, value, onInput }) => { showRGB: false, showAlpha: true }); - const pickerClipboard = pickerElementRef.current.querySelector('.a-color-picker-clipbaord'); + const pickerClipboard = pickerElementRef.current.querySelector('.a-color-picker-clipboard'); if (pickerClipboard instanceof HTMLElement) { pickerClipboard.tabIndex = -1; } diff --git a/src/components/ColorInput/ColorPicker/styles.less b/src/components/ColorInput/ColorPicker/styles.less index 7156d440d..536228ed1 100644 --- a/src/components/ColorInput/ColorPicker/styles.less +++ b/src/components/ColorInput/ColorPicker/styles.less @@ -16,7 +16,7 @@ box-shadow: 0 0 .2rem var(--color-surfacedark); } - :global(.a-color-picker-clipbaord) { + :global(.a-color-picker-clipboard) { pointer-events: none; } } \ No newline at end of file diff --git a/src/components/ContinueWatchingItem/index.js b/src/components/ContinueWatchingItem/index.js index 5d3b2dd76..2c50f3c22 100644 --- a/src/components/ContinueWatchingItem/index.js +++ b/src/components/ContinueWatchingItem/index.js @@ -1,5 +1,5 @@ // Copyright (C) 2017-2023 Smart code 203358507 -const ContineWatchingItem = require('./ContinueWatchingItem'); +const ContinueWatchingItem = require('./ContinueWatchingItem'); -module.exports = ContineWatchingItem; +module.exports = ContinueWatchingItem; diff --git a/src/components/MetaPreview/ActionButton/styles.less b/src/components/MetaPreview/ActionButton/styles.less index 5382c26af..c07b80fb9 100644 --- a/src/components/MetaPreview/ActionButton/styles.less +++ b/src/components/MetaPreview/ActionButton/styles.less @@ -66,6 +66,15 @@ .icon-container { width: 2rem; + + .icon { + width: 2rem; + height: 2rem; + } + } + + .label-container { + display: none; } } } \ No newline at end of file diff --git a/src/components/ShortcutsGroup/Combos/Combos.less b/src/components/ShortcutsGroup/Combos/Combos.less new file mode 100644 index 000000000..a862d54ca --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Combos.less @@ -0,0 +1,22 @@ +.combos { + position: relative; + display: flex; + overflow: visible; + + .combo { + position: relative; + display: flex; + overflow: visible; + + .separator { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 3.5rem; + font-size: 1rem; + color: var(--primary-foreground-color); + opacity: 0.6; + } + } +} \ No newline at end of file diff --git a/src/components/ShortcutsGroup/Combos/Combos.tsx b/src/components/ShortcutsGroup/Combos/Combos.tsx new file mode 100644 index 000000000..0168441bc --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Combos.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Keys from './Keys'; +import styles from './Combos.less'; + +type Props = { + combos: string[][], +}; + +const Combos = ({ combos }: Props) => { + const { t } = useTranslation(); + + return ( +
+ { + combos.map((keys, index) => ( +
+ + { + index < (combos.length - 1) && ( +
+ { t('SETTINGS_SHORTCUT_OR') } +
+ ) + } +
+ )) + } +
+ ); +}; + +export default Combos; diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.less b/src/components/ShortcutsGroup/Combos/Keys/Keys.less new file mode 100644 index 000000000..7bb8c76e7 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.less @@ -0,0 +1,26 @@ +kbd { + flex: none; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.5rem; + min-width: 2.5rem; + padding: 0 1rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + border-radius: 0.25em; + box-shadow: 0 4px 0 1px rgba(255, 255, 255, 0.1); + background-color: var(--overlay-color); +} + +.separator { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + font-size: 1rem; + color: var(--primary-foreground-color); +} \ No newline at end of file diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx new file mode 100644 index 000000000..71ec610da --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx @@ -0,0 +1,51 @@ +import React, { Fragment, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './Keys.less'; + +type Props = { + keys: string[], +}; + +const Keys = ({ keys }: Props) => { + const { t } = useTranslation(); + + const keyLabelMap: Record = useMemo(() => ({ + 'Shift': `⇧ ${t('SETTINGS_SHORTCUT_SHIFT')}`, + 'Space': t('SETTINGS_SHORTCUT_SPACE'), + 'Ctrl': t('SETTINGS_SHORTCUT_CTRL'), + 'Escape': t('SETTINGS_SHORTCUT_ESC'), + 'ArrowUp': '↑', + 'ArrowDown': '↓', + 'ArrowLeft': '←', + 'ArrowRight': '→', + }), [t]); + + const isRange = useMemo(() => { + return keys.length > 1 && keys.every((key) => !Number.isNaN(parseInt(key))); + }, [keys]); + + const filteredKeys = useMemo(() => { + return isRange ? [keys[0], keys[keys.length - 1]] : keys; + }, [keys, isRange]); + + return ( + filteredKeys.map((key, index) => ( + + + {keyLabelMap[key] ?? key.toUpperCase()} + + { + index < (filteredKeys.length - 1) && ( +
+ { + isRange ? t('SETTINGS_SHORTCUT_TO') : '+' + } +
+ ) + } +
+ )) + ); +}; + +export default Keys; diff --git a/src/components/ShortcutsGroup/Combos/Keys/index.ts b/src/components/ShortcutsGroup/Combos/Keys/index.ts new file mode 100644 index 000000000..ba8d58731 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/index.ts @@ -0,0 +1,2 @@ +import Keys from './Keys'; +export default Keys; diff --git a/src/components/ShortcutsGroup/Combos/index.ts b/src/components/ShortcutsGroup/Combos/index.ts new file mode 100644 index 000000000..c66667f91 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/index.ts @@ -0,0 +1,2 @@ +import Combos from './Combos'; +export default Combos; diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.less b/src/components/ShortcutsGroup/ShortcutsGroup.less new file mode 100644 index 000000000..f0fdd975c --- /dev/null +++ b/src/components/ShortcutsGroup/ShortcutsGroup.less @@ -0,0 +1,44 @@ +.shortcuts-group { + flex: 1 1 0; + position: relative; + min-width: 30rem; + display: flex; + flex-direction: column; + gap: 2rem; + overflow: visible; + + .title { + flex: none; + display: flex; + font-size: 1rem; + font-weight: 400; + color: var(--primary-foreground-color); + opacity: 0.6; + } + + .shortcuts { + position: relative; + display: flex; + flex-direction: column; + gap: 2rem; + overflow: visible; + + .shortcut { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; + overflow: visible; + + .label { + position: relative; + font-size: 1rem; + color: var(--primary-foreground-color); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } +} diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.tsx b/src/components/ShortcutsGroup/ShortcutsGroup.tsx new file mode 100644 index 000000000..069d5d1e8 --- /dev/null +++ b/src/components/ShortcutsGroup/ShortcutsGroup.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import Combos from './Combos'; +import styles from './ShortcutsGroup.less'; + +type Props = { + className?: string, + label: string, + shortcuts: Shortcut[], +}; + +const ShortcutsGroup = ({ className, label, shortcuts }: Props) => { + const { t } = useTranslation(); + + return ( +
+
+ {t(label)} +
+ +
+ { + shortcuts.map(({ name, label, combos }) => ( +
+
+ {t(label)} +
+ +
+ )) + } +
+
+ ); +}; + +export default ShortcutsGroup; diff --git a/src/components/ShortcutsGroup/index.ts b/src/components/ShortcutsGroup/index.ts new file mode 100644 index 000000000..11f8d0678 --- /dev/null +++ b/src/components/ShortcutsGroup/index.ts @@ -0,0 +1,2 @@ +import ShortcutsGroup from './ShortcutsGroup'; +export default ShortcutsGroup; diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 229fe758d..21415153f 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -12,11 +12,12 @@ const useProfile = require('stremio/common/useProfile'); const VideoPlaceholder = require('./VideoPlaceholder'); const styles = require('./styles'); -const Video = React.forwardRef(({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }, ref) => { +const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, selected, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => { const routeFocused = useRouteFocused(); const profile = useProfile(); const { t } = useTranslation(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + const popupLabelOnMouseUp = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented) { if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) { @@ -68,27 +69,19 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo } } }, [deepLinks]); - const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ref: popupRef, ...props }) { + const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ref, ...props }) { const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched; - const handleRef = React.useCallback((node) => { - if (popupRef) { - if (typeof popupRef === 'function') { - popupRef(node); - } else { - popupRef.current = node; - } - } - if (ref) { - if (typeof ref === 'function') { - ref(node); - } else { - ref.current = node; - } - } - }, [popupRef]); + + React.useEffect(() => { + selected && !watched && ref.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start' + }); + }, [selected]); return ( - ); - }, []); + }, [selected]); const renderMenu = React.useMemo(() => function renderMenu() { return (
@@ -203,7 +196,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo renderMenu={renderMenu} /> ); -}); +}; Video.Placeholder = VideoPlaceholder; @@ -220,6 +213,7 @@ Video.propTypes = { progress: PropTypes.number, scheduled: PropTypes.bool, seasonWatched: PropTypes.bool, + selected: PropTypes.bool, deepLinks: PropTypes.shape({ metaDetailsStreams: PropTypes.string, player: PropTypes.string diff --git a/src/components/index.ts b/src/components/index.ts index a5638007e..a47c2c709 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -25,6 +25,7 @@ import RadioButton from './RadioButton'; import SearchBar from './SearchBar'; import SharePrompt from './SharePrompt'; import Slider from './Slider'; +import ShortcutsGroup from './ShortcutsGroup'; import TextInput from './TextInput'; import Toggle from './Toggle'; import Transition from './Transition'; @@ -59,6 +60,7 @@ export { SearchBar, SharePrompt, Slider, + ShortcutsGroup, TextInput, Toggle, Transition, diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index 9e6228490..afd2a3889 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -2,6 +2,25 @@ @import (reference) '~stremio/common/screen-sizes.less'; +.disable-cell-items() { + .cell { + .items { + .item { + pointer-events: none; + } + } + } +} + +.compact-items() { + .cell { + .items { + padding: 1px; + gap: 0.15rem; + } + } +} + .cell { position: relative; display: flex; @@ -27,12 +46,9 @@ } .heading { - flex: none; position: relative; - height: 3rem; display: flex; - align-items: center; - padding: 0 1rem; + align-items: flex-start; .day { flex: none; @@ -50,12 +66,15 @@ } .items { - flex: 0 1 10rem; position: relative; display: flex; flex-direction: row; - gap: 1rem; - padding: 0 0.5rem 0.5rem 0.5rem; + gap: 0.2rem; + padding: 0.1rem; + flex: 1 1 60%; + overflow-x: auto; + overflow-y: hidden; + min-width: 0; .item { flex: none; @@ -64,7 +83,9 @@ justify-content: center; height: 100%; aspect-ratio: 2 / 3; - border-radius: var(--border-radius); + border-radius: calc(var(--border-radius) / 2); + max-height: 100%; + max-width: 100%; .icon { flex: none; @@ -80,13 +101,11 @@ } .poster { - flex: auto; - z-index: 0; - position: relative; - height: 100%; - width: 100%; + height: auto; + max-height: 100%; + aspect-ratio: 2 / 3; object-fit: cover; - opacity: 1; + border-radius: inherit } .icon, .poster { @@ -117,8 +136,11 @@ &.today { .heading { + padding: 0.3rem; .day { background-color: var(--primary-accent-color); + height: 1.5rem; + width: 1.5rem; } } } @@ -134,56 +156,55 @@ } } -@media only screen and (max-height: @minimum) and (orientation: portrait) { - .cell { - .heading { - justify-content: center; - } - - .items { - display: none; - } - - .more { - display: flex; - } - } +@media only screen and (max-width: @minimum) { + .disable-cell-items(); } -@media only screen and (max-height: @xxsmall) and (orientation: landscape) { +@media @phone-portrait { + .cell { + flex-direction: column; + display: grid; + } + .compact-items(); + .disable-cell-items(); +} + +@media @phone-landscape { .cell { flex-direction: row; - align-items: center; - - .items { - display: none; - } - - .more { - display: flex; - } } + .compact-items(); + .disable-cell-items(); } -@media only screen and (max-height: @xsmall) and (max-width: @xsmall) { +@media only screen and (max-height: @medium) and (max-width: @medium) and (orientation: landscape) { .cell { gap: 0; .heading { - height: 2rem; - .day { + padding: 0; font-size: 0.875rem; } } .items { - padding: 0.25rem; - - .item { - pointer-events: none; - border-radius: calc(var(--border-radius) / 2); - } + width: 100%; + padding-left: 0.5rem; } } -} \ No newline at end of file +} + +@media only screen and (max-width: @minimum) and (orientation: portrait) and (pointer: fine) { + .cell { + display: flex; + + .heading { + flex: 1 1 33%; + } + } +} + +@media screen and (max-width: @small) and (orientation: portrait) { + .disable-cell-items(); +} diff --git a/src/routes/Calendar/Table/Table.less b/src/routes/Calendar/Table/Table.less index 65a9b01e9..14fc89f7c 100644 --- a/src/routes/Calendar/Table/Table.less +++ b/src/routes/Calendar/Table/Table.less @@ -45,6 +45,7 @@ display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; + grid-auto-rows: 1fr; } } diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 811c4859c..f04302fc7 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -387,7 +387,7 @@ const Intro = ({ queryParams }) => { { state.form === SIGNUP_FORM ? : null @@ -395,7 +395,7 @@ const Intro = ({ queryParams }) => { { state.form === LOGIN_FORM ? : null @@ -403,7 +403,7 @@ const Intro = ({ queryParams }) => { { state.form === SIGNUP_FORM ? : null diff --git a/src/routes/Intro/styles.less b/src/routes/Intro/styles.less index 32fc73d79..31a09d54c 100644 --- a/src/routes/Intro/styles.less +++ b/src/routes/Intro/styles.less @@ -101,10 +101,6 @@ color: var(--primary-foreground-color); text-align: center; } - - .uppercase { - text-transform: uppercase; - } } .submit-button, .guest-login-button, .signup-form-button, .login-form-button { diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index d806ffed5..fd27478b5 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -190,6 +190,7 @@ const MetaDetails = ({ urlParams, queryParams }) => { metaItem={metaDetails.metaItem} libraryItem={metaDetails.libraryItem} season={season} + selectedVideoId={metaDetails.libraryItem?.state?.video_id} seasonOnSelect={seasonOnSelect} toggleNotifications={toggleNotifications} /> diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 88d53d427..8891947db 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -11,9 +11,10 @@ const SeasonsBar = require('./SeasonsBar'); const { default: EpisodePicker } = require('../EpisodePicker'); const styles = require('./styles'); -const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { +const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, selectedVideoId, toggleNotifications }) => { const { core } = useServices(); const profile = useProfile(); + const showNotificationsToggle = React.useMemo(() => { return metaItem?.content?.content?.inLibrary && metaItem?.content?.content?.videos?.length; }, [metaItem]); @@ -178,6 +179,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, deepLinks={video.deepLinks} scheduled={video.scheduled} seasonWatched={seasonWatched} + selected={video.id === selectedVideoId} onMarkVideoAsWatched={onMarkVideoAsWatched} onMarkSeasonAsWatched={onMarkSeasonAsWatched} /> @@ -195,6 +197,7 @@ VideosList.propTypes = { metaItem: PropTypes.object, libraryItem: PropTypes.object, season: PropTypes.number, + selectedVideoId: PropTypes.string, seasonOnSelect: PropTypes.func, toggleNotifications: PropTypes.func, }; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 436f1a781..bcd973267 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,7 +8,7 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell } = require('stremio/common'); +const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform } = require('stremio/common'); const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); @@ -43,6 +43,7 @@ const Player = ({ urlParams, queryParams }) => { const statistics = useStatistics(player, streamingServer); const video = useVideo(); const routeFocused = useRouteFocused(); + const platform = usePlatform(); const toast = useToast(); const [seeking, setSeeking] = React.useState(false); @@ -345,6 +346,8 @@ const Player = ({ urlParams, queryParams }) => { forceTranscoding: forceTranscoding || casting, maxAudioChannels: settings.surroundSound ? 32 : 2, hardwareDecoding: settings.hardwareDecoding, + videoMode: settings.videoMode, + platform: platform.name, streamingServerURL: streamingServer.baseUrl ? casting ? streamingServer.baseUrl @@ -532,6 +535,53 @@ const Player = ({ urlParams, queryParams }) => { } }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); + // Media Session PlaybackState + React.useEffect(() => { + if (!navigator.mediaSession) return; + + const playbackState = !video.state.paused ? 'playing' : 'paused'; + navigator.mediaSession.playbackState = playbackState; + + return () => navigator.mediaSession.playbackState = 'none'; + }, [video.state.paused]); + + // Media Session Metadata + React.useEffect(() => { + if (!navigator.mediaSession) return; + + const metaItem = player.metaItem && player.metaItem?.type === 'Ready' ? player.metaItem.content : null; + const videoId = player.selected ? player.selected?.streamRequest?.path?.id : null; + const video = metaItem ? metaItem.videos.find(({ id }) => id === videoId) : null; + + const videoInfo = video && video.season && video.episode ? ` (${video.season}x${video.episode})`: null; + const videoTitle = video ? `${video.title}${videoInfo}` : null; + const metaTitle = metaItem ? metaItem.name : null; + const imageUrl = metaItem ? metaItem.logo : null; + + const title = videoTitle ?? metaTitle; + const artist = videoTitle ? metaTitle : undefined; + const artwork = imageUrl ? [{ src: imageUrl }] : undefined; + + if (title) { + navigator.mediaSession.metadata = new MediaMetadata({ + title, + artist, + artwork, + }); + } + }, [player.metaItem, player.selected]); + + // Media Session Actions + React.useEffect(() => { + if (!navigator.mediaSession) return; + + navigator.mediaSession.setActionHandler('play', onPlayRequested); + navigator.mediaSession.setActionHandler('pause', onPauseRequested); + + const nexVideoCallback = player.nextVideo ? onNextVideoRequested : null; + navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback); + }, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]); + React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index 299c37b23..d5b13e11f 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { useMemo, useCallback, useState, forwardRef, memo, useRef } from 'react'; +import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react'; import classNames from 'classnames'; import Icon from '@stremio/stremio-icons/react'; import { useServices } from 'stremio/services'; @@ -21,7 +21,8 @@ type Props = { const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => { const { core } = useServices(); const [season, setSeason] = useState(seriesInfo?.season); - const selectedVideoRef = useRef(null); + const [selectedVideoId, setSelectedVideoId] = useState(null); + const metaItem = useMemo(() => { return seriesInfo ? { @@ -78,11 +79,9 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa event.stopPropagation(); }; - const onTransitionEnd = () => { - selectedVideoRef.current?.scrollIntoView({ - behavior: 'smooth', - }); - }; + const onTransitionEnd = useCallback(() => { + setSelectedVideoId(selected); + }, [selected]); return (
@@ -114,7 +113,6 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa {videos.map((video, index) => (