Compare commits

...

248 commits

Author SHA1 Message Date
Timothy Z.
5db6df4fc2
Merge pull request #1217 from Stremio/fix/subtitles-selections-issues
Some checks are pending
Build / build (push) Waiting to run
2026-04-20 17:30:27 +03:00
Timothy Z.
5580da8ecf fix: reselection loop 2026-04-20 16:57:45 +03:00
Timothy Z.
38a811ce75 bump: stremio-core-web v0.56.4 2026-04-20 16:43:56 +03:00
Timothy Z.
de78766151 bump: stremio-video to 0.0.75 2026-04-20 16:38:32 +03:00
Timothy Z.
5bc74a9da4
Merge pull request #1199 from Stremio/fix/statistics-menu-handler
Some checks are pending
Build / build (push) Waiting to run
Player: Fix statistics menu shortcut handler
2026-04-20 12:20:55 +03:00
Timothy Z.
fe651a2cca
Merge pull request #1198 from Stremio/fix/blur-window-cancel-shortcut-events
Player: Fix cancel shortcut events on blur
2026-04-20 12:20:47 +03:00
Timothy Z.
377d57cf16
Merge pull request #1197 from Stremio/fix/correctly-select-embedded-subs-first
Player: Prioritize embedded subs first
2026-04-20 12:20:35 +03:00
Timothy Z.
694fb833aa fix: statistics menu handler 2026-04-17 21:03:11 +03:00
Timothy Z.
f74f9aafa0 fix: on blue cancel shortcut events 2026-04-17 20:53:48 +03:00
Timothy Z.
bbfe25177b fix: prioritize embedded subs first 2026-04-17 17:38:31 +03:00
Timothy Z.
5560813b37
Merge pull request #1196 from Stremio/fix/space-bar-events-play-pause-handing 2026-04-16 12:27:02 +03:00
Timothy Z.
8fcffa2c3e Update Player.js 2026-04-16 12:17:22 +03:00
Timothy Z.
3e93afbe21 fix: space bar holding issues 2026-04-16 12:13:48 +03:00
Timothy Z.
81fdb18536 bump: release v5.0.0-beta.34 2026-04-15 00:47:24 +03:00
Timothy Z.
5e8db65551
Merge pull request #1194 from Stremio/fix/subtitles-grouping-logic
Player: Use combination of set with map to avoid crashes on SubtitlesMenu
2026-04-15 00:45:47 +03:00
Timothy Z.
a322de9093 fix: use set with map to avoid crash on older devices 2026-04-14 21:34:10 +03:00
Tim
35970825fa
Merge pull request #1179 from ignaciojsoler/feat/persist-subtitle-language
fix: persist subtitle language preference across streams
2026-04-14 13:16:39 +02:00
Timothy Z.
7b30c4e805 bump: stremio-video to v0.0.73 2026-04-14 13:32:34 +03:00
Timothy Z.
b48740f5c2
Merge pull request #1193 from Stremio/dependabot/github_actions/pnpm/action-setup-6
chore(deps): bump pnpm/action-setup from 5 to 6
2026-04-14 11:59:19 +03:00
Timothy Z.
79a771cbc3
Merge pull request #1192 from Stremio/dependabot/github_actions/actions/github-script-9
chore(deps): bump actions/github-script from 8 to 9
2026-04-14 11:59:06 +03:00
dependabot[bot]
a1e19f0ea5
chore(deps): bump pnpm/action-setup from 5 to 6
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 5 to 6.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 22:03:50 +00:00
dependabot[bot]
73917c14a4
chore(deps): bump actions/github-script from 8 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 22:03:46 +00:00
Timothy Z.
bf65b6001e bump: release v5.0.0-beta.33 2026-04-13 20:17:17 +03:00
Timothy Z.
35b7b7082c Revert "bump: release v5.0.0-beta.33"
This reverts commit 33d09916cb.
2026-04-13 20:16:48 +03:00
Timothy Z.
33d09916cb bump: release v5.0.0-beta.33 2026-04-13 20:12:33 +03:00
Timothy Z.
29d491d00b bump: stremio-core to v0.56.3 2026-04-13 20:06:40 +03:00
Timothy Z.
3ce58df582 bump: stremio-video 0.0.72 2026-04-13 19:49:54 +03:00
Timothy Z.
7cd439a656 chore: bump core to v0.56.2 2026-04-13 19:30:19 +03:00
Timothy Z.
9e0dd0e942 bump: release v5.0.0-beta.33 2026-04-13 17:50:17 +03:00
Timothy Z.
b003f64f4f
Merge pull request #1060 from higorgoulart/feat/989/watched-on-discover-and-details
Details | Discover: Implement mark as watched button
2026-04-13 17:20:03 +03:00
Timothy Z.
b058eb536e
Merge branch 'development' into feat/989/watched-on-discover-and-details 2026-04-13 17:19:53 +03:00
Timothy Z.
0502c9e47a chore: update core to 0.56.1 2026-04-13 17:19:17 +03:00
Timothy Z.
1f293b7cbc
Merge pull request #1180 from Stremio/feat/magnet-http-streams-handing
feat: handle http/magnets from search bar
2026-04-13 17:17:42 +03:00
Ignacio
b69165e868 fix: pass track object to subtitle callbacks and fix lang fallback chain 2026-04-10 10:27:25 -03:00
Timothy Z.
234d83ad34 chore: use dev build 2026-04-08 13:49:00 +03:00
Timothy Z.
3052a724f4 Merge branch 'development' into pr/1060 2026-04-08 13:48:17 +03:00
Timothy Z.
880304f46f
Merge pull request #1191 from Stremio/refactor/player-events
Player: refactor events
2026-04-07 13:13:25 +03:00
Timothy Z.
e860752f41 fix(player): cancel hold-to-speed-up timer when menus open (2) 2026-04-06 21:33:18 +03:00
Timothy Z.
b2d81c09ee fix(player): cancel hold-to-speed-up timer when menus open 2026-04-06 21:33:08 +03:00
Timothy Z.
49240a4e0f fix(player/statistics): prevent menu from closing on inside click 2026-04-06 21:31:36 +03:00
Timothy Z.
129f510047 fix(player): block mute and speed/subtitle shortcuts while a menu is open 2026-04-06 21:30:46 +03:00
Timothy Z.
33c3f6d9f0 refactor(player): fold nextVideoPopupOpen into menusOpen and gate playback shortcuts via onShortcut 2026-04-06 21:29:33 +03:00
Timothy Z.
117f932596 chore(shortcuts): add enabled param to onShortcut hook 2026-04-06 21:25:56 +03:00
Timothy Z.
de5d43225b
Merge pull request #1190 from Stremio/refactor/context-menu-transition-usage
ContextMenu: align with transition usage
2026-04-06 21:15:47 +03:00
Timothy Z.
aa76396a84 ContextMenu: align with transition usage 2026-04-06 21:14:57 +03:00
Timothy Z.
c99d7c5d82 use new action metaItemMarkAsWatched 2026-04-05 15:05:47 +03:00
Timothy Z.
4e735afacc
Merge pull request #1185 from mrcanelas/fix/windows-emoji-rendering
feat(ui): add Twemoji font fallback for country flags on Windows
2026-04-05 13:20:27 +03:00
Tim
6dfd0fcbde refactor(player): momoize menus 2026-04-02 09:56:21 +02:00
Tim
1b9e2a194f fix: cancel animation frame on cleanup for Transition 2026-04-02 09:54:19 +02:00
Tim
3f5dedd072 feat(player): add transitions to menus 2026-04-02 09:53:20 +02:00
mrcanelas
0e1d22d279 feat(ui): add font fallback for country flags on Windows 2026-04-01 18:52:22 -03:00
Tim
eb23d3e4db
Merge pull request #1184 from Stremio/feat/player-subtitles-sort-default-language
Player: Sort subtitles menu languages by language settings
2026-04-01 22:50:45 +02:00
Tim
73409fff8c feat(player): sort subtitles menu languages by default language 2026-04-01 22:39:48 +02:00
Tim
70bcf339be
Merge pull request #1182 from Stremio/fix/shell-subtitles-menu-split-languages
Player: Fix multiple languages entries from player subtitles menu
2026-04-01 18:04:55 +02:00
Tim
50cd992f1c fix(Player): embedded track labels on subtitles menu 2026-04-01 18:03:36 +02:00
Tim
c131d7f75e fix: normalize subtitles languages from player menu 2026-04-01 17:32:24 +02:00
Timothy Z.
e260e52322 refactor: change type to info 2026-03-31 13:45:08 +03:00
Timothy Z.
de32e3a765 fix: correct message for fail 2026-03-31 13:11:48 +03:00
Timothy Z.
6cb0c75555 feat: add loading state 2026-03-31 12:56:08 +03:00
Timothy Z.
0a88fe5d01
Merge pull request #1181 from mrcanelas/feat/addon-name-breakline
feat: support line breaks in addon names to match TV app behavior
2026-03-30 21:40:05 +03:00
Timothy Z.
ccc5832611 chore update core for testing 2026-03-30 21:39:13 +03:00
mrcanelas
c2c18bc4f6 feat: support line breaks in addon names to match TV app behavior 2026-03-30 13:26:36 -03:00
Timothy Z.
e0961ec686 remove the try catch 2026-03-30 18:47:20 +03:00
Timothy Z.
fcd85bdcf4 feat: handle http/magnets from search bar 2026-03-30 18:40:24 +03:00
Ignacio
9fe7430bc7 fix: persist subtitle language preference across streams 2026-03-29 11:03:42 -03:00
Timothy Z.
9dd44a7ef1
Merge pull request #1177 from Stremio/fix/discover-deeplinks-crash
Fix crash when no extra option is selected in Discover
2026-03-27 15:34:35 +02:00
Timothy Z.
f2023c5a5a
Merge pull request #1176 from Stremio/fix/streams-list-loading-layout-shift
fix: move streams loading indicator below list to prevent layout shift
2026-03-27 15:32:58 +02:00
Timothy Z.
ebc7c5bdde
Merge pull request #1175 from Stremio/fix/hold-speed-up-during-menus
fix: prevent hold-to-speed-up when menus are open
2026-03-27 15:31:52 +02:00
Timothy Z.
070807f148 Fix crash when no extra option is selected in Discover
Guard against `selectedExtra` being undefined when `options.find()`
returns no match, preventing "Cannot read properties of undefined
(reading 'deepLinks')" TypeError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:00:21 +02:00
Timothy Z.
d2199e49f0 fix: add border radius to top corners of loading indicator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:40:17 +02:00
Timothy Z.
2cc122ea1c fix: sticky loading indicator on mobile to keep it visible
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:33:54 +02:00
Timothy Z.
079d37cd77 fix: move streams loading indicator below the list to prevent layout shift
Closes Stremio/stremio-bugs#2187

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:29:01 +02:00
Timothy Z.
003ea9f2dc fix: prevent hold-to-speed-up when menus or popups are open
Closes Stremio/stremio-bugs#2440

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:56:09 +02:00
Timothy Z.
7ab75cfc88
Merge pull request #1120 from ArtificialSloth/fix/prevent-autoplay-when-using-external-player
Fix/Prevent autoplay when using an external player
2026-03-26 19:57:14 +02:00
Timothy Z.
504a3bb0be
Merge pull request #1173 from Stremio/fix/space-hold-reset-playback-speed
Player: Fix spacebar and left click hold behavior
2026-03-26 19:54:14 +02:00
Timothy Z.
1f56ddf8e3
Merge pull request #1174 from Stremio/feat/shortcut-playback-speed-increase-decrease
Shortcuts: Decrease "[" and increase "]" playback speed
2026-03-26 19:54:01 +02:00
Botzy
80e065778f chore: update translations 2026-03-26 18:27:52 +02:00
Botzy
0286994773 feat(Shortcuts): added shortcuts for increasing and decreasing playback speed 2026-03-26 18:15:46 +02:00
Botzy
3fdda2cdad fix: set initial playback speed to 1 instead of null 2026-03-26 17:56:46 +02:00
Botzy
866db891ef fix: mouse hold and release to not reset playback speed to 1x 2026-03-26 17:13:11 +02:00
Botzy
48ba0b8fba fix: mouse hold click to take effect only outside control bar 2026-03-26 17:04:40 +02:00
Botzy
0c78e39a02 fix: after spacebar key up set previously selected playback speed 2026-03-26 17:04:05 +02:00
Timothy Z.
89233243f2
Merge pull request #1170 from Stremio/dependabot/github_actions/pnpm/action-setup-5
chore(deps): bump pnpm/action-setup from 4 to 5
2026-03-24 14:19:17 +02:00
dependabot[bot]
6a981f275b
chore(deps): bump pnpm/action-setup from 4 to 5
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 5.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 21:24:19 +00:00
Timothy Z.
74f1d660be
Merge pull request #1168 from mrcanelas/fix/streams-list-episode-title
fix: improve episode title formatting in StreamsList component
2026-03-23 17:15:14 +02:00
mrcanelas
562854d902 fix: improve episode title formatting in StreamsList component 2026-03-22 19:29:27 -03:00
Timothy Z.
effa6e430e
Merge pull request #1167 from schobiDotDev/fix/subtitle-addon-label
fix: use addon-provided subtitle label instead of URL
2026-03-22 15:07:06 +02:00
schobidotdev
3c2e144f1d fix: use addon-provided subtitle label instead of URL 2026-03-22 13:03:29 +01:00
Timothy Z.
70ea2a5772
Merge pull request #1151 from Stremio/feat/stream-copy-magnet-option
MetaDetails: Add Copy Magnet Link option
2026-03-21 19:26:52 +02:00
Timothy Z.
ca46494480
Merge branch 'development' into feat/stream-copy-magnet-option 2026-03-21 19:23:12 +02:00
Timothy Z.
b1a2d0354b update core 2026-03-21 19:22:03 +02:00
Tim
50ab7d9f23 chore: v5.0.0-beta.32 2026-03-19 16:48:37 +01:00
Tim
b32cf42b96 chore: update core 2026-03-19 16:47:45 +01:00
Tim
2f4dff7dd8
Merge pull request #1160 from Stremio/dependabot/github_actions/svenstaro/upload-release-action-2.11.5
chore(deps): bump svenstaro/upload-release-action from 2.11.4 to 2.11.5
2026-03-16 23:39:45 +01:00
dependabot[bot]
1565b85ea3
chore(deps): bump svenstaro/upload-release-action from 2.11.4 to 2.11.5
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.11.4 to 2.11.5.
- [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.4...2.11.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 21:33:37 +00:00
Timothy Z.
7823a783ea feat: update translations 2026-03-13 20:41:28 +02:00
Timothy Z.
6e53d5131e
Merge pull request #1156 from Stremio/feat/player-next-episode-shortcut
Player: Shortcut [Shift + N] for playing next episode
2026-03-13 15:11:23 +02:00
Botzy
eb4f4b9cce fix: translation key for shortcut Shift + N 2026-03-13 11:05:42 +02:00
Botzy
041748ef71 feat(Player): added shortcut for next episode 2026-03-11 18:56:32 +02:00
Timothy Z.
c3f67454ff
Merge pull request #1150 from Stremio/fix/shortcuts-section-button-visibility
Settings: Correct shortcuts menu button visibility
2026-03-11 18:01:09 +02:00
Timothy Z.
89515a2a75
Merge pull request #1154 from Stremio/feat/hold-to-speedup
Player: Hold spacebar or left click to speed up 2x
2026-03-11 17:26:49 +02:00
Botzy
5b83fa00b5 feat: hold left mouse btn or spacebar to speed up 2x 2026-03-09 18:15:08 +02:00
Botzy
182782a60f fix: speed menu to display 0.25 speed option 2026-03-09 15:19:02 +02:00
Tim
b24426250b
Merge pull request #1153 from m-dragoev/codex/i18n-key-fixes
fix: use valid translation keys
2026-03-08 13:23:14 +01:00
Miroslav Dragoev
cdb65a7973 fix: use valid i18n keys in player and seasons placeholder 2026-03-08 14:19:18 +02:00
Timothy Z.
097857d27c chore: update core 2026-03-07 06:55:16 +05:00
Tim
31a5cc6f1a chore: update langs 2026-03-06 14:27:33 +01:00
Timothy Z.
eef3cef1d4 add copy magnet link option to stream 2026-03-04 21:14:03 +02:00
Timothy Z.
3d119db049 improve selected section logic for edge cases 2026-03-04 20:48:01 +02:00
Timothy Z.
df69e6eb18 change shortcuts visibility on mobile 2026-03-04 20:20:31 +02:00
Timothy Z.
6f0e7aa290 bump v5.0.0-beta.31 2026-03-04 17:14:33 +02:00
Timothy Z.
389a91aeca
Merge pull request #1147 from Stremio/feat/add-support-for-more-external-players
Settings: support infuse & vidhub external players
2026-03-04 14:52:28 +02:00
Timothy Z.
d8278e99da
Merge pull request #1148 from Stremio/refactor/improve-ios-vision-os-detection
Dev: improve ios visionos detection
2026-03-04 14:45:44 +02:00
Timothy Z.
0567bbd8ac chore: bump core 2026-03-04 14:44:57 +02:00
Timothy Z.
c8d107d036 refactor: improve ios vision os detection 2026-03-04 13:53:40 +02:00
Timothy Z.
c8f3a70f41 feat: support infuse & vidhub external players 2026-03-04 13:52:52 +02:00
Timothy Z.
8e87d2515b
Merge pull request #1145 from Stremio/dependabot/github_actions/svenstaro/upload-release-action-2.11.4
chore(deps): bump svenstaro/upload-release-action from 2.11.3 to 2.11.4
2026-03-03 12:51:21 +02:00
dependabot[bot]
cc799b610f
chore(deps): bump svenstaro/upload-release-action from 2.11.3 to 2.11.4
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.11.3 to 2.11.4.
- [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.3...2.11.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 23:01:15 +00:00
Timothy Z.
83ff6d54a4
Merge pull request #1130 from fawazorg/fix/translate-hardcoded-strings
fix: hardcoded strings and add missing translation keys
2026-03-02 15:18:40 +02:00
Timothy Z.
2f0005080b chore: update translations pkg 2026-03-02 15:14:49 +02:00
Timothy Z.
14765d8c22 Merge branch 'development' into pr/1130 2026-03-02 15:13:10 +02:00
Timothy Z.
8367dce25d
Merge pull request #704 from ArtificialSloth/feat/LibItem-behavior
Library: Improve LibraryItem behaviour hints
2026-03-02 13:41:36 +02:00
Timothy Z.
68c0a4fb13 refactor: simplify handling on the libitem 2026-03-02 13:38:45 +02:00
Timothy Z.
e26c33fe33 Merge branch 'development' into pr/704 2026-03-02 13:33:22 +02:00
Timothy Z.
091f94e8fd
Merge pull request #1140 from Stremio/fix/apple-pwa-status-bar-color
PWA: navigation bar styles, fullscreen, safe areas
2026-02-23 17:31:54 +03:00
Timothy Z.
3f5097f0a0 fix(sidedrawer): always respect safe areas 2026-02-23 16:07:41 +02:00
Timothy Z.
ac9ca71b53 fix: ios pwa styles for the standalone display 2026-02-23 15:59:29 +02:00
Timothy Z.
fab318e647 lower the bottom safe inset revert to primary bg 2026-02-23 15:44:01 +02:00
Timothy Z.
c95f314a50 fix: sideDrawer safe areas 2026-02-23 15:35:55 +02:00
Timothy Z.
8623627f4d refactor: use both vars for html background 2026-02-23 15:23:37 +02:00
Timothy Z.
b49eb516fd
Merge pull request #1139 from Stremio/fix/correct-allowed-hosts-logic
Dev: Fix host whitelist logic
2026-02-23 16:12:41 +03:00
Timothy Z.
df0d24a7d9 fix: PWA safe area inset bg color 2026-02-23 15:08:24 +02:00
Timothy Z.
3098f6417f fix: ios PWA status bar style 2026-02-23 14:50:17 +02:00
Timothy Z.
ec6db02829 fix: host whitelist logic 2026-02-23 13:07:04 +02:00
Fawazorg
1e0963e8af fix: missing key translate for server add url 2026-02-09 00:17:46 +03:00
Fawazorg
ce2c021e5f fix: normalize catalog name casing for add-on filter translation key 2026-02-08 20:54:35 +03:00
Fawazorg
6bd28847f2 fix: disabled translation key for next video popup 2026-02-07 23:18:27 +03:00
Tim
a77faea0b9
Merge pull request #1127 from fawazorg/fix/translate-hardcoded-strings
fix: replace hardcoded strings with translation keys
2026-02-07 19:10:56 +01:00
Fawazorg
93ed428e8b Fix Email key 2026-02-07 20:54:57 +03:00
Fawazorg
fa13597748 fix: replace hardcoded strings with translation keys 2026-02-07 20:29:13 +03:00
Timothy Z.
c368951952
Merge pull request #1122 from Stremio/fix/use-translate-key-prefix-correction
MetaDetails: Correctly translate metadata
2026-02-03 11:16:51 +02:00
Botzy
0a1746dfe2 fix: links prefix and revert change to useTranslate 2026-02-03 11:13:31 +02:00
Timothy Z.
e29adde4bd fix: use translate prefix correction 2026-02-03 16:44:25 +08:00
ArtificialSloth
c28a52a73c fix: add playingOnExternalDevice flag used to pause the video if it starts playing while using an external player. 2026-01-27 20:00:58 -05:00
Timothy Z.
0df0cdb44b
Merge pull request #1118 from Stremio/fix/image-linter-errors
Image: update return renderFallback type
2026-01-26 16:09:44 +02:00
Timothy Z.
1c9813ebc9 fix(Image): update return renderFallback type 2026-01-22 21:04:57 +02:00
Timothy Z.
dbed391a86 chore: bump v5.0.0-beta.30 2026-01-22 16:44:08 +02:00
Timothy Z.
9503e90e54
Merge pull request #675 from GaryGosh/feat/captions-shortkey
Player: Implement shortkey to toggle caption
2026-01-22 15:22:33 +02:00
Timothy Z.
3cad491040 chore: update translations 2026-01-22 15:20:21 +02:00
Timothy Z.
d456adff0e refactor: correctly set tracks 2026-01-22 15:20:09 +02:00
Timothy Z.
1c441b9bc0 Update Player.js 2026-01-22 15:13:36 +02:00
Timothy Z.
8911473210 refactor: remove the block which is handled by useff 2026-01-22 14:54:33 +02:00
Tim
54b017c39f Merge branch 'development' of https://github.com/Stremio/stremio-web into development 2026-01-22 13:51:03 +01:00
Timothy Z.
ea5e302af7 refactor: simplfy subs handling 2026-01-22 14:48:52 +02:00
Timothy Z.
67358359bf refactor: remember the sub track
handle both external and embedded
2026-01-22 14:38:59 +02:00
Timothy Z.
92d0644c9f Merge branch 'development' into pr/675 2026-01-22 14:18:05 +02:00
Timothy Z.
9c478148bc Merge branch 'development' into pr/704 2026-01-22 13:07:56 +02:00
Timothy Z.
58e38a6077
Merge pull request #1049 from Stremio/fix/meta-details-path-for-episode-picker
fix: metaDetails redirect when route ends with /
2026-01-22 12:46:21 +02:00
Timothy Z.
f2c9bb6d88 chore: remove comments 2026-01-22 12:37:53 +02:00
Timothy Z.
86db4c47e4 Merge branch 'development' into fix/meta-details-path-for-episode-picker 2026-01-22 12:36:31 +02:00
Timothy Z.
3b7944a6f7
Merge pull request #1091 from Stremio/feat/dispatching-addon-install-action-can-throw-exeception
Dev: Addon install action exceptions
2026-01-22 12:33:53 +02:00
Timothy Z.
4ce5ef3744
Merge pull request #1111 from Stremio/fix/copy-download-and-copy-streaming-urls
fix(streams): copy stream link returns streamable url
2026-01-22 12:32:51 +02:00
Timothy Z.
370443609b Merge branch 'development' into fix/copy-download-and-copy-streaming-urls 2026-01-22 12:25:43 +02:00
Timothy Z.
cf0ff1f4ca
fix: trnslations 2026-01-22 12:25:20 +02:00
Tim
17ee0e95e4 fix(Player): side drawer layout on large screens 2026-01-22 11:22:27 +01:00
Timothy Z.
a5265bacf9
Merge branch 'development' into feat/dispatching-addon-install-action-can-throw-exeception 2026-01-22 12:15:09 +02:00
Tim
e6e05573cb fix(Player): disable value labels of subtitles settings if value is null 2026-01-22 10:48:14 +01:00
Tim
cce556e639 feat(Player): show disabled state when ends of range are reached for subtitles settings 2026-01-22 10:44:28 +01:00
Tim
15575ee699 refactor(Player): set audio menu max-height same as subtitles menu 2026-01-22 10:27:11 +01:00
Tim
e85b67268d refactor(Player): wrap video setProp calls in functions 2026-01-22 10:21:42 +01:00
Tim
89fbbb3451
Merge pull request #1116 from Stremio/feat/player-remember-subtitles-settings
Player: Remember subtitles settings
2026-01-22 10:04:19 +01:00
Tim
a30307789c feat: remember subtitles settings on player 2026-01-22 09:57:46 +01:00
Tim
11f51dd86c
Merge pull request #1066 from Stremio/feat/ass-subtitles-styling-setting
feat: add ass subtitles styling setting
2026-01-22 00:16:56 +01:00
Tim
9853c28683 Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/ass-subtitles-styling-setting 2026-01-22 00:14:36 +01:00
Tim
0dcc07c469 chore: update core 2026-01-22 00:12:53 +01:00
Tim
cd111942a5
Merge pull request #1115 from Stremio/feat/player-remember-selected-tracks
Player: Remember selected tracks
2026-01-21 10:07:33 +01:00
Tim
8339dc8a00
Merge pull request #1114 from Stremio/refactor/settings-interface-section
Settings: Move interface settings to dedicated section
2026-01-21 09:54:06 +01:00
Tim
39741d1372 feat: remember selected tracks on player 2026-01-21 09:48:08 +01:00
Tim
7cd49b516f refactor(Settings): move interface settings to dedicated section 2026-01-20 22:01:33 +01:00
Tim
5aaee64549 chore: add new interface languages 2026-01-20 21:39:03 +01:00
Tim
f046e65e73 chore: update translations 2026-01-20 21:38:44 +01:00
Tim
487fde70e0 chore: update video 2026-01-20 21:25:32 +01:00
Lachezar Lechev
e11366b374
fix(streams): copy stream link returns streamable url
feat(streams): add copy download link

Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2026-01-15 17:58:43 +02:00
Tim
9966ae43d5
Merge pull request #1109 from PL7963/patch-1
Update README.md image paths
2026-01-14 17:17:45 +01:00
Coolkie
889dface67
Update README.md image paths 2026-01-14 22:12:05 +08:00
Tim
bb0aecc194 chore: remove commit hash from fonts path 2026-01-14 04:40:01 +01:00
Tim
64b13d6092 chore: move assets to dedicated folder 2026-01-14 04:39:34 +01:00
Tim
a11df877a6 chore: remove deprecated webpack-pwa-manifest 2026-01-14 04:15:41 +01:00
Tim
f37e119644 chore: replace clean-webpack-plugin 2026-01-14 03:21:19 +01:00
Tim
9dbf950a40 chore: replace deprecated lodash.isequal 2026-01-14 03:12:15 +01:00
Tim
55b86179ca Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/ass-subtitles-styling-setting 2026-01-13 18:21:31 +01:00
Tim
3e8c9999fe refactor(Settings): move ASS subtitles setting to advanced 2026-01-13 18:19:37 +01:00
Tim
0b179b88e8 chore: update translations 2026-01-13 18:18:38 +01:00
Tim
da675cd56c chore: update caniuse 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
higorgoulart
b9540af66f Merge branch 'development' into feat/989/watched-on-discover-and-details 2026-01-06 11:48:35 -03:00
Timothy Z.
55963fd23e
Merge pull request #1106 from Stremio/chore/align-error-styles-across-app
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
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
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
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
Lachezar Lechev
673c22a014
chore: bump core-web to feature branch
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-12-15 11:34:00 +02:00
Lachezar Lechev
07d2744f66
feat(Addon): add addon shows toast if url is not valid
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-12-15 11:30:36 +02:00
Timothy Z.
bfb5c484fc
Merge pull request #1079 from sagarchaulagai/development
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
[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
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
Tim
e8bee4997a feat: add ass subtitles styling setting 2025-11-21 09:11:35 +01:00
Timothy Z.
71e0bb4481 chore: update copyright 2025-11-19 14:48:11 +02:00
Timothy Z.
6bf3b8147d refactor(ActionsGroup): simplify 2025-11-19 14:47:17 +02:00
Timothy Z.
f73fa5931e refactor(Discover): simplify 2025-11-19 14:31:04 +02:00
higorgoulart
a9d9c8d808 feat: load model 2025-11-17 19:39:03 -03:00
higorgoulart
c70211153e feat: review facts 2025-11-15 14:04:14 -03:00
Neeraj TK
5eb55d3aaf Added the toggle subtitles shortcut 2025-11-14 17:45:45 +05:30
Neeraj TK
e1e6fe075b
Merge branch 'development' into feat/captions-shortkey 2025-11-14 17:32:10 +05:30
higorgoulart
9ccc6b8271 feat: change metaDetails action 2025-11-12 18:37:41 -03:00
higorgoulart
67f4f349bb feat: remove add to library 2025-11-11 18:48:24 -03:00
higorgoulart
97c3b7d004 feat: rename component & fix style 2025-11-11 17:30:00 -03:00
higorgoulart
373ccf351a feat: review facts 2025-11-08 14:00:01 -03:00
higorgoulart
987201edd3 feat: review facts 2025-11-08 13:59:10 -03:00
higorgoulart
ff08e377fc feat: remove unused component & fix spacing 2025-11-07 17:57:06 -03:00
higorgoulart
3b2d1f365c feat: icons group component 2025-11-04 19:23:39 -03:00
higorgoulart
852f478f1e feat: change trailer order & fix discover mark as watched 2025-11-04 17:52:09 -03:00
higorgoulart
6833bb719d feat: watched on discover & details 2025-10-31 19:52:30 -03:00
Lachezar Lechev
2de2e89446
fix: meta details - don't set streamPath if videoId is empty string
- fix season selection path inconsistencies

Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-17 14:09:27 +03:00
Lachezar Lechev
ea69521912
fix: metaDetails redirect when route ends with /
Signed-off-by: Lachezar Lechev <lachezar@ambire.com>
2025-10-17 12:46:46 +03:00
Timothy Z.
56989781c8 Merge branch 'development' into pr/704 2025-07-16 17:40:00 +03:00
ArtificialSloth
5ab324f125 resolve upstream conflicts 2025-06-30 00:58:33 -04:00
Neeraj TK
ea5d05c31d
Merge branch 'development' into feat/captions-shortkey 2025-01-14 00:55:20 +05:30
Neeraj TK
881c808003 formatting revert 2024-09-23 20:07:11 +05:30
Neeraj TK
d2db62f33a retain last selected subtitle upon toggling 2024-09-23 20:05:36 +05:30
Neeraj TK
3b730a2bd8 added shortkey to toggle caption. UX improvement. 2024-08-11 04:32:23 +05:30
132 changed files with 1548 additions and 1742 deletions

View file

@ -14,7 +14,7 @@ jobs:
# Auto assign PR to author
- name: Auto Assign PR to Author
if: github.event.pull_request.head.repo.fork == false && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@ -34,7 +34,7 @@ jobs:
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View file

@ -22,7 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10
run_install: false

View file

@ -11,7 +11,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 10
run_install: false
@ -24,7 +24,7 @@ jobs:
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.11.3
uses: svenstaro/upload-release-action@2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip

View file

@ -41,15 +41,15 @@ docker run -p 8080:8080 stremio-web
### Board
![Board](/screenshots/board.png)
![Board](/assets/screenshots/board.png)
### Discover
![Discover](/screenshots/discover.png)
![Discover](/assets/screenshots/discover.png)
### Meta Details
![Meta Details](/screenshots/metadetails.png)
![Meta Details](/assets/screenshots/metadetails.png)
## License

View file

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 652 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View file

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View file

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

59
manifest.json Normal file
View file

@ -0,0 +1,59 @@
{
"name": "Stremio Web",
"short_name": "Stremio",
"description": "Freedom To Stream",
"background_color": "#161523",
"theme_color": "#2a2843",
"orientation": "any",
"display": "standalone",
"display_override": ["standalone"],
"scope": "./",
"start_url": "./",
"icons": [
{
"src": "favicons/icon_256x256.ico",
"sizes": "256x256",
"type": "image/vnd.microsoft.icon"
},
{
"src": "images/maskable_icon_512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "images/maskable_icon_196x196.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "images/icon_512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "images/icon_196x196.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "any"
}
],
"screenshots": [
{
"src": "screenshots/board_wide.webp",
"sizes": "1440x900",
"type": "image/webp",
"form_factor": "wide",
"label": "Homescreen of Stremio"
},
{
"src": "screenshots/board_narrow.webp",
"sizes": "414x896",
"type": "image/webp",
"form_factor": "narrow",
"label": "Homescreen of Stremio"
}
]
}

View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.29",
"version": "5.0.0-beta.34",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -17,21 +17,21 @@
"@babel/runtime": "7.26.0",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.51.1",
"@stremio/stremio-core-web": "0.56.4",
"@stremio/stremio-icons": "5.8.0",
"@stremio/stremio-video": "0.0.64",
"@stremio/stremio-video": "0.0.75",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
"classnames": "2.5.1",
"eventemitter3": "5.0.1",
"fast-equals": "^6.0.0",
"filter-invalid-dom-props": "3.0.1",
"hat": "^0.0.3",
"i18next": "^24.0.5",
"langs": "github:Stremio/nodejs-langs",
"lodash.debounce": "4.0.8",
"lodash.intersection": "4.4.0",
"lodash.isequal": "4.5.0",
"lodash.throttle": "4.1.1",
"magnet-uri": "6.2.0",
"prop-types": "15.8.1",
@ -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#01aaa201e419782b26b9f2cbe4430795021426e5",
"stremio-translations": "github:Stremio/stremio-translations#90ea718c18750a0e9cd6824b0ef7c512a41cb90b",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@ -53,12 +53,10 @@
"@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",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"babel-loader": "9.2.1",
"clean-webpack-plugin": "4.0.0",
"copy-webpack-plugin": "12.0.2",
"css-loader": "6.11.0",
"cssnano": "7.0.6",
@ -82,7 +80,6 @@
"webpack": "5.97.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "^5.1.0",
"webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^7.3.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ const ErrorDialog = ({ className }) => {
<div className={classnames(className, styles['error-container'])}>
<Image
className={styles['error-image']}
src={require('/images/empty.png')}
src={require('/assets/images/empty.png')}
alt={' '}
/>
<div className={styles['error-message']}>

View file

@ -1,7 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const isEqual = require('lodash.isequal');
const { deepEqual } = require('fast-equals');
const { withCoreSuspender, useProfile, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
@ -18,7 +18,7 @@ const SearchParamsHandler = () => {
setSearchParams((previousSearchParams) => {
const currentSearchParams = Object.fromEntries(searchParams.entries());
return isEqual(previousSearchParams, currentSearchParams) ? previousSearchParams : currentSearchParams;
return deepEqual(previousSearchParams, currentSearchParams) ? previousSearchParams : currentSearchParams;
});
};

View file

@ -44,7 +44,7 @@ const ServicesToaster = () => {
}
case 'MagnetParsed': {
toast.show({
type: 'success',
type: 'info',
title: 'Magnet link parsed',
timeout: 4000
});

View file

@ -5,7 +5,13 @@
@font-face {
font-family: 'PlusJakartaSans';
src: url('/fonts/PlusJakartaSans.ttf') format('truetype');
src: url('/assets/fonts/PlusJakartaSans.ttf') format('truetype');
}
@font-face {
font-family: 'TwemojiFlags';
src: url('/assets/fonts/TwemojiFlags.woff2') format('woff2');
unicode-range: U+1F1E6-1F1FF;
}
:global {
@ -23,8 +29,8 @@
// HTML sizes
@html-width: ~"calc(max(var(--small-viewport-width), var(--dynamic-viewport-width)))";
@html-height: ~"calc(max(var(--small-viewport-height), var(--dynamic-viewport-height)))";
@html-standalone-width: ~"calc(max(100%, var(--small-viewport-width)))";
@html-standalone-height: ~"calc(max(100%, var(--small-viewport-height)))";
@html-standalone-width: ~"calc(max(100%, var(--large-viewport-width)))";
@html-standalone-height: ~"calc(max(100%, var(--large-viewport-height)))";
// Safe area insets
@safe-area-inset-top: env(safe-area-inset-top, 0rem);
@ -48,7 +54,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;
@ -151,11 +157,12 @@ svg {
html {
width: @html-width;
height: @html-height;
font-family: 'PlusJakartaSans', 'Arial', 'Helvetica', 'sans-serif';
font-family: 'PlusJakartaSans', 'Arial', 'Helvetica', 'TwemojiFlags', 'sans-serif';
overflow: auto;
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
background-color: var(--primary-background-color);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {

View file

@ -2,6 +2,8 @@
const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const DEFAULT_STREAMING_SERVER_URL = 'http://127.0.0.1:11470/';
const DEFAULT_SUBTITLES_LANGUAGE = 'eng';
const LOCAL_SUBTITLES_LANGUAGE = 'local';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['PlusJakartaSans', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const SEEK_TIME_DURATIONS = [3000, 5000, 10000, 15000, 20000, 30000];
@ -97,6 +99,16 @@ const EXTERNAL_PLAYERS = [
value: 'moonplayer',
platforms: ['visionos'],
},
{
label: 'Infuse',
value: 'infuse',
platforms: ['ios', 'visionos', 'macos'],
},
{
label: 'Vidhub',
value: 'vidhub',
platforms: ['ios'],
},
{
label: 'M3U Playlist',
value: 'm3u',
@ -111,6 +123,8 @@ const PROTOCOL = 'stremio:';
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
DEFAULT_STREAMING_SERVER_URL,
DEFAULT_SUBTITLES_LANGUAGE,
LOCAL_SUBTITLES_LANGUAGE,
SUBTITLES_SIZES,
SUBTITLES_FONTS,
SEEK_TIME_DURATIONS,

View file

@ -18,7 +18,9 @@ const PlatformProvider = ({ children }: Props) => {
const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) =>
hostname === host || hostname.endsWith('.' + host)
);
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
window.open(finalUrl, '_blank');

View file

@ -11,13 +11,21 @@ const APPLE_MOBILE_DEVICES = [
const { userAgent, platform, maxTouchPoints } = globalThis.navigator;
// this detects ipad properly in safari
// while bowser does not
const isIOS = APPLE_MOBILE_DEVICES.includes(platform) || (userAgent.includes('Mac') && 'ontouchend' in document);
// Vision Pro uniquely supports the WebXR Device API (navigator.xr),
// while iPads and iPhones do not — this is the most reliable discriminator.
// Both Vision Pro and iPads (iPadOS 13+) report 'Macintosh' in the UA
// and have maxTouchPoints > 1, so we cannot rely on those alone.
const isMacLikeWithTouch = userAgent.includes('Macintosh') && maxTouchPoints > 1;
const isVisionOS = isMacLikeWithTouch && 'xr' in globalThis.navigator;
// Edge case: iPad is included in this function
// Keep in mind maxTouchPoints for Vision Pro might change in the future
const isVisionOS = userAgent.includes('Macintosh') && maxTouchPoints === 5;
// Detect iOS/iPadOS devices:
// - Older iPads expose 'iPad' in navigator.platform
// - iPadOS 13+ exposes 'MacIntel' but has touch support ('ontouchend' in document)
// - Exclude Vision OS devices which also pass the touch check
const isIOS = !isVisionOS && (
APPLE_MOBILE_DEVICES.includes(platform) ||
(userAgent.includes('Mac') && 'ontouchend' in document)
);
const bowser = Bowser.getParser(userAgent);
const os = bowser.getOSName().toLowerCase();

View file

@ -1,13 +1,15 @@
import React, { createContext, useCallback, useContext, useEffect } from 'react';
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 = () => void;
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);
@ -18,27 +20,38 @@ type Props = {
};
const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const onKeyDown = useCallback(({ ctrlKey, shiftKey, key }: KeyboardEvent) => {
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(key.toUpperCase())) {
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);
};
return () => document.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
return (
<ShortcutsContext.Provider value={{ grouped: shortcuts }}>
<ShortcutsContext.Provider value={{ grouped: shortcuts, on, off }}>
{children}
</ShortcutsContext.Provider>
);
@ -50,5 +63,5 @@ const useShortcuts = () => {
export {
ShortcutsProvider,
useShortcuts
useShortcuts,
};

View file

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

View file

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

View file

@ -59,6 +59,11 @@
"label": "SETTINGS_SHORTCUT_VOLUME_DOWN",
"combos": [["ArrowDown"]]
},
{
"name": "mute",
"label": "SETTINGS_SHORTCUT_MUTE",
"combos": [["M"]]
},
{
"name": "subtitlesSize",
"label": "SETTINGS_SHORTCUT_SUBTITLES_SIZE",
@ -69,6 +74,21 @@
"label": "SETTINGS_SHORTCUT_SUBTITLES_DELAY",
"combos": [["G"], ["H"]]
},
{
"name": "speedDown",
"label": "SETTINGS_SHORTCUT_DECREASE_PLAYBACK_SPEED",
"combos": [["["]]
},
{
"name": "speedUp",
"label": "SETTINGS_SHORTCUT_INCREASE_PLAYBACK_SPEED",
"combos": [["]"]]
},
{
"name": "toggleSubtitles",
"label": "SETTINGS_SHORTCUT_TOGGLE_SUBTITLES",
"combos": [["C"]]
},
{
"name": "subtitlesMenu",
"label": "SETTINGS_SHORTCUT_MENU_SUBTITLES",
@ -83,6 +103,21 @@
"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"]]
},
{
"name": "playNext",
"label": "SETTINGS_SHORTCUT_PLAY_NEXT",
"combos": [["Shift", "N"]]
}
]
}

View file

@ -6,6 +6,7 @@ const React = require('react');
const ToastContext = React.createContext({
show: () => { },
remove: () => { },
clear: () => { }
});

View file

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

View file

@ -42,7 +42,7 @@ const ToastProvider = ({ className, children }) => {
},
show: (item) => {
if (filters.some((filter) => filter(item))) {
return;
return null;
}
const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ?
@ -64,6 +64,11 @@ const ToastProvider = ({ className, children }) => {
onClose: itemOnClose
}
});
return id;
},
remove: (id) => {
clearTimeout(id);
dispatch({ type: 'remove', id });
},
clear: () => {
dispatch({ type: 'clear' });

View file

@ -88,7 +88,7 @@
.fade-active {
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0);
transition: opacity 0.1s cubic-bezier(0.32, 0, 0.67, 0);
}
.fade-exit {

View file

@ -1,25 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const comparatorWithPriorities = (priorities) => {
return (a, b) => {
if (isNaN(priorities[a]) && isNaN(priorities[b])) {
return a.localeCompare(b);
} else if (isNaN(priorities[a])) {
if (priorities[b] === Number.NEGATIVE_INFINITY) {
return -1;
} else {
return 1;
}
} else if (isNaN(priorities[b])) {
if (priorities[a] === Number.NEGATIVE_INFINITY) {
return 1;
} else {
return -1;
}
} else {
return priorities[b] - priorities[a];
}
};
};
module.exports = comparatorWithPriorities;

View file

@ -4,8 +4,7 @@ const { FileDropProvider, onFileDrop } = require('./FileDrop');
const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const { ShortcutsProvider, useShortcuts } = require('./Shortcuts');
const comparatorWithPriorities = require('./comparatorWithPriorities');
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
@ -26,6 +25,7 @@ const { default: useSettings } = require('./useSettings');
const { default: useShell } = require('./useShell');
const useStreamingServer = require('./useStreamingServer');
const { default: useTimeout } = require('./useTimeout');
const { default: usePlayUrl } = require('./usePlayUrl');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const { default: useOrientation } = require('./useOrientation');
@ -38,11 +38,11 @@ module.exports = {
usePlatform,
ShortcutsProvider,
useShortcuts,
onShortcut,
ToastProvider,
useToast,
TooltipProvider,
Tooltip,
comparatorWithPriorities,
CONSTANTS,
withCoreSuspender,
useCoreSuspender,
@ -64,6 +64,7 @@ module.exports = {
useShell,
useStreamingServer,
useTimeout,
usePlayUrl,
useTorrent,
useTranslate,
useOrientation,

View file

@ -3,6 +3,10 @@
"name": "العربية",
"codes": ["ar-AR", "ara"]
},
{
"name": "Беларуская",
"codes": ["be-BY", "bel"]
},
{
"name": "български език",
"codes": ["bg-BG", "bul"]
@ -13,7 +17,7 @@
},
{
"name": "català",
"codes": ["ca-CA", "cat"]
"codes": ["ca-ES", "cat"]
},
{
"name": "čeština",
@ -43,6 +47,10 @@
"name": "español",
"codes": ["es-ES", "spa"]
},
{
"name": "Eesti",
"codes": ["et-EE", "est"]
},
{
"name": "euskara",
"codes": ["eu-ES", "eus"]
@ -91,6 +99,10 @@
"name": "한국어",
"codes": ["ko-KR", "kor"]
},
{
"name": "Lietuvių",
"codes": ["lt-LT", "ltu"]
},
{
"name": "македонски јазик",
"codes": ["mk-MK", "mkd"]
@ -99,6 +111,10 @@
"name": "ဗမာစာ",
"codes": ["my-BM", "mya"]
},
{
"name": "नेपाली",
"codes": ["ne-NP", "nep"]
},
{
"name": "Norsk bokmål",
"codes": ["nb-NO", "nob"]
@ -111,6 +127,10 @@
"name": "Norsk nynorsk",
"codes": ["nn-NO", "nno"]
},
{
"name": "ਪੰਜਾਬੀ",
"codes": ["pa-IN", "pan"]
},
{
"name": "język polski",
"codes": ["pl-PL", "pol"]
@ -151,6 +171,10 @@
"name": "తెలుగు",
"codes": ["te-IN", "tel"]
},
{
"name": "தமிழ்",
"codes": ["tl-TM", "tam"]
},
{
"name": "Türkçe",
"codes": ["tr-TR", "tur"]
@ -159,6 +183,10 @@
"name": "українська мова",
"codes": ["uk-UA", "ukr"]
},
{
"name": "اُرْدُو",
"codes": ["ur-PK", "urd"]
},
{
"name": "Tiếng Việt",
"codes": ["vi-VN", "vie"]

View file

@ -6,11 +6,16 @@ const all = langs.all().map((lang) => ({
label: lang.local,
alpha2: lang['1'],
alpha3: [lang['2'], lang['2B'], lang['2T'], lang['3']],
locale: lang['locale'],
ietf: lang['ietf'],
}));
const find = (code: string) => {
return all.find(({ alpha2, alpha3, locale }) => [alpha2, ...alpha3, locale].includes(code));
return all.find(({ alpha2, alpha3, ietf }) => [alpha2, ...alpha3, ietf].includes(code));
};
const toCode = (code: string) => {
const language = find(code);
return language?.[2] ?? code;
};
const label = (code: string) => {
@ -21,5 +26,6 @@ const label = (code: string) => {
export {
all,
find,
toCode,
label,
};

View file

@ -2,7 +2,7 @@
const React = require('react');
const throttle = require('lodash.throttle');
const isEqual = require('lodash.isequal');
const { deepEqual } = require('fast-equals');
const intersection = require('lodash.intersection');
const { useCoreSuspender } = require('stremio/common/CoreSuspender');
const { useRouteFocused } = require('stremio-router');
@ -19,7 +19,7 @@ const useModelState = ({ action, ...args }) => {
const [state, setState] = React.useReducer(
(prevState, nextState) => {
return Object.keys(prevState).reduce((result, key) => {
result[key] = isEqual(prevState[key], nextState[key]) ? prevState[key] : nextState[key];
result[key] = deepEqual(prevState[key], nextState[key]) ? prevState[key] : nextState[key];
return result;
}, {});
},

65
src/common/usePlayUrl.ts Normal file
View file

@ -0,0 +1,65 @@
import { useCallback } from 'react';
import magnet from 'magnet-uri';
import { useServices } from 'stremio/services';
import useToast from 'stremio/common/Toast/useToast';
import useTorrent from 'stremio/common/useTorrent';
import useStreamingServer from 'stremio/common/useStreamingServer';
const HTTP_REGEX = /^https?:\/\/.+/i;
const usePlayUrl = () => {
const { core } = useServices();
const toast = useToast();
const { createTorrentFromMagnet } = useTorrent();
const streamingServer = useStreamingServer();
const handlePlayUrl = useCallback(async (text: string): Promise<boolean> => {
if (!text || !text.trim()) return false;
const trimmed = text.trim();
if (HTTP_REGEX.test(trimmed)) {
toast.show({
type: 'success',
title: 'Loading HTTP stream…',
timeout: 3000
});
try {
const encoded = await core.transport.encodeStream({ url: trimmed });
if (typeof encoded === 'string') {
window.location.hash = `#/player/${encodeURIComponent(encoded)}`;
return true;
}
} catch (e) {
console.error('Failed to encode stream:', e);
}
toast.show({
type: 'error',
title: 'Failed to load HTTP stream.',
timeout: 5000
});
return false;
}
const parsed = magnet.decode(trimmed);
if (parsed && typeof parsed.infoHash === 'string') {
const serverReady = streamingServer.settings !== null
&& streamingServer.settings.type === 'Ready';
if (!serverReady) {
toast.show({
type: 'error',
title: 'Streaming server is not available. Cannot play magnet links.',
timeout: 5000
});
return false;
}
createTorrentFromMagnet(trimmed);
return true;
}
return false;
}, [streamingServer.settings, createTorrentFromMagnet]);
return { handlePlayUrl };
};
export default usePlayUrl;

View file

@ -6,14 +6,22 @@ const { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const useStreamingServer = require('stremio/common/useStreamingServer');
const CREATE_TORRENT_TIMEOUT = 20000;
const useTorrent = () => {
const { core } = useServices();
const streamingServer = useStreamingServer();
const toast = useToast();
const createTorrentTimeout = React.useRef(null);
const parsingToastId = React.useRef(null);
const createTorrentFromMagnet = React.useCallback((text) => {
const parsed = magnet.decode(text);
if (parsed && typeof parsed.infoHash === 'string') {
parsingToastId.current = toast.show({
type: 'success',
title: 'Loading magnet link…',
timeout: CREATE_TORRENT_TIMEOUT
});
core.transport.dispatch({
action: 'StreamingServer',
args: {
@ -23,12 +31,13 @@ const useTorrent = () => {
});
clearTimeout(createTorrentTimeout.current);
createTorrentTimeout.current = setTimeout(() => {
toast.remove(parsingToastId.current);
toast.show({
type: 'error',
title: 'It\'s taking a long time to get metadata from the torrent.',
timeout: 10000
title: 'Failed to parse magnet link.',
timeout: 8000
});
}, 10000);
}, CREATE_TORRENT_TIMEOUT);
}
}, []);
React.useEffect(() => {
@ -36,6 +45,7 @@ const useTorrent = () => {
const [, { type }] = streamingServer.torrent;
if (type === 'Ready') {
clearTimeout(createTorrentTimeout.current);
toast.remove(parsingToastId.current);
}
}
}, [streamingServer.torrent]);

View file

@ -8,7 +8,7 @@
@width-mobile: 3rem;
.ratings-container {
.group-container {
display: flex;
flex-direction: row;
align-items: center;
@ -17,6 +17,7 @@
border-radius: 2rem;
height: @height;
width: fit-content;
backdrop-filter: blur(5px);
.icon-container {
display: flex;
@ -45,7 +46,7 @@
}
@media @phone-landscape {
.ratings-container {
.group-container {
height: @height-mobile;
.icon-container {

View file

@ -0,0 +1,45 @@
// Copyright (C) 2017-2025 Smart code 203358507
import classNames from 'classnames';
import React from 'react';
import Icon from '@stremio/stremio-icons/react';
import { Tooltip } from 'stremio/common/Tooltips';
import styles from './ActionsGroup.less';
type Item = {
icon: string;
label?: string;
filled?: string;
disabled?: boolean;
className?: string;
onClick?: () => void;
};
type Props = {
items: Item[];
className?: string;
};
const ActionsGroup = ({ items, className }: Props) => {
return (
<div className={classNames(styles['group-container'], className)}>
{
items.map((item, index) => (
<div
key={index}
className={classNames(styles['icon-container'], item.className, { [styles['disabled']]: item.disabled })}
onClick={item.onClick}
>
{
item.label &&
<Tooltip label={item.label} position={'top'} />
}
<Icon name={item.icon} className={styles['icon']} />
</div>
))
}
</div>
);
};
export default ActionsGroup;

View file

@ -0,0 +1,6 @@
// Copyright (C) 2017-2025 Smart code 203358507
import ActionsGroup from './ActionsGroup';
export default ActionsGroup;

View file

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

View file

@ -1,5 +1,6 @@
import React, { memo, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import Transition from '../Transition';
import styles from './ContextMenu.less';
const PADDING = 8;
@ -47,7 +48,6 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
}, [position, containerSize]);
const close = () => {
setPosition([0, 0]);
setActive(false);
};
@ -78,23 +78,25 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
};
}, [on]);
return active && createPortal((
<div
className={styles['context-menu-container']}
onMouseDown={close}
onTouchStart={close}
>
return createPortal((
<Transition when={active} name={'fade'}>
<div
ref={ref}
className={styles['context-menu']}
style={style}
onMouseDown={stopPropagation}
onTouchStart={stopPropagation}
onClick={onClick}
className={styles['context-menu-container']}
onMouseDown={close}
onTouchStart={close}
>
{children}
<div
ref={ref}
className={styles['context-menu']}
style={style}
onMouseDown={stopPropagation}
onTouchStart={stopPropagation}
onClick={onClick}
>
{children}
</div>
</div>
</div>
</Transition>
), document.body);
};

View file

@ -5,24 +5,11 @@ const PropTypes = require('prop-types');
const { useServices } = require('stremio/services');
const LibItem = require('stremio/components/LibItem');
const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => {
const ContinueWatchingItem = ({ _id, notifications, ...props }) => {
const { core } = useServices();
const onClick = React.useCallback(() => {
if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) {
window.location = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams;
}
}, [deepLinks]);
const onPlayClick = React.useCallback((event) => {
event.stopPropagation();
if (deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos) {
window.location = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos;
}
}, [deepLinks]);
const onDismissClick = React.useCallback((event) => {
event.stopPropagation();
event.preventDefault();
if (typeof _id === 'string') {
core.transport.dispatch({
action: 'Ctx',
@ -47,8 +34,6 @@ const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => {
_id={_id}
posterChangeCursor={true}
notifications={notifications}
onClick={onClick}
onPlayClick={onPlayClick}
onDismissClick={onDismissClick}
/>
);

View file

@ -7,7 +7,7 @@ type Props = {
src: string,
alt: string,
fallbackSrc: string,
renderFallback: () => void,
renderFallback: () => React.ReactNode,
onError: (event: React.SyntheticEvent<HTMLImageElement>) => void,
};

View file

@ -29,7 +29,7 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
case 'details':
return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'watched':
return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
return typeof watched !== 'undefined' && props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'dismiss':
return typeof _id === 'string' && props.progress !== null && !isNaN(props.progress) && props.progress > 0;
case 'remove':
@ -119,6 +119,16 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
}
}, [_id, props.deepLinks, props.optionOnSelect]);
const onPlayClick = React.useMemo(() => {
if (props.deepLinks && typeof props.deepLinks.player === 'string') {
return (event) => {
event.preventDefault();
window.location = props.deepLinks.player;
};
}
return null;
}, [props.deepLinks]);
return (
<MetaItem
{...props}
@ -126,6 +136,7 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
newVideos={newVideos}
options={options}
optionOnSelect={optionOnSelect}
onPlayClick={onPlayClick}
/>
);
};

View file

@ -18,14 +18,14 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, poste
const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false);
const href = React.useMemo(() => {
return deepLinks ?
typeof deepLinks.player === 'string' ?
deepLinks.player
typeof deepLinks.metaDetailsStreams === 'string' ?
deepLinks.metaDetailsStreams
:
typeof deepLinks.metaDetailsStreams === 'string' ?
deepLinks.metaDetailsStreams
typeof deepLinks.metaDetailsVideos === 'string' ?
deepLinks.metaDetailsVideos
:
typeof deepLinks.metaDetailsVideos === 'string' ?
deepLinks.metaDetailsVideos
typeof deepLinks.player === 'string' ?
deepLinks.player
:
null
:

View file

@ -14,7 +14,7 @@ const MetaLinks = ({ className, label, links }) => {
{
typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}>
{ stringWithPrefix(label.toUpperCase(), 'LINKS') }
{ stringWithPrefix(label.toUpperCase(), 'LINKS_') }
</div>
:
null

View file

@ -8,6 +8,7 @@ const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { default: Button } = require('stremio/components/Button');
const { default: Image } = require('stremio/components/Image');
const { default: ActionsGroup } = require('stremio/components/ActionsGroup');
const ModalDialog = require('stremio/components/ModalDialog');
const SharePrompt = require('stremio/components/SharePrompt');
const CONSTANTS = require('stremio/common/CONSTANTS');
@ -25,7 +26,7 @@ const ALLOWED_LINK_REDIRECTS = [
routesRegexp.metadetails.regexp
];
const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, ratingInfo }, ref) => {
const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, watched, toggleWatched, ratingInfo }, ref) => {
const { t } = useTranslation();
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
const linksGroups = React.useMemo(() => {
@ -98,6 +99,18 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
const renderLogoFallback = React.useCallback(() => (
<div className={styles['logo-placeholder']}>{name}</div>
), [name]);
const metaItemActions = React.useMemo(() => [
{
icon: inLibrary ? 'remove-from-library' : 'add-to-library',
label: inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB'),
onClick: typeof toggleInLibrary === 'function' ? toggleInLibrary : null,
},
{
icon: watched ? 'eye-off' : 'eye',
label: watched ? t('CTX_MARK_UNWATCHED') : t('CTX_MARK_WATCHED'),
onClick: typeof toggleWatched === 'function' ? toggleWatched : undefined,
},
], [inLibrary, watched, toggleInLibrary, toggleWatched]);
return (
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
{
@ -195,19 +208,6 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
}
</div>
<div className={styles['action-buttons-container']}>
{
typeof toggleInLibrary === 'function' ?
<ActionButton
className={styles['action-button']}
icon={inLibrary ? 'remove-from-library' : 'add-to-library'}
label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')}
tooltip={compact}
tabIndex={compact ? -1 : 0}
onClick={toggleInLibrary}
/>
:
null
}
{
typeof trailerHref === 'string' ?
<ActionButton
@ -221,6 +221,11 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
:
null
}
{
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
? <ActionsGroup items={metaItemActions} className={styles['group-container']} />
: null
}
{
typeof showHref === 'string' && compact ?
<ActionButton
@ -237,7 +242,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
!compact && ratingInfo !== null ?
<Ratings
ratingInfo={ratingInfo}
className={styles['ratings']}
className={styles['group-container']}
/>
:
null
@ -298,6 +303,8 @@ MetaPreview.propTypes = {
trailerStreams: PropTypes.array,
inLibrary: PropTypes.bool,
toggleInLibrary: PropTypes.func,
watched: PropTypes.bool,
toggleWatched: PropTypes.func,
ratingInfo: PropTypes.object,
};

View file

@ -2,9 +2,7 @@
import React, { useMemo } from 'react';
import useRating from './useRating';
import styles from './Ratings.less';
import Icon from '@stremio/stremio-icons/react';
import classNames from 'classnames';
import { ActionsGroup } from 'stremio/components';
type Props = {
metaId?: string;
@ -16,15 +14,21 @@ const Ratings = ({ ratingInfo, className }: Props) => {
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]);
const items = useMemo(() => [
{
icon: liked ? 'thumbs-up' : 'thumbs-up-outline',
disabled,
onClick: onLiked,
},
{
icon: loved ? 'heart' : 'heart-outline',
disabled,
onClick: onLoved,
},
], [liked, loved, disabled]);
return (
<div className={classNames(styles['ratings-container'], className)}>
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLiked}>
<Icon name={liked ? 'thumbs-up' : 'thumbs-up-outline'} className={styles['icon']} />
</div>
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLoved}>
<Icon name={loved ? 'heart' : 'heart-outline'} className={styles['icon']} />
</div>
</div>
<ActionsGroup items={items} className={className} />
);
};

View file

@ -32,7 +32,7 @@
.action-buttons-container {
justify-content: space-between;
.action-button:not(:last-child) {
.action-button:not(:last-child), .group-container:not(:last-child) {
margin-right: 0;
}
}
@ -207,11 +207,20 @@
}
}
}
}
.group-container {
margin-bottom: 1rem;
.ratings {
margin-bottom: 1rem;
margin-right: 1rem;
&:global(.wide) {
width: auto;
padding: 0 2rem;
border-radius: 4rem;
}
&:not(:last-child) {
margin-right: 1rem;
}
}
}
}
@ -233,17 +242,13 @@
padding-top: 1.5rem;
gap: 0.5rem;
.action-button {
.action-button, .group-container {
padding: 0 1.5rem !important;
margin-right: 0rem !important;
height: 3rem;
border-radius: 2rem;
}
}
.ratings {
margin-right: 0;
}
}
}
@ -272,6 +277,10 @@
&::-webkit-scrollbar {
display: none;
}
.action-button {
padding: 0 1rem !important;
}
}
}

View file

@ -35,7 +35,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
<div className={styles['logo-container']}>
<Image
className={styles['logo']}
src={require('/images/stremio_symbol.png')}
src={require('/assets/images/stremio_symbol.png')}
alt={' '}
/>
</div>

View file

@ -10,7 +10,8 @@ const { Button } = require('stremio/components');
const { default: useFullscreen } = require('stremio/common/useFullscreen');
const useProfile = require('stremio/common/useProfile');
const usePWA = require('stremio/common/usePWA');
const useTorrent = require('stremio/common/useTorrent');
const { default: usePlayUrl } = require('stremio/common/usePlayUrl');
const useToast = require('stremio/common/Toast/useToast');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useStreamingServer = require('stremio/common/useStreamingServer');
const styles = require('./styles');
@ -20,7 +21,8 @@ const NavMenuContent = ({ onClick }) => {
const { core } = useServices();
const profile = useProfile();
const streamingServer = useStreamingServer();
const { createTorrentFromMagnet } = useTorrent();
const { handlePlayUrl } = usePlayUrl();
const toast = useToast();
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
const [isIOSPWA, isAndroidPWA] = usePWA();
const streamingServerWarningDismissed = React.useMemo(() => {
@ -40,11 +42,18 @@ const NavMenuContent = ({ onClick }) => {
const onPlayMagnetLinkClick = React.useCallback(async () => {
try {
const clipboardText = await navigator.clipboard.readText();
createTorrentFromMagnet(clipboardText);
const handled = await handlePlayUrl(clipboardText);
if (!handled) {
toast.show({
type: 'error',
title: 'Clipboard does not contain a valid URL or magnet link.',
timeout: 5000
});
}
} catch(e) {
console.error(e);
}
}, []);
}, [handlePlayUrl]);
return (
<div className={classnames(styles['nav-menu-container'], 'animation-fade-in', { [styles['with-warning']]: !streamingServerWarningDismissed } )} onClick={onClick}>
<div className={styles['user-info-container']}>
@ -52,12 +61,12 @@ const NavMenuContent = ({ onClick }) => {
className={styles['avatar-container']}
style={{
backgroundImage: profile.auth === null ?
`url('${require('/images/anonymous.png')}')`
`url('${require('/assets/images/anonymous.png')}')`
:
profile.auth.user.avatar ?
`url('${profile.auth.user.avatar}')`
:
`url('${require('/images/default_avatar.png')}')`
`url('${require('/assets/images/default_avatar.png')}')`
}}
/>
<div className={styles['user-info-details']}>

View file

@ -9,7 +9,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const Button = require('stremio/components/Button').default;
const TextInput = require('stremio/components/TextInput').default;
const useTorrent = require('stremio/common/useTorrent');
const { default: usePlayUrl } = require('stremio/common/usePlayUrl');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useSearchHistory = require('./useSearchHistory');
const useLocalSearch = require('./useLocalSearch');
@ -21,7 +21,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
const routeFocused = useRouteFocused();
const searchHistory = useSearchHistory();
const localSearch = useLocalSearch();
const { createTorrentFromMagnet } = useTorrent();
const { handlePlayUrl } = usePlayUrl();
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false);
const [currentQuery, setCurrentQuery] = React.useState(query || '');
@ -52,12 +52,14 @@ const SearchBar = React.memo(({ className, query, active }) => {
const value = searchInputRef.current.value;
setCurrentQuery(value);
openHistory();
try {
createTorrentFromMagnet(value);
} catch (error) {
console.error('Failed to create torrent from magnet:', error);
}, []);
const queryInputOnPaste = React.useCallback((event) => {
const pasted = event.clipboardData.getData('text');
if (pasted) {
handlePlayUrl(pasted);
}
}, [createTorrentFromMagnet]);
}, [handlePlayUrl]);
const queryInputOnSubmit = React.useCallback((event) => {
event.preventDefault();
@ -108,6 +110,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
defaultValue={query}
tabIndex={-1}
onChange={queryInputOnChange}
onPaste={queryInputOnPaste}
onSubmit={queryInputOnSubmit}
onClick={openHistory}
/>

View file

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

View file

@ -5,9 +5,10 @@ type Props = {
children: JSX.Element,
when: boolean,
name: string,
duration?: number,
};
const Transition = ({ children, when, name }: Props) => {
const Transition = ({ children, when, name, duration }: Props) => {
const [element, setElement] = useState<HTMLElement | null>(null);
const [mounted, setMounted] = useState(false);
@ -29,6 +30,10 @@ const Transition = ({ children, when, name }: Props) => {
);
}, [name, state, active, children]);
const style = useMemo(() => {
if (duration) return { transitionDuration: `${duration}ms` };
}, [duration]);
const onTransitionEnd = useCallback(() => {
state === 'exit' && setMounted(false);
}, [state]);
@ -39,9 +44,10 @@ const Transition = ({ children, when, name }: Props) => {
}, [when]);
useEffect(() => {
requestAnimationFrame(() => {
const animationFrame = requestAnimationFrame(() => {
setActive(!!element);
});
return () => cancelAnimationFrame(animationFrame);
}, [element]);
useEffect(() => {
@ -53,6 +59,7 @@ const Transition = ({ children, when, name }: Props) => {
mounted && cloneElement(children, {
ref: callbackRef,
className,
style,
})
);
};

View file

@ -19,6 +19,7 @@
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;

View file

@ -30,6 +30,7 @@ import TextInput from './TextInput';
import Toggle from './Toggle';
import Transition from './Transition';
import Video from './Video';
import ActionsGroup from './ActionsGroup';
export {
AddonDetailsModal,
@ -65,4 +66,5 @@ export {
Toggle,
Transition,
Video,
ActionsGroup
};

View file

@ -5,8 +5,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Stremio">
<link rel="icon" type="image/x-icon" href="<%= htmlWebpackPlugin.options.faviconsPath %>/favicon.ico">
<link rel="manifest" href="manifest.json" />
<title>Stremio - Freedom to Stream</title>
<%= htmlWebpackPlugin.tags.headTags %>
</head>

View file

@ -5,7 +5,7 @@ const ReactIs = require('react-is');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const UrlUtils = require('url');
const isEqual = require('lodash.isequal');
const { deepEqual } = require('fast-equals');
const { RouteFocusedProvider } = require('../RouteFocusedContext');
const Route = require('../Route');
const routeConfigForPath = require('./routeConfigForPath');
@ -54,11 +54,11 @@ const Router = ({ className, onPathNotMatch, onRouteChange, ...props }) => {
return {
key: `${routeViewIndex}${routeIndex}`,
component: routeConfig.component,
urlParams: view !== null && isEqual(view.urlParams, urlParams) ?
urlParams: view !== null && deepEqual(view.urlParams, urlParams) ?
view.urlParams
:
urlParams,
queryParams: view !== null && isEqual(Array.from(view.queryParams.entries()), Array.from(queryParams.entries())) ?
queryParams: view !== null && deepEqual(Array.from(view.queryParams.entries()), Array.from(queryParams.entries())) ?
view.queryParams
:
queryParams

View file

@ -8,6 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common');
const { AddonDetailsModal, Button, Image, MainNavBars, ModalDialog, SearchBar, SharePrompt, TextInput, MultiselectMenu } = require('stremio/components');
const { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons');
const useRemoteAddons = require('./useRemoteAddons');
@ -20,6 +21,7 @@ const Addons = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const platform = usePlatform();
const { core } = useServices();
const toast = useToast();
const installedAddons = useInstalledAddons(urlParams);
const remoteAddons = useRemoteAddons(urlParams);
const [addonDetailsTransportUrl, setAddonDetailsTransportUrl] = useAddonDetailsTransportUrl(urlParams, queryParams);
@ -29,7 +31,17 @@ const Addons = ({ urlParams, queryParams }) => {
const addAddonUrlInputRef = React.useRef(null);
const addAddonOnSubmit = React.useCallback(() => {
if (addAddonUrlInputRef.current !== null) {
setAddonDetailsTransportUrl(addAddonUrlInputRef.current.value);
try {
let url = new URL(addAddonUrlInputRef.current.value).toString();
setAddonDetailsTransportUrl(url);
} catch (e) {
toast.show({
type: 'error',
title: `Failed to parse addon url: ${addAddonUrlInputRef.current.value}`,
timeout: 10000
});
console.error('Failed to parse addon url:', e);
}
}
}, [setAddonDetailsTransportUrl]);
const addAddonModalButtons = React.useMemo(() => {

View file

@ -18,7 +18,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
() => {
const selectableCatalog = remoteAddons.selectable.catalogs
.find(({ id }) => id === remoteAddons.selected.request.path.id);
return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name, 'ADDON_') : remoteAddons.selected.request.path.id;
return selectableCatalog ? t.stringWithPrefix(selectableCatalog.name.toUpperCase(), 'ADDON_') : remoteAddons.selected.request.path.id;
}
: null,
onSelect: (value) => {
@ -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

@ -17,7 +17,7 @@ const Placeholder = () => {
<div className={styles['image-container']}>
<Image
className={styles['image']}
src={require('/images/calendar_placeholder.png')}
src={require('/assets/images/calendar_placeholder.png')}
alt={' '}
/>
</div>

View file

@ -23,6 +23,11 @@ const Discover = ({ urlParams, queryParams }) => {
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0);
const selectedMetaItem = React.useMemo(() => {
return discover.catalog?.content.type === 'Ready' &&
discover.catalog.content.content[selectedMetaItemIndex] || null;
}, [discover.catalog, selectedMetaItemIndex]);
const metasContainerRef = React.useRef();
const metaPreviewRef = React.useRef();
@ -40,14 +45,6 @@ const Discover = ({ urlParams, queryParams }) => {
}
}
}, [hasNextPage, loadNextPage]);
const selectedMetaItem = React.useMemo(() => {
return discover.catalog !== null &&
discover.catalog.content.type === 'Ready' &&
discover.catalog.content.content[selectedMetaItemIndex] ?
discover.catalog.content.content[selectedMetaItemIndex]
:
null;
}, [discover.catalog, selectedMetaItemIndex]);
const addToLibrary = React.useCallback(() => {
if (selectedMetaItem === null) {
return;
@ -74,6 +71,22 @@ const Discover = ({ urlParams, queryParams }) => {
}
});
}, [selectedMetaItem]);
const toggleWatched = React.useCallback(() => {
if (selectedMetaItem === null) {
return;
}
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'MetaItemMarkAsWatched',
args: {
meta_item: selectedMetaItem,
is_watched: !selectedMetaItem.watched,
}
}
});
}, [selectedMetaItem]);
const metaItemsOnFocusCapture = React.useCallback((event) => {
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
@ -133,14 +146,14 @@ const Discover = ({ urlParams, queryParams }) => {
discover.catalog === null ?
<DelayedRenderer delay={500}>
<div className={styles['message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>{t('NO_CATALOG_SELECTED')}</div>
</div>
</DelayedRenderer>
:
discover.catalog.content.type === 'Err' ?
<div className={styles['message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>{discover.catalog.content.content}</div>
</div>
:
@ -193,6 +206,8 @@ const Discover = ({ urlParams, queryParams }) => {
trailerStreams={selectedMetaItem.trailerStreams}
inLibrary={selectedMetaItem.inLibrary}
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
watched={selectedMetaItem.watched}
toggleWatched={toggleWatched}
metaId={selectedMetaItem.id}
like={selectedMetaItem.like}
/>

View file

@ -54,13 +54,13 @@ const mapSelectableInputs = (discover, t) => {
value
})
})),
value: JSON.stringify({
value: selectedExtra ? JSON.stringify({
href: selectedExtra.deepLinks.discover,
value: selectedExtra.value,
}),
}) : undefined,
title: options.some(({ selected, value }) => selected && value === null) ?
() => t.string(name.toUpperCase())
: t.string(selectedExtra.value),
: selectedExtra ? t.string(selectedExtra.value) : () => t.string(name.toUpperCase()),
onSelect: (value) => {
const { href } = JSON.parse(value);
window.location = href;

View file

@ -183,7 +183,7 @@ const Intro = ({ queryParams }) => {
return;
}
if (!state.privacyPolicyAccepted) {
dispatch({ type: 'error', error: 'You must accept the Privacy Policy' });
dispatch({ type: 'error', error: t('MUST_ACCEPT_PRIVACY_POLICY') });
return;
}
openLoaderModal();
@ -296,7 +296,7 @@ const Intro = ({ queryParams }) => {
<div className={styles['background-container']} />
<div className={styles['heading-container']}>
<div className={styles['logo-container']}>
<Image className={styles['logo']} src={require('/images/logo.png')} alt={' '} />
<Image className={styles['logo']} src={require('/assets/images/logo.png')} alt={' '} />
</div>
<div className={styles['title-container']}>
{t('WEBSITE_SLOGAN_NEW_NEW')}

View file

@ -19,7 +19,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
emailRef.current.value.length > 0 && emailRef.current.validity.valid ?
platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
:
setError('Invalid email');
setError(t('INVALID_EMAIL'));
}, []);
const passwordResetModalButtons = React.useMemo(() => {
return [
@ -31,7 +31,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
}
},
{
label: t('SEND'),
label: t('BUTTON_SEND'),
props: {
onClick: goToPasswordReset
}
@ -52,7 +52,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
placeholder={'Email'}
placeholder={t('EMAIL')}
defaultValue={typeof email === 'string' ? email : ''}
onChange={emailOnChange}
onSubmit={goToPasswordReset}

View file

@ -19,7 +19,7 @@
bottom: -1rem;
left: -1rem;
right: -1rem;
background: url('/images/background_1.svg'), url('/images/background_2.svg');
background: url('/assets/images/background_1.svg'), url('/assets/images/background_2.svg');
background-color: var(--primary-background-color);
background-position: bottom left, top right;
background-size: 53%, 54%;

View file

@ -85,7 +85,7 @@ const Library = ({ model, urlParams, queryParams }) => {
<div className={styles['message-container']}>
<Image
className={styles['image']}
src={require('/images/empty.png')}
src={require('/assets/images/empty.png')}
alt={' '}
/>
<div className={styles['message-label']}>{model === 'library' ? t('LIBRARY_NOT_LOADED') : t('BOARD_CONTINUE_WATCHING_NOT_LOADED')}</div>
@ -96,7 +96,7 @@ const Library = ({ model, urlParams, queryParams }) => {
<div className={styles['message-container']}>
<Image
className={styles['image']}
src={require('/images/empty.png')}
src={require('/assets/images/empty.png')}
alt={' '}
/>
<div className={styles['message-label']}>{model === 'library' ? t('LIBRARY_EMPTY') : t('BOARD_CONTINUE_WATCHING_EMPTY')}</div>

View file

@ -17,7 +17,7 @@ const Placeholder = () => {
<div className={styles['image-container']}>
<Image
className={styles['image']}
src={require('/images/library_placeholder.png')}
src={require('/assets/images/library_placeholder.png')}
alt={' '}
/>
</div>

View file

@ -16,6 +16,9 @@ const EpisodePicker = ({ className, onSubmit }: Props) => {
const { initialSeason, initialEpisode } = useMemo(() => {
const splitPath = window.location.hash.split('/');
if (splitPath[splitPath.length - 1] === '') {
splitPath.pop();
}
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : [];
return {

View file

@ -64,6 +64,19 @@ const MetaDetails = ({ urlParams, queryParams }) => {
}
});
}, [metaDetails]);
const toggleWatched = React.useCallback(() => {
if (metaDetails.metaItem === null || metaDetails.metaItem.content.type !== 'Ready') {
return;
}
core.transport.dispatch({
action: 'MetaDetails',
args: {
action: 'MarkAsWatched',
args: !metaDetails.metaItem.content.content.watched
}
});
}, [metaDetails]);
const toggleNotifications = React.useCallback(() => {
if (metaDetails.libraryItem) {
core.transport.dispatch({
@ -81,7 +94,11 @@ const MetaDetails = ({ urlParams, queryParams }) => {
const handleEpisodeSearch = React.useCallback((season, episode) => {
const searchVideoHash = encodeURIComponent(`${urlParams.id}:${season}:${episode}`);
const url = window.location.hash;
const searchVideoPath = url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash);
const searchVideoPath = (urlParams.videoId === undefined || urlParams.videoId === null || urlParams.videoId === '') ?
url + (!url.endsWith('/') ? '/' : '') + searchVideoHash
: url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash);
window.location = searchVideoPath;
}, [urlParams, window.location]);
@ -130,20 +147,20 @@ const MetaDetails = ({ urlParams, queryParams }) => {
metaPath === null ?
<DelayedRenderer delay={500}>
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>{t('ERR_NO_META_SELECTED')}</div>
</div>
</DelayedRenderer>
:
metaDetails.metaItem === null ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>{t('ERR_NO_ADDONS_FOR_META')}</div>
</div>
:
metaDetails.metaItem.content.type === 'Err' ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>{t('ERR_NO_META_FOUND')}</div>
</div>
:
@ -168,6 +185,8 @@ const MetaDetails = ({ urlParams, queryParams }) => {
trailerStreams={metaDetails.metaItem.content.content.trailerStreams}
inLibrary={metaDetails.metaItem.content.content.inLibrary}
toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary}
watched={metaDetails.metaItem.content.content.watched}
toggleWatched={toggleWatched}
metaId={metaDetails.metaItem.content.content.id}
ratingInfo={metaDetails.ratingInfo}
/>

View file

@ -86,9 +86,17 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
}, [href, deepLinks]);
const streamLink = React.useMemo(() => {
return deepLinks?.externalPlayer?.streaming;
}, [deepLinks]);
const downloadLink = React.useMemo(() => {
return deepLinks?.externalPlayer?.download;
}, [deepLinks]);
const magnetLink = React.useMemo(() => {
return deepLinks?.externalPlayer?.magnet;
}, [deepLinks]);
const markVideoAsWatched = React.useCallback(() => {
if (typeof videoId === 'string') {
core.transport.dispatch({
@ -102,6 +110,10 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
}, [videoId, videoReleased]);
const onClick = React.useCallback((event) => {
if (event.nativeEvent.togglePopupPrevented) {
return;
}
if (profile.settings.playerType !== null) {
markVideoAsWatched();
toast.show({
@ -116,6 +128,50 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
}
}, [props.onClick, profile.settings, markVideoAsWatched]);
const copyMagnetLink = React.useCallback((event) => {
event.preventDefault();
closeMenu();
if (magnetLink) {
navigator.clipboard.writeText(magnetLink)
.then(() => {
toast.show({
type: 'success',
title: t('PLAYER_COPY_MAGNET_LINK_SUCCESS'),
timeout: 4000
});
})
.catch(() => {
toast.show({
type: 'error',
title: t('PLAYER_COPY_MAGNET_LINK_ERROR'),
timeout: 4000,
});
});
}
}, [magnetLink]);
const copyDownloadLink = React.useCallback((event) => {
event.preventDefault();
closeMenu();
if (downloadLink) {
navigator.clipboard.writeText(downloadLink)
.then(() => {
toast.show({
type: 'success',
title: t('PLAYER_COPY_DOWNLOAD_LINK_SUCCESS'),
timeout: 4000
});
})
.catch(() => {
toast.show({
type: 'error',
title: t('PLAYER_COPY_DOWNLOAD_LINK_ERROR'),
timeout: 4000,
});
});
}
}, [downloadLink]);
const copyStreamLink = React.useCallback((event) => {
event.preventDefault();
closeMenu();
@ -195,6 +251,20 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_STREAM_LINK')}</div>
</Button>
}
{
magnetLink &&
<Button className={styles['context-menu-option-container']} title={t('CTX_COPY_MAGNET_LINK')} onClick={copyMagnetLink}>
<Icon className={styles['menu-icon']} name={'magnet-link'} />
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_MAGNET_LINK')}</div>
</Button>
}
{
downloadLink &&
<Button className={styles['context-menu-option-container']} title={t('CTX_DOWNLOAD_VIDEO')} onClick={copyDownloadLink}>
<Icon className={styles['menu-icon']} name={'download'} />
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_VIDEO_DOWNLOAD_LINK')}</div>
</Button>
}
</div>
);
}, [copyStreamLink, onClick]);
@ -234,6 +304,7 @@ Stream.propTypes = {
player: PropTypes.string,
externalPlayer: PropTypes.shape({
download: PropTypes.string,
magnet: PropTypes.string,
streaming: PropTypes.string,
playlist: PropTypes.string,
fileName: PropTypes.string,

View file

@ -60,6 +60,7 @@
width: 7rem;
font-size: 1.1rem;
text-align: left;
white-space: pre-wrap;
color: var(--primary-foreground-color);
}

View file

@ -108,7 +108,9 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
<Icon className={styles['icon']} name={'chevron-back'} />
</Button>
<div className={styles['episode-title']}>
{`S${video?.season}E${video?.episode} ${(video?.title)}`}
{typeof video.season === 'number' && typeof video.episode === 'number'
? `S${video.season}E${video.episode}${video.title ? ` ${video.title}` : ''}`
: (video.title ?? '')}
</div>
</React.Fragment>
:
@ -132,7 +134,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
<SeasonEpisodePicker className={styles['search']} onSubmit={handleEpisodePicker} />
: null
}
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['label']}>{t('ERR_NO_ADDONS_FOR_STREAMS')}</div>
</div>
:
@ -148,7 +150,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
<div className={styles['label']}>{t('UPCOMING')}...</div>
: null
}
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['label']}>{t('NO_STREAM')}</div>
{
showInstallAddonsButton ?
@ -168,17 +170,6 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
</div>
:
<React.Fragment>
{
countLoadingAddons > 0 ?
<div className={styles['addons-loading-container']}>
<div className={styles['addons-loading']}>
{countLoadingAddons} {t('MOBILE_ADDONS_LOADING')}
</div>
<span className={styles['addons-loading-bar']}></span>
</div>
:
null
}
<div className={styles['streams-container']} ref={streamsContainerRef}>
{filteredStreams.map((stream, index) => (
<Stream
@ -204,6 +195,17 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
null
}
</div>
{
countLoadingAddons > 0 ?
<div className={styles['addons-loading-container']}>
<div className={styles['addons-loading']}>
{countLoadingAddons} {t('MOBILE_ADDONS_LOADING')}
</div>
<span className={styles['addons-loading-bar']}></span>
</div>
:
null
}
</React.Fragment>
}
</div>

View file

@ -50,11 +50,12 @@
display: flex;
z-index: 1;
overflow: visible;
margin: 2em 1em 0 1em;
margin: 2em;
gap: 1em;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: var(--border-radius) var(--border-radius) 0 0;
.addons-loading {
color: var(--primary-foreground-color);
@ -198,5 +199,13 @@
background-color: transparent;
}
}
.addons-loading-container {
position: sticky;
bottom: 0;
margin: 0;
padding: 1em;
background-color: var(--primary-background-color);
}
}
}

View file

@ -13,14 +13,14 @@ const SeasonsBarPlaceholder = ({ className }) => {
<div className={classnames(className, styles['seasons-bar-placeholder-container'])}>
<div className={styles['prev-season-button']}>
<Icon className={styles['icon']} name={'chevron-back'} />
<div className={styles['label']}>{t('SEASON_PREV')}</div>
<div className={styles['label']}>{t('PREV_SEASON')}</div>
</div>
<div className={styles['seasons-popup-label-container']}>
<div className={styles['seasons-popup-label']}>{t('SEASON_NUMBER', { season: 1 })}</div>
<Icon className={styles['seasons-popup-icon']} name={'caret-down'} />
</div>
<div className={styles['next-season-button']}>
<div className={styles['label']}>{t('SEASON_NEXT')}</div>
<div className={styles['label']}>{t('NEXT_SEASON')}</div>
<Icon className={styles['icon']} name={'chevron-forward'} />
</div>
</div>

View file

@ -124,7 +124,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
metaItem.content.type === 'Err' || videosForSeason.length === 0 ?
<div className={styles['message-container']}>
<EpisodePicker className={styles['episode-picker']} onSubmit={onSeasonSearch} />
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<Image className={styles['image']} src={require('/assets/images/empty.png')} alt={' '} />
<div className={styles['label']}>{t('ERR_NO_VIDEOS_FOR_META')}</div>
</div>
:

View file

@ -48,7 +48,7 @@ const useMetaDetails = (urlParams) => {
id: urlParams.id,
extra: []
},
streamPath: typeof urlParams.videoId === 'string' ?
streamPath: typeof urlParams.videoId === 'string' && urlParams.videoId !== '' ?
{
resource: 'stream',
type: urlParams.type,

View file

@ -12,7 +12,11 @@ const useSeason = (urlParams, queryParams) => {
const setSeason = React.useCallback((season) => {
const nextQueryParams = new URLSearchParams(queryParams);
nextQueryParams.set('season', season);
window.location.replace(`#${urlParams.path}?${nextQueryParams}`);
const path = urlParams.path.endsWith('/') ?
urlParams.path.slice(0, -1):
urlParams.path;
window.location.replace(`#${path}?${nextQueryParams}`);
}, [urlParams, queryParams]);
return [season, setSeason];
};

View file

@ -19,7 +19,7 @@ const NotFound = () => {
<div className={styles['not-found-content']}>
<Image
className={styles['not-found-image']}
src={require('/images/empty.png')}
src={require('/assets/images/empty.png')}
alt={' '}
/>
<div className={styles['not-found-label']}>{t('PAGE_NOT_FOUND')}</div>

View file

@ -7,6 +7,7 @@
align-self: stretch;
display: flex;
flex-direction: column;
max-height: 25rem;
width: 16rem;
.header {

View file

@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react';
import React, { forwardRef, memo, MouseEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { languages } from 'stremio/common';
@ -12,7 +12,7 @@ type Props = {
onAudioTrackSelected: (id: string) => void,
};
const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props) => {
const AudioMenu = memo(forwardRef<HTMLDivElement, Props>(({ className, selectedAudioTrackId, audioTracks, onAudioTrackSelected }: Props, ref) => {
const { t } = useTranslation();
const onAudioTrackClick = useCallback(({ currentTarget }: MouseEvent) => {
@ -26,7 +26,7 @@ const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackS
};
return (
<div className={classNames(className, styles['audio-menu'])} onMouseDown={onMouseDown}>
<div ref={ref} className={classNames(className, styles['audio-menu'])} onMouseDown={onMouseDown}>
<div className={styles['container']}>
<div className={styles['header']}>
{ t('AUDIO_TRACKS') }
@ -62,6 +62,6 @@ const AudioMenu = ({ className, selectedAudioTrackId, audioTracks, onAudioTrackS
</div>
</div>
);
};
}));
export default AudioMenu;

View file

@ -13,7 +13,7 @@ const BufferingLoader = React.forwardRef(({ className, logo }, ref) => {
className={styles['buffering-loader']}
src={logo}
alt={' '}
fallbackSrc={require('/images/stremio_symbol.png')}
fallbackSrc={require('/assets/images/stremio_symbol.png')}
/>
</div>
);

View file

@ -12,7 +12,7 @@ const styles = require('./styles');
const { useBinaryState, usePlatform } = require('stremio/common');
const { t } = require('i18next');
const ControlBar = ({
const ControlBar = React.forwardRef(({
className,
paused,
time,
@ -42,7 +42,7 @@ const ControlBar = ({
onToggleStatisticsMenu,
onTouchEnd,
...props
}) => {
}, ref) => {
const { chromecast } = useServices();
const platform = usePlatform();
const [chromecastServiceActive, setChromecastServiceActive] = React.useState(() => chromecast.active);
@ -105,7 +105,7 @@ const ControlBar = ({
};
}, []);
return (
<div {...props} onTouchStart={props.onMouseOver} onTouchMove={props.onMouseMove} onTouchEnd={onTouchEnd} className={classnames(className, styles['control-bar-container'])}>
<div ref={ref} {...props} onTouchStart={props.onMouseOver} onTouchMove={props.onMouseMove} onTouchEnd={onTouchEnd} className={classnames(className, styles['control-bar-container'])}>
<SeekBar
className={styles['seek-bar']}
time={time}
@ -183,7 +183,7 @@ const ControlBar = ({
</div>
</div>
);
};
});
ControlBar.propTypes = {
className: PropTypes.string,

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