Compare commits

...

189 Commits

Author SHA1 Message Date
Bill Thornton
6cae5c2646 Merge pull request #6041 from grafixeyehero/Fix-list-view-item-undefined
Some checks failed
Quality checks / Run stylelint (push) Has been cancelled
Quality checks / Run TypeScript build check (push) Has been cancelled
Quality checks / Run tests (push) Has been cancelled
Build / Run production build (push) Has been cancelled
CodeQL / Run CodeQL (push) Has been cancelled
Quality checks / Run es-check (push) Has been cancelled
Quality checks / Run eslint (push) Has been cancelled
2024-09-08 14:32:30 -04:00
grafixeyehero
5195006f7c Fix list view item undefined 2024-09-08 20:45:32 +03:00
Jellyfin Release Bot
6f203b9d1d Bump version to 10.9.11 2024-09-07 18:10:52 -04:00
Bill Thornton
2682098f61 Merge pull request #6016 from tcely/patch-2
Show slideshow controls when touched
2024-09-04 10:14:57 -04:00
tcely
455db67286 slideshow: show controls when touched
It appears that a previous commit changed the blocks so that only mouse events were recognized.
2024-09-03 20:09:57 -04:00
Bill Thornton
6b1352a855 Merge pull request #6015 from thornbill/fix-touch-events
Fix touch events in experimental video player
2024-09-03 15:24:40 -04:00
Bill Thornton
a1721ddd17 Merge pull request #6013 from thornbill/fix-autocast-maybe
Fix autocast when already connected
2024-09-03 14:53:14 -04:00
Bill Thornton
5051ee2d8e Fix touch events in experimental video player 2024-09-03 14:50:20 -04:00
Bill Thornton
7d30057c37 Merge pull request #6012 from thornbill/hide-collection-studios
Hide studios for collections and playlists
2024-09-03 12:36:00 -04:00
Bill Thornton
2b1f3470f4 Fix autocast when already connected 2024-09-03 12:30:48 -04:00
Bill Thornton
d2e09f9cae Hide studios for collections and playlists 2024-09-03 11:01:08 -04:00
Bill Thornton
b9925ebf73 Merge pull request #6011 from thornbill/fix-network-mode
Fix network mode for localhost server
2024-09-03 10:32:15 -04:00
Bill Thornton
2ebf0c9fe4 Merge pull request #5983 from nyanmisaka/fix-dovi-level-test
Fix overly strict dovi level testing
2024-09-03 10:07:08 -04:00
Bill Thornton
838e14e89f Fix network mode for localhost server 2024-09-03 09:59:39 -04:00
Bill Thornton
b9760eac75 Merge pull request #6010 from thornbill/create-library-crash
Fix create library crashing when no path specified
2024-09-03 09:51:55 -04:00
jwaresoft
61eb481d20 5968: move prevent default to top to prevent modal from crashing with promise 2024-09-03 09:44:38 -04:00
nyanmisaka
b1a6fd5d4e Fix overly strict dovi level testing
4k@60fps is usually only seen in demos so it's a bit overkill
for testing dovi caps. Lower it to the more common 4k@24fps.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-08-29 00:20:57 +08:00
Bill Thornton
20ea6041a7 Merge pull request #5972 from dmitrylyzo/max-audio-channels
Apply Maximum Allowed Audio Channels to DirectPlay
2024-08-27 16:48:29 -04:00
viown
ef00d439b1 Fix play all & shuffle not working on genres (#5949)
* Fix play all/shuffle not working on genres

* re-order

* add check for genre type

* Update src/controllers/list.js

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-27 16:39:34 -04:00
Dmitry Lyzo
7807f8f062 Apply Maximum Allowed Audio Channels to DirectPlay 2024-08-27 13:22:08 +03:00
Jellyfin Release Bot
7949ff4f0a Bump version to 10.9.10 2024-08-25 02:34:39 -04:00
viown
d47023855e Fix undefined serverId in Person card (#5817)
* Fix undefined serverId in Person card

* Use ServerConnections instead of globals

* Update src/components/cardbuilder/cardBuilder.js

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>

---------

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-08-20 12:23:30 -04:00
gnattu
6a8f21e462 Fix safari volume being reset when track changed (#5923) 2024-08-17 22:21:27 -04:00
Bill Thornton
90236c25ee Merge pull request #5920 from gnattu/fix-safari-volume-10.9 2024-08-17 13:44:41 -04:00
gnattu
2305240cb9 Fix Safari volume control 2024-08-17 10:57:39 +08:00
Bill Thornton
8bc954468a Merge pull request #5915 from viown/fix-paused-state
Fix incorrect initial play icon in remote control section
2024-08-15 23:29:45 -04:00
viown
76a28e125e Fix incorrect default play icon 2024-08-15 23:17:10 +03:00
Matteo Pietro Dazzi
2e4e4050cd fix: use navigate instead of resolver (#5823)
* fix: routes redirect

* Apply suggestions from code review
2024-08-13 11:42:26 -04:00
James Chuong
4071c44437 Fix "Download All" for Safari (#5910)
* Fix download all for Safari

Added check to use fallback downloader for iOS
Added check for safari to use the delayed download function
Remove old comment about firefox, as a.click() supported since Firefox
75 (2020)

Fixes: #5672

* Change download to always use setTimeout

Instead of conditionally using setTimeout based on browser, we should
always use it since it sometimes also misses some episodes.

* Update formatting

---------

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-08-13 11:28:24 -04:00
viown
44afbc2357 Fix swipe gestures on android for book reader (#5843)
* Fix swipe gestures on android for book reader

* Fix swipe gestures on safari

* fix eslint

* fix unbind
2024-08-13 10:53:10 -04:00
Bill Thornton
a0e6da790c Merge pull request #5730 from Doxterpepper/remove-caching-system-info
Add no-cache attribute for fetch requests to /system/info/public to prevent stale server info
2024-08-13 10:19:50 -04:00
Dock O'Neal
f1ecb967bf adding no-cache Cache-Control header policy.
Adding no-cache Cache-Control header policy to prevent caching of server version. This ensures the correct server version is always retrieved.
2024-08-13 10:15:29 -04:00
Dock O'Neal
fc4f396808 Changing System/Info/Public to no-cache. Prevents from identifying the correct server version during upgrade 2024-08-13 10:15:29 -04:00
Bill Thornton
7f575d724e Merge pull request #5898 from thornbill/fix-autocast-race
Fix autoCast race condition
2024-08-09 22:52:04 -04:00
Bill Thornton
0c5a433bbf Fix autocast race condition 2024-08-09 10:24:59 -04:00
Jellyfin Release Bot
219cda9c06 Bump version to 10.9.9 2024-08-04 22:01:38 -04:00
Bill Thornton
4598d66688 Merge pull request #5826 from thornbill/fix-view-cache 2024-08-04 21:57:06 -04:00
Bill Thornton
3eeebf9bd2 Fix rerender on location state updates 2024-08-01 13:35:27 -04:00
Bill Thornton
4de2a05264 Fix overly aggressive view caching 2024-08-01 01:11:29 -04:00
Bill Thornton
7acdb66e14 Merge pull request #5825 from thornbill/router-v13 2024-08-01 01:07:05 -04:00
Bill Thornton
665678d5d7 Merge pull request #5669 from RaafatAkkad/patch-2
Force DoVi on browser.xboxOne as edgeUWP says it can't play it
2024-07-26 10:55:04 -04:00
Bill Thornton
7991d15177 Merge pull request #5829 from polyzen/capital-mute
Display mute keyboard shortcut in uppercase
2024-07-25 22:59:06 -04:00
Daniel M. Capella
f6acb157c6 Display mute keyboard shortcut in uppercase 2024-07-25 18:38:13 -04:00
Bill Thornton
8ddd9ecd9d Add legacy bang url redirects 2024-07-24 15:12:33 -04:00
Bill Thornton
1adaf00cb3 Add RouterHistory to replace syncing for compatibility 2024-07-24 15:12:10 -04:00
Bill Thornton
3235e2e594 Unify app routers 2024-07-24 14:59:16 -04:00
Jellyfin Release Bot
b6844e61e2 Bump version to 10.9.8 2024-07-21 01:11:38 -04:00
Bill Thornton
91d40a0c4e Merge pull request #5812 from thornbill/fix-dashboard-titles
Fix stuck page titles on admin dashboard
2024-07-19 13:44:51 -04:00
Bill Thornton
c98822a7c6 Merge pull request #5810 from thornbill/fix-mixed-chapter-options
Fix chapter type options not showing for mixed libraries
2024-07-19 12:58:25 -04:00
venkata nadha reddy
83503936cc Fix stuck page titles on admin dashboard (#5735)
* Fixed stuck page titles on admin dashboard.

* Updating contributors
2024-07-19 12:54:59 -04:00
Bill Thornton
ebe3f0feb7 Fix chapter type options not showing for mixed libraries 2024-07-19 12:44:04 -04:00
Bill Thornton
f3bb9f2eef Merge pull request #5806 from thornbill/fix-dashboard-class
Fix dashboard body class sometimes missing
2024-07-18 12:25:15 -04:00
Bill Thornton
8c06742d2d Fix dashboard body class sometimes missing 2024-07-18 11:42:56 -04:00
Daniel M. Capella
2d68f94ec6 Display previous/next keyboard shortcuts (#5759)
* Display previous/next track keyboard shortcuts

* Display keyboard shortcuts in uppercase

* Display previous/next chapter keyboard shortcuts

* Allow capital letters for keyboard shortcuts

> On YouTube, Shift and CapsLock have no effect on these actions.
2024-07-16 10:34:13 -04:00
Bill Thornton
9501c5097b Merge pull request #5719 from dmitrylyzo/fix-bubble-trickplay
Fix Trickplay thumbnail in older web engines
2024-07-15 12:14:57 -04:00
Bill Thornton
86ff77924e Merge pull request #5553 from GeorgeH005/older-web0s-dovi-fix
Fix Dolby Vision playback on webOS
2024-07-15 10:12:47 -04:00
Bill Thornton
9e7ad28eaf Merge pull request #5776 from thornbill/fix-invisible-headings
Fix invisible headings
2024-07-12 16:13:56 -04:00
Bill Thornton
765394b6f2 Fix invisible headings 2024-07-12 15:47:46 -04:00
Bill Thornton
798b408bd7 Merge pull request #5377 from ConnorS1110/fix-multiselect-filter
Fix changing filters not resetting multiselected media cards
2024-07-12 15:24:33 -04:00
Bill Thornton
e669a9be02 Merge pull request #5741 from thornbill/epubjs-no-scripts 2024-07-10 23:32:54 -04:00
Bill Thornton
e0a0c92b43 Merge pull request #5740 from dmitrylyzo/fix-tv-volume
Don't change volume if it is physically controlled
2024-07-09 23:38:47 -04:00
Bill Thornton
2d2d5bef94 Merge pull request #5732 from thornbill/search-param-hook
Fix dashboard user page crash
2024-07-09 23:33:26 -04:00
George Haidos
0b34c1812e Allow Dolby Vision in TS for WebOS
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-07-08 21:36:53 +03:00
George Haidos
edd32297ee Disable fmp4 by default for Tizen and WebOS 2024-07-08 21:36:45 +03:00
George Haidos
7372e837ee Force support DoVi profile 8 for webOS TVs that support it but report otherwise 2024-07-08 21:36:33 +03:00
Bill Thornton
71d4f7083d Disallow scripted content in epubs 2024-06-27 14:07:39 -04:00
Dmitry Lyzo
bc8b83be5e Don't change volume if it is physically controlled 2024-06-27 12:58:13 +03:00
Bill Thornton
c89846c039 Fix getLocationSearch when search and hash search exist 2024-06-25 12:13:19 -04:00
Bill Thornton
4ffa90cdd7 Use search param hook from react-router 2024-06-25 12:11:46 -04:00
Jellyfin Release Bot
21ced03987 Bump version to 10.9.7 2024-06-24 20:19:30 -04:00
Bill Thornton
18061ce247 Merge pull request #5718 from dmitrylyzo/videoplayer-cleanup 2024-06-23 11:44:52 -04:00
Dmitry Lyzo
f507bfb016 Remove libjass leftovers 2024-06-20 13:12:13 +03:00
Dmitry Lyzo
98207228d6 Fix current aspect ratio reset
The media element may still have some CSS styles related to aspect
ratio, so we need to reset the current aspect ratio after
destroying the media element.
2024-06-20 13:12:13 +03:00
Dmitry Lyzo
30c1926e4e Fix trickplay thumbnail positioning
In webOS 1.2, the image is scaled to fit in the `chapterThumpWrapper`
and ignores position. Since it doesn't seem to support `resize` nor
`object-fit` to prevent scaling, use background instead.
2024-06-18 16:44:13 +03:00
Connor Smith
84e7b59e03 Fix changing filters not resetting multiselected media cards 2024-06-10 17:05:29 -04:00
Raafat Akkad
cb085ff955 Force DoVi on browser.xboxOne as edgeUWP says it can't play it
Unsure if DoVi profile 8 works on Xbox but the video plays
2024-06-10 20:41:32 +01:00
Bill Thornton
4bb0c67340 Merge pull request #5694 from thornbill/pdfjs-patch 2024-06-10 12:12:45 -04:00
Bill Thornton
4ec0e2f086 Disable eval support in pdfjs 2024-06-10 11:57:18 -04:00
Bill Thornton
674b0b118f Merge pull request #5681 from dmitrylyzo/fix-loading
Fix loading hides too early
2024-06-08 18:29:57 -04:00
Bill Thornton
aed4ffa2cd Merge pull request #5680 from Chaitanya-Shahare/chaitanya/fix-episode-overview-markdown
Fix episode overview markdown render
2024-06-08 18:26:43 -04:00
Chaitanya Shahare
a031aab622 Fix markdown is rendered properly in episode's overview 2024-06-08 19:58:06 +05:30
Dmitry Lyzo
fa4b109037 Fix loading hides too early 2024-06-07 14:46:36 +03:00
Jellyfin Release Bot
3bb9d44f85 Bump version to 10.9.6 2024-06-06 14:41:13 -04:00
Bill Thornton
7e20d3032f Merge pull request #5668 from thornbill/no-imdb
Remove IMDb references
2024-06-06 10:41:54 -04:00
Bill Thornton
a0c2202e64 Remove IMDb references 2024-06-06 10:30:20 -04:00
Bill Thornton
5495ef220a Merge pull request #5667 from dmitrylyzo/fix-slider-float 2024-06-06 09:52:57 -04:00
Dmitry Lyzo
7a88d5f02d Snap slider value
Firefox doesn't automatically snap the value of the range
element, which causes uneven values to be displayed.
2024-06-06 14:52:51 +03:00
Jellyfin Release Bot
24f4833742 Bump version to 10.9.5 2024-06-05 18:04:21 -04:00
Joshua M. Boniface
d898afdf10 Merge pull request #5664 from thornbill/livetv-image
Fix live tv images being ignored
2024-06-05 17:50:03 -04:00
Bill Thornton
238b44f1bb Fix live tv images being ignored 2024-06-05 17:44:50 -04:00
Bill Thornton
52aa8ebd49 Merge pull request #5662 from gnattu/mark-opera-as-hevc-av1-ready
Mark desktop Opera as AV1 and HEVC ready in fmp4
2024-06-05 14:14:04 -04:00
Bill Thornton
7865170eb6 Merge pull request #5621 from FintasticMan/webos_prioritise_hevc
Prioritise HEVC over H264 in HLS TS streams on webOS
2024-06-05 14:07:58 -04:00
Bill Thornton
2a110f6b5d Merge pull request #5661 from thornbill/missing-episode-search
Use display missing episodes setting in search
2024-06-05 14:03:06 -04:00
Bill Thornton
5680c18ade Merge pull request #5660 from ConnorS1110/fix-firefox-multiselect
Fixed being unable to properly long press on cards to multiselect on Firefox
2024-06-05 13:32:02 -04:00
gnattu
07bbe67927 Mark desktop Opera as AV1 and HEVC ready in fmp4
Users report that the desktop Opera can play AV1 and HEVC just fine in fmp4
2024-06-05 23:30:57 +08:00
Bill Thornton
611922d260 Use display missing episodes setting in search 2024-06-05 10:23:35 -04:00
Connor Smith
c35cec5c77 Fixed being unable to properly long press on cards to multiselect on Firefox 2024-06-04 22:37:08 -04:00
Bill Thornton
ab781678c1 Merge pull request #5658 from thornbill/activity-table-widths
Update activity table column widths
2024-06-04 16:14:44 -04:00
Bill Thornton
574eddada8 Merge pull request #5657 from thornbill/always-check-server
Revert "Fix extra requests in standalone mode"
2024-06-04 16:10:25 -04:00
Bill Thornton
7d057c58cf Update activity table column widths 2024-06-04 16:04:59 -04:00
Bill Thornton
4dd44dfd9f Revert "Fix extra requests in standalone mode"
This reverts commit 4161220965.
2024-06-04 15:28:02 -04:00
Bill Thornton
74a3bd8768 Merge pull request #5653 from dmitrylyzo/fix-videoosd-hide
Fix video OSD not fully hiding
2024-06-04 14:55:30 -04:00
Bill Thornton
7854c4b20b Merge pull request #5640 from mihawk90/theme-videos-fix
Fix background being invisible with theme videos
2024-06-04 14:54:01 -04:00
Tarulia
ba36747dbb Fix background being invisible with theme videos 2024-06-04 14:23:33 -04:00
Dmitry Lyzo
cbedc384b3 Fix video OSD not hiding
Sometimes (maybe in some browsers) onHideAnimationComplete
is called on btnPause, and the event listener is disconnecting
because it was connected with "once: true".

As a result, the `hide` class is not added to the OSD element,
allowing the user to interact with transparent elements.

Don't connect listener with "once: true".
2024-06-03 16:48:37 +03:00
Jellyfin Release Bot
d9c5440864 Bump version to 10.9.4 2024-06-01 18:39:03 -04:00
Bill Thornton
60af8a68f8 Merge pull request #5638 from grafixeyehero/TV-Guide-only-covers-half-the-screen 2024-05-31 03:17:41 -04:00
grafixeyehero
2a9892db85 Fix TV Guide only covers half the screen 2024-05-31 03:42:13 +03:00
Bill Thornton
4129676ed8 Merge pull request #5636 from grafixeyehero/Fix-Clear-query-and-view-cache-on-user-logout 2024-05-30 18:22:12 -04:00
grafixeyehero
75ef961530 Clear query and view cache on user logout
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-05-31 01:04:15 +03:00
Bill Thornton
4959a777c9 Merge pull request #5619 from grafixeyehero/Fix-Clear-query-cache-on-user-logout
Clear the cache view on user logout
2024-05-30 17:27:05 -04:00
Bill Thornton
003bc94e02 Merge pull request #5610 from grafixeyehero/Fix-Tv-Mode-search-field
Fix Search Field for Tv Mode
2024-05-30 11:13:44 -04:00
Bill Thornton
40e7dc9007 Merge pull request #5617 from gnattu/relax-remux-requirement-for-remote
Allow VideoStreamCopy for remote source fallback
2024-05-30 11:11:57 -04:00
grafixeyehero
c135be012c apply suggestion 2024-05-30 15:39:25 +03:00
grafixeyehero
ee6909325d Reset cached views
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-05-30 15:29:58 +03:00
gnattu
3a0be7d345 Simplify syntax
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-05-30 00:12:16 +08:00
gnattu
a67fd2e5ac Try EnableDirectStream when possible
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-05-30 00:12:00 +08:00
FintasticMan
472fc09a50 Also prioritise HEVC for Tizen 2024-05-29 10:40:47 +02:00
FintasticMan
4eeb79b3e1 Prioritise HEVC in TS containers on webOS 2024-05-28 23:58:05 +02:00
gnattu
a35c81a0eb Merge branch 'fork/relax-remux-requirement-for-remote' 2024-05-29 00:20:49 +08:00
gnattu
adcea4467d Fix HLS stream check
The TranscodingSubProtocol is no longer nullable on the server side and direct playing media will have a value of http. Check container type when TranscodingSubProtocol is not HLS
2024-05-29 00:20:10 +08:00
gnattu
6feb46fecb Don't check specific reason
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-05-28 20:27:11 +08:00
gnattu
20e29b81b5 Always try transcode as last resort 2024-05-28 05:45:29 +08:00
gnattu
2b59a9f998 fix lint 2024-05-28 01:41:56 +08:00
gnattu
a66e4d6d1a Allow VideoStreamCopy for remote source fallback
During LiveTV playback, a fallback is usually needed because the first attempt would be try to direct play the remote url of that channel. If that failed we should still allow stream copy because the playback would still success in this case. The server side will enforce the most compatible format (h264+ts) and still do transcoding if that condition is not met.
2024-05-28 01:33:29 +08:00
Jellyfin Release Bot
ea1cadf4b6 Bump version to 10.9.3 2024-05-26 20:00:31 -04:00
Bill Thornton
788ce37c43 Merge pull request #5612 from scampower3/regex-fix 2024-05-26 17:05:00 -04:00
LJQ
c1db082629 Fix ua detection 2024-05-27 02:55:00 +08:00
grafixeyehero
a51d700eff Fix onAlphaPicked callback, the query (search term) is not updated Properly 2024-05-26 20:04:16 +03:00
Bill Thornton
61976b8101 Merge pull request #5589 from thornbill/mui-themes 2024-05-25 12:04:21 -04:00
Joshua M. Boniface
35e4fe497e Update GitHub workflows from Master
Backport fixes from:
 - #5478
 - #5470
2024-05-25 12:02:22 -04:00
Bill Thornton
1ea598968c Merge pull request #5599 from NotSaifA/fix-release-date 2024-05-25 02:22:19 -04:00
Bill Thornton
167515dbf0 Merge pull request #5601 from thornbill/fix-marker-updating 2024-05-25 01:50:07 -04:00
Bill Thornton
f7f5ac99b0 Merge pull request #5590 from thornbill/fix-main-page-position 2024-05-25 01:49:14 -04:00
Bill Thornton
a88d03fe8f Merge pull request #5600 from thornbill/reset-cache-logout 2024-05-24 13:15:46 -04:00
Bill Thornton
3630ac0436 Fix chapter markers not updating 2024-05-24 09:33:22 -04:00
Bill Thornton
a71fe63684 Reset query cache on user logout 2024-05-24 08:47:55 -04:00
NotSaifA
b49eb09a08 Calculate timezone offset based on input 2024-05-24 06:17:19 -04:00
Bill Thornton
bd03c43716 Merge pull request #5593 from thornbill/fix-chapter-markers 2024-05-23 15:28:12 -04:00
Bill Thornton
e56c46a913 Fix chapter marker re-rendering 2024-05-23 14:37:52 -04:00
Bill Thornton
9e34ae8b42 Merge pull request #5587 from scampower3/fix-square-posters 2024-05-23 13:18:46 -04:00
Bill Thornton
7342e43bd4 Fix positioning of the main animated page elements 2024-05-23 12:48:38 -04:00
Bill Thornton
b5fda71a27 Add support for user themes for mui components 2024-05-23 10:18:42 -04:00
LJQ
14075c641a Update test 2024-05-23 15:12:39 +08:00
LJQ
e53c78a8ff Add changes to other aspect ratio comparisons 2024-05-23 15:11:08 +08:00
LJQ
adb662eb0b Fix square posters 2024-05-23 11:32:48 +08:00
Bill Thornton
bb9b4ce8bb Merge pull request #5581 from thornbill/fix-download-all-access 2024-05-21 16:40:14 -04:00
Bill Thornton
6da3dd7c86 Merge pull request #5526 from gnattu/remove-redundant-keybindings 2024-05-21 16:38:56 -04:00
Bill Thornton
9d9b69edd5 Merge pull request #5452 from dmitrylyzo/webos-flac-2ch 2024-05-21 16:36:35 -04:00
Dmitry Lyzo
fb87dfbf5e Add specialized video transcoding profile with FLAC for webOS
webOS doesn't seem to support FLAC with more than 2 channels.
Split each video transcoding profile with FLAC so that the
containing FLAC is only applied to 2 channels audio.
2024-05-21 23:20:28 +03:00
Dmitry Lyzo
1342bedad0 Limit maximum FLAC channels to 2 on webOS 2024-05-21 23:20:28 +03:00
Bill Thornton
292240df46 Fix missing policy check for download all 2024-05-21 16:18:42 -04:00
Bill Thornton
add01e332b Merge pull request #5573 from thornbill/fix-playback-access
Fix playback interceptor rejecting
2024-05-21 16:16:59 -04:00
Bill Thornton
2280d98785 Restore promise rejects 2024-05-21 12:08:21 -04:00
gnattu
11e3bf395e use browser.tv
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2024-05-21 22:37:35 +08:00
gnattu
0271ae42c0 Use modern syntax & slight perf improvement 2024-05-21 17:55:11 +08:00
gnattu
68bac17a46 Only bind media keys for TV and browsers not having mediaSession 2024-05-21 14:13:19 +08:00
Bill Thornton
7eb54e029f Merge pull request #5563 from nielsvanvelzen/fix-chapter-xss 2024-05-20 22:04:24 -04:00
Bill Thornton
70b9aa4611 Fix playback interceptor rejecting 2024-05-20 17:49:41 -04:00
Bill Thornton
3dcb42daac Merge pull request #5566 from gnattu/dont-do-smart-url-guessing 2024-05-20 11:57:35 -04:00
Niels van Velzen
b8a7cf214d Fix chapter name XSS injection in progress bar 2024-05-20 16:49:35 +02:00
gnattu
c0b86a39c7 Only connect to manuall addresses specified by user
This should never be enabled on the web because users are not expected to connect to an address they don't specify. For specific use cases like auto endpoint switching between networks, this should be managed by DNS or the router itself, not by the application. 

Having an address that is not always connectable causes our Android users to be unable to reliably connect to the server. It also breaks many reverse-proxy setups, as this address exposed by the server usually bypasses the proxy unless explicitly configured by the user. This has far more negative impact than the benifit it brings.
2024-05-20 22:37:19 +08:00
Bill Thornton
a806eeb3a7 Merge pull request #5558 from dmitrylyzo/fix-headers 2024-05-19 16:47:10 -04:00
Dmitry Lyzo
ab70cc07a8 Patch 'Headers' to accept 'undefined'
Fixes `TypeError: Failed to construct 'Headers': No matching constructor signature.`
2024-05-19 22:41:03 +03:00
Jellyfin Release Bot
ed321c4cdb Bump version to 10.9.2 2024-05-17 16:09:37 -04:00
Bill Thornton
7ce8c070b3 Merge pull request #5529 from thornbill/fix-plugin-tabs
Restore library menu tabs functionality
2024-05-17 13:09:49 -04:00
Bill Thornton
3402f1beba Merge pull request #5530 from thornbill/fix-schedule-dialog
Fix scheduled task dialog z-index
2024-05-17 13:09:11 -04:00
Bill Thornton
1e035d5867 Fix scheduled task dialog z-index 2024-05-17 12:31:08 -04:00
Bill Thornton
91961de0ce Restore librarymenu.setTabs functionality 2024-05-17 10:28:57 -04:00
Bill Thornton
ea1d069e90 Merge pull request #5517 from thornbill/fix-experimental-osd 2024-05-17 08:55:41 -04:00
gnattu
bdce41c3ae Don’t bind to keyevents of media keys
These events are already handled by MediaSession. On some operating systems, like Windows, the browser will send both the MediaSession event and the keydown event to the webpage, causing the event to be handled twice and resulting in issues.
2024-05-17 16:08:51 +08:00
Bill Thornton
b17ca028f8 Merge pull request #5519 from Schoggi0815/release-10.9.z
Fix chapter markings not displayed properly
2024-05-16 17:12:50 -04:00
Bill Thornton
37b1d5cbea Add more public paths to toolbar check 2024-05-16 16:58:40 -04:00
Bill Thornton
1a172bdb1b Move event type to enum 2024-05-16 12:34:41 -04:00
Matteo Bossi
7de4ebf33a Fix chapter markings not displayed properly 2024-05-16 13:21:30 +02:00
Bill Thornton
32a91eabf1 Use constant for event name 2024-05-16 00:28:07 -04:00
Bill Thornton
25b1bcab50 Merge pull request #5518 from thornbill/fix-syncplay-crash 2024-05-15 19:45:30 -04:00
Bill Thornton
703ec1b488 Fix syncplay playback starting before listener created 2024-05-15 17:38:03 -04:00
Bill Thornton
c0467b1f13 Fix video osd not hiding in experimental layout 2024-05-15 14:56:59 -04:00
Bill Thornton
0fcb1ff983 Merge pull request #5512 from thornbill/fix-sort-order 2024-05-15 08:29:25 -04:00
Bill Thornton
1ad7dfb5c0 Fix invalid sort order values 2024-05-15 02:39:50 -04:00
Bill Thornton
a358d34ea9 Merge pull request #5507 from thornbill/min-server-version 2024-05-15 00:05:19 -04:00
Bill Thornton
ea8ceaa727 Merge pull request #5500 from thornbill/fix-logout-credentials
Fix stored credentials not updating on logout
2024-05-14 17:22:11 -04:00
Bill Thornton
d17c35acc3 Fix stored credentials not updating on logout 2024-05-14 17:16:12 -04:00
Bill Thornton
df26f36a09 Merge pull request #5506 from thornbill/sdk-0.9.0
Update @jellyfin/sdk to 0.9.0
2024-05-14 17:15:46 -04:00
Bill Thornton
f980e38530 Set the minimum server version to match the sdk 2024-05-14 17:01:38 -04:00
Bill Thornton
1d883f445b Update @jellyfin/sdk to 0.9.0 2024-05-14 16:54:17 -04:00
Jellyfin Release Bot
ac8c2239ca Bump version to 10.9.1 2024-05-12 20:10:27 -04:00
Jellyfin Release Bot
7dc9c1d7aa Bump version to 10.9.0 2024-05-11 14:24:02 -04:00
144 changed files with 1613 additions and 992 deletions

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
- uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -46,30 +46,3 @@ jobs:
name: jellyfin-web__prod
path: |
dist
pr_context:
name: Save PR context as artifact
if: ${{ always() && !cancelled() && github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- run-build-prod
steps:
- name: Save PR context
env:
PR_BRANCH: ${{ github.ref_name }}
PR_NUMBER: ${{ github.event.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: |
echo $PR_BRANCH > PR_branch
echo $PR_NUMBER > PR_number
echo $PR_SHA > PR_sha
- name: Upload PR number as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: PR_context
path: |
PR_branch
PR_number
PR_sha

View File

@@ -19,16 +19,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Initialize CodeQL
uses: github/codeql-action/init@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with:
languages: javascript
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6

View File

@@ -18,7 +18,7 @@ jobs:
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -33,6 +33,6 @@ jobs:
- name: Run eslint
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
uses: CatChen/eslint-suggestion-action@34e2a6c4193eba18a7a20710b5ae37850fc984c3 # v3.1.5
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,42 +8,26 @@ on:
- completed
jobs:
pr-context:
name: PR context
if: ${{ github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
outputs:
branch: ${{ env.pr_branch }}
commit: ${{ env.pr_sha }}
pr_number: ${{ env.pr_number }}
steps:
- name: Get PR context
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
id: pr_context
with:
run_id: ${{ github.event.workflow_run.id }}
name: PR_context
- name: Set PR context environment variables
if: ${{ steps.pr_context.conclusion == 'success' }}
run: |
echo "pr_branch=$(cat PR_branch)" >> $GITHUB_ENV
echo "pr_number=$(cat PR_number)" >> $GITHUB_ENV
echo "pr_sha=$(cat PR_sha)" >> $GITHUB_ENV
publish:
permissions:
contents: read
deployments: write
name: Deploy to Cloudflare Pages
if: ${{ always() }}
runs-on: ubuntu-latest
needs:
- pr-context
permissions:
contents: read
deployments: write
# We set the environment variable here (and as an output) because,
# given no real runner is dispatched in compose-comment job (it's dispatched in the reusable workflow) in this workflow definition,
# the env. context is not valid.
env:
TARGET_BRANCH: |
${{
github.event.workflow_run.head_repository.full_name == github.repository
&& github.event.workflow_run.head_branch
|| format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch)
}}
outputs:
url: ${{ steps.cf.outputs.url }}
branch: ${{ env.TARGET_BRANCH }}
steps:
- name: Download workflow artifact
@@ -60,7 +44,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: jellyfin-web
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
branch: ${{ env.TARGET_BRANCH }}
directory: dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
@@ -70,11 +54,10 @@ jobs:
uses: ./.github/workflows/job-messages.yml
needs:
- publish
- pr-context
with:
branch: ${{ needs.pr-context.outputs.branch || github.ref_name }}
commit: ${{ needs.pr-context.outputs.commit != '' && needs.pr-context.outputs.commit || github.event.workflow_run.head_sha }}
branch: ${{ needs.publish.outputs.branch }}
commit: ${{ github.event.workflow_run.head_commit.id }}
preview_url: ${{ needs.publish.outputs.url }}
build_workflow_run_id: ${{ github.event.workflow_run.id }}
commenting_workflow_run_id: ${{ github.run_id }}
@@ -85,11 +68,10 @@ jobs:
if: |
always() &&
github.event.workflow_run.event == 'pull_request' &&
needs.pr-context.outputs.pr_number != ''
github.event.workflow_run.pull_requests[0].number != ''
runs-on: ubuntu-latest
needs:
- compose-comment
- pr-context
steps:
- name: Update job summary in PR comment
@@ -97,6 +79,6 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: ${{ needs.compose-comment.outputs.msg }}
pr_number: ${{ needs.pr-context.outputs.pr_number }}
pr_number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment_tag: ${{ needs.compose-comment.outputs.marker }}
mode: recreate

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -41,7 +41,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -86,7 +86,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
@@ -107,7 +107,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: master
token: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -87,6 +87,9 @@
- [JPUC1143](https://github.com/Jpuc1143)
- [David Angel](https://github.com/davidangel)
- [Pithaya](https://github.com/Pithaya)
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
- [Connor Smith](https://github.com/ConnorS1110)
- [Venkat Karasani](https://github.com/venkat-karasani)
## Emby Contributors

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "jellyfin-web",
"version": "10.9.0",
"version": "10.9.11",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jellyfin-web",
"version": "10.9.0",
"version": "10.9.11",
"license": "GPL-2.0-or-later",
"dependencies": {
"@emotion/react": "11.11.4",
@@ -18,7 +18,7 @@
"@fontsource/noto-sans-sc": "5.0.18",
"@fontsource/noto-sans-tc": "5.0.18",
"@jellyfin/libass-wasm": "4.2.1",
"@jellyfin/sdk": "0.0.0-unstable.202405050501",
"@jellyfin/sdk": "0.9.0",
"@loadable/component": "5.16.3",
"@mui/icons-material": "5.15.11",
"@mui/material": "5.15.11",
@@ -3728,9 +3728,9 @@
"integrity": "sha512-oWK2yz8fFlMXkIuxUc9g/bqN2h56AB+8b6vF/Ikns6WZ/nmcGJ/5lcVaLI4csE83yWgmco4gHO3HyJDsM9EXcQ=="
},
"node_modules/@jellyfin/sdk": {
"version": "0.0.0-unstable.202405050501",
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202405050501.tgz",
"integrity": "sha512-d7TvTH3gGltNH7WrcuJsC+NiTV4HMCxKhzEeW1dGchA6aXRS1aEcnTqsR/ArONQDzlM6ac9Y+y9gfvJYJ6Bgyg==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.9.0.tgz",
"integrity": "sha512-C8XmAE1LFIAJRYC8F9umlkjWW1lKrcQhCiILme5Da3XYhA8fvu57I1cucuOyFc5NqVPKeaQEOcoJMkuiNMejJw==",
"peerDependencies": {
"axios": "^1.3.4"
}
@@ -25587,9 +25587,9 @@
"integrity": "sha512-oWK2yz8fFlMXkIuxUc9g/bqN2h56AB+8b6vF/Ikns6WZ/nmcGJ/5lcVaLI4csE83yWgmco4gHO3HyJDsM9EXcQ=="
},
"@jellyfin/sdk": {
"version": "0.0.0-unstable.202405050501",
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202405050501.tgz",
"integrity": "sha512-d7TvTH3gGltNH7WrcuJsC+NiTV4HMCxKhzEeW1dGchA6aXRS1aEcnTqsR/ArONQDzlM6ac9Y+y9gfvJYJ6Bgyg==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.9.0.tgz",
"integrity": "sha512-C8XmAE1LFIAJRYC8F9umlkjWW1lKrcQhCiILme5Da3XYhA8fvu57I1cucuOyFc5NqVPKeaQEOcoJMkuiNMejJw==",
"requires": {}
},
"@jest/schemas": {

View File

@@ -1,6 +1,6 @@
{
"name": "jellyfin-web",
"version": "10.9.0",
"version": "10.9.11",
"description": "Web interface for Jellyfin",
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
@@ -79,7 +79,7 @@
"@fontsource/noto-sans-sc": "5.0.18",
"@fontsource/noto-sans-tc": "5.0.18",
"@jellyfin/libass-wasm": "4.2.1",
"@jellyfin/sdk": "0.0.0-unstable.202405050501",
"@jellyfin/sdk": "0.9.0",
"@loadable/component": "5.16.3",
"@mui/icons-material": "5.15.11",
"@mui/material": "5.15.11",

View File

@@ -1,32 +1,19 @@
import loadable from '@loadable/component';
import { ThemeProvider } from '@mui/material/styles';
import { History } from '@remix-run/router';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import theme from 'themes/theme';
import { queryClient } from 'utils/query/queryClient';
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
const RootAppRouter = loadable(() => import('./RootAppRouter'));
const RootApp = ({ history }: Readonly<{ history: History }>) => {
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
import RootAppRouter from './RootAppRouter';
const RootApp = () => {
return (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<ThemeProvider theme={theme}>
{isExperimentalLayout ?
<RootAppRouter history={history} /> :
<StableAppRouter history={history} />
}
</ThemeProvider>
<RootAppRouter />
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />

View File

@@ -1,31 +1,36 @@
import { History } from '@remix-run/router';
import React from 'react';
import {
RouterProvider,
createHashRouter,
Outlet
Outlet,
useLocation
} from 'react-router-dom';
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
import { createRouterHistory } from 'components/router/routerHistory';
import UserThemeProvider from 'themes/UserThemeProvider';
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const router = createHashRouter([
{
element: <RootAppLayout />,
children: [
...EXPERIMENTAL_APP_ROUTES,
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
...DASHBOARD_APP_ROUTES
]
}
]);
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
useLegacyRouterSync({ router, history });
export const history = createRouterHistory(router);
export default function RootAppRouter() {
return <RouterProvider router={router} />;
}
@@ -34,12 +39,16 @@ export default function RootAppRouter({ history }: Readonly<{ history: History}>
* NOTE: The app will crash if these get removed from the DOM.
*/
function RootAppLayout() {
const location = useLocation();
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
.some(path => location.pathname.startsWith(`/${path}`));
return (
<>
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
<Outlet />
</>
</UserThemeProvider>
);
}

View File

@@ -2,7 +2,7 @@ import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { type Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { FC, useCallback, useState } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
@@ -11,6 +11,7 @@ import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import './AppOverrides.scss';
@@ -35,6 +36,15 @@ const AppLayout: FC<AppLayoutProps> = ({
setIsDrawerActive(!isDrawerActive);
}, [ isDrawerActive, setIsDrawerActive ]);
// Update body class
useEffect(() => {
document.body.classList.add('dashboardDocument');
return () => {
document.body.classList.remove('dashboardDocument');
};
}, []);
return (
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={false}>
@@ -55,7 +65,9 @@ const AppLayout: FC<AppLayoutProps> = ({
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
>
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
</AppBar>
</ElevationScroll>

View File

@@ -5,10 +5,14 @@ $mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
$drawer-width: 240px;
// Fix dashboard pages layout to work with drawer
.dashboardDocument {
.mainAnimatedPage {
position: relative;
@media all and (min-width: $mui-bp-md) {
left: $drawer-width;
}
}
.skinBody {
@@ -16,7 +20,15 @@ $mui-bp-xl: 1536px;
}
// Fix the padding of dashboard pages
.content-primary.content-primary {
padding-top: 3.25rem !important;
.content-primary {
padding-top: 3.25rem;
}
// Tabbed pages
.withTabs .content-primary {
padding-top: 6.5rem;
@media all and (min-width: $mui-bp-lg) {
padding-top: 3.25rem;
}
}
}

View File

@@ -0,0 +1,96 @@
import { Theme } from '@mui/material/styles';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import useMediaQuery from '@mui/material/useMediaQuery';
import debounce from 'lodash-es/debounce';
import isEqual from 'lodash-es/isEqual';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { EventType } from 'types/eventType';
import Events, { type Event } from 'utils/events';
interface AppTabsParams {
isDrawerOpen: boolean
}
interface TabDefinition {
href: string
name: string
}
const handleResize = debounce(() => window.dispatchEvent(new Event('resize')), 100);
const AppTabs: FC<AppTabsParams> = ({
isDrawerOpen
}) => {
const documentRef = useRef<Document>(document);
const [ activeIndex, setActiveIndex ] = useState(0);
const [ tabs, setTabs ] = useState<TabDefinition[]>();
const isBigScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
const onTabsUpdate = useCallback((
_e: Event,
_newView?: string,
newIndex: number | undefined = 0,
newTabs?: TabDefinition[]
) => {
setActiveIndex(newIndex);
if (!isEqual(tabs, newTabs)) {
setTabs(newTabs);
}
}, [ tabs ]);
useEffect(() => {
const doc = documentRef.current;
if (doc) Events.on(doc, EventType.SET_TABS, onTabsUpdate);
return () => {
if (doc) Events.off(doc, EventType.SET_TABS, onTabsUpdate);
};
}, [ onTabsUpdate ]);
// HACK: Force resizing to workaround upstream bug with tab resizing
// https://github.com/mui/material-ui/issues/24011
useEffect(() => {
handleResize();
}, [ isDrawerOpen ]);
if (!tabs?.length) return null;
return (
<Tabs
value={activeIndex}
sx={{
width: '100%',
flexShrink: {
xs: 0,
lg: 'unset'
},
order: {
xs: 100,
lg: 'unset'
}
}}
variant={isBigScreen ? 'standard' : 'scrollable'}
centered={isBigScreen}
>
{
tabs.map(({ href, name }, index) => (
<Tab
key={`tab-${name}`}
label={name}
data-tab-index={`${index}`}
component={Link}
to={href}
/>
))
}
</Tabs>
);
};
export default AppTabs;

View File

@@ -107,7 +107,7 @@ const Activity = () => {
{
field: 'Name',
headerName: globalize.translate('LabelName'),
width: 200
width: 300
},
{
field: 'Overview',
@@ -121,11 +121,12 @@ const Activity = () => {
{
field: 'Type',
headerName: globalize.translate('LabelType'),
width: 120
width: 180
},
{
field: 'actions',
type: 'actions',
width: 50,
getActions: ({ row }) => {
const actions = [];

View File

@@ -140,6 +140,7 @@ const PlaybackTrickplay: FunctionComponent = () => {
<Page
id='trickplayConfigurationPage'
className='mainAnimatedPage type-interior playbackConfigurationPage'
title={globalize.translate('Trickplay')}
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>

View File

@@ -1,5 +1,6 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu';
@@ -7,7 +8,6 @@ import globalize from '../../../../scripts/globalize';
import toast from '../../../../components/toast/toast';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import ButtonElement from '../../../../elements/ButtonElement';
import { getParameterByName } from '../../../../utils/url';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
@@ -21,6 +21,8 @@ type ItemsArr = {
};
const UserLibraryAccess: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@@ -37,7 +39,7 @@ const UserLibraryAccess: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userlibraryaccess] Unexpected null page reference');
return;
}
@@ -64,7 +66,7 @@ const UserLibraryAccess: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userlibraryaccess] Unexpected null page reference');
return;
}
@@ -97,7 +99,7 @@ const UserLibraryAccess: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userlibraryaccess] Unexpected null page reference');
return;
}
@@ -138,7 +140,6 @@ const UserLibraryAccess: FunctionComponent = () => {
const loadData = useCallback(() => {
loading.show();
const userId = getParameterByName('userId');
const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} });
const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
@@ -150,21 +151,25 @@ const UserLibraryAccess: FunctionComponent = () => {
}).catch(err => {
console.error('[userlibraryaccess] failed to load data', err);
});
}, [loadUser]);
}, [loadUser, userId]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userlibraryaccess] Unexpected null page reference');
return;
}
loadData();
const onSubmit = (e: Event) => {
if (!userId) {
console.error('[userlibraryaccess] missing user id');
return;
}
loading.show();
const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
}).catch(err => {

View File

@@ -159,6 +159,7 @@ const UserProfiles: FunctionComponent = () => {
<Page
id='userProfilesPage'
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
title={globalize.translate('HeaderUsers')}
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>

View File

@@ -1,7 +1,8 @@
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import escapeHTML from 'escape-html';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu';
@@ -12,7 +13,6 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import { getParameterByName } from '../../../../utils/url';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
@@ -57,6 +57,8 @@ function handleSaveUser(
}
const UserParentalControl: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
@@ -95,7 +97,7 @@ const UserParentalControl: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -144,7 +146,7 @@ const UserParentalControl: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -165,7 +167,7 @@ const UserParentalControl: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -186,7 +188,7 @@ const UserParentalControl: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -208,7 +210,7 @@ const UserParentalControl: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -241,8 +243,12 @@ const UserParentalControl: FunctionComponent = () => {
}, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => {
if (!userId) {
console.error('[userparentalcontrol.loadData] missing user id');
return;
}
loading.show();
const userId = getParameterByName('userId');
const promise1 = window.ApiClient.getUser(userId);
const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
@@ -250,13 +256,13 @@ const UserParentalControl: FunctionComponent = () => {
}).catch(err => {
console.error('[userparentalcontrol] failed to load data', err);
});
}, [loadUser]);
}, [loadUser, userId]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
@@ -344,8 +350,12 @@ const UserParentalControl: FunctionComponent = () => {
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
const onSubmit = (e: Event) => {
if (!userId) {
console.error('[userparentalcontrol.onSubmit] missing user id');
return;
}
loading.show();
const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
}).catch(err => {

View File

@@ -1,17 +1,23 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import { getParameterByName } from '../../../../utils/url';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Page from '../../../../components/Page';
import loading from '../../../../components/loading/loading';
const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => {
if (!userId) {
console.error('[userpassword] missing user id');
return;
}
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {

View File

@@ -1,6 +1,7 @@
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import escapeHTML from 'escape-html';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../scripts/globalize';
@@ -13,7 +14,6 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import { getParameterByName } from '../../../../utils/url';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
@@ -41,6 +41,8 @@ function onSaveComplete() {
}
const UserEdit: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
@@ -57,7 +59,7 @@ const UserEdit: FunctionComponent = () => {
};
const getUser = () => {
const userId = getParameterByName('userId');
if (!userId) throw new Error('missing user id');
return window.ApiClient.getUser(userId);
};
@@ -144,7 +146,7 @@ const UserEdit: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[useredit] Unexpected null page reference');
return;
}
@@ -217,7 +219,7 @@ const UserEdit: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[useredit] Unexpected null page reference');
return;
}

View File

@@ -29,7 +29,7 @@ const AppLayout = () => {
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ display: 'flex' }}>
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'

View File

@@ -5,9 +5,17 @@ $mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
$drawer-width: 240px;
#reactRoot {
height: 100%;
}
// Fix main pages layout to work with drawer
.mainAnimatedPage {
position: relative;
@media all and (min-width: $mui-bp-md) {
left: $drawer-width;
}
}
// Hide some items from the user "settings" page that are in the drawer

View File

@@ -18,17 +18,30 @@ interface AppToolbarProps {
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
}
const PUBLIC_PATHS = [
'/addserver.html',
'/selectserver.html',
'/login.html',
'/forgotpassword.html',
'/forgotpasswordpin.html'
];
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
isDrawerAvailable,
isDrawerOpen,
onDrawerButtonClick
}) => {
const location = useLocation();
// The video osd does not show the standard toolbar
if (location.pathname === '/video') return null;
const isTabsAvailable = isTabPath(location.pathname);
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
return (
<AppToolbar
buttons={
buttons={!isPublicPath && (
<>
<SyncPlayButton />
<RemotePlayButton />
@@ -45,10 +58,11 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
</IconButton>
</Tooltip>
</>
}
)}
isDrawerAvailable={isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onDrawerButtonClick}
isUserMenuAvailable={!isPublicPath}
>
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
</AppToolbar>

View File

@@ -12,7 +12,7 @@ import { playbackManager } from 'components/playback/playbackmanager';
import React, { FC, useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import { enable, isEnabled, supported } from 'scripts/autocast';
import { enable, isEnabled } from 'scripts/autocast';
import globalize from 'scripts/globalize';
interface RemotePlayActiveMenuProps extends MenuProps {
@@ -43,11 +43,10 @@ const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
}, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]);
const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled());
const isAutoCastSupported = supported();
const toggleAutoCast = useCallback(() => {
enable(!isAutoCastEnabled);
setIsAutoCastEnabled(!isAutoCastEnabled);
}, [ isAutoCastEnabled, setIsAutoCastEnabled ]);
}, [ isAutoCastEnabled ]);
const remotePlayerName = playerInfo?.deviceName || playerInfo?.name;
@@ -117,20 +116,18 @@ const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
</MenuItem>
)}
{isAutoCastSupported && (
<MenuItem onClick={toggleAutoCast}>
{isAutoCastEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isAutoCastEnabled}>
{globalize.translate('EnableAutoCast')}
</ListItemText>
</MenuItem>
)}
<MenuItem onClick={toggleAutoCast}>
{isAutoCastEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isAutoCastEnabled}>
{globalize.translate('EnableAutoCast')}
</ListItemText>
</MenuItem>
{(isDisplayMirrorSupported || isAutoCastSupported) && <Divider />}
<Divider />
<MenuItem
component={Link}

View File

@@ -1,4 +1,5 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import Box from '@mui/material/Box';
import Guide from 'components/guide/guide';
import 'material-design-icons-iconfont';
import 'elements/emby-programcell/emby-programcell';
@@ -45,7 +46,20 @@ const GuideView: FC = () => {
};
}, [initGuide]);
return <div ref={tvGuideContainerRef} />;
return <Box
ref={tvGuideContainerRef}
className='absolutePageTabContent'
sx={{
display: 'flex !important',
width: 'auto',
paddingTop: '0',
paddingBottom: '0 !important',
top: {
xs: '6.9em !important',
lg: '4em !important'
}
}}
/>;
};
export default GuideView;

View File

@@ -26,7 +26,7 @@ type SortOptionsMapping = Record<string, SortOption[]>;
const movieOrFavoriteOptions = [
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionRandom', value: ItemSortBy.Random },
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
@@ -40,7 +40,7 @@ const sortOptionsMapping: SortOptionsMapping = {
[LibraryTab.Movies]: movieOrFavoriteOptions,
[LibraryTab.Trailers]: [
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
{ label: 'OptionParentalRating', value: ItemSortBy.OfficialRating },
@@ -51,7 +51,7 @@ const sortOptionsMapping: SortOptionsMapping = {
[LibraryTab.Series]: [
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionRandom', value: ItemSortBy.Random },
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionDateShowAdded', value: ItemSortBy.DateCreated },
{ label: 'OptionDateEpisodeAdded', value: ItemSortBy.DateLastContentAdded },
{ label: 'OptionDatePlayed', value: ItemSortBy.SeriesDatePlayed },
@@ -60,7 +60,7 @@ const sortOptionsMapping: SortOptionsMapping = {
],
[LibraryTab.Episodes]: [
{ label: 'Name', value: ItemSortBy.SeriesSortName },
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
@@ -73,7 +73,7 @@ const sortOptionsMapping: SortOptionsMapping = {
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionRandom', value: ItemSortBy.Random },
{ label: 'AlbumArtist', value: ItemSortBy.AlbumArtist },
{ label: 'OptionImdbRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCommunityRating', value: ItemSortBy.CommunityRating },
{ label: 'OptionCriticRating', value: ItemSortBy.CriticRating },
{ label: 'OptionReleaseDate', value: ItemSortBy.ProductionYear },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }

View File

@@ -49,16 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'user/subtitles/index',
view: 'user/subtitles/index.html'
}
}, {
path: 'video',
pageProps: {
controller: 'playback/video/index',
view: 'playback/video/index.html',
type: 'video-osd',
isFullscreen: true,
isNowPlayingBarEnabled: false,
isThemeMediaSupported: true
}
}, {
path: 'queue',
pageProps: {

View File

@@ -1,14 +1,19 @@
import React from 'react';
import { RouteObject, redirect } from 'react-router-dom';
import { Navigate, RouteObject } from 'react-router-dom';
import { REDIRECTS } from 'apps/dashboard/routes/_redirects';
import ConnectionRequired from 'components/ConnectionRequired';
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import AppLayout from '../AppLayout';
import { ASYNC_USER_ROUTES } from './asyncRoutes';
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
import VideoPage from './video';
import loadable from '@loadable/component';
import BangRedirect from 'components/router/BangRedirect';
const AppLayout = loadable(() => import('../AppLayout'));
export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
{
@@ -20,16 +25,27 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
element: <ConnectionRequired isUserRequired />,
children: [
...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute),
// The video page is special since it combines new controls with the legacy view
{
path: 'video',
element: <VideoPage />
}
]
},
/* Public routes */
{ index: true, loader: () => redirect('/home.html') },
{ index: true, element: <Navigate replace to='/home.html' /> },
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
]
},
{
path: '!/*',
Component: BangRedirect
},
/* Redirects for old paths */
...REDIRECTS.map(toRedirectRoute)
];

View File

@@ -12,10 +12,11 @@ import React, { Fragment } from 'react';
import { appHost } from 'components/apphost';
import { useApi } from 'hooks/useApi';
import { useThemes } from 'hooks/useThemes';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
import { useScreensavers } from './hooks/useScreensavers';
import { useServerThemes } from './hooks/useServerThemes';
interface DisplayPreferencesProps {
onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void;
@@ -25,7 +26,7 @@ interface DisplayPreferencesProps {
export function DisplayPreferences({ onChange, values }: Readonly<DisplayPreferencesProps>) {
const { user } = useApi();
const { screensavers } = useScreensavers();
const { themes } = useServerThemes();
const { themes } = useThemes();
return (
<Stack spacing={3}>

View File

@@ -1,32 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import themeManager from 'scripts/themeManager';
import { Theme } from 'types/webConfig';
export function useServerThemes() {
const [themes, setThemes] = useState<Theme[]>();
useEffect(() => {
async function getServerThemes() {
const loadedThemes = await themeManager.getThemes();
setThemes(loadedThemes ?? []);
}
if (!themes) {
void getServerThemes();
}
// We've intentionally left the dependency array here to ensure that the effect happens only once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const defaultTheme = useMemo(() => {
if (!themes) return null;
return themes.find((theme) => theme.default);
}, [themes]);
return {
themes: themes ?? [],
defaultTheme
};
}

View File

@@ -0,0 +1,72 @@
import Box from '@mui/material/Box/Box';
import Fade from '@mui/material/Fade/Fade';
import React, { useRef, type FC, useEffect, useState } from 'react';
import RemotePlayButton from 'apps/experimental/components/AppToolbar/RemotePlayButton';
import SyncPlayButton from 'apps/experimental/components/AppToolbar/SyncPlayButton';
import AppToolbar from 'components/toolbar/AppToolbar';
import ViewManagerPage from 'components/viewManager/ViewManagerPage';
import { EventType } from 'types/eventType';
import Events, { type Event } from 'utils/events';
/**
* Video player page component that renders mui controls for the top controls and the legacy view for everything else.
*/
const VideoPage: FC = () => {
const documentRef = useRef<Document>(document);
const [ isVisible, setIsVisible ] = useState(true);
const onShowVideoOsd = (_e: Event, isShowing: boolean) => {
setIsVisible(isShowing);
};
useEffect(() => {
const doc = documentRef.current;
if (doc) Events.on(doc, EventType.SHOW_VIDEO_OSD, onShowVideoOsd);
return () => {
if (doc) Events.off(doc, EventType.SHOW_VIDEO_OSD, onShowVideoOsd);
};
}, []);
return (
<>
<Fade
in={isVisible}
easing='fade-out'
>
<Box sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
color: 'white'
}}>
<AppToolbar
isDrawerAvailable={false}
isDrawerOpen={false}
isUserMenuAvailable={false}
buttons={
<>
<SyncPlayButton />
<RemotePlayButton />
</>
}
/>
</Box>
</Fade>
<ViewManagerPage
controller='playback/video/index'
view='playback/video/index.html'
type='video-osd'
isFullscreen
isNowPlayingBarEnabled={false}
isThemeMediaSupported
/>
</>
);
};
export default VideoPage;

View File

@@ -1,42 +0,0 @@
import { History } from '@remix-run/router';
import React from 'react';
import { Outlet, RouterProvider, createHashRouter, useLocation } from 'react-router-dom';
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
import { STABLE_APP_ROUTES } from './routes/routes';
import Backdrop from 'components/Backdrop';
import AppHeader from 'components/AppHeader';
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
const router = createHashRouter([{
element: <StableAppLayout />,
children: [
...STABLE_APP_ROUTES,
...DASHBOARD_APP_ROUTES
]
}]);
export default function StableAppRouter({ history }: Readonly<{ history: History }>) {
useLegacyRouterSync({ router, history });
return <RouterProvider router={router} />;
}
/**
* Layout component that renders legacy components required on all pages.
* NOTE: The app will crash if these get removed from the DOM.
*/
function StableAppLayout() {
const location = useLocation();
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
.some(path => location.pathname.startsWith(`/${path}`));
return (
<>
<Backdrop />
<AppHeader isHidden={isNewLayoutPath} />
<Outlet />
</>
);
}

View File

@@ -1,4 +1,4 @@
import { RouteObject, redirect } from 'react-router-dom';
import { Navigate, RouteObject } from 'react-router-dom';
import React from 'react';
import ConnectionRequired from 'components/ConnectionRequired';
@@ -11,6 +11,7 @@ import AppLayout from '../AppLayout';
import { REDIRECTS } from './_redirects';
import { ASYNC_USER_ROUTES } from './asyncRoutes';
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
import BangRedirect from 'components/router/BangRedirect';
export const STABLE_APP_ROUTES: RouteObject[] = [
{
@@ -27,11 +28,16 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
},
/* Public routes */
{ index: true, loader: () => redirect('/home.html') },
{ index: true, element: <Navigate replace to='/home.html' /> },
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
]
},
{
path: '!/*',
Component: BangRedirect
},
/* Redirects for old paths */
...REDIRECTS.map(toRedirectRoute)
];

View File

@@ -1,6 +1,7 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../scripts/globalize';
@@ -11,11 +12,11 @@ import ButtonElement from '../../../../elements/ButtonElement';
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import { getParameterByName } from '../../../../utils/url';
import Page from '../../../../components/Page';
const UserProfile: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const element = useRef<HTMLDivElement>(null);
@@ -24,7 +25,12 @@ const UserProfile: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userprofile] Unexpected null page reference');
return;
}
if (!userId) {
console.error('[userprofile] missing user id');
return;
}
@@ -72,7 +78,7 @@ const UserProfile: FunctionComponent = () => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[userprofile] Unexpected null page reference');
return;
}
@@ -110,6 +116,11 @@ const UserProfile: FunctionComponent = () => {
reader.onerror = onFileReaderError;
reader.onabort = onFileReaderAbort;
reader.onload = () => {
if (!userId) {
console.error('[userprofile] missing user id');
return;
}
userImage.style.backgroundImage = 'url(' + reader.result + ')';
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
loading.hide();
@@ -123,6 +134,11 @@ const UserProfile: FunctionComponent = () => {
};
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
if (!userId) {
console.error('[userprofile] missing user id');
return;
}
confirm(
globalize.translate('DeleteImageConfirmation'),
globalize.translate('DeleteImage')

View File

@@ -83,7 +83,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
if (firstConnection.State === ConnectionState.ServerSignIn) {
// Verify the wizard is complete
try {
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`, { cache: 'no-cache' });
if (!infoResponse.ok) {
throw new Error('Public system info request failed');
}

View File

@@ -1,3 +1,4 @@
import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions';
import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient';
import { appHost } from './apphost';
@@ -33,8 +34,13 @@ class ServerConnections extends ConnectionManager {
super(...arguments);
this.localApiClient = null;
// Set the apiclient minimum version to match the SDK
this._minServerVersion = MINIMUM_VERSION;
Events.on(this, 'localusersignedout', (_e, logoutInfo) => {
setUserInfo(null, null);
// Ensure the updated credentials are persisted to storage
credentialProvider.credentials(credentialProvider.credentials());
if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') {
window.NativeShell.onLocalUserSignedOut(logoutInfo);
@@ -59,7 +65,7 @@ class ServerConnections extends ConnectionManager {
);
apiClient.enableAutomaticNetworking = false;
apiClient.manualAddressOnly = false;
apiClient.manualAddressOnly = true;
this.addApiClient(apiClient);
@@ -128,12 +134,12 @@ class ServerConnections extends ConnectionManager {
}
}
const credentials = new Credentials();
const credentialProvider = new Credentials();
const capabilities = Dashboard.capabilities(appHost);
export default new ServerConnections(
credentials,
credentialProvider,
appHost.appName(),
appHost.appVersion(),
appHost.deviceName(),

View File

@@ -242,7 +242,7 @@ const supportedFeatures = function () {
features.push('fullscreenchange');
}
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile) {
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile || browser.ipad) {
features.push('physicalvolumecontrol');
}

View File

@@ -89,7 +89,7 @@ export function setCardData(items, options) {
options.coverImage = true;
} else if (primaryImageAspectRatio >= 1.33) {
options.shape = getBackdropShape(requestedShape === 'autooverflow');
} else if (primaryImageAspectRatio > 0.71) {
} else if (primaryImageAspectRatio > 0.8) {
options.shape = getSquareShape(requestedShape === 'autooverflow');
} else {
options.shape = getPortraitShape(requestedShape === 'autooverflow');
@@ -1139,7 +1139,9 @@ function getHoverMenuHtml(item, action) {
let html = '';
html += '<div class="cardOverlayContainer itemAction" data-action="' + action + '">';
const url = appRouter.getRouteUrl(item);
const url = appRouter.getRouteUrl(item, {
serverId: item.ServerId || ServerConnections.currentApiClient().serverId()
});
html += '<a href="' + url + '" class="cardImageContainer"></a>';
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';

View File

@@ -454,15 +454,15 @@ describe('resolveMixedShapeByAspectRatio', () => {
expect(resolveMixedShapeByAspectRatio(1.34)).toEqual('mixedBackdrop');
});
test('primary aspect ratio is > 0.71', () => {
expect(resolveMixedShapeByAspectRatio(0.72)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(0.73)).toEqual('mixedSquare');
test('primary aspect ratio is > 0.8', () => {
expect(resolveMixedShapeByAspectRatio(0.81)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(0.82)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(1.32)).toEqual('mixedSquare');
});
test('primary aspect ratio is <= 0.71', () => {
expect(resolveMixedShapeByAspectRatio(0.71)).toEqual('mixedPortrait');
expect(resolveMixedShapeByAspectRatio(0.70)).toEqual('mixedPortrait');
test('primary aspect ratio is <= 0.8', () => {
expect(resolveMixedShapeByAspectRatio(0.8)).toEqual('mixedPortrait');
expect(resolveMixedShapeByAspectRatio(0.79)).toEqual('mixedPortrait');
expect(resolveMixedShapeByAspectRatio(0.01)).toEqual('mixedPortrait');
});

View File

@@ -60,7 +60,7 @@ export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number |
if (primaryImageAspectRatio >= 1.33) {
return CardShape.MixedBackdrop;
} else if (primaryImageAspectRatio > 0.71) {
} else if (primaryImageAspectRatio > 0.8) {
return CardShape.MixedSquare;
} else {
return CardShape.MixedPortrait;

View File

@@ -21,7 +21,7 @@ export function buildCardImage(
shape = CardShape.Banner;
} else if (item.PrimaryImageAspectRatio >= 1.33) {
shape = CardShape.Backdrop;
} else if (item.PrimaryImageAspectRatio > 0.71) {
} else if (item.PrimaryImageAspectRatio > 0.8) {
shape = CardShape.Square;
} else {
shape = CardShape.Portrait;

View File

@@ -9,7 +9,7 @@ import ButtonElement from '../../../elements/ButtonElement';
import InputElement from '../../../elements/InputElement';
type IProps = {
userId: string;
userId: string | null;
};
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
@@ -19,7 +19,12 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[UserPasswordForm] Unexpected null page reference');
return;
}
if (!userId) {
console.error('[UserPasswordForm] missing user id');
return;
}
@@ -58,7 +63,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
console.error('[UserPasswordForm] Unexpected null page reference');
return;
}
@@ -79,6 +84,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
};
const savePassword = () => {
if (!userId) {
console.error('[UserPasswordForm.savePassword] missing user id');
return;
}
let currentPassword = (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value;
const newPassword = (page.querySelector('#txtNewPassword') as HTMLInputElement).value;
@@ -105,6 +115,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
};
const resetPassword = () => {
if (!userId) {
console.error('[UserPasswordForm.resetPassword] missing user id');
return;
}
const msg = globalize.translate('PasswordResetConfirmation');
confirm(msg, globalize.translate('ResetPassword')).then(function () {
loading.show();

View File

@@ -1,4 +1,3 @@
import { history } from '../router/appRouter';
import focusManager from '../focusManager';
import browser from '../../scripts/browser';
import layoutManager from '../layoutManager';
@@ -6,6 +5,8 @@ import inputManager from '../../scripts/inputManager';
import { toBoolean } from '../../utils/string.ts';
import dom from '../../scripts/dom';
import { history } from 'RootAppRouter';
import './dialoghelper.scss';
import '../../styles/scrollstyles.scss';

View File

@@ -7,6 +7,7 @@ import '../../elements/emby-collapse/emby-collapse';
import './style.scss';
import ServerConnections from '../ServerConnections';
import template from './filterdialog.template.html';
import { stopMultiSelect } from '../../components/multiSelect/multiSelect';
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
const elem = context.querySelector(selector);
@@ -104,6 +105,7 @@ function updateFilterControls(context, options) {
* @param instance {FilterDialog} An instance of FilterDialog
*/
function triggerChange(instance) {
stopMultiSelect();
Events.trigger(instance, 'filterchange');
}

View File

@@ -169,12 +169,30 @@ export function getCommands(options) {
});
}
if (item.Type === 'Season' || item.Type == 'Series') {
commands.push({
name: globalize.translate('DownloadAll'),
id: 'downloadall',
icon: 'file_download'
});
if (appHost.supports('filedownload')) {
// CanDownload should probably be updated to return true for these items?
if (user.Policy.EnableContentDownloading && (item.Type === 'Season' || item.Type == 'Series')) {
commands.push({
name: globalize.translate('DownloadAll'),
id: 'downloadall',
icon: 'file_download'
});
}
// Books are promoted to major download Button and therefor excluded in the context menu
if (item.CanDownload && item.Type !== 'Book') {
commands.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
});
commands.push({
name: globalize.translate('CopyStreamURL'),
id: 'copy-stream',
icon: 'content_copy'
});
}
}
if (item.CanDelete && options.deleteItem !== false) {
@@ -193,21 +211,6 @@ export function getCommands(options) {
}
}
// Books are promoted to major download Button and therefor excluded in the context menu
if ((item.CanDownload && appHost.supports('filedownload')) && item.Type !== 'Book') {
commands.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
});
commands.push({
name: globalize.translate('CopyStreamURL'),
id: 'copy-stream',
icon: 'content_copy'
});
}
if (commands.length) {
commands.push({
divider: true

View File

@@ -4,7 +4,9 @@
* @module components/libraryoptionseditor/libraryoptionseditor
*/
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import escapeHtml from 'escape-html';
import globalize from '../../scripts/globalize';
import dom from '../../scripts/dom';
import '../../elements/emby-checkbox/emby-checkbox';
@@ -383,6 +385,13 @@ export async function embed(parent, contentType, libraryOptions) {
});
}
const CHAPTER_CONTENT_TYPES = [
CollectionType.Homevideos,
CollectionType.Movies,
CollectionType.Musicvideos,
CollectionType.Tvshows
];
export function setContentType(parent, contentType) {
if (contentType === 'homevideos' || contentType === 'photos') {
parent.querySelector('.chkEnablePhotosContainer').classList.remove('hide');
@@ -390,13 +399,9 @@ export function setContentType(parent, contentType) {
parent.querySelector('.chkEnablePhotosContainer').classList.add('hide');
}
if (contentType !== 'tvshows' && contentType !== 'movies' && contentType !== 'homevideos' && contentType !== 'musicvideos' && contentType !== 'mixed') {
parent.querySelector('.trickplaySettingsSection').classList.add('hide');
parent.querySelector('.chapterSettingsSection').classList.add('hide');
} else {
parent.querySelector('.trickplaySettingsSection').classList.remove('hide');
parent.querySelector('.chapterSettingsSection').classList.remove('hide');
}
const hasChapterOptions = !contentType /* Mixed */ || CHAPTER_CONTENT_TYPES.includes(contentType);
parent.querySelector('.trickplaySettingsSection').classList.toggle('hide', !hasChapterOptions);
parent.querySelector('.chapterSettingsSection').classList.toggle('hide', !hasChapterOptions);
if (contentType === 'tvshows') {
parent.querySelector('.chkAutomaticallyGroupSeriesContainer').classList.remove('hide');
@@ -623,8 +628,8 @@ let currentLibraryOptions;
let currentAvailableOptions;
export default {
embed: embed,
setContentType: setContentType,
getLibraryOptions: getLibraryOptions,
setLibraryOptions: setLibraryOptions
embed,
setContentType,
getLibraryOptions,
setLibraryOptions
};

View File

@@ -17,6 +17,8 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton';
import '../../elements/emby-playstatebutton/emby-playstatebutton';
import ServerConnections from '../ServerConnections';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
import markdownIt from 'markdown-it';
import DOMPurify from 'dompurify';
function getIndex(item, options) {
if (options.index === 'disc') {
@@ -415,8 +417,9 @@ export function getListViewHtml(options) {
}
if (enableOverview && item.Overview) {
const overview = DOMPurify.sanitize(markdownIt({ html: true }).render(item.Overview || ''));
html += '<div class="secondary listItem-overview listItemBodyText">';
html += '<bdi>' + item.Overview + '</bdi>';
html += '<bdi>' + overview + '</bdi>';
html += '</div>';
}

View File

@@ -25,6 +25,8 @@ import alert from '../alert';
import template from './mediaLibraryCreator.template.html';
function onAddLibrary(e) {
e.preventDefault();
if (isCreating) {
return false;
}
@@ -61,7 +63,6 @@ function onAddLibrary(e) {
isCreating = false;
loading.hide();
});
e.preventDefault();
}
function getCollectionTypeOptionsHtml(collectionTypeOptions) {

View File

@@ -99,9 +99,7 @@ function showSelection(item, isChecked) {
parent.appendChild(itemSelectionPanel);
let cssClass = 'chkItemSelect';
if (isChecked && !browser.firefox) {
// In firefox, the initial tap hold doesnt' get treated as a click
// In other browsers it does, so we need to make sure that initial click is ignored
if (isChecked) {
cssClass += ' checkedInitial';
}
const checkedAttribute = isChecked ? ' checked' : '';
@@ -573,3 +571,7 @@ export default function (options) {
export const startMultiSelect = (card) => {
showSelections(card);
};
export const stopMultiSelect = () => {
hideSelections();
};

View File

@@ -33,6 +33,10 @@ function enableLocalPlaylistManagement(player) {
return player.isLocalPlayer;
}
function supportsPhysicalVolumeControl(player) {
return player.isLocalPlayer && appHost.supports('physicalvolumecontrol');
}
function bindToFullscreenChange(player) {
if (Screenfull.isEnabled) {
Screenfull.on('change', function () {
@@ -1157,7 +1161,7 @@ class PlaybackManager {
self.setVolume = function (val, player) {
player = player || self._currentPlayer;
if (player) {
if (player && !supportsPhysicalVolumeControl(player)) {
player.setVolume(val);
}
};
@@ -1165,15 +1169,17 @@ class PlaybackManager {
self.getVolume = function (player) {
player = player || self._currentPlayer;
if (player) {
if (player && !supportsPhysicalVolumeControl(player)) {
return player.getVolume();
}
return 1;
};
self.volumeUp = function (player) {
player = player || self._currentPlayer;
if (player) {
if (player && !supportsPhysicalVolumeControl(player)) {
player.volumeUp();
}
};
@@ -1181,7 +1187,7 @@ class PlaybackManager {
self.volumeDown = function (player) {
player = player || self._currentPlayer;
if (player) {
if (player && !supportsPhysicalVolumeControl(player)) {
player.volumeDown();
}
};
@@ -1856,6 +1862,15 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Audio'
}, queryOptions));
case 'Genre':
return getItemsForPlayback(serverId, mergePlaybackQueries({
GenreIds: firstItem.Id,
ParentId: firstItem.ParentId,
Filters: 'IsNotFolder',
Recursive: true,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Video'
}, queryOptions));
case 'Series':
case 'Season':
return getSeriesOrSeasonPlaybackPromise(firstItem, options, items);
@@ -2284,33 +2299,18 @@ class PlaybackManager {
// TODO: This should be the media type requested, not the original media type
const mediaType = item.MediaType;
if (playOptions.fullscreen) {
loading.show();
}
return runInterceptors(item, playOptions)
.then(() => {
if (playOptions.fullscreen) {
loading.show();
}
if (!isServerItem(item) || itemHelper.isLocalItem(item)) {
return Promise.reject('skip bitrate detection');
}
return apiClient.getEndpointInfo().then((endpointInfo) => {
if ((mediaType === 'Video' || mediaType === 'Audio') && appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType)) {
return apiClient.detectBitrate().then((bitrate) => {
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
return bitrate;
});
}
return Promise.reject('skip bitrate detection');
});
})
.catch(() => getSavedMaxStreamingBitrate(apiClient, mediaType))
.then((bitrate) => {
return playAfterBitrateDetect(bitrate, item, playOptions, onPlaybackStartedFn, prevSource);
})
.catch(onInterceptorRejection)
.finally(() => {
.then(() => detectBitrate(apiClient, item, mediaType))
.then((bitrate) => {
return playAfterBitrateDetect(bitrate, item, playOptions, onPlaybackStartedFn, prevSource)
.catch(onPlaybackRejection);
})
.catch(() => {
if (playOptions.fullscreen) {
loading.hide();
}
@@ -2328,7 +2328,13 @@ class PlaybackManager {
Events.trigger(self, 'playbackcancelled');
}
function onInterceptorRejection(e) {
function onInterceptorRejection() {
cancelPlayback();
return Promise.reject();
}
function onPlaybackRejection(e) {
cancelPlayback();
let displayErrorCode = 'ErrorDefault';
@@ -2363,8 +2369,6 @@ class PlaybackManager {
return;
}
loading.hide();
const options = Object.assign({}, playOptions);
options.mediaType = item.MediaType;
@@ -2502,6 +2506,29 @@ class PlaybackManager {
}
}
function detectBitrate(apiClient, item, mediaType) {
// FIXME: This is gnarly, but don't want to change too much here in a bugfix
return Promise.resolve()
.then(() => {
if (!isServerItem(item) || itemHelper.isLocalItem(item)) {
return Promise.reject('skip bitrate detection');
}
return apiClient.getEndpointInfo()
.then((endpointInfo) => {
if ((mediaType === 'Video' || mediaType === 'Audio') && appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType)) {
return apiClient.detectBitrate().then((bitrate) => {
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
return bitrate;
});
}
return Promise.reject('skip bitrate detection');
});
})
.catch(() => getSavedMaxStreamingBitrate(apiClient, mediaType));
}
function playAfterBitrateDetect(maxBitrate, item, playOptions, onPlaybackStartedFn, prevSource) {
const startPosition = playOptions.startPositionTicks;
@@ -3282,18 +3309,21 @@ class PlaybackManager {
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
if (streamInfo?.url) {
const isAlreadyFallbacking = streamInfo.url.toLowerCase().includes('transcodereasons');
const currentlyPreventsVideoStreamCopy = streamInfo.url.toLowerCase().indexOf('allowvideostreamcopy=false') !== -1;
const currentlyPreventsAudioStreamCopy = streamInfo.url.toLowerCase().indexOf('allowaudiostreamcopy=false') !== -1;
// Auto switch to transcoding
if (enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy)) {
const startTime = getCurrentTicks(player) || streamInfo.playerStartPositionTicks;
const isRemoteSource = streamInfo.item.LocationType === 'Remote';
// force transcoding and only allow remuxing for remote source like liveTV, but only for initial trial
const tryVideoStreamCopy = isRemoteSource && !isAlreadyFallbacking;
changeStream(player, startTime, {
// force transcoding
EnableDirectPlay: false,
EnableDirectStream: false,
AllowVideoStreamCopy: false,
EnableDirectStream: tryVideoStreamCopy,
AllowVideoStreamCopy: tryVideoStreamCopy,
AllowAudioStreamCopy: currentlyPreventsAudioStreamCopy || currentlyPreventsVideoStreamCopy ? false : null
});

View File

@@ -6,7 +6,7 @@ import { pluginManager } from '../pluginManager';
import { appRouter } from '../router/appRouter';
import globalize from '../../scripts/globalize';
import { appHost } from '../apphost';
import { enable, isEnabled, supported } from '../../scripts/autocast';
import { enable, isEnabled } from '../../scripts/autocast';
import '../../elements/emby-checkbox/emby-checkbox';
import '../../elements/emby-button/emby-button';
import dialog from '../dialog/dialog';
@@ -200,13 +200,11 @@ function showActivePlayerMenuInternal(playerInfo) {
html += '</div>';
if (supported()) {
html += '<div><label class="checkboxContainer">';
const checkedHtmlAC = isEnabled() ? ' checked' : '';
html += '<input type="checkbox" is="emby-checkbox" class="chkAutoCast"' + checkedHtmlAC + '/>';
html += '<span>' + globalize.translate('EnableAutoCast') + '</span>';
html += '</label></div>';
}
html += '<div><label class="checkboxContainer">';
const checkedHtmlAC = isEnabled() ? ' checked' : '';
html += '<input type="checkbox" is="emby-checkbox" class="chkAutoCast"' + checkedHtmlAC + '/>';
html += '<span>' + globalize.translate('EnableAutoCast') + '</span>';
html += '</label></div>';
html += '<div style="margin-top:1em;display:flex;justify-content: flex-end;">';

View File

@@ -0,0 +1,34 @@
import React, { useMemo } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
const BangRedirect = () => {
const location = useLocation();
const to = useMemo(() => {
const _to = {
search: location.search,
hash: location.hash
};
if (location.pathname.startsWith('/!/')) {
return { ..._to, pathname: location.pathname.substring(2) };
} else if (location.pathname.startsWith('/!')) {
return { ..._to, pathname: location.pathname.replace(/^\/!/, '/') };
} else if (location.pathname.startsWith('!')) {
return { ..._to, pathname: location.pathname.substring(1) };
}
}, [ location ]);
if (!to) return null;
console.warn('[BangRedirect] You are using a deprecated URL format. This will stop working in a future Jellyfin update.');
return (
<Navigate
replace
to={to}
/>
);
};
export default BangRedirect;

View File

@@ -1,5 +1,5 @@
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { Action, createHashHistory } from 'history';
import { Action } from 'history';
import { appHost } from '../apphost';
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
@@ -15,8 +15,7 @@ import { queryClient } from 'utils/query/queryClient';
import { getItemQuery } from 'hooks/useItem';
import { toApi } from 'utils/jellyfin-apiclient/compat';
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts';
export const history = createHashHistory();
import { history } from 'RootAppRouter';
/**
* Page types of "no return" (when "Go back" should behave differently, probably quitting the application).
@@ -388,7 +387,7 @@ class AppRouter {
if (firstResult) {
if (firstResult.State === ConnectionState.ServerSignIn) {
const url = firstResult.ApiClient.serverAddress() + '/System/Info/Public';
fetch(url).then(response => {
fetch(url, { cache: 'no-cache' }).then(response => {
if (!response.ok) return Promise.reject('fetch failed');
return response.json();
}).then(data => {

View File

@@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Router, RouterState } from '@remix-run/router';
import type { History, Listener, To } from 'history';
import Events, { type Event } from 'utils/events';
const HISTORY_UPDATE_EVENT = 'HISTORY_UPDATE';
export class RouterHistory implements History {
_router: Router;
createHref: (arg: any) => string;
constructor(router: Router) {
this._router = router;
this._router.subscribe(state => {
console.debug('[RouterHistory] history update', state);
Events.trigger(document, HISTORY_UPDATE_EVENT, [ state ]);
});
this.createHref = router.createHref;
}
get action() {
return this._router.state.historyAction;
}
get location() {
return this._router.state.location;
}
back() {
void this._router.navigate(-1);
}
forward() {
void this._router.navigate(1);
}
go(delta: number) {
void this._router.navigate(delta);
}
push(to: To, state?: any) {
void this._router.navigate(to, { state });
}
replace(to: To, state?: any): void {
void this._router.navigate(to, { state, replace: true });
}
block() {
// NOTE: We don't seem to use this functionality, so leaving it unimplemented.
throw new Error('`history.block()` is not implemented');
return () => undefined;
}
listen(listener: Listener) {
const compatListener = (_e: Event, state: RouterState) => {
return listener({ action: state.historyAction, location: state.location });
};
Events.on(document, HISTORY_UPDATE_EVENT, compatListener);
return () => Events.off(document, HISTORY_UPDATE_EVENT, compatListener);
}
}
export const createRouterHistory = (router: Router): History => {
return new RouterHistory(router);
};
/* eslint-enable @typescript-eslint/no-explicit-any */

View File

@@ -1,4 +1,4 @@
import React, { type ChangeEvent, type FC, useCallback } from 'react';
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
import Input from 'elements/emby-input/Input';
@@ -20,15 +20,18 @@ const SearchFields: FC<SearchFieldsProps> = ({
onSearch = () => { /* no-op */ },
query
}: SearchFieldsProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const onAlphaPicked = useCallback((e: Event) => {
const value = (e as CustomEvent).detail.value;
const inputValue = inputRef.current?.value || '';
if (value === 'backspace') {
onSearch(query.length ? query.substring(0, query.length - 1) : '');
onSearch(inputValue.length ? inputValue.substring(0, inputValue.length - 1) : '');
} else {
onSearch(query + value);
onSearch(inputValue + value);
}
}, [ onSearch, query ]);
}, [onSearch]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
onSearch(e.target.value);
@@ -43,6 +46,7 @@ const SearchFields: FC<SearchFieldsProps> = ({
style={{ marginBottom: 0 }}
>
<Input
ref={inputRef}
id='searchTextInput'
className='searchfields-txtSearch'
type='text'

View File

@@ -1,4 +1,5 @@
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import type { ApiClient } from 'jellyfin-apiclient';
import classNames from 'classnames';
import React, { type FC, useCallback, useEffect, useState } from 'react';
@@ -77,16 +78,32 @@ const SearchResults: FC<SearchResultsProps> = ({ serverId = window.ApiClient.ser
).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getItems(
apiClient.getCurrentUserId(),
{
...getDefaultParameters(),
IncludeMedia: true,
...params
const fetchItems = useCallback(async (apiClient?: ApiClient, params = {}) => {
if (!apiClient) {
console.error('[SearchResults] no apiClient; unable to fetch items');
return {
Items: []
};
}
const options = {
...getDefaultParameters(),
IncludeMedia: true,
...params
};
if (params.IncludeItemTypes === BaseItemKind.Episode) {
const user = await apiClient.getCurrentUser();
if (!user?.Configuration?.DisplayMissingEpisodes) {
options.IsMissing = false;
}
).then(ensureNonNullItems)
), [getDefaultParameters]);
}
return apiClient.getItems(
apiClient.getCurrentUserId(),
options
).then(ensureNonNullItems);
}, [getDefaultParameters]);
const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getPeople(

View File

@@ -732,9 +732,8 @@ export default function (options) {
obj.x = eventX;
obj.y = eventY;
showOsd();
}
showOsd();
}
/**

View File

@@ -5,7 +5,6 @@ import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import React, { FC, ReactNode } from 'react';
import { useLocation } from 'react-router-dom';
import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
@@ -17,7 +16,8 @@ interface AppToolbarProps {
buttons?: ReactNode
isDrawerAvailable: boolean
isDrawerOpen: boolean
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
onDrawerButtonClick?: (event: React.MouseEvent<HTMLElement>) => void,
isUserMenuAvailable?: boolean
}
const onBackButtonClick = () => {
@@ -32,17 +32,14 @@ const AppToolbar: FC<AppToolbarProps> = ({
children,
isDrawerAvailable,
isDrawerOpen,
onDrawerButtonClick
onDrawerButtonClick = () => { /* no-op */ },
isUserMenuAvailable = true
}) => {
const { user } = useApi();
const isUserLoggedIn = Boolean(user);
const currentLocation = useLocation();
const isBackButtonAvailable = appRouter.canGoBack();
// Handles a specific case to hide the user menu on the select server page while authenticated
const isUserMenuAvailable = currentLocation.pathname !== '/selectserver.html';
return (
<Toolbar
variant='dense'
@@ -84,16 +81,14 @@ const AppToolbar: FC<AppToolbarProps> = ({
{children}
{isUserLoggedIn && isUserMenuAvailable && (
<>
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
{buttons}
</Box>
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
{buttons}
</Box>
<Box sx={{ flexGrow: 0 }}>
<UserMenuButton />
</Box>
</>
{isUserLoggedIn && isUserMenuAvailable && (
<Box sx={{ flexGrow: 0 }}>
<UserMenuButton />
</Box>
)}
</Toolbar>
);

View File

@@ -1,5 +1,6 @@
import { Action } from 'history';
import { FunctionComponent, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigationType } from 'react-router-dom';
import globalize from '../../scripts/globalize';
import type { RestoreViewFailResponse } from '../../types/viewManager';
@@ -15,6 +16,34 @@ export interface ViewManagerPageProps {
transition?: string
}
interface ViewOptions {
url: string
type?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: any
autoFocus: boolean
fullscreen?: boolean
transition?: string
options: {
supportsThemeMedia?: boolean
enableMediaControl?: boolean
}
}
const loadView = async (controller: string, view: string, viewOptions: ViewOptions) => {
const [ controllerFactory, viewHtml ] = await Promise.all([
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
.then(html => globalize.translateHtml(html))
]);
viewManager.loadView({
...viewOptions,
controllerFactory,
view: viewHtml
});
};
/**
* Page component that renders legacy views via the ViewManager.
* NOTE: Any new pages should use the generic Page component instead.
@@ -29,6 +58,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
transition
}) => {
const location = useLocation();
const navigationType = useNavigationType();
useEffect(() => {
const loadPage = () => {
@@ -45,27 +75,24 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
}
};
viewManager.tryRestoreView(viewOptions)
if (navigationType !== Action.Pop) {
console.debug('[ViewManagerPage] loading view [%s]', view);
return loadView(controller, view, viewOptions);
}
console.debug('[ViewManagerPage] restoring view [%s]', view);
return viewManager.tryRestoreView(viewOptions)
.catch(async (result?: RestoreViewFailResponse) => {
if (!result?.cancelled) {
const [ controllerFactory, viewHtml ] = await Promise.all([
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
.then(html => globalize.translateHtml(html))
]);
viewManager.loadView({
...viewOptions,
controllerFactory,
view: viewHtml
});
console.debug('[ViewManagerPage] restore failed; loading view [%s]', view);
return loadView(controller, view, viewOptions);
}
});
};
loadPage();
},
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
// location.state and navigationType are NOT included as dependencies here since dialogs will update state while the current view stays the same
// eslint-disable-next-line react-hooks/exhaustive-deps
[
controller,

View File

@@ -1,4 +1,4 @@
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent">
<div id="apiKeysPage" data-role="page" class="page type-interior advancedConfigurationPage fullWidthContent" data-title="${HeaderApiKeys}">
<div>
<div class="content-primary">
<div class="detailSectionHeader">

View File

@@ -1,4 +1,4 @@
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent">
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent" data-title="${TabDashboard}">
<div class="content-primary">
<div class="dashboardSections" style="padding-top:.5em;">
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">

View File

@@ -319,7 +319,7 @@ function renderActiveConnections(view, sessions) {
html += '<div class="sessionCardButtons flex align-items-center justify-content-center">';
let btnCssClass = session.ServerId && session.NowPlayingItem && session.SupportsRemoteControl ? '' : ' hide';
const playIcon = session.PlayState.IsPaused ? 'pause' : 'play_arrow';
const playIcon = session.PlayState.IsPaused ? 'play_arrow' : 'pause';
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionPlayPause paper-icon-button-light ' + btnCssClass + '"><span class="material-icons ' + playIcon + '" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionStop paper-icon-button-light ' + btnCssClass + '"><span class="material-icons stop" aria-hidden="true"></span></button>';

View File

@@ -1,4 +1,4 @@
<div id="devicesPage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage">
<div id="devicesPage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage" data-title="${HeaderDevices}">
<div>
<div class="content-primary">
<div class="verticalSection verticalSection">

View File

@@ -1,4 +1,4 @@
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TitlePlayback}">
<div>
<div class="content-primary">
<form class="encodingSettingsForm">

View File

@@ -2,7 +2,6 @@ import 'jquery';
import loading from '../../components/loading/loading';
import globalize from '../../scripts/globalize';
import dom from '../../scripts/dom';
import libraryMenu from '../../scripts/libraryMenu';
import Dashboard from '../../utils/dashboard';
import alert from '../../components/alert';
@@ -167,22 +166,6 @@ function setDecodingCodecsVisible(context, value) {
}
}
function getTabs() {
return [{
href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding')
}, {
href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume')
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
let systemInfo;
function getSystemInfo() {
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
@@ -292,7 +275,6 @@ $(document).on('pageinit', '#encodingSettingsPage', function () {
$('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#encodingSettingsPage', function () {
loading.show();
libraryMenu.setTabs('playback', 0, getTabs);
const page = this;
ApiClient.getNamedConfiguration('encoding').then(function (config) {
ApiClient.getSystemInfo().then(function (fetchedSystemInfo) {

View File

@@ -1,4 +1,4 @@
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage">
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage" data-title="${General}">
<div>
<div class="content-primary">
<form class="dashboardGeneralForm">

View File

@@ -1,4 +1,4 @@
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage withTabs fullWidthContent">
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent" data-title="${HeaderLibraries}">
<div>
<div class="content-primary">
<div class="padded-top padded-bottom">

View File

@@ -2,7 +2,6 @@ import escapeHtml from 'escape-html';
import 'jquery';
import taskButton from '../../scripts/taskbutton';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import dom from '../../scripts/dom';
import imageHelper from '../../utils/image';
@@ -358,22 +357,6 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
return html;
}
function getTabs() {
return [{
href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries')
}, {
href: '#/dashboard/libraries/display',
name: globalize.translate('Display')
}, {
href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata')
}, {
href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings')
}];
}
window.WizardLibraryPage = {
next: function () {
Dashboard.navigate('wizardsettings.html');
@@ -383,8 +366,6 @@ pageClassOn('pageshow', 'mediaLibraryPage', function () {
reloadLibrary(this);
});
pageIdOn('pageshow', 'mediaLibraryPage', function () {
libraryMenu.setTabs('librarysetup', 0, getTabs);
const page = this;
taskButton({
mode: 'on',

View File

@@ -1,4 +1,4 @@
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage withTabs">
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage" data-title="${Display}">
<div>
<div class="content-primary">
<form>

View File

@@ -1,26 +1,8 @@
import globalize from '../../scripts/globalize';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import '../../elements/emby-checkbox/emby-checkbox';
import '../../elements/emby-button/emby-button';
import Dashboard from '../../utils/dashboard';
function getTabs() {
return [{
href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries')
}, {
href: '#/dashboard/libraries/display',
name: globalize.translate('Display')
}, {
href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata')
}, {
href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings')
}];
}
export default function(view) {
function loadData() {
ApiClient.getServerConfiguration().then(function(config) {
@@ -57,7 +39,6 @@ export default function(view) {
});
view.addEventListener('viewshow', function() {
libraryMenu.setTabs('librarysetup', 1, getTabs);
loadData();
ApiClient.getSystemInfo().then(function(info) {
if (info.OperatingSystem === 'Windows') {

View File

@@ -1,4 +1,4 @@
<div id="logPage" data-role="page" class="page type-interior">
<div id="logPage" data-role="page" class="page type-interior" data-title="${TabLogs}">
<div>
<div class="content-primary">
<form class="logsForm">

View File

@@ -3,7 +3,6 @@ import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image
import 'jquery';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
@@ -86,26 +85,9 @@ function onSubmit() {
return false;
}
function getTabs() {
return [{
href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries')
}, {
href: '#/dashboard/libraries/display',
name: globalize.translate('Display')
}, {
href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata')
}, {
href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings')
}];
}
$(document).on('pageinit', '#metadataImagesConfigurationPage', function() {
$('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#metadataImagesConfigurationPage', function() {
libraryMenu.setTabs('metadata', 2, getTabs);
loading.show();
loadPage(this);
});

View File

@@ -1,4 +1,4 @@
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage withTabs">
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${Metadata}">
<div>

View File

@@ -1,4 +1,4 @@
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage withTabs">
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${TabNfoSettings}">
<div>

View File

@@ -1,7 +1,6 @@
import escapeHtml from 'escape-html';
import 'jquery';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
import alert from '../../components/alert';
@@ -44,27 +43,10 @@ function showConfirmMessage() {
});
}
function getTabs() {
return [{
href: '#/dashboard/libraries',
name: globalize.translate('HeaderLibraries')
}, {
href: '#/dashboard/libraries/display',
name: globalize.translate('Display')
}, {
href: '#/dashboard/libraries/metadata',
name: globalize.translate('Metadata')
}, {
href: '#/dashboard/libraries/nfo',
name: globalize.translate('TabNfoSettings')
}];
}
const metadataKey = 'xbmcmetadata';
$(document).on('pageinit', '#metadataNfoPage', function () {
$('.metadataNfoForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#metadataNfoPage', function () {
libraryMenu.setTabs('metadata', 3, getTabs);
loading.show();
const page = this;
const promise1 = ApiClient.getUsers();

View File

@@ -1,4 +1,4 @@
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage">
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage" data-title="${TabNetworking}">
<div>
<div class="content-primary">
<form class="dashboardHostingForm">

View File

@@ -1,4 +1,4 @@
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${ButtonResume}">
<div>
<div class="content-primary">
<form class="playbackConfigurationForm">

View File

@@ -1,7 +1,5 @@
import 'jquery';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
function loadPage(page, config) {
@@ -29,27 +27,10 @@ function onSubmit() {
return false;
}
function getTabs() {
return [{
href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding')
}, {
href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume')
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
$(document).on('pageinit', '#playbackConfigurationPage', function () {
$('.playbackConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#playbackConfigurationPage', function () {
loading.show();
libraryMenu.setTabs('playback', 1, getTabs);
const page = this;
ApiClient.getServerConfiguration().then(function (config) {
loadPage(page, config);

View File

@@ -1,4 +1,4 @@
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage withTabs fullWidthContent">
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabCatalog}">
<div>
<div class="content-primary">
<div class="inputContainer">

View File

@@ -1,7 +1,6 @@
import escapeHTML from 'escape-html';
import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu';
import globalize from '../../../../scripts/globalize';
import '../../../../components/cardbuilder/card.scss';
import '../../../../elements/emby-button/emby-button';
@@ -159,22 +158,8 @@ function getPluginHtml(plugin, options, installedPlugins) {
return html;
}
function getTabs() {
return [{
href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins')
}, {
href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog')
}, {
href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories')
}];
}
export default function (view) {
view.addEventListener('viewshow', function () {
libraryMenu.setTabs('plugins', 1, getTabs);
reloadList(this);
});
}

View File

@@ -1,4 +1,4 @@
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage withTabs fullWidthContent">
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabPlugins}">
<div>
<div class="content-primary">
<div class="inputContainer">

View File

@@ -1,5 +1,4 @@
import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu';
import dom from '../../../../scripts/dom';
import globalize from '../../../../scripts/globalize';
import '../../../../components/cardbuilder/card.scss';
@@ -219,19 +218,6 @@ function reloadList(page) {
});
}
function getTabs() {
return [{
href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins')
}, {
href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog')
}, {
href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories')
}];
}
function onInstalledPluginsClick(e) {
if (dom.parentWithClass(e.target, 'noConfigPluginCard')) {
showNoConfigurationMessage();
@@ -257,7 +243,6 @@ function onFilterType(page, searchBar) {
}
pageIdOn('pageshow', 'pluginsPage', function () {
libraryMenu.setTabs('plugins', 0, getTabs);
reloadList(this);
});

View File

@@ -1,4 +1,4 @@
<div id="repositories" data-role="page" class="page type-interior withTabs fullWidthContent">
<div id="repositories" data-role="page" class="page type-interior fullWidthContent" data-title="${TabRepositories}">
<div>
<div class="content-primary">
<div class="sectionTitleContainer flex align-items-center">

View File

@@ -1,5 +1,4 @@
import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu';
import globalize from '../../../../scripts/globalize';
import dialogHelper from '../../../../components/dialogHelper/dialogHelper';
import confirm from '../../../../components/confirm/confirm';
@@ -103,22 +102,8 @@ function getRepositoryElement(repository) {
return listItem;
}
function getTabs() {
return [{
href: '#/dashboard/plugins',
name: globalize.translate('TabMyPlugins')
}, {
href: '#/dashboard/plugins/catalog',
name: globalize.translate('TabCatalog')
}, {
href: '#/dashboard/plugins/repositories',
name: globalize.translate('TabRepositories')
}];
}
export default function(view) {
view.addEventListener('viewshow', function () {
libraryMenu.setTabs('plugins', 2, getTabs);
reloadList(this);
const save = this;

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%;">
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%; z-index: 999999;">
<form class="addTriggerForm" style="padding:1em;">
<div class="ui-bar-a">
<h3>${ButtonAddScheduledTaskTrigger}</h3>

View File

@@ -1,4 +1,4 @@
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage">
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage" data-title="${TabScheduledTasks}">
<style>
.taskProgressOuter {
height: 6px;

View File

@@ -1,4 +1,4 @@
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TabStreaming}">
<div>
<div class="content-primary">
<form class="streamingSettingsForm">

View File

@@ -1,7 +1,5 @@
import 'jquery';
import libraryMenu from '../../scripts/libraryMenu';
import loading from '../../components/loading/loading';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
function loadPage(page, config) {
@@ -20,27 +18,10 @@ function onSubmit() {
return false;
}
function getTabs() {
return [{
href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding')
}, {
href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume')
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
$(document).on('pageinit', '#streamingSettingsPage', function () {
$('.streamingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#streamingSettingsPage', function () {
loading.show();
libraryMenu.setTabs('playback', 2, getTabs);
const page = this;
ApiClient.getServerConfiguration().then(function (config) {
loadPage(page, config);

View File

@@ -1,3 +1,4 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
import { intervalToDuration } from 'date-fns';
import DOMPurify from 'dompurify';
@@ -988,6 +989,9 @@ function renderDirector(page, item, context) {
}
function renderStudio(page, item, context) {
// The list of studios can be massive for collections of items
if ([BaseItemKind.BoxSet, BaseItemKind.Playlist].includes(item.Type)) return;
const studios = item.Studios || [];
const html = studios.map(function (studio) {

View File

@@ -14,6 +14,7 @@ import ServerConnections from '../components/ServerConnections';
import LibraryMenu from '../scripts/libraryMenu';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
import { stopMultiSelect } from 'components/multiSelect/multiSelect';
function getInitialLiveTvQuery(instance, params, startIndex = 0, limit = 300) {
const query = {
@@ -850,6 +851,10 @@ class ItemsView {
setTitle(null);
getItem(params).then(function (item) {
setTitle(item);
if (item && item.Type == 'Genre') {
item.ParentId = params.parentId;
}
self.currentItem = item;
const refresh = !isRestored;
self.itemsContainer.resume({
@@ -1139,6 +1144,9 @@ class ItemsView {
setFilterStatus(hasFilters) {
this.hasFilters = hasFilters;
if (this.hasFilters) {
stopMultiSelect();
}
const filterButtons = this.filterButtons;
if (filterButtons.length) {
@@ -1301,4 +1309,3 @@ class ItemsView {
}
export default ItemsView;

View File

@@ -1,4 +1,4 @@
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage">
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage" data-title="${HeaderDVR}">
<div>
<div class="content-primary">
<div class="verticalSection">

View File

@@ -1,4 +1,4 @@
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage">
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
<div>
<div class="content-primary">
<div class="verticalSection verticalSection-extrabottompadding">

View File

@@ -207,7 +207,7 @@ export default function (view, params, tabContent) {
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
name: globalize.translate('OptionCommunityRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),

View File

@@ -212,7 +212,7 @@ export default function (view, params, tabContent, options) {
name: globalize.translate('OptionRandom'),
id: 'Random'
}, {
name: globalize.translate('OptionImdbRating'),
name: globalize.translate('OptionCommunityRating'),
id: 'CommunityRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionCriticRating'),
@@ -321,4 +321,3 @@ export default function (view, params, tabContent, options) {
itemsContainer = null;
};
}

View File

@@ -234,7 +234,7 @@ export default function (view, params, tabContent) {
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
name: globalize.translate('OptionCommunityRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),

View File

@@ -33,15 +33,15 @@
<span class="xlargePaperIconButton material-icons fiber_manual_record" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnPreviousTrack autoSize hide" title="${PreviousTrack}">
<button is="paper-icon-button-light" class="btnPreviousTrack autoSize hide" title="${PreviousTrack} (Shift+P)" aria-label="${PreviousTrack}">
<span class="xlargePaperIconButton material-icons skip_previous" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnPreviousChapter autoSize hide" title="${PreviousChapter}">
<button is="paper-icon-button-light" class="btnPreviousChapter autoSize hide" title="${PreviousChapter} (PageDown)" aria-label="${PreviousChapter}">
<span class="xlargePaperIconButton material-icons undo" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnRewind" title="${Rewind} (j)" aria-label="${Rewind}">
<button is="paper-icon-button-light" class="btnRewind" title="${Rewind} (J)" aria-label="${Rewind}">
<span class="xlargePaperIconButton material-icons fast_rewind" aria-hidden="true"></span>
</button>
@@ -49,15 +49,15 @@
<span class="xlargePaperIconButton material-icons pause" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnFastForward" title="${FastForward} (l)" aria-label="${FastForward}">
<button is="paper-icon-button-light" class="btnFastForward" title="${FastForward} (L)" aria-label="${FastForward}">
<span class="xlargePaperIconButton material-icons fast_forward" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnNextChapter autoSize hide" title="${NextChapter}">
<button is="paper-icon-button-light" class="btnNextChapter autoSize hide" title="${NextChapter} (PageUp)" aria-label="${NextChapter}">
<span class="xlargePaperIconButton material-icons redo" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnNextTrack autoSize hide" title="${NextTrack}">
<button is="paper-icon-button-light" class="btnNextTrack autoSize hide" title="${NextTrack} (Shift+N)" aria-label="${NextTrack}">
<span class="xlargePaperIconButton material-icons skip_next" aria-hidden="true"></span>
</button>
</div>
@@ -65,7 +65,7 @@
<div class="osdTimeText">
<span class="endsAtText"></span>
</div>
<div class="osdRatingsText">
</div>
@@ -80,7 +80,7 @@
<span class="xlargePaperIconButton material-icons audiotrack" aria-hidden="true"></span>
</button>
<div class="volumeButtons hide-mouse-idle-tv">
<button is="paper-icon-button-light" class="buttonMute autoSize" title="${Mute} (m)" aria-label="${Mute}">
<button is="paper-icon-button-light" class="buttonMute autoSize" title="${Mute} (M)" aria-label="${Mute}">
<span class="xlargePaperIconButton material-icons volume_up" aria-hidden="true"></span>
</button>
<div class="sliderContainer osdVolumeSliderContainer">
@@ -96,7 +96,7 @@
<button is="paper-icon-button-light" class="btnPip hide autoSize" title="${PictureInPicture}">
<span class="xlargePaperIconButton material-icons picture_in_picture_alt" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnFullscreen hide autoSize" title="${Fullscreen} (f)" aria-label="${Fullscreen}">
<button is="paper-icon-button-light" class="btnFullscreen hide autoSize" title="${Fullscreen} (F)" aria-label="${Fullscreen}">
<span class="xlargePaperIconButton material-icons fullscreen" aria-hidden="true"></span>
</button>
</div>

View File

@@ -28,6 +28,7 @@ import LibraryMenu from '../../../scripts/libraryMenu';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop';
import { pluginManager } from '../../../components/pluginManager';
import { PluginType } from '../../../types/plugin.ts';
import { EventType } from 'types/eventType';
const TICKS_PER_MINUTE = 600000000;
const TICKS_PER_SECOND = 10000000;
@@ -280,12 +281,14 @@ export default function (view) {
let mouseIsDown = false;
function showOsd(focusElement) {
Events.trigger(document, EventType.SHOW_VIDEO_OSD, [ true ]);
slideDownToShow(headerElement);
showMainOsdControls(focusElement);
resetIdle();
}
function hideOsd() {
Events.trigger(document, EventType.SHOW_VIDEO_OSD, [ false ]);
slideUpToHide(headerElement);
hideMainOsdControls();
mouseManager.hideCursor();
@@ -320,18 +323,14 @@ export default function (view) {
}
function clearHideAnimationEventListeners(elem) {
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
once: true
});
elem.removeEventListener(transitionEndEventName, onHideAnimationComplete);
}
function onHideAnimationComplete(e) {
const elem = e.target;
if (elem != osdBottomElement) return;
elem.classList.add('hide');
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
once: true
});
elem.removeEventListener(transitionEndEventName, onHideAnimationComplete);
}
const _focus = debounce((focusElement) => focusManager.focus(focusElement), 50);
@@ -361,9 +360,7 @@ export default function (view) {
clearHideAnimationEventListeners(elem);
elem.classList.add('videoOsdBottom-hidden');
dom.addEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
once: true
});
elem.addEventListener(transitionEndEventName, onHideAnimationComplete);
currentVisibleMenu = null;
toggleSubtitleSync('hide');
@@ -499,10 +496,10 @@ export default function (view) {
icon.classList.remove('fullscreen_exit', 'fullscreen');
if (playbackManager.isFullscreen(currentPlayer)) {
button.setAttribute('title', globalize.translate('ExitFullscreen') + ' (f)');
button.setAttribute('title', globalize.translate('ExitFullscreen') + ' (F)');
icon.classList.add('fullscreen_exit');
} else {
button.setAttribute('title', globalize.translate('Fullscreen') + ' (f)');
button.setAttribute('title', globalize.translate('Fullscreen') + ' (F)');
icon.classList.add('fullscreen');
}
}
@@ -724,7 +721,7 @@ export default function (view) {
}
btnPlayPauseIcon.classList.add(icon);
dom.setElementTitle(btnPlayPause, title + ' (k)', title);
dom.setElementTitle(btnPlayPause, title + ' (K)', title);
}
function updatePlayerStateInternal(event, player, state) {
@@ -873,10 +870,10 @@ export default function (view) {
buttonMuteIcon.classList.remove('volume_off', 'volume_up');
if (isMuted) {
buttonMute.setAttribute('title', globalize.translate('Unmute') + ' (m)');
buttonMute.setAttribute('title', globalize.translate('Unmute') + ' (M)');
buttonMuteIcon.classList.add('volume_off');
} else {
buttonMute.setAttribute('title', globalize.translate('Mute') + ' (m)');
buttonMute.setAttribute('title', globalize.translate('Mute') + ' (M)');
buttonMuteIcon.classList.add('volume_up');
}
@@ -1248,6 +1245,7 @@ export default function (view) {
}
break;
case 'k':
case 'K':
playbackManager.playPause(currentPlayer);
showOsd(btnPlayPause);
break;
@@ -1260,23 +1258,27 @@ export default function (view) {
playbackManager.volumeDown(currentPlayer);
break;
case 'l':
case 'L':
case 'ArrowRight':
case 'Right':
playbackManager.fastForward(currentPlayer);
showOsd(btnFastForward);
break;
case 'j':
case 'J':
case 'ArrowLeft':
case 'Left':
playbackManager.rewind(currentPlayer);
showOsd(btnRewind);
break;
case 'f':
case 'F':
if (!e.ctrlKey && !e.metaKey) {
playbackManager.toggleFullscreen(currentPlayer);
}
break;
case 'm':
case 'M':
playbackManager.toggleMute(currentPlayer);
break;
case 'p':
@@ -1385,7 +1387,7 @@ export default function (view) {
// Create bubble elements if they don't already exist
if (chapterThumbContainer) {
chapterThumb = chapterThumbContainer.querySelector('.chapterThumb');
chapterThumb = chapterThumbContainer.querySelector('.chapterThumbWrapper');
chapterThumbText = chapterThumbContainer.querySelector('.chapterThumbText');
} else {
doFullUpdate = true;
@@ -1394,22 +1396,12 @@ export default function (view) {
chapterThumbContainer.classList.add('chapterThumbContainer');
chapterThumbContainer.style.overflow = 'hidden';
const chapterThumbWrapper = document.createElement('div');
chapterThumbWrapper.classList.add('chapterThumbWrapper');
chapterThumbWrapper.style.overflow = 'hidden';
chapterThumbWrapper.style.position = 'relative';
chapterThumbWrapper.style.width = trickplayInfo.Width + 'px';
chapterThumbWrapper.style.height = trickplayInfo.Height + 'px';
chapterThumbContainer.appendChild(chapterThumbWrapper);
chapterThumb = document.createElement('img');
chapterThumb.classList.add('chapterThumb');
chapterThumb.style.position = 'absolute';
chapterThumb.style.width = 'unset';
chapterThumb.style.minWidth = 'unset';
chapterThumb.style.height = 'unset';
chapterThumb.style.minHeight = 'unset';
chapterThumbWrapper.appendChild(chapterThumb);
chapterThumb = document.createElement('div');
chapterThumb.classList.add('chapterThumbWrapper');
chapterThumb.style.overflow = 'hidden';
chapterThumb.style.width = trickplayInfo.Width + 'px';
chapterThumb.style.height = trickplayInfo.Height + 'px';
chapterThumbContainer.appendChild(chapterThumb);
const chapterThumbTextContainer = document.createElement('div');
chapterThumbTextContainer.classList.add('chapterThumbTextContainer');
@@ -1438,9 +1430,9 @@ export default function (view) {
MediaSourceId: mediaSourceId
});
if (chapterThumb.src != imgSrc) chapterThumb.src = imgSrc;
chapterThumb.style.left = offsetX + 'px';
chapterThumb.style.top = offsetY + 'px';
chapterThumb.style.backgroundImage = `url('${imgSrc}')`;
chapterThumb.style.backgroundPositionX = offsetX + 'px';
chapterThumb.style.backgroundPositionY = offsetY + 'px';
chapterThumbText.textContent = datetime.getDisplayRunningTime(positionTicks);
@@ -1832,22 +1824,11 @@ export default function (view) {
};
nowPlayingPositionSlider.getMarkerInfo = function () {
const markers = [];
const item = currentItem;
// use markers based on chapters
if (item?.Chapters?.length) {
item.Chapters.forEach(currentChapter => {
markers.push({
className: 'chapterMarker',
name: currentChapter.Name,
progress: currentChapter.StartPositionTicks / item.RunTimeTicks
});
});
}
return markers;
return currentItem?.Chapters?.map(currentChapter => ({
name: currentChapter.Name,
progress: currentChapter.StartPositionTicks / currentItem.RunTimeTicks
})) || [];
};
view.querySelector('.btnPreviousTrack').addEventListener('click', function () {

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