Compare commits

...

234 commits

Author SHA1 Message Date
Tim
da675cd56c chore: update caniuse
Some checks failed
Build / build (push) Has been cancelled
2026-01-10 01:12:20 +01:00
Tim
9b3b0d67ba
Merge pull request #1095 from Stremio/feat/player-mute-shortcut-2
Player: Add mute shortcut
2026-01-10 01:02:18 +01:00
Tim
fc2d906a42 Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-mute-shortcut-2 2026-01-10 00:56:47 +01:00
Tim
c15ca17d2d
Merge pull request #1097 from Stremio/refactor/player-shortcuts
Dev: use shortcuts provider on player
2026-01-09 23:21:16 +01:00
Timothy Z.
55963fd23e
Merge pull request #1106 from Stremio/chore/align-error-styles-across-app
Some checks failed
Build / build (push) Has been cancelled
App: update & align error color styles
2025-12-31 17:33:05 +02:00
Timothy Z.
80066b2f3f chore: update styles after trakt icon change 2025-12-31 17:31:37 +02:00
Timothy Z.
c8dfc31e6b
Merge pull request #1100 from PL7963/development
Some checks failed
Build / build (push) Has been cancelled
MetaDetails: Add missing backdrop filter to ratings
2025-12-26 20:01:13 +01:00
Coolkie
84a172d1bf fix(Ratings): move backdrop filter to ratings container 2025-12-26 08:24:04 +00:00
Coolkie
6fbc08a720 fix(Ratings): add backdrop filter to icon container 2025-12-25 17:57:57 +00:00
Tim
2bc0f3468c chore: update translations 2025-12-18 16:11:53 +01:00
Tim
c9a40aabd7 refactor: use shortcuts provider on player 2025-12-18 13:46:05 +01:00
Lachezar Lechev
7046622fb6
feat: player - mute shortcut
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-12-17 14:15:06 +02:00
Timothy Z.
5dc088b798
Merge pull request #1094 from Stremio/fix/selected-video-styles
Some checks failed
Build / build (push) Has been cancelled
SideDrawer: Always show selected video border
2025-12-16 12:05:26 +02:00
Timothy Z.
b5bd75fd94 Update styles.less 2025-12-16 11:47:58 +02:00
Timothy Z.
16b2eb8d17 chore: revert change 2025-12-16 11:47:24 +02:00
Botzy
c4ab2dc546 fix(Video): always show border of selected video 2025-12-16 11:00:04 +02:00
Timothy Z.
227f21c10f
Merge pull request #1092 from Stremio/fix/trakt-logo
Some checks are pending
Build / build (push) Waiting to run
Settings: update trakt logo styling
2025-12-15 18:54:51 +02:00
Timothy Z.
d21be690de chore: correct size 2025-12-15 17:29:07 +02:00
dexter21767-dev
6c7a2755fb update trakt logo styling 2025-12-15 16:17:49 +01:00
Timothy Z.
bfb5c484fc
Merge pull request #1079 from sagarchaulagai/development
Some checks failed
Build / build (push) Has been cancelled
Settings: Fix incorrect tab highlighting
2025-12-10 11:52:13 +02:00
Sagar Prasad Chaulagain
88fca500f1 fixes #1078 2025-12-09 08:48:12 +05:45
Timothy Z.
058bb58bfb
Merge pull request #1084 from Stremio/fix/addons-selectable-inputs
Some checks failed
Build / build (push) Has been cancelled
[Addons]: Fix default title for addons type select
2025-12-08 17:36:08 +02:00
Botzy
9a9cd2de12 fix: default title for addon type select 2025-12-08 17:06:50 +02:00
Timothy Z.
4881f2c340
Merge pull request #1082 from Stremio/fix/streaming-server-warning
Some checks are pending
Build / build (push) Waiting to run
Fix: Show Streaming Server warning correctly
2025-12-08 10:24:07 +01:00
Botzy
a744932949 fix: correct check for showing streaming server warning 2025-12-03 16:53:00 +02:00
Sagar Prasad Chaulagain
8148a2f8fe fixes #1078 2025-12-01 13:23:09 +05:45
Sagar Prasad Chaulagain
6aef6e1d04 Added small tolerance of 10px, fixes #1078 2025-12-01 13:13:55 +05:45
Timothy Z.
9cbfd15793 chore: bump v5.0.0-beta.29
Some checks failed
Build / build (push) Has been cancelled
bug fix release
2025-11-28 16:03:08 +02:00
Tim
292cd9d03e Merge branch 'development' of https://github.com/Stremio/stremio-web into development 2025-11-28 14:54:57 +01:00
Tim
cb74f3be65 fix(Settings): scroll sections error 2025-11-28 14:54:43 +01:00
Timothy Z.
f688a11751 chore: bump core v0.51.1 2025-11-28 15:46:39 +02:00
Tim
1f93175e98
Merge pull request #1077 from Stremio/fix/settings-shortcuts-overflow
Settings: Fix shortcuts layout issue on mobile
2025-11-28 14:20:03 +01:00
Tim
4b10795113 fix(Settings): shortcuts layout issue on mobile 2025-11-28 14:02:10 +01:00
Tim
aa571a7f8f
Merge pull request #1076 from Stremio/refactor/settings-remove-shortcuts-mobile
Settings: Remove shortcuts section on mobile
2025-11-28 13:34:01 +01:00
Tim
5e278a5244 refactor(Settings): remove shortcuts on mobile 2025-11-28 13:01:56 +01:00
Tim
17746db439 ci(release): add pnpm setup
Some checks are pending
Build / build (push) Waiting to run
2025-11-27 17:45:41 +01:00
Timothy Z.
d86bc3bbd9 chore: v5.0.0-beta.28 2025-11-27 18:27:28 +02:00
Timothy Z.
199b00b290
Merge pull request #1054 from Stremio/fix/player-next-video-behaviour
Player: next video behavior
2025-11-27 18:25:47 +02:00
Timothy Z.
57b2632486 correct bingewatching use 2025-11-27 18:24:42 +02:00
Timothy Z.
12c36f4df3
Merge pull request #998 from Stremio/feat/stream-converted-source
Feat/stream converted source
2025-11-27 17:26:32 +02:00
Timothy Z.
135ca80bd3
Merge pull request #1074 from Stremio/fix/player-slider-thumb-movement
Player: Fix Slider thumb movement
2025-11-27 17:26:09 +02:00
Timothy Z.
a037afd983 fix(Slider): thumb movement 2025-11-27 16:42:20 +02:00
Timothy Z.
0c833330a1
Merge pull request #1059 from Stremio/feat/details-selected-video-styles
Some checks are pending
Build / build (push) Waiting to run
Details: improve selected video logic
2025-11-27 16:26:00 +02:00
Timothy Z.
074daeeae8 Update pnpm-lock.yaml 2025-11-27 14:51:13 +02:00
Timothy Z.
5fe0353be5
Merge branch 'development' into fix/player-next-video-behaviour 2025-11-27 14:49:15 +02:00
Lachezar Lechev
3c8d62f3b6
fix: Player - video.load hook needs player.stream
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-11-27 13:03:29 +02:00
Lachezar Lechev
924cd715d2
chore: bump stremio-core-web to 0.51.0
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-11-27 12:52:40 +02:00
Lachezar Lechev
4c407392dd
fix(player): options video - download video should prefer downloadUrl instead of streamingUrl
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-11-27 12:14:02 +02:00
Lachezar Lechev
5f1841bfb8
fix: player and useStatistics - use player.stream
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-11-26 21:10:23 +02:00
Lachezar Lechev
4fba2a3770
Merge branch 'development' into feat/stream-converted-source 2025-11-26 20:01:19 +02:00
Tim
3bb7fc4dcc fix: meta preview import
Some checks failed
Build / build (push) Has been cancelled
2025-11-25 12:14:09 +01:00
Tim
c7ccf39cfd
Merge pull request #1071 from Stremio/chore/update-icons
Chore: Update icons
2025-11-25 12:09:29 +01:00
Tim
b66c3654e5
Merge pull request #1070 from Stremio/dependabot/github_actions/actions/checkout-6
chore(deps): bump actions/checkout from 5 to 6
2025-11-25 12:08:11 +01:00
Tim
1fec4bd0c5
Merge pull request #1069 from Stremio/dependabot/github_actions/svenstaro/upload-release-action-2.11.3
chore(deps): bump svenstaro/upload-release-action from 2.11.2 to 2.11.3
2025-11-25 12:07:55 +01:00
Botzy
67300d0159 update stremio icons 2025-11-25 12:21:39 +02:00
dependabot[bot]
9d81069398
chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 21:21:50 +00:00
dependabot[bot]
3017af0df9
chore(deps): bump svenstaro/upload-release-action from 2.11.2 to 2.11.3
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.11.2 to 2.11.3.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.11.2...2.11.3)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-version: 2.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 21:21:45 +00:00
Tim
eac24c0360
Merge pull request #1067 from Stremio/fix/meta-preview-links-label
Some checks failed
Build / build (push) Has been cancelled
fix: links label on meta preview
2025-11-21 10:08:57 +01:00
Tim
af225b0135 fix: links label on meta preview 2025-11-21 10:00:53 +01:00
Timothy Z.
c3f1f6c911
Merge pull request #1057 from Stremio/fix/trailer-player-crashes
Some checks failed
Build / build (push) Has been cancelled
Player: prevent crash when destroying null video
2025-11-07 12:52:35 +02:00
Timothy Z.
c0bc34eb40 fix(Player): binge disabled navigation back 2025-10-31 16:42:53 +02:00
Timothy Z.
75804bac10 refactor(Video): add scroll-margin 2025-10-30 18:21:40 +02:00
Timothy Z.
a9c77da3c4 feat(Details): improve selected video logic 2025-10-30 18:01:44 +02:00
Timothy Z.
b876c920fc fix: useVideo check current before destroying 2025-10-30 11:28:51 +02:00
Tim
536be36005 build: fix Dockerfile
Some checks failed
Build / build (push) Has been cancelled
2025-10-27 16:21:50 +01:00
Timothy Z.
000d5be639
Merge pull request #974 from Stremio/feat/details-scroll-to-last-watched-video
Some checks are pending
Build / build (push) Waiting to run
Details: Scroll to last watched video
2025-10-27 16:49:23 +02:00
Tim
00ebd6c4d0 fix(Player): media session trailers crash 2025-10-27 15:46:44 +01:00
Lachezar Lechev
7456e8f15a
chore: revert space added by formatter 2025-10-27 15:21:45 +02:00
Lachezar Lechev
04e6780395
chore: remove package-lock.json 2025-10-27 15:20:33 +02:00
Timothy Z.
e316b07649 refactor(Video): add !watched check 2025-10-27 12:41:05 +02:00
Timothy Z.
8ab582080d fix(Video): content shifts during scroll 2025-10-27 12:31:45 +02:00
Tim
a35f7e7878
Merge pull request #1039 from Stremio/fix/update-dockerfile-pnpm
Some checks failed
Build / build (push) Has been cancelled
Dev: Update Dockerfile
2025-10-24 13:48:56 +02:00
Tim
1b6f4d09d3 build: update Dockerfile 2025-10-24 13:44:00 +02:00
Lachezar Lechev
3579a99df3
fix: settings - player - keep next video popup enabled regardless of bingeWatching setting
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-24 10:24:39 +03:00
Lachezar Lechev
3d163cf440
chore: player - clean up and use handleNextVideoNavigation
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-24 10:23:58 +03:00
Lachezar Lechev
32cf4cc12e
fix: package-lock.json & pnpm-lock.yaml - core bumped fix build
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-24 09:39:37 +03:00
Lachezar Lechev
54bcdf6360
Merge branch 'development' into fix/player-next-video-behaviour 2025-10-24 09:34:15 +03:00
Lachezar Lechev
1b15f0b5f1
fix: player - popup for next video should show up on disabled binge_watching
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-24 09:28:28 +03:00
Tim
cbc708bc64 chore: add missing interface languages
Some checks are pending
Build / build (push) Waiting to run
2025-10-23 20:41:44 +02:00
Timothy Z.
16877fa4bf Merge branch 'development' into feat/details-scroll-to-last-watched-video 2025-10-23 17:05:46 +03:00
Tim
af806bbfb1
Merge pull request #1043 from Stremio/feat/video-mode-setting
Some checks are pending
Build / build (push) Waiting to run
Settings: Add player video mode
2025-10-23 16:01:31 +02:00
Tim
6e65fa03d8 chore: update video 2025-10-23 15:59:34 +02:00
Tim
e3c4bc14bb Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/video-mode-setting 2025-10-23 15:58:32 +02:00
Tim
5969bc9251
Merge pull request #1041 from Stremio/feat/shortcuts-modal
App: Add shortcuts modal
2025-10-23 15:57:34 +02:00
Tim
cf93c2dcbe chore: update translations 2025-10-23 15:55:39 +02:00
Timothy Z.
c416971d22
Merge pull request #1009 from actuallylost/chore/typos
chore: fix all typos and misspellings
2025-10-23 16:51:45 +03:00
Timothy Z.
72aa110d48
Merge pull request #1036 from v1ctorsales/fix/calendar-thumbnails
Calendar: Poster visibility improvements
2025-10-23 16:44:08 +03:00
Timothy Z.
309956b237
Merge pull request #1002 from ASiD-0/fix/#1000
Intro: make all text lowercase to match the rest
2025-10-23 16:35:02 +03:00
Lachezar Lechev
2f566f8626
chore: bump core to the fix branch
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-23 10:16:50 +03:00
Lachezar Lechev
20c7ba672a
fix: player - redirect to next video player deeplink only if bingeWatching is enabled, else go to stream list
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-23 10:05:35 +03:00
Tim
00ae74e9af
Merge pull request #994 from Stremio/chore/update-pull-user-from-api-action
Some checks failed
Build / build (push) Has been cancelled
Dev: Update PullUserFromAPI core action
2025-10-22 13:44:00 +02:00
Tim
8c8d3376db chore: update core 2025-10-22 13:41:51 +02:00
Tim
7b2e5305e0 Merge branch 'development' of https://github.com/Stremio/stremio-web into chore/update-pull-user-from-api-action 2025-10-22 13:41:11 +02:00
Tim
585a84ccd6
Merge pull request #1053 from Stremio/dependabot/github_actions/actions/setup-node-6
Some checks failed
Build / build (push) Has been cancelled
chore(deps): bump actions/setup-node from 5 to 6
2025-10-20 23:46:50 +02:00
dependabot[bot]
e7b0a1d1be
chore(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 21:13:21 +00:00
Victor Sales
eb61ad6943 fix: remove unused mediaquery 2025-10-17 18:20:31 +03:00
Victor Sales
18617b32c9 refactor: reduce duplicated CSS using less variables 2025-10-17 17:45:53 +03:00
Victor Sales
0433da66c1 fix: point event none for tablets on portrait mode 2025-10-16 19:59:00 +03:00
Tim
2776741e8c feat(Player): pass platform name to video 2025-10-16 15:21:28 +02:00
Victor Sales
3e0308dff1 fix: align banners with day for small desktops 2025-10-15 20:45:46 +03:00
Victor Sales
4361792cae fix: adapt items display for mobile landscape 2025-10-15 20:36:13 +03:00
Victor Sales
83752eb647 fix(calendar): adaptive display and style fixes 2025-10-14 18:34:43 +03:00
Tim
5c3b2b0b22 refactor(Shortcuts): use json to declare shortcuts 2025-10-14 17:22:08 +02:00
Tim
0143bf914c feat: add video mode setting 2025-10-14 16:48:37 +02:00
Tim
cf73c7942d
Merge pull request #1045 from Stremio/fix/use-fullscreen
Some checks failed
Build / build (push) Has been cancelled
fix: useFullscreen - catch exception on Firefox when using F shortcut
2025-10-14 11:03:38 +02:00
Lachezar Lechev
91fbfc1178
fix: useFullscreen - catch exception on Firefox when using keyboard F shortcut in web
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-14 12:00:43 +03:00
Lachezar Lechev
56b60beedb
fix: useFullscreen - catch exception on Firefox when using keyboard F shortcut in web
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-14 11:39:28 +03:00
Victor Sales
d2d28be6de style(responsive): add @phone-landscape media query 2025-10-13 16:27:15 +03:00
Tim
a97dd01869 refactor(shortcuts): use Ctrl + / for shortcuts modal 2025-10-13 12:55:12 +02:00
Victor Sales
e74072ebd5 fix(calendar): disable banner click in phone-portrait mode 2025-10-13 13:40:13 +03:00
Tim
9923152de7
Merge pull request #1014 from Stremio/feat/player-media-session
Player: Add media session support
2025-10-13 12:35:55 +02:00
Tim
3eff7f0903 refactor(Player): use poster for media session artwork 2025-10-13 12:33:46 +02:00
Tim
122e43dbe5 refactor(Player): remove handling of media keys 2025-10-13 12:26:42 +02:00
Tim
910242b201 Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-media-session 2025-10-13 12:25:56 +02:00
Victor Sales
2e1ad64d02 refactor(calendar): replace fixed width with max-width for better banner scaling 2025-10-13 13:06:52 +03:00
Tim
4860a028c2 chore: update translations 2025-10-13 11:29:16 +02:00
Tim
9e99a2b308
Merge pull request #1042 from PeterDaveHello/patch-1
Fix GitHub Actions badge in README
2025-10-12 20:55:41 +02:00
Peter Dave Hello
62a650018b
Fix GitHub Actions badge in README 2025-10-13 01:14:13 +08:00
Victor Sales
fb9497a856 feat(calendar): redesign calendar cell layout for responsiveness and banner support 2025-10-12 18:23:34 +03:00
Victor Sales
539a7ebc10 fix(calendar): align day and more indicator inline in narrow desktop viewports 2025-10-12 13:41:42 +03:00
Tim
9fa0e46423 feat: add shortcuts modal 2025-10-12 12:06:59 +02:00
Victor Sales
b40ef9f3dc fix(calendar): redesign cell layout with rows for desktop 2025-10-12 00:58:36 +03:00
Timothy Z.
b05f28cc54 fix(docker): update dockerfile with pnpm 2025-10-12 00:44:39 +03:00
Victor Sales
f2c7382729 fix(calendar): apply grid-auto-rows 1fr for equal row height 2025-10-11 22:55:26 +03:00
Victor Sales
a3a7e14d15 chore(calendar): remove duplicated aspect-ratio rule from CSS 2025-10-11 16:26:56 +03:00
Victor Sales
c35c7c06e9 style(calendar): normalize indentation and align with project style 2025-10-11 16:22:27 +03:00
Victor Sales
d8904bdb5a style(calendar): normalize indentation to match project formatting 2025-10-11 16:17:10 +03:00
Victor Sales
06365262d1 fix(calendar): improve poster visibility and responsive scaling 2025-10-11 16:05:14 +03:00
Timothy Z.
b7863bf319
Merge branch 'development' into chore/typos 2025-10-11 15:02:32 +03:00
Timothy Z.
2dcc582cc2
Merge pull request #1035 from Stremio/refactor/meta-preview-action-buttons-styles
MetaPreview: hide ActionButton label on mobile
2025-10-10 19:41:04 +03:00
Timothy Z.
d832a9c136 refactor(ActionButton): hide labels on mobile 2025-10-10 18:22:06 +03:00
Timothy Z.
4a1b0d3287
Merge pull request #1030 from arkia09/screenshotsUpdate 2025-10-10 14:25:51 +03:00
Timothy Z.
23c4587564
Update CODE_OF_CONDUCT.md 2025-10-10 11:57:10 +03:00
Tim
10faa1c105 ci(pages_cleanup): rename job to 'cleanup' 2025-10-10 08:33:49 +02:00
Tim
593c8d55d0 Merge branch 'development' of https://github.com/Stremio/stremio-web into development 2025-10-10 08:28:37 +02:00
Tim
9e33186708 ci(pages_cleanup): remove dirs that don't have existing related branch 2025-10-10 08:28:18 +02:00
Simran Kaur
cd980af475 Updated metadetails to match original page 2025-10-10 11:01:22 +05:30
Simran Kaur
f44fa98502 Adjusted screnshots to match original aspect ratio 2025-10-10 10:50:40 +05:30
Timothy Z.
a56d3aafd3
Merge pull request #1025 from PsyGuy007-sys/feature/media-playpause
Player: Add support for media play/pause keys
2025-10-09 22:48:51 +03:00
Timothy Z.
5edbc899ea
Merge pull request #1032 from bashSunny101/patch-1
chore: fix typos and enhance clarity in CODE_OF_CONDUCT
2025-10-09 22:43:47 +03:00
Sunny Pal
ca3a2774d2
Fix typos and enhance clarity in CODE_OF_CONDUCT
Corrected typos and improved clarity in the CODE_OF_CONDUCT.md.

Fixes: #1031
2025-10-10 00:21:41 +05:30
Simran Kaur
a70828f168 New Updated Screenshots in README 2025-10-09 22:04:18 +05:30
PsyGuy007-sys
2dee307ac3 Add media key play/pause shortcuts 2025-10-08 09:29:55 +02:00
Tim
d38cf32773
Merge pull request #1016 from ckorber/pr/cause_args
Dev: Improve error logs
2025-10-07 21:24:46 +02:00
Christian
a0615bda42 Complete addition of cause argument
As mentioned in #296 error cast are now added with cause argument.

Signed-off-by: Christian <chr.korber@gmail.com>
2025-10-07 19:57:29 +02:00
Tim
ad5ab5c634
Merge pull request #1022 from NachoLZ/feature/add-docker-instructions
docs: Add instructions for running with Docker
2025-10-07 19:09:00 +02:00
Ignacio Lizana
e78866a77d docs: Add instructions for running with Docker 2025-10-07 18:38:38 +02:00
Tim
19c6e042fb
Merge pull request #1018 from NachoLZ/feature/disable-service-worker
Dev: add option to disable service worker
2025-10-07 17:56:23 +02:00
Ignacio Lizana
57571cf1fc
fix: Handle boolean value for SERVICE_WORKER_DISABLED
The `webpack.EnvironmentPlugin` provides the default value for `SERVICE_WORKER_DISABLED` as a boolean (`false`).

The previous implementation only checked for the string `'true'`, which would fail to correctly identify the boolean `true` case, causing the feature to not work as intended when the variable was set without being explicitly a string.

This commit updates the conditional check to handle both the boolean `true` and the string `'true'` to ensure the service worker is reliably disabled.

Co-authored-by: Tim <tymmesyde@gmail.com>
2025-10-07 17:44:23 +02:00
Ignacio Lizana
670f119027 feat: add option to disable service worker 2025-10-07 15:57:14 +02:00
Tim
49c11973d6 doc: update README 2025-10-07 13:25:57 +02:00
Tim
4a9c0fe5b4
Merge pull request #1008 from actuallylost/build/use-pnpm-instead-of-npm
build: replace npm with pnpm
2025-10-07 13:23:59 +02:00
Tim
c26dac2154 feat(Player): add media session support 2025-10-06 14:49:50 +02:00
Lachezar Lechev
f39944373b
chore: bump core-web revision
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-03 15:01:17 +03:00
Lachezar Lechev
ec34a84154
chore: bump core-web revision
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-02 09:21:41 +03:00
actuallylost
39bdb374e1
chore: remove unused no-var comment 2025-09-29 14:00:19 +03:00
actuallylost
ecca656c68
deps: update stylistic eslint to v5 & stylistic eslint react to v4; change eslint rule name to match actual 2025-09-29 13:59:23 +03:00
actuallylost
c3d6b315d5
revert: workflow double to single quote 2025-09-29 12:20:51 +03:00
Tim
1e241c7926 ci(auto_assign): only run from stremio repo
Some checks failed
Build / build (push) Has been cancelled
2025-09-29 10:55:19 +02:00
actuallylost
67f5446030
chore: fix all typos and misspellings 2025-09-27 16:23:06 +03:00
actuallylost
f3a14403de
build: migrate workflow and npm scripts to use pnpm; add pnpm-lock; add es2016 tsconfig lib 2025-09-27 15:33:31 +03:00
Tim
90f834e893
Merge pull request #981 from asnaek/development
Some checks failed
Build / build (push) Has been cancelled
Update SearchParamsHandler.js
2025-09-25 08:09:07 +02:00
Timothy Z.
10a98fcecf chore: code styles 2025-09-23 23:45:57 +03:00
Aris Sidiropoulos
5bea8a83c6 fix(Intro): clean up unused css class 2025-09-19 16:45:25 +03:00
Aris Sidiropoulos
010f2e0390 fix(Intro): follow up commit, best practice solution 2025-09-19 11:43:36 +03:00
Timothy Z.
cf3119b0a0
Merge pull request #1003 from CDrosos/patch-2
Some checks failed
Build / build (push) Has been cancelled
Update Intro.js added translatable error messages
2025-09-19 11:17:59 +03:00
Christopher Drosos
83bb34e505
Update Intro.js added translatable error messages 2025-09-18 16:57:34 +03:00
Aris Sidiropoulos
cfa99f0e38 fix(Intro): make all text lowercase to match the rest 2025-09-17 20:11:35 +03:00
Lachezar Lechev
3de03989bd
Merge branch 'development' into feat/stream-converted-source 2025-09-16 16:57:45 +03:00
Timothy Z.
3f685173c1
Merge pull request #999 from Stremio/refactor/code-of-conduct
refactor(CODE_OF_CONDUCT): simplify and add AI clarifications
2025-09-16 16:49:49 +03:00
Tim
872243fc5c chore: v5.0.0-beta.27 2025-09-16 11:51:42 +02:00
Timothy Z.
185740c834 Update CODE_OF_CONDUCT.md 2025-09-12 17:05:01 +03:00
Timothy Z.
b9b79a833d Update CODE_OF_CONDUCT.md 2025-09-12 16:54:38 +03:00
Timothy Z.
ed0ca136d1
refactor(CODE_OF_CONDUCT): simplify and AI clarifications 2025-09-12 15:59:37 +03:00
Timothy Z.
6aabd75d5e
Merge pull request #997 from Stremio/dependabot/github_actions/actions/setup-node-5
chore(deps): bump actions/setup-node from 4 to 5
2025-09-12 14:25:48 +03:00
Timothy Z.
8005cd849a
Merge pull request #996 from Stremio/dependabot/github_actions/actions/github-script-8
chore(deps): bump actions/github-script from 7 to 8
2025-09-12 14:25:11 +03:00
Tim
138e3c0d48 chore: update stremio-video 2025-09-11 08:52:19 +02:00
Lachezar Lechev
4001ea3acc
chore: update stremio-core-web dep 2025-09-10 21:35:57 +03:00
Lachezar Lechev
88ed546414
chore: change PullUserFromAPI action and include args
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-09-10 21:35:38 +03:00
a snaek
f271c97502
Merge branch 'Stremio:development' into development 2025-09-10 16:44:39 +02:00
dependabot[bot]
9e55bc8273
chore(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 21:23:04 +00:00
dependabot[bot]
fd675e5926
chore(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 21:23:01 +00:00
Lachezar Lechev
672dbdeb28
Merge branch 'development' into feat/stream-converted-source 2025-09-04 18:26:53 +03:00
Lachezar Lechev
d177f86018
chore: change PullUserFromAPI action and include args
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-09-03 12:07:30 +03:00
Timothy Z.
be072e8391
Merge pull request #991 from Stremio/fix/search-page-dvc
Some checks failed
Build / build (push) Has been cancelled
Search: dynamic view calculation fix on PWA
2025-08-25 19:16:11 +03:00
Timothy Z.
8000a7089a fix(Search): dynamic view calc 2025-08-25 17:23:48 +03:00
Timothy Z.
f9b059d9e4
Merge pull request #990 from Stremio/fix/meta-preview-buttons-position
Some checks are pending
Build / build (push) Waiting to run
MetaPreview: MetaActions Button positioning fix
2025-08-25 15:11:59 +03:00
Timothy Z.
36721b40f1 fix(MetaPreview): button positioning 2025-08-25 14:56:23 +03:00
Timothy Z.
4eb297a4f2
Merge pull request #984 from Stremio/dependabot/github_actions/actions/checkout-5
Some checks failed
Build / build (push) Has been cancelled
chore(deps): bump actions/checkout from 4 to 5
2025-08-12 12:34:12 +03:00
dependabot[bot]
178974ddfd
chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 05:09:18 +00:00
a snaek
80f25b8d45
Merge branch 'Stremio:development' into development 2025-08-12 00:51:59 +02:00
Timothy Z.
665cf7dd2a
Merge pull request #975 from Stremio/fix/player-negative-number-from-stremio-video
Some checks failed
Build / build (push) Has been cancelled
fix: Player - TimeChanged & Seek time & duration now have Math.max(0, x)
2025-08-07 12:24:55 +03:00
a snaek
53dfddec74
Update SearchParamsHandler.js 2025-08-05 10:23:15 +02:00
Lachezar Lechev
3c2f8cb89b
fix: Player - selected check before video.load
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-07-18 12:08:48 +03:00
Lachezar Lechev
0bec58b158
fix: package-lock
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-07-18 12:08:21 +03:00
Lachezar Lechev
20bbe12a8a
fix: Player - TimeChanged & Seek duration now has Math.max(0, x)
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-07-15 22:04:57 +03:00
Lachezar Lechev
3c2914aca2
fix: Player - TimeChanged & Seek time now has Math.max(0, x)
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-07-15 21:58:17 +03:00
Lachezar Lechev
746e5ba0d8
feat: Player - use player.stream field
chore: bump core-web to feature branch

Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-07-15 21:56:47 +03:00
Tim
0e3aa9c2c8 feat: scroll to last watched video on details page 2025-07-15 17:00:08 +02:00
Tim
40c77d3d17
Merge pull request #972 from Stremio/dependabot/github_actions/svenstaro/upload-release-action-2.11.2
Some checks failed
Build / build (push) Has been cancelled
chore(deps): bump svenstaro/upload-release-action from 2.11.1 to 2.11.2
2025-07-09 18:29:50 +02:00
dependabot[bot]
41863e5d75
chore(deps): bump svenstaro/upload-release-action from 2.11.1 to 2.11.2
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.11.1 to 2.11.2.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.11.1...2.11.2)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-version: 2.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-08 00:13:48 +00:00
Tim
a9b9631241
Merge pull request #970 from Stremio/feat/player-subtitles-size-shortcuts
Some checks failed
Build / build (push) Has been cancelled
Player: Add subtitles size shortcuts
2025-07-07 14:33:37 +02:00
Tim
85fea50c15
Merge pull request #965 from Stremio/feat/player-subtitles-settings-hold-click
Player: Support holding click for subtitles settings
2025-07-07 14:27:44 +02:00
Tim
e98bdf2023
Merge pull request #964 from Stremio/feat/player-download-subtitles
Player: Add option to download subtitles
2025-07-07 14:27:32 +02:00
Tim
fb3f8d6918 chore: update stremio-translations 2025-07-07 14:11:58 +02:00
Tim
59953e991d feat: add subtitles size shortcuts 2025-07-04 16:35:18 +02:00
Tim
5adc0937dd
Merge pull request #969 from Stremio/refactor/remove-settings-videos-menu-shortcut
Some checks failed
Build / build (push) Has been cancelled
Settings: remove videos menu shortcut
2025-07-04 15:40:04 +02:00
Tim
71bf98dac8 refactor(Settings): remove videos menu shortcut 2025-07-04 15:35:51 +02:00
Tim
083ec3a12e ci(release): remove netlify artifact upload
Some checks are pending
Build / build (push) Waiting to run
2025-07-04 08:41:38 +02:00
Tim
4419eeb33f ci(pages_cleanup): fetch full history
Some checks are pending
Build / build (push) Waiting to run
2025-07-03 17:14:52 +02:00
Tim
05371f3617 ci(pages_cleanup): add cron and manual trigger 2025-07-03 17:09:16 +02:00
Tim
4ad0fb2962 ci: add pages cleanup workflow 2025-07-03 16:59:09 +02:00
Tim
b742e385ea
Merge pull request #968 from Stremio/fix/shell-external-subtitles-disabled
Player(Desktop): Fix external subtitles selection
2025-07-03 15:51:49 +02:00
Tim
6c0d5288d3 chore: update stremio-video 2025-07-03 15:48:12 +02:00
Tim
c042c553e3 chore: update stremio-video 2025-07-03 14:39:19 +02:00
Tim
1b09119e52
Merge pull request #967 from Stremio/fix/transition-prop-error
Some checks are pending
Build / build (push) Waiting to run
fix: Transition transitionEnded prop error
2025-07-03 12:06:43 +02:00
Tim
595dfb22a3 fix: Transition transitionEnded prop error 2025-07-03 11:56:06 +02:00
Tim
aef0ecb5be
Merge pull request #966 from Stremio/fix/player-indicator-subtitles-menu
Player: Hide subtitles delay indicator if subtitles menu is open
2025-07-03 11:30:18 +02:00
Tim
4632d6e09a fix(Player): hide indicator if subtitles menu is open 2025-07-03 11:22:14 +02:00
Tim
f04948240a feat: support holding click for subtitles settings 2025-07-03 08:45:22 +02:00
Tim
cff57d7d59 feat(Player): add option to download subtitles 2025-07-02 15:31:56 +02:00
Timothy Z.
c2a2eca4e9
Merge pull request #960 from Stremio/dependabot/github_actions/svenstaro/upload-release-action-2.11.1
Some checks failed
Build / build (push) Has been cancelled
chore(deps): bump svenstaro/upload-release-action from 2.10.0 to 2.11.1
2025-07-01 12:19:46 +03:00
dependabot[bot]
76e8aa9091
chore(deps): bump svenstaro/upload-release-action from 2.10.0 to 2.11.1
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.10.0 to 2.11.1.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.10.0...2.11.1)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 00:49:34 +00:00
Timothy Z.
3083b812be bump(bug fix): v5.0.0-beta.26
Some checks are pending
Build / build (push) Waiting to run
2025-06-30 14:31:02 +03:00
Timothy Z.
8a82ff083a
Merge pull request #958 from Stremio/fix/fullscreen-shortcut-search-bug
App(Search): fullscreen shortcut interference
2025-06-30 14:20:05 +03:00
Timothy Z.
066819d283 fix(Search): fullscreen shortcut interference 2025-06-30 13:09:04 +03:00
Timothy Z.
0b16f1c80e
Merge pull request #957 from Stremio/fix/trailers-button-crash
Some checks failed
Build / build (push) Has been cancelled
fix(MetaDetails): trailers button crash
2025-06-28 14:57:18 +03:00
Timothy Z.
8bd0456e09
Merge pull request #956 from Stremio/refactor/ratings-remove-transition
refactor(Ratings): remove btn transition
2025-06-28 14:45:30 +03:00
Timothy Z.
8c985619b8 fix(MetaDetails): trailers button crash 2025-06-28 14:44:49 +03:00
Timothy Z.
1780b49a38 refactor(Ratings): remove btn transition 2025-06-28 14:42:29 +03:00
Timothy Z.
63aecc6764 bump(bug fix): v5.0.0-beta.25
Some checks are pending
Build / build (push) Waiting to run
2025-06-27 13:57:44 +03:00
Timothy Z.
c1c08cdfa1
Merge pull request #954 from Stremio/fix/right-click-video-menu-crash
Video: right click menu crash
2025-06-27 13:54:43 +03:00
Timothy Z.
8f0b58f38e chore(styles): lint 2025-06-27 13:10:20 +03:00
Timothy Z.
8821eaf4a1 fix(Video): right click menu crash 2025-06-27 13:09:22 +03:00
100 changed files with 12513 additions and 16179 deletions

View file

@ -13,8 +13,8 @@ jobs:
steps:
# Auto assign PR to author
- name: Auto Assign PR to Author
if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
uses: actions/github-script@v7
if: github.event.pull_request.head.repo.fork == false && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@ -31,10 +31,10 @@ jobs:
# Dynamic labeling based on PR/Issue title
- name: Label PRs and Issues
if: github.actor != 'dependabot[bot]'
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View file

@ -5,7 +5,7 @@ on:
branches:
- development
tags-ignore:
- '**'
- "**"
pull_request:
branches:
- development
@ -20,20 +20,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install NPM dependencies
run: npm ci
run: pnpm install
- name: Build
run: npm run build
run: pnpm build
- name: Test
run: npm test
run: pnpm test
- name: Lint
run: npm run lint
# Create recursivelly the destiantion dir with
run: pnpm lint
# Create recursively the destination dir with
# "--parrents where no error if existing, make parent directories as needed."
- run: mkdir -p ./build/${{ github.head_ref || github.ref_name }}
- name: Deploy to GitHub Pages

53
.github/workflows/pages_cleanup.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: GitHub Pages Cleanup
on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
permissions:
contents: write
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: gh-pages
fetch-depth: 0
- name: Delete directories that don't have existing branch
run: |
branches=( $(git branch -r | grep origin | grep -v HEAD | sed 's|origin/||') )
declare -p branches
find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*' | while read -r dir; do
path="${dir#./}"
if [[ " ${branches[*]} " =~ " $path " ]]; then
continue
fi
keep_parent=false
for branch in "${branches[@]}"; do
if [[ "$branch" == "$path/"* ]]; then
keep_parent=true
break
fi
done
if ! $keep_parent; then
echo "Deleting $dir"
rm -rf "$dir"
fi
done
- name: Commit and push
run: |
git config --global user.name 'GitHub Pages Cleanup'
git config --global user.email 'actions@stremio.com'
git add -A
git diff --cached --quiet || git commit -m "cleanup"
git push origin gh-pages

View file

@ -9,26 +9,25 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install NPM dependencies
run: npm install
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: npm run build
run: pnpm build
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.10.0
uses: svenstaro/upload-release-action@2.11.3
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip
asset_name: stremio-web.zip
tag: ${{ github.ref }}
overwrite: true
- name: Upload build artifact to Netlify
run: |
curl -H "Content-Type: application/zip" \
-H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \
--data-binary "@stremio-web.zip" \
https://api.netlify.com/api/v1/sites/stremio-development.netlify.com/deploys

View file

@ -2,35 +2,42 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, level of experience, education, nationality or race.
We as contributors and maintainers want to make contributing to our project and community a nice experience for everyone.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
Examples of positive behavior:
- Using welcoming and inclusive language.
- Being respectful of differing viewpoints and experiences.
- Using welcoming language.
- Being respectful.
- Accepting constructive criticism.
- Focusing on what is best for the community.
- Showing empathy towards other community members.
Examples of unacceptable behavior by participants include:
Examples of bad behavior:
- The use of sexualized language or imagery and unwelcome sexual attention or advances.
- Trolling, insulting/derogatory comments, and personal or political attacks.
- Use of sexualized language.
- Trolling, insulting comments, and personal or political attacks.
- Public or private harassment.
- Publishing others private information, such as a physical or electronic address, without explicit permission.
- Other conduct which could reasonably be considered inappropriate in a professional setting.
- Submitting entirely generated by AI PRs with agents such as Devin, Claude Code, Cursor Agent etc.
- Submitting PRs which in majority contain only AI generated code (including docs & comments) and do not solve an actual issue.
- Spamming issues because of no ETAs on issues.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers are responsible for enforcing this code of conduct. They can remove or edit comments, code, and other contributions that don't follow these rules. They can also ban users who behave inappropriately.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, pull requests, and other contributions that do not align with this Code of Conduct, as well as to temporarily or permanently ban any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Suggestions for newbies
- Contributors are welcomed to use AI models as "help" in solving issues, but you must always double check the code that you're submitting.
- Refrain from excessive comments generated by AI.
- Refrain from docs generated entirely by AI.
- Always check what files you are committing and submitting to the PR when you are using any agent for help or an AI model.
- If you don't know how to tackle a problem and AI can't help you, please just ask or look in Stack Overlflow, Google, Medium etc.
- Learning how to code is fun and easier when using AI, but sometimes it might be just too much ... what are you going to learn, if AI does everything for you and you don't know what the code you are submitting actually does?!
## Scope
This Code of Conduct applies within all `stremio-web` spaces, and also applies when an individual is officially representing the project or its community in public spaces.
This Code of Conduct applies everywhere in `stremio-web` repository, and also applies when an individual is officially representing the project or its community in other spaces.
## Enforcement

View file

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

View file

@ -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.
@ -10,24 +10,31 @@ Stremio is a modern media center that's a one-stop solution for your video enter
### Prerequisites
* Node.js 12 or higher
* npm 6 or higher
* [pnpm](https://pnpm.io/installation) 10 or higher
### Install dependencies
```bash
npm install
pnpm install
```
### Start development server
```bash
npm start
pnpm start
```
### Production build
```bash
npm run build
pnpm run build
```
### Run with Docker
```bash
docker build -t stremio-web .
docker run -p 8080:8080 stremio-web
```
## Screenshots

View file

@ -82,7 +82,7 @@ export default [
'@stylistic/semi-spacing': 'error',
'@stylistic/space-before-blocks': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/func-call-spacing': 'error',
'@stylistic/function-call-spacing': 'error',
'@stylistic/semi': 'error',
'@stylistic/no-extra-semi': 'error',
'@stylistic/eol-last': 'error',

15562
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-beta.24",
"version": "5.0.0-beta.29",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -11,15 +11,15 @@
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src",
"scan-translations": "npx jest ./tests/i18nScan.test.js"
"scan-translations": "pnpx jest ./tests/i18nScan.test.js"
},
"dependencies": {
"@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-icons": "5.7.1",
"@stremio/stremio-video": "0.0.60",
"@stremio/stremio-core-web": "0.51.1",
"@stremio/stremio-icons": "5.8.0",
"@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#8212fa77c4febd22ddb611590e9fb574dc845416",
"stremio-translations": "github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@ -50,8 +50,8 @@
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@eslint/js": "^9.16.0",
"@stylistic/eslint-plugin": "^2.11.0",
"@stylistic/eslint-plugin-jsx": "^2.11.0",
"@stylistic/eslint-plugin": "^5.4.0",
"@stylistic/eslint-plugin-jsx": "^4.4.1",
"@types/hat": "^0.0.4",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",

11030
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -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 = () => {
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
<ShortcutsProvider onShortcut={onShortcut}>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</ShortcutsProvider>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>

View file

@ -36,7 +36,13 @@ const SearchParamsHandler = () => {
},
},
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddServerUrl',
args: streamingServerUrl,
},
});
toast.show({
type: 'success',
title: `Using streaming server at ${streamingServerUrl}`,

View file

@ -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((
<div className={styles['shortcuts-modal']}>
<div className={styles['backdrop']} onClick={onClose} />
<div className={styles['container']}>
<div className={styles['header']}>
<div className={styles['title']}>
{t('SETTINGS_NAV_SHORTCUTS')}
</div>
<Button className={styles['close-button']} title={t('BUTTON_CLOSE')} onClick={onClose}>
<Icon className={styles['icon']} name={'close'} />
</Button>
</div>
<div className={styles['content']}>
{
grouped.map(({ name, label, shortcuts }) => (
<ShortcutsGroup
key={name}
label={label}
shortcuts={shortcuts}
/>
))
}
</div>
</div>
</div>
), document.body);
};
export default ShortcutsModal;

View file

@ -0,0 +1,2 @@
import ShortcutsModal from './ShortcutsModal';
export default ShortcutsModal;

View file

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

View file

@ -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;
@ -48,7 +48,7 @@
--color-x: #000000;
--color-reddit: #FF4500;
--color-imdb: #f5c518;
--color-trakt: #ED2224;
--color-trakt: rgb(255, 255, 255);
--color-placeholder: #60606080;
--color-placeholder-text: @color-surface-50;
--color-placeholder-background: @color-surface-dark5-20;
@ -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;

View file

@ -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));
});
}

View file

@ -0,0 +1,67 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import shortcuts from './shortcuts.json';
const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
export type ShortcutName = string;
export type ShortcutListener = (combo: number) => void;
interface ShortcutsContext {
grouped: ShortcutGroup[],
on: (name: ShortcutName, listener: ShortcutListener) => void,
off: (name: ShortcutName, listener: ShortcutListener) => void,
}
const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext);
type Props = {
children: JSX.Element,
onShortcut: (name: ShortcutName) => void,
};
const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, 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(code) || keys.includes(key.toUpperCase()))) {
const combo = combos.indexOf(keys);
listeners.current.get(name)?.forEach((listener) => listener(combo));
onShortcut(name as ShortcutName);
}
}));
}, [onShortcut]);
const on = (name: ShortcutName, listener: ShortcutListener) => {
!listeners.current.has(name) && listeners.current.set(name, new Set());
listeners.current.get(name)!.add(listener);
};
const off = (name: ShortcutName, listener: ShortcutListener) => {
listeners.current.get(name)?.delete(listener);
};
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
return (
<ShortcutsContext.Provider value={{ grouped: shortcuts, on, off }}>
{children}
</ShortcutsContext.Provider>
);
};
const useShortcuts = () => {
return useContext(ShortcutsContext);
};
export {
ShortcutsProvider,
useShortcuts,
};

View file

@ -0,0 +1,8 @@
import { ShortcutsProvider, useShortcuts } from './Shortcuts';
import onShortcut from './onShortcut';
export {
ShortcutsProvider,
useShortcuts,
onShortcut,
};

View file

@ -0,0 +1,15 @@
import { DependencyList, useCallback, useEffect } from 'react';
import { ShortcutListener, ShortcutName, useShortcuts } from './Shortcuts';
const onShortcut = (name: ShortcutName, listener: ShortcutListener, deps: DependencyList) => {
const shortcuts = useShortcuts();
const listenerCallback = useCallback(listener, deps);
useEffect(() => {
shortcuts.on(name, listenerCallback);
return () => shortcuts.off(name, listenerCallback);
}, [listenerCallback]);
};
export default onShortcut;

View file

@ -0,0 +1,104 @@
[
{
"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": "mute",
"label": "SETTINGS_SHORTCUT_MUTE",
"combos": [["M"]]
},
{
"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"]]
},
{
"name": "speedMenu",
"label": "SETTINGS_SHORTCUT_MENU_PLAYBACK_SPEED",
"combos": [["R"]]
},
{
"name": "statisticsMenu",
"label": "SETTINGS_SHORTCUT_MENU_STATISTICS",
"combos": [["D"]]
}
]
}
]

11
src/common/Shortcuts/types.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
type Shortcut = {
name: string,
label: string,
combos: string[][],
};
type ShortcutGroup = {
name: string,
label: string,
shortcuts: Shortcut[],
};

View file

@ -27,7 +27,7 @@
&.error {
.icon-container {
.icon {
color: var(--color-trakt);
color: var(--danger-accent-color);
}
}
}

View file

@ -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, onShortcut } = require('./Shortcuts');
const comparatorWithPriorities = require('./comparatorWithPriorities');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
@ -15,6 +16,7 @@ const routesRegexp = require('./routesRegexp');
const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
const { default: useFullscreen } = require('./useFullscreen');
const { default: useInterval } = require('./useInterval');
const useLiveRef = require('./useLiveRef');
const useModelState = require('./useModelState');
const useNotifications = require('./useNotifications');
@ -23,6 +25,7 @@ const useProfile = require('./useProfile');
const { default: useSettings } = require('./useSettings');
const { default: useShell } = require('./useShell');
const useStreamingServer = require('./useStreamingServer');
const { default: useTimeout } = require('./useTimeout');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const { default: useOrientation } = require('./useOrientation');
@ -33,6 +36,9 @@ module.exports = {
onFileDrop,
PlatformProvider,
usePlatform,
ShortcutsProvider,
useShortcuts,
onShortcut,
ToastProvider,
useToast,
TooltipProvider,
@ -49,6 +55,7 @@ module.exports = {
useAnimationFrame,
useBinaryState,
useFullscreen,
useInterval,
useLiveRef,
useModelState,
useNotifications,
@ -57,6 +64,7 @@ module.exports = {
useSettings,
useShell,
useStreamingServer,
useTimeout,
useTorrent,
useTranslate,
useOrientation,

View file

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

View file

@ -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);
}
}
}, []);
@ -42,11 +46,21 @@ const useFullscreen = () => {
};
const onKeyDown = (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement;
const inputFocused =
activeElement &&
(activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.isContentEditable);
if (event.code === 'Escape' && settings.escExitFullscreen) {
exitFullscreen();
}
if (event.code === 'KeyF') {
if (event.code === 'KeyF' && !inputFocused) {
toggleFullscreen();
}

26
src/common/useInterval.ts Normal file
View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
const useInterval = (duration: number) => {
const interval = useRef<NodeJS.Timer | null>(null);
const start = (callback: () => void) => {
cancel();
interval.current = setInterval(callback, duration);
};
const cancel = () => {
interval.current && clearInterval(interval.current);
interval.current = null;
};
useEffect(() => {
return () => cancel();
}, []);
return {
start,
cancel,
};
};
export default useInterval;

View file

@ -1,2 +1,2 @@
declare const useNotifcations: () => Notifications;
export = useNotifcations;
declare const useNotifications: () => Notifications;
export = useNotifications;

26
src/common/useTimeout.ts Normal file
View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
const useTimeout = (duration: number) => {
const timeout = useRef<NodeJS.Timeout | null>(null);
const start = (callback: () => void) => {
cancel();
timeout.current = setTimeout(callback, duration);
};
const cancel = () => {
timeout.current && clearTimeout(timeout.current);
timeout.current = null;
};
useEffect(() => {
return () => cancel();
}, []);
return {
start,
cancel,
};
};
export default useTimeout;

View file

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

View file

@ -16,6 +16,8 @@ type Props = {
children: React.ReactNode,
onKeyDown?: (event: React.KeyboardEvent) => void,
onMouseDown?: (event: React.MouseEvent) => void,
onMouseUp?: (event: React.MouseEvent) => void,
onMouseLeave?: (event: React.MouseEvent) => void,
onLongPress?: () => void,
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
onDoubleClick?: () => void,

View file

@ -70,7 +70,7 @@
}
&.error {
border-color: var(--color-trakt);
border-color: var(--danger-accent-color);
}
&.checked {

View file

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

View file

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

View file

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

View file

@ -65,8 +65,16 @@
padding: 0 1rem;
.icon-container {
height: 2rem;
width: 2rem;
.icon {
width: 2rem;
height: 2rem;
}
}
.label-container {
display: none;
}
}
}

View file

@ -3,18 +3,18 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { Button } = require('stremio/components');
const useTranslate = require('stremio/common/useTranslate');
const styles = require('./styles');
const MetaLinks = ({ className, label, links }) => {
const { t } = useTranslation();
const { string, stringWithPrefix } = useTranslate();
return (
<div className={classnames(className, styles['meta-links-container'])}>
{
typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}>
{t(`LINKS_${label.toUpperCase()}`)}
{ stringWithPrefix(label.toUpperCase(), 'LINKS') }
</div>
:
null
@ -24,7 +24,7 @@ const MetaLinks = ({ className, label, links }) => {
<div className={styles['links-container']}>
{links.map(({ label, href }, index) => (
<Button key={index} className={styles['link-container']} title={label} href={href}>
{ t(label) }
{ string(label) }
</Button>
))}
</div>

View file

@ -17,6 +17,7 @@
border-radius: 2rem;
height: @height;
width: fit-content;
backdrop-filter: blur(5px);
.icon-container {
display: flex;
@ -32,7 +33,6 @@
height: calc(@height / 2);
color: var(--primary-foreground-color);
opacity: 0.7;
transition: 0.3s all ease-in-out;
&:hover {
opacity: 1;

View file

@ -240,6 +240,10 @@
border-radius: 2rem;
}
}
.ratings {
margin-right: 0;
}
}
}

View file

@ -52,7 +52,7 @@
}
&.error {
border-color: var(--color-trakt);
border-color: var(--danger-accent-color);
}
&.selected {

View file

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

View file

@ -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 (
<div className={styles['combos']}>
{
combos.map((keys, index) => (
<div className={styles['combo']} key={index}>
<Keys keys={keys} />
{
index < (combos.length - 1) && (
<div className={styles['separator']}>
{ t('SETTINGS_SHORTCUT_OR') }
</div>
)
}
</div>
))
}
</div>
);
};
export default Combos;

View file

@ -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);
}

View file

@ -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<string, string> = 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) => (
<Fragment key={key}>
<kbd>
{keyLabelMap[key] ?? key.toUpperCase()}
</kbd>
{
index < (filteredKeys.length - 1) && (
<div className={styles['separator']}>
{
isRange ? t('SETTINGS_SHORTCUT_TO') : '+'
}
</div>
)
}
</Fragment>
))
);
};
export default Keys;

View file

@ -0,0 +1,2 @@
import Keys from './Keys';
export default Keys;

View file

@ -0,0 +1,2 @@
import Combos from './Combos';
export default Combos;

View file

@ -0,0 +1,44 @@
.shortcuts-group {
flex: 1 1 0;
position: relative;
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;
}
}
}
}

View file

@ -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 (
<div className={classNames(className, styles['shortcuts-group'])}>
<div className={styles['title']}>
{t(label)}
</div>
<div className={styles['shortcuts']}>
{
shortcuts.map(({ name, label, combos }) => (
<div className={styles['shortcut']} key={name}>
<div className={styles['label']}>
{t(label)}
</div>
<Combos combos={combos} />
</div>
))
}
</div>
</div>
);
};
export default ShortcutsGroup;

View file

@ -0,0 +1,2 @@
import ShortcutsGroup from './ShortcutsGroup';
export default ShortcutsGroup;

View file

@ -142,11 +142,11 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
<div className={styles['layer']}>
<div
className={classnames(styles['track-after'], { [styles['audio-boost']]: audioBoost })}
style={{ '--mask-width': `calc(${thumbPosition} * 100%)` }}
style={{ '--mask-width': `calc(${thumbPosition.toFixed(3)} * 100%)` }}
/>
</div>
<div className={styles['layer']}>
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition.toFixed(3)})` }} />
</div>
</div>
);

View file

@ -13,7 +13,6 @@ const Transition = ({ children, when, name }: Props) => {
const [state, setState] = useState('enter');
const [active, setActive] = useState(false);
const [transitionEnded, setTransitionEnded] = useState(false);
const callbackRef = useCallback((element: HTMLElement | null) => {
setElement(element);
@ -31,14 +30,12 @@ const Transition = ({ children, when, name }: Props) => {
}, [name, state, active, children]);
const onTransitionEnd = useCallback(() => {
setTransitionEnded(true);
state === 'exit' && setMounted(false);
}, [state]);
useEffect(() => {
setState(when ? 'enter' : 'exit');
when && setMounted(true);
setTransitionEnded(false);
}, [when]);
useEffect(() => {
@ -56,7 +53,6 @@ const Transition = ({ children, when, name }: Props) => {
mounted && cloneElement(children, {
ref: callbackRef,
className,
transitionEnded
})
);
};

View file

@ -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,10 +69,23 @@ 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, ...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;
React.useEffect(() => {
if (selected && ref.current) {
if ((progress && watched) || !watched) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start'
});
}
}
}, [selected]);
return (
<Button {...props} className={classnames(className, styles['video-container'])} title={title} ref={ref}>
<Button {...props} ref={ref} className={classnames(className, styles['video-container'], { [styles['selected']]: selected })} title={title}>
{
typeof thumbnail === 'string' && thumbnail.length > 0 ?
<div className={styles['thumbnail-container']}>
@ -142,7 +156,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
{children}
</Button>
);
}, []);
}, [selected]);
const renderMenu = React.useMemo(() => function renderMenu() {
return (
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
@ -186,7 +200,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo
renderMenu={renderMenu}
/>
);
});
};
Video.Placeholder = VideoPlaceholder;
@ -203,6 +217,7 @@ Video.propTypes = {
progress: PropTypes.number,
scheduled: PropTypes.bool,
seasonWatched: PropTypes.bool,
selected: PropTypes.bool,
deepLinks: PropTypes.shape({
metaDetailsStreams: PropTypes.string,
player: PropTypes.string

View file

@ -19,6 +19,11 @@
padding: 0.5rem;
margin-bottom: 0.5rem;
border-radius: var(--border-radius);
border: 0.15rem solid transparent;
@supports (scroll-margin: 1.25rem) {
scroll-margin: 1.25rem;
}
&:hover,
&:focus,
@ -172,6 +177,20 @@
}
}
&.selected {
animation: border 3s ease-in-out forwards;
}
@keyframes border {
0% {
border: 0.15rem solid var(--primary-accent-color);
}
100% {
border: 0.15rem solid transparent;
}
}
.context-menu-container {
max-width: calc(90% - 1.5rem);
z-index: 2;

View file

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

View file

@ -36,7 +36,7 @@ i18n
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
if (process.env.NODE_ENV === 'production' && process.env.SERVICE_WORKER_DISABLED !== 'true' && process.env.SERVICE_WORKER_DISABLED !== true && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('service-worker.js')
.catch((registrationError) => {

View file

@ -50,7 +50,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
remoteAddons.selected !== null ?
t.stringWithPrefix(remoteAddons.selected.request.path.type, 'TYPE_')
:
typeSelect.title;
t.string('SELECT_TYPE');
},
onSelect: (value) => {
window.location = value;

View file

@ -22,11 +22,10 @@ const Board = () => {
const profile = useProfile();
const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0;
const scrollContainerRef = React.useRef();
const streamingServerWarningDismissed = React.useMemo(() => {
return streamingServer.settings !== null && streamingServer.settings.type === 'Ready' || (
!isNaN(profile.settings.streamingServerWarningDismissed.getTime()) &&
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()
);
const showStreamingServerWarning = React.useMemo(() => {
return streamingServer.settings !== null && streamingServer.settings.type === 'Err' && (
isNaN(profile.settings.streamingServerWarningDismissed.getTime()) ||
profile.settings.streamingServerWarningDismissed.getTime() < Date.now());
}, [profile.settings, streamingServer.settings]);
const onVisibleRangeChange = React.useCallback(() => {
const range = getVisibleChildrenRange(scrollContainerRef.current);
@ -103,7 +102,7 @@ const Board = () => {
</div>
</MainNavBars>
{
!streamingServerWarningDismissed ?
showStreamingServerWarning ?
<StreamingServerWarning className={styles['board-warning-container']} />
:
null

View file

@ -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;
}
}
}
}
@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();
}

View file

@ -45,6 +45,7 @@
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
grid-auto-rows: 1fr;
}
}

View file

@ -138,11 +138,11 @@ const Intro = ({ queryParams }) => {
}, []);
const loginWithEmail = React.useCallback(() => {
if (typeof state.email !== 'string' || state.email.length === 0 || !emailRef.current.validity.valid) {
dispatch({ type: 'error', error: 'Invalid email' });
dispatch({ type: 'error', error: t('INVALID_EMAIL') });
return;
}
if (typeof state.password !== 'string' || state.password.length === 0) {
dispatch({ type: 'error', error: 'Invalid password' });
dispatch({ type: 'error', error: t('INVALID_PASSWORD') });
return;
}
openLoaderModal();
@ -160,26 +160,26 @@ const Intro = ({ queryParams }) => {
}, [state.email, state.password]);
const loginAsGuest = React.useCallback(() => {
if (!state.termsAccepted) {
dispatch({ type: 'error', error: 'You must accept the Terms of Service' });
dispatch({ type: 'error', error: t('MUST_ACCEPT_TERMS') });
return;
}
window.location = '#/';
}, [state.termsAccepted]);
const signup = React.useCallback(() => {
if (typeof state.email !== 'string' || state.email.length === 0 || !emailRef.current.validity.valid) {
dispatch({ type: 'error', error: 'Invalid email' });
dispatch({ type: 'error', error: t('INVALID_EMAIL') });
return;
}
if (typeof state.password !== 'string' || state.password.length === 0) {
dispatch({ type: 'error', error: 'Invalid password' });
dispatch({ type: 'error', error: t('INVALID_PASSWORD') });
return;
}
if (state.password !== state.confirmPassword) {
dispatch({ type: 'error', error: 'Passwords do not match' });
dispatch({ type: 'error', error: t('PASSWORDS_NOMATCH') });
return;
}
if (!state.termsAccepted) {
dispatch({ type: 'error', error: 'You must accept the Terms of Service' });
dispatch({ type: 'error', error: t('MUST_ACCEPT_TERMS') });
return;
}
if (!state.privacyPolicyAccepted) {
@ -387,7 +387,7 @@ const Intro = ({ queryParams }) => {
{
state.form === SIGNUP_FORM ?
<Button className={classnames(styles['form-button'], styles['login-form-button'])} onClick={switchFormOnClick}>
<div className={classnames(styles['label'], styles['uppercase'])}>{t('LOG_IN')}</div>
<div className={styles['label']}>{t('LOG_IN')}</div>
</Button>
:
null
@ -395,7 +395,7 @@ const Intro = ({ queryParams }) => {
{
state.form === LOGIN_FORM ?
<Button className={classnames(styles['form-button'], styles['signup-form-button'])} onClick={switchFormOnClick}>
<div className={classnames(styles['label'], styles['uppercase'])}>{t('SIGN_UP_EMAIL')}</div>
<div className={styles['label']}>{t('SIGN_UP_EMAIL')}</div>
</Button>
:
null
@ -403,7 +403,7 @@ const Intro = ({ queryParams }) => {
{
state.form === SIGNUP_FORM ?
<Button className={classnames(styles['form-button'], styles['guest-login-button'])} onClick={loginAsGuest}>
<div className={classnames(styles['label'], styles['uppercase'])}>{t('GUEST_LOGIN')}</div>
<div className={styles['label']}>{t('GUEST_LOGIN')}</div>
</Button>
:
null

View file

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

View file

@ -49,7 +49,7 @@ const useAppleLogin = (): [() => Promise<AppleLoginResponse>, () => void] => {
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
if (tries >= MAX_TRIES)
return reject(new Error('Failed to authenticate with Apple'));
return reject(new Error('Failed to authenticate with Apple', { cause: 'Number of allowed tries exceeded!' }));
tries++;

View file

@ -39,7 +39,7 @@ const useFacebookLogin = () => {
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
if (tries >= MAX_TRIES)
return reject(new Error('Failed to authenticate with facebook'));
return reject(new Error('Failed to authenticate with facebook', { cause: 'Number of allowed tries exceeded!' }));
tries++;

View file

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

View file

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

View file

@ -22,9 +22,10 @@ type VideoState = Record<string, number>;
type Props = {
className: string,
videoState: VideoState,
disabled: boolean,
};
const Indicator = ({ className, videoState }: Props) => {
const Indicator = ({ className, videoState, disabled }: Props) => {
const timeout = useRef<NodeJS.Timeout | null>(null);
const prevVideoState = useRef<VideoState>(videoState);
@ -60,7 +61,7 @@ const Indicator = ({ className, videoState }: Props) => {
}, [videoState]);
return (
<Transition when={shown} name={'fade'}>
<Transition when={shown && !disabled} name={'fade'}>
<div className={classNames(className, styles['indicator-container'])}>
<div className={styles['indicator']}>
<div>{label} {value}</div>

View file

@ -9,7 +9,7 @@ const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
const OptionsMenu = ({ className, stream, playbackDevices }) => {
const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
@ -25,6 +25,12 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
const externalDevices = React.useMemo(() => {
return playbackDevices.filter(({ type }) => type === 'external');
}, [playbackDevices]);
const subtitlesTrackUrl = React.useMemo(() => {
const track = extraSubtitlesTracks?.find(({ id }) => id === selectedExtraSubtitlesTrackId);
return track?.fallbackUrl ?? track?.url ?? null;
}, [extraSubtitlesTracks, selectedExtraSubtitlesTrackId]);
const onCopyStreamButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
navigator.clipboard.writeText(streamingUrl || downloadUrl)
@ -48,10 +54,15 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
}
}, [streamingUrl, downloadUrl]);
const onDownloadVideoButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
platform.openExternal(streamingUrl || downloadUrl);
if (downloadUrl || streamingUrl ) {
platform.openExternal(downloadUrl || streamingUrl);
}
}, [streamingUrl, downloadUrl]);
const onDownloadSubtitlesClick = React.useCallback(() => {
subtitlesTrackUrl && platform.openExternal(subtitlesTrackUrl);
}, [subtitlesTrackUrl]);
const onExternalDeviceRequested = React.useCallback((deviceId) => {
if (streamingUrl) {
core.transport.dispatch({
@ -94,6 +105,17 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
:
null
}
{
subtitlesTrackUrl ?
<Option
icon={'download'}
label={t('CTX_DOWNLOAD_SUBS')}
disabled={stream === null}
onClick={onDownloadSubtitlesClick}
/>
:
null
}
{
streamingUrl && externalDevices.map(({ id, name }) => (
<Option
@ -114,6 +136,8 @@ OptionsMenu.propTypes = {
className: PropTypes.string,
stream: PropTypes.object,
playbackDevices: PropTypes.array,
extraSubtitlesTracks: PropTypes.array,
selectedExtraSubtitlesTrackId: PropTypes.string,
};
module.exports = OptionsMenu;

View file

@ -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, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -36,13 +36,14 @@ const Player = ({ urlParams, queryParams }) => {
const forceTranscoding = React.useMemo(() => {
return queryParams.has('forceTranscoding');
}, [queryParams]);
const profile = useProfile();
const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings();
const streamingServer = useStreamingServer();
const statistics = useStatistics(player, streamingServer);
const video = useVideo();
const routeFocused = useRouteFocused();
const platform = usePlatform();
const toast = useToast();
const [seeking, setSeeking] = React.useState(false);
@ -104,13 +105,27 @@ const Player = ({ urlParams, queryParams }) => {
video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
}, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
const handleNextVideoNavigation = React.useCallback((deepLinks) => {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
if (ended) {
if (bingeWatching) {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
}
} else {
window.history.back();
}
} else {
if (deepLinks.player) {
isNavigating.current = true;
window.location.replace(deepLinks.player);
} else if (deepLinks.metaDetailsStreams) {
isNavigating.current = true;
window.location.replace(deepLinks.metaDetailsStreams);
}
}
}, []);
@ -126,7 +141,8 @@ const Player = ({ urlParams, queryParams }) => {
nextVideo();
const deepLinks = window.playerNextVideo.deepLinks;
handleNextVideoNavigation(deepLinks);
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, true);
} else {
window.history.back();
}
@ -236,6 +252,12 @@ const Player = ({ urlParams, queryParams }) => {
updateSettings({ subtitlesSize: size });
}, [updateSettings]);
const onUpdateSubtitlesSize = React.useCallback((delta) => {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize);
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
onSubtitlesSizeChanged(size);
}, [video.state.subtitlesSize, onSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
updateSettings({ subtitlesOffset: offset });
}, [updateSettings]);
@ -250,9 +272,9 @@ const Player = ({ urlParams, queryParams }) => {
nextVideo();
const deepLinks = player.nextVideo.deepLinks;
handleNextVideoNavigation(deepLinks);
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, false);
}
}, [player.nextVideo, handleNextVideoNavigation]);
}, [player.nextVideo, handleNextVideoNavigation, profile.settings]);
const onVideoClick = React.useCallback(() => {
if (video.state.paused !== null) {
@ -316,10 +338,10 @@ const Player = ({ urlParams, queryParams }) => {
setError(null);
video.unload();
if (player.selected && streamingServer.settings?.type !== 'Loading') {
if (player.selected && player.stream?.type === 'Ready' && streamingServer.settings?.type !== 'Loading') {
video.load({
stream: {
...player.selected.stream,
...player.stream.content,
subtitles: Array.isArray(player.selected.stream.subtitles) ?
player.selected.stream.subtitles.map((subtitles) => ({
...subtitles,
@ -339,6 +361,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
@ -352,7 +376,7 @@ const Player = ({ urlParams, queryParams }) => {
shellTransport: services.shell.active ? services.shell.transport : null,
});
}
}, [streamingServer.baseUrl, player.selected, forceTranscoding, casting]);
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
React.useEffect(() => {
if (video.state.stream !== null) {
const tracks = player.subtitles.map((subtitles) => ({
@ -403,7 +427,7 @@ const Player = ({ urlParams, queryParams }) => {
}, [video.state.videoParams]);
React.useEffect(() => {
if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
if (player.nextVideo !== null && !nextVideoPopupDismissed.current) {
if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
openNextVideoPopup();
} else {
@ -526,109 +550,146 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
React.useLayoutEffect(() => {
const onKeyDown = (event) => {
switch (event.code) {
case 'Space': {
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
if (video.state.paused) {
onPlayRequested();
setSeeking(false);
} else {
onPauseRequested();
}
}
// Media Session PlaybackState
React.useEffect(() => {
if (!navigator.mediaSession) return;
break;
}
case 'ArrowRight': {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
setSeeking(true);
onSeekRequested(video.state.time + seekDuration);
}
const playbackState = !video.state.paused ? 'playing' : 'paused';
navigator.mediaSession.playbackState = playbackState;
break;
}
case 'ArrowLeft': {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
setSeeking(true);
onSeekRequested(video.state.time - seekDuration);
}
return () => navigator.mediaSession.playbackState = 'none';
}, [video.state.paused]);
break;
}
case 'ArrowUp': {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
}
// Media Session Metadata
React.useEffect(() => {
if (!navigator.mediaSession) return;
break;
}
case 'ArrowDown': {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
}
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;
break;
}
case 'KeyS': {
closeMenus();
if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
(Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0)) {
toggleSubtitlesMenu();
}
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;
break;
}
case 'KeyA': {
closeMenus();
if (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0) {
toggleAudioMenu();
}
const title = videoTitle ?? metaTitle;
const artist = videoTitle ? metaTitle : undefined;
const artwork = imageUrl ? [{ src: imageUrl }] : undefined;
break;
}
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleSideDrawer();
}
if (title) {
navigator.mediaSession.metadata = new MediaMetadata({
title,
artist,
artwork,
});
}
}, [player.metaItem, player.selected]);
break;
}
case 'KeyR': {
closeMenus();
if (video.state.playbackSpeed !== null) {
toggleSpeedMenu();
}
// Media Session Actions
React.useEffect(() => {
if (!navigator.mediaSession) return;
break;
}
case 'KeyD': {
closeMenus();
if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
toggleStatisticsMenu();
}
navigator.mediaSession.setActionHandler('play', onPlayRequested);
navigator.mediaSession.setActionHandler('pause', onPauseRequested);
break;
}
case 'KeyG': {
onDecreaseSubtitlesDelay();
break;
}
case 'KeyH': {
onIncreaseSubtitlesDelay();
break;
}
case 'Escape': {
closeMenus();
!settings.escExitFullscreen && window.history.back();
break;
}
const nexVideoCallback = player.nextVideo ? onNextVideoRequested : null;
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
onShortcut('playPause', () => {
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
if (video.state.paused) {
onPlayRequested();
setSeeking(false);
} else {
onPauseRequested();
}
};
}
}, [menusOpen, nextVideoPopupOpen, video.state.paused, onPlayRequested, onPauseRequested]);
onShortcut('seekForward', (combo) => {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
setSeeking(true);
onSeekRequested(video.state.time + seekDuration);
}
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
onShortcut('seekBackward', (combo) => {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
setSeeking(true);
onSeekRequested(video.state.time - seekDuration);
}
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
onShortcut('mute', () => {
video.state.muted === true ? onUnmuteRequested() : onMuteRequested();
}, [video.state.muted]);
onShortcut('volumeUp', () => {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
}
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
onShortcut('volumeDown', () => {
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
onVolumeChangeRequested(Math.min(video.state.volume - 5, 200));
}
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
onShortcut('subtitlesDelay', (combo) => {
combo === 1 ? onIncreaseSubtitlesDelay() : onDecreaseSubtitlesDelay();
}, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay]);
onShortcut('subtitlesSize', (combo) => {
combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1);
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
onShortcut('subtitlesMenu', () => {
closeMenus();
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
toggleSubtitlesMenu();
}
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, toggleSubtitlesMenu]);
onShortcut('audioMenu', () => {
closeMenus();
if (video.state?.audioTracks?.length > 0) {
toggleAudioMenu();
}
}, [video.state.audioTracks, toggleAudioMenu]);
onShortcut('infoMenu', () => {
closeMenus();
if (player.metaItem?.type === 'Ready') {
toggleSideDrawer();
}
}, [player.metaItem, toggleSideDrawer]);
onShortcut('speedMenu', () => {
closeMenus();
if (video.state.playbackSpeed !== null) {
toggleSpeedMenu();
}
}, [video.state.playbackSpeed, toggleSpeedMenu]);
onShortcut('statisticsMenu', () => {
closeMenus();
const stream = player.selected?.stream;
if (streamingServer?.statistics?.type !== 'Err' && typeof stream === 'string' && typeof stream === 'number') {
toggleStatisticsMenu();
}
}, [player.selected, streamingServer.statistics, toggleStatisticsMenu]);
onShortcut('exit', () => {
closeMenus();
!settings.escExitFullscreen && window.history.back();
}, [settings.escExitFullscreen]);
React.useLayoutEffect(() => {
const onKeyUp = (event) => {
if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
setSeeking(false);
@ -646,38 +707,14 @@ const Player = ({ urlParams, queryParams }) => {
}
};
if (routeFocused) {
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('wheel', onWheel);
}
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('wheel', onWheel);
};
}, [
player.metaItem,
player.selected,
streamingServer.statistics,
settings.seekTimeDuration,
settings.seekShortTimeDuration,
settings.escExitFullscreen,
routeFocused,
menusOpen,
nextVideoPopupOpen,
video.state.paused,
video.state.time,
video.state.volume,
video.state.audioTracks,
video.state.subtitlesTracks,
video.state.extraSubtitlesTracks,
video.state.playbackSpeed,
toggleSubtitlesMenu,
toggleStatisticsMenu,
toggleSideDrawer,
onDecreaseSubtitlesDelay,
onIncreaseSubtitlesDelay,
]);
}, [routeFocused, menusOpen, video.state.volume]);
React.useEffect(() => {
video.events.on('error', onError);
@ -766,6 +803,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player?.selected?.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
</ContextMenu>
<HorizontalNavBar
@ -820,6 +859,7 @@ const Player = ({ urlParams, queryParams }) => {
<Indicator
className={classnames(styles['layer'], styles['indicator-layer'])}
videoState={video.state}
disabled={subtitlesMenuOpen}
/>
{
nextVideoPopupOpen ?
@ -848,7 +888,7 @@ const Player = ({ urlParams, queryParams }) => {
metaItem={player.metaItem?.content}
seriesInfo={player.seriesInfo}
closeSideDrawer={closeSideDrawer}
selected={player.selected?.streamRequest.path.id}
selected={player.selected?.streamRequest?.path.id}
/>
</Transition>
{
@ -902,6 +942,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
:
null

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo, useCallback, useState, forwardRef, memo, useRef, useEffect } 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';
@ -18,10 +18,11 @@ type Props = {
transitionEnded: boolean;
};
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, transitionEnded, ...props }: Props, ref) => {
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => {
const { core } = useServices();
const [season, setSeason] = useState<number>(seriesInfo?.season);
const selectedVideoRef = useRef<HTMLDivElement>(null);
const [selectedVideoId, setSelectedVideoId] = useState<string | null>(null);
const metaItem = useMemo(() => {
return seriesInfo ?
{
@ -78,16 +79,12 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
event.stopPropagation();
};
const getSelectedRef = useCallback((video: Video) => {
return video.id === selected ? selectedVideoRef : null;
const onTransitionEnd = useCallback(() => {
setSelectedVideoId(selected);
}, [selected]);
useEffect(() => {
transitionEnded && selectedVideoRef?.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [transitionEnded]);
return (
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown}>
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown} onTransitionEnd={onTransitionEnd}>
<div className={styles['close-button']} onClick={closeSideDrawer}>
<Icon className={styles['icon']} name={'chevron-forward'} />
</div>
@ -129,9 +126,9 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
progress={video.progress}
deepLinks={video.deepLinks}
scheduled={video.scheduled}
selected={video.id === selectedVideoId}
onMarkVideoAsWatched={onMarkVideoAsWatched}
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
ref={getSelectedRef(video)}
/>
))}
</div>

View file

@ -1,49 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button } = require('stremio/components');
const styles = require('./styles');
const DiscreteSelectInput = ({ className, value, label, disabled, dataset, onChange }) => {
const { t } = useTranslation();
const buttonOnClick = React.useCallback((event) => {
if (typeof onChange === 'function') {
onChange({
type: 'change',
value: event.currentTarget.dataset.type,
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [dataset, onChange]);
return (
<div className={classnames(className, styles['discrete-input-container'], { 'disabled': disabled })}>
<div className={styles['header']}>{label}</div>
<div className={styles['input-container']} title={disabled ? t('DISABLED_LABEL', { label }) : null}>
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'decrement'} onClick={buttonOnClick}>
<Icon className={styles['icon']} name={'remove'} />
</Button>
<div className={styles['option-label']} title={value}>{value}</div>
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'increment'} onClick={buttonOnClick}>
<Icon className={styles['icon']} name={'add'} />
</Button>
</div>
</div>
);
};
DiscreteSelectInput.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
label: PropTypes.string,
disabled: PropTypes.bool,
dataset: PropTypes.object,
onChange: PropTypes.func
};
module.exports = DiscreteSelectInput;

View file

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

View file

@ -1,14 +1,10 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.discrete-input-container {
.stepper {
&:global(.disabled) {
.header {
color: var(--primary-foreground-color);
}
.input-container {
.content {
opacity: 0.4;
}
}
@ -19,14 +15,14 @@
opacity: 0.6;
}
.input-container {
.content {
display: flex;
flex-direction: row;
align-items: center;
border-radius: 3.5rem;
background: var(--overlay-color);
.button-container {
.button {
flex: none;
width: 3.5rem;
height: 3.5rem;
@ -42,7 +38,7 @@
}
}
.option-label {
.value {
flex: 1;
font-weight: 500;
text-align: center;

View file

@ -0,0 +1,98 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { Button } from 'stremio/components';
import { useInterval, useTimeout } from 'stremio/common';
import styles from './Stepper.less';
const clamp = (value: number, min?: number, max?: number) => {
const minClamped = typeof min === 'number' ? Math.max(value, min) : value;
const maxClamped = typeof max === 'number' ? Math.min(minClamped, max) : minClamped;
return maxClamped;
};
type Props = {
className: string,
label: string,
value: number,
unit?: string,
step: number,
min?: number,
max?: number,
disabled?: boolean,
onChange: (value: number) => void,
};
const Stepper = ({ className, label, value, unit, step, min, max, disabled, onChange }: Props) => {
const { t } = useTranslation();
const localValue = useRef(value);
const interval = useInterval(100);
const timeout = useTimeout(250);
const cancel = () => {
interval.cancel();
timeout.cancel();
};
const updateValue = useCallback((delta: number) => {
onChange(clamp(localValue.current + delta, min, max));
}, [onChange]);
const onDecrementMouseDown = useCallback(() => {
cancel();
timeout.start(() => interval.start(() => updateValue(-step)));
}, [updateValue]);
const onDecrementMouseUp = useCallback(() => {
cancel();
updateValue(-step);
}, [updateValue]);
const onIncrementMouseDown = useCallback(() => {
cancel();
timeout.start(() => interval.start(() => updateValue(step)));
}, [updateValue]);
const onIncrementMouseUp = useCallback(() => {
cancel();
updateValue(step);
}, [updateValue]);
useEffect(() => {
localValue.current = value;
}, [value]);
return (
<div className={classNames(styles['stepper'], className)}>
<div className={styles['header']}>
{ t(label) }
</div>
<div className={styles['content']}>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
onMouseDown={onDecrementMouseDown}
onMouseUp={onDecrementMouseUp}
onMouseLeave={cancel}
>
<Icon className={styles['icon']} name={'remove'} />
</Button>
<div className={styles['value']}>
{ disabled ? '--' : `${value}${unit}` }
</div>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
onMouseDown={onIncrementMouseDown}
onMouseUp={onIncrementMouseUp}
onMouseLeave={cancel}
>
<Icon className={styles['icon']} name={'add'} />
</Button>
</div>
</div>
);
};
export default Stepper;

View file

@ -0,0 +1,2 @@
import Stepper from './Stepper';
export default Stepper;

View file

@ -3,11 +3,12 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { CONSTANTS, comparatorWithPriorities, languages } = require('stremio/common');
const { comparatorWithPriorities, languages } = require('stremio/common');
const { SUBTITLES_SIZES } = require('stremio/common/CONSTANTS');
const { Button } = require('stremio/components');
const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles');
const { t } = require('i18next');
const { default: Stepper } = require('./Stepper');
const ORIGIN_PRIORITIES = {
'LOCAL': 3,
@ -98,51 +99,41 @@ const SubtitlesMenu = React.memo((props) => {
}
}
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
const onSubtitlesDelayChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 250 : -250;
const onSubtitlesDelayChanged = React.useCallback((value) => {
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
const extraDelay = props.extraSubtitlesDelay + delta;
if (typeof props.onExtraSubtitlesDelayChanged === 'function') {
props.onExtraSubtitlesDelayChanged(extraDelay);
props.onExtraSubtitlesDelayChanged(value * 1000);
}
}
}
}, [props.selectedExtraSubtitlesTrackId, props.extraSubtitlesDelay, props.onExtraSubtitlesDelayChanged]);
const onSubtitlesSizeChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 1 : -1;
const onSubtitlesSizeChanged = React.useCallback((value) => {
if (typeof props.selectedSubtitlesTrackId === 'string') {
if (props.subtitlesSize !== null && !isNaN(props.subtitlesSize)) {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.subtitlesSize);
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
if (typeof props.onSubtitlesSizeChanged === 'function') {
props.onSubtitlesSizeChanged(size);
props.onSubtitlesSizeChanged(value);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize)) {
const extraSizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.extraSubtitlesSize);
const extraSize = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, extraSizeIndex + delta))];
if (typeof props.onExtraSubtitlesSizeChanged === 'function') {
props.onExtraSubtitlesSizeChanged(extraSize);
props.onExtraSubtitlesSizeChanged(value);
}
}
}
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesSize, props.extraSubtitlesSize, props.onSubtitlesSizeChanged, props.onExtraSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 1 : -1;
const onSubtitlesOffsetChanged = React.useCallback((value) => {
if (typeof props.selectedSubtitlesTrackId === 'string') {
if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.subtitlesOffset + delta)));
if (typeof props.onSubtitlesOffsetChanged === 'function') {
props.onSubtitlesOffsetChanged(offset);
props.onSubtitlesOffsetChanged(value);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta)));
if (typeof props.onExtraSubtitlesOffsetChanged === 'function') {
props.onExtraSubtitlesOffsetChanged(offset);
props.onExtraSubtitlesOffsetChanged(value);
}
}
}
@ -215,57 +206,35 @@ const SubtitlesMenu = React.memo((props) => {
<div className={styles['subtitles-settings-container']}>
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
<div className={styles['settings-list']}>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('DELAY')}
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
<Stepper
className={styles['stepper']}
label={'DELAY'}
value={props.extraSubtitlesDelay / 1000}
unit={'s'}
step={0.25}
disabled={props.extraSubtitlesDelay === null}
onChange={onSubtitlesDelayChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('SIZE')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize === null || isNaN(props.subtitlesSize)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
:
true
}
<Stepper
className={styles['stepper']}
label={'SIZE'}
value={props.selectedSubtitlesTrackId ? props.subtitlesSize : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesSize : null}
unit={'%'}
step={25}
min={SUBTITLES_SIZES[0]}
max={SUBTITLES_SIZES[SUBTITLES_SIZES.length - 1]}
disabled={(props.selectedSubtitlesTrackId && props.subtitlesSize === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesSize === null)}
onChange={onSubtitlesSizeChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
:
true
}
<Stepper
className={styles['stepper']}
label={'PLAYER_SUBTITLES_VERTICAL_POSITION'}
value={props.selectedSubtitlesTrackId ? props.subtitlesOffset : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesOffset : null}
unit={'%'}
step={1}
min={0}
max={100}
disabled={(props.selectedSubtitlesTrackId && props.subtitlesOffset === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesOffset === null)}
onChange={onSubtitlesOffsetChanged}
/>
</div>

View file

@ -114,7 +114,7 @@
flex: 1;
}
.discrete-input {
.stepper {
padding: 0 1.5rem 1rem;
}
}

View file

@ -102,8 +102,8 @@ const usePlayer = (urlParams) => {
args: {
action: 'TimeChanged',
args: {
time: Math.round(time),
duration,
time: Math.max(0, Math.round(time)),
duration: Math.max(0, Math.round(duration)),
device,
}
}
@ -118,8 +118,8 @@ const usePlayer = (urlParams) => {
args: {
action: 'Seek',
args: {
time: Math.round(time),
duration,
time: Math.max(0, Math.round(time)),
duration: Math.max(0, Math.round(duration)),
device,
}
}

View file

@ -7,11 +7,12 @@ const useStatistics = (player, streamingServer) => {
const { core } = useServices();
const stream = React.useMemo(() => {
return player.selected?.stream ?
player.selected.stream
:
null;
}, [player.selected]);
if (player.stream?.type === 'Ready') {
return player.stream.content;
} else {
return null;
}
}, [player.stream]);
const infoHash = React.useMemo(() => {
return stream?.infoHash ?

View file

@ -152,7 +152,15 @@ const useVideo = () => {
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.current.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
return () => video.current.destroy();
return () => {
if (video.current) {
try {
video.current.destroy();
} catch (err) {
console.error('Error destroying video:', err);
}
}
};
}, []);
return {

View file

@ -12,7 +12,7 @@
}
.search-container {
height: 100%;
height: calc(100% - var(--safe-area-inset-bottom));
width: 100%;
background-color: transparent;

View file

@ -3,6 +3,7 @@ import { ColorInput, MultiselectMenu, Toggle } from 'stremio/components';
import { useServices } from 'stremio/services';
import { Category, Option, Section } from '../components';
import usePlayerOptions from './usePlayerOptions';
import { usePlatform } from 'stremio/common';
type Props = {
profile: Profile,
@ -10,6 +11,7 @@ type Props = {
const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { shell } = useServices();
const platform = usePlatform();
const {
subtitlesLanguageSelect,
@ -26,6 +28,7 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
bingeWatchingToggle,
playInBackgroundToggle,
hardwareDecodingToggle,
videoModeSelect,
pauseOnMinimizeToggle,
} = usePlayerOptions(profile);
@ -108,7 +111,6 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
<Option label={'SETTINGS_NEXT_VIDEO_POPUP_DURATION'}>
<MultiselectMenu
className={'multiselect'}
disabled={!profile.settings.bingeWatching}
{...nextVideoPopupDurationSelect}
/>
</Option>
@ -129,6 +131,15 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
/>
</Option>
}
{
shell.active && platform.name === 'windows' &&
<Option label={'SETTINGS_VIDEO_MODE'}>
<MultiselectMenu
className={'multiselect'}
{...videoModeSelect}
/>
</Option>
}
{
shell.active &&
<Option label={'SETTINGS_PAUSE_MINIMIZED'}>

View file

@ -287,6 +287,38 @@ const usePlayerOptions = (profile: Profile) => {
}
}), [profile.settings]);
const videoModeSelect = useMemo(() => ({
options: [
{
value: null,
label: t('SETTINGS_VIDEO_MODE_DEFAULT'),
},
{
value: 'legacy',
label: t('SETTINGS_VIDEO_MODE_LEGACY'),
}
],
value: profile.settings.videoMode,
title: () => {
return profile.settings.videoMode === 'legacy' ?
t('SETTINGS_VIDEO_MODE_LEGACY')
:
t('SETTINGS_VIDEO_MODE_DEFAULT');
},
onSelect: (value: string | null) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
videoMode: value,
}
}
});
}
}), [profile.settings]);
const pauseOnMinimizeToggle = useMemo(() => ({
checked: profile.settings.pauseOnMinimize,
onClick: () => {
@ -318,6 +350,7 @@ const usePlayerOptions = (profile: Profile) => {
bingeWatchingToggle,
playInBackgroundToggle,
hardwareDecodingToggle,
videoModeSelect,
pauseOnMinimizeToggle,
};
};

View file

@ -4,7 +4,7 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from '
import classnames from 'classnames';
import throttle from 'lodash.throttle';
import { useRouteFocused } from 'stremio-router';
import { useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
import { usePlatform, useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
import { MainNavBars } from 'stremio/components';
import { SECTIONS } from './constants';
import Menu from './Menu';
@ -18,6 +18,7 @@ import styles from './Settings.less';
const Settings = () => {
const { routeFocused } = useRouteFocused();
const profile = useProfile();
const platform = usePlatform();
const streamingServer = useStreamingServer();
const sectionsContainerRef = useRef<HTMLDivElement>(null);
@ -37,14 +38,10 @@ const Settings = () => {
const updateSelectedSectionId = useCallback(() => {
const container = sectionsContainerRef.current;
if (container!.scrollTop + container!.clientHeight >= container!.scrollHeight - 50) {
setSelectedSectionId(sections[sections.length - 1].id);
} else {
for (let i = sections.length - 1; i >= 0; i--) {
if (sections[i].ref.current!.offsetTop - container!.offsetTop <= container!.scrollTop) {
setSelectedSectionId(sections[i].id);
break;
}
for (const section of sections) {
const sectionContainer = section.ref.current;
if (sectionContainer && (sectionContainer.offsetTop + container!.offsetTop) < container!.scrollTop + 50) {
setSelectedSectionId(section.id);
}
}
}, []);
@ -94,7 +91,9 @@ const Settings = () => {
profile={profile}
streamingServer={streamingServer}
/>
<Shortcuts ref={shortcutsSectionRef} />
{
!platform.isMobile && <Shortcuts ref={shortcutsSectionRef} />
}
<Info streamingServer={streamingServer} />
</div>
</div>

View file

@ -1,27 +1,4 @@
.shortcut-container {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
overflow: visible;
kbd {
flex: 0 1 auto;
height: 2.5rem;
min-width: 2.5rem;
line-height: 2.5rem;
padding: 0 1rem;
font-weight: 500;
color: var(--primary-foreground-color);
border-radius: 0.25em;
box-shadow: 0 4px 0 1px var(--modal-background-color);
background-color: var(--overlay-color);
}
.label {
flex: none;
margin: 0 1rem;
white-space: nowrap;
color: var(--primary-foreground-color);
}
.shortcuts-group {
width: 100%;
margin-bottom: 3rem;
}

View file

@ -1,95 +1,24 @@
import React, { forwardRef } from 'react';
import { Section, Option } from '../components';
import { Section } from '../components';
import { ShortcutsGroup } from 'stremio/components';
import { useShortcuts } from 'stremio/common';
import styles from './Shortcuts.less';
import { useTranslation } from 'react-i18next';
const Shortcuts = forwardRef<HTMLDivElement>((_, ref) => {
const { t } = useTranslation();
const { grouped } = useShortcuts();
return (
<Section ref={ref} label={'SETTINGS_NAV_SHORTCUTS'}>
<Option label={'SETTINGS_SHORTCUT_PLAY_PAUSE'}>
<div className={styles['shortcut-container']}>
<kbd>{t('SETTINGS_SHORTCUT_SPACE')}</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_SEEK_FORWARD'}>
<div className={styles['shortcut-container']}>
<kbd></kbd>
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
<kbd> {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
<div className={styles['label']}>+</div>
<kbd></kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_SEEK_BACKWARD'}>
<div className={styles['shortcut-container']}>
<kbd></kbd>
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_OR')}</div>
<kbd> {t('SETTINGS_SHORTCUT_SHIFT')}</kbd>
<div className={styles['label']}>+</div>
<kbd></kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_VOLUME_UP'}>
<div className={styles['shortcut-container']}>
<kbd></kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_VOLUME_DOWN'}>
<div className={styles['shortcut-container']}>
<kbd></kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_MENU_SUBTITLES'}>
<div className={styles['shortcut-container']}>
<kbd>S</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_MENU_AUDIO'}>
<div className={styles['shortcut-container']}>
<kbd>A</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_MENU_INFO'}>
<div className={styles['shortcut-container']}>
<kbd>I</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_MENU_VIDEOS'}>
<div className={styles['shortcut-container']}>
<kbd>V</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
<div className={styles['shortcut-container']}>
<kbd>F</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_DELAY'}>
<div className={styles['shortcut-container']}>
<kbd>G</kbd>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
<kbd>H</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_NAVIGATE_MENUS'}>
<div className={styles['shortcut-container']}>
<kbd>1</kbd>
<div className={styles['label']}>{t('SETTINGS_SHORTCUT_TO')}</div>
<kbd>6</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_GO_TO_SEARCH'}>
<div className={styles['shortcut-container']}>
<kbd>0</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_EXIT_BACK'}>
<div className={styles['shortcut-container']}>
<kbd>{t('SETTINGS_SHORTCUT_ESC')}</kbd>
</div>
</Option>
{
grouped.map(({ name, label, shortcuts }) => (
<ShortcutsGroup
key={name}
className={styles['shortcuts-group']}
label={label}
shortcuts={shortcuts}
/>
))
}
</Section>
);
});

View file

@ -69,7 +69,7 @@
.cancel {
&:hover {
.icon {
color: var(--color-trakt);
color: var(--danger-accent-color);
}
}
}

View file

@ -17,7 +17,7 @@ const AddItem = ({ onCancel, handleAddUrl }: Props) => {
setInputValue(target.value);
}, []);
const onSumbit = useCallback(() => {
const onSubmit = useCallback(() => {
handleAddUrl(inputValue);
}, [inputValue]);
@ -27,11 +27,11 @@ const AddItem = ({ onCancel, handleAddUrl }: Props) => {
className={styles['input']}
value={inputValue}
onChange={handleValueChange}
onSubmit={onSumbit}
onSubmit={onSubmit}
placeholder={'Enter URL'}
/>
<div className={styles['actions']}>
<Button className={styles['add']} onClick={onSumbit}>
<Button className={styles['add']} onClick={onSubmit}>
<Icon name={'checkmark'} className={styles['icon']} />
</Button>
<Button className={styles['cancel']} onClick={onCancel}>

View file

@ -52,7 +52,7 @@
}
&.error {
background-color: var(--color-trakt);
background-color: var(--danger-accent-color);
}
}
@ -92,7 +92,7 @@
background-color: var(--overlay-color);
.icon {
color: var(--color-trakt);
color: var(--danger-accent-color);
opacity: 1 !important;
}
}

View file

@ -22,8 +22,8 @@
gap: 0.75rem;
.icon {
width: 3rem;
height: 3rem;
width: 4rem;
height: 4rem;
color: var(--primary-foreground-color);
}

View file

@ -21,7 +21,7 @@ const initialize = () => {
if (castAPIAvailable) {
resolve();
} else {
reject(new Error('window.cast api not available'));
reject(new Error('window.cast api not available', { cause: 'castAPIAvailable is null.' }));
}
}
if (castAPIAvailable !== null) {
@ -167,7 +167,7 @@ function ChromecastTransport() {
});
}));
} else {
return Promise.reject(new Error('Session not started'));
return Promise.reject(new Error('Session not started', { cause: 'castSession is null.' }));
}
};
}

View file

@ -63,7 +63,7 @@ function Shell() {
} catch (e) {
console.error(e);
active = false;
error = new Error(e);
error = new Error('Failed to initialize shell transport', { cause: e });
starting = false;
onStateChanged();
transport = null;

View file

@ -1,5 +1,3 @@
/* eslint-disable no-var */
type QtTransportMessage = {
data: string;
};
@ -28,4 +26,4 @@ declare global {
var chrome: Chrome | undefined;
}
export {};
export { };

View file

@ -19,6 +19,7 @@ type Settings = {
autoFrameRateMatching: boolean,
bingeWatching: boolean,
hardwareDecoding: boolean,
videoMode: string | null,
escExitFullscreen: boolean,
interfaceLanguage: string,
quitOnClose: boolean,

View file

@ -3,7 +3,7 @@ type LibraryItemPlayer = Pick<LibraryItem, '_id'> & {
};
type VideoPlayer = Video & {
upcomming: boolean,
upcoming: boolean,
watched: boolean,
progress: boolean | null,
scheduled: boolean,

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable"],
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "react",
"baseUrl": "./src",
"outDir": "./dist",

View file

@ -12,7 +12,7 @@ const WorkboxPlugin = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
const pachageJson = require('./package.json');
const packageJson = require('./package.json');
const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim();
@ -213,8 +213,9 @@ module.exports = (env, argv) => ({
new webpack.EnvironmentPlugin({
SENTRY_DSN: null,
...env,
SERVICE_WORKER_DISABLED: false,
DEBUG: argv.mode !== 'production',
VERSION: pachageJson.version,
VERSION: packageJson.version,
COMMIT_HASH
}),
new webpack.ProvidePlugin({