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
405 changed files with 7752 additions and 12331 deletions

View File

@@ -79,7 +79,6 @@ module.exports = {
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
'@typescript-eslint/prefer-for-of': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
@@ -264,7 +263,6 @@ module.exports = {
'UserParentalControlPage': 'writable',
'Windows': 'readonly',
// Build time definitions
__COMMIT_SHA__: 'readonly',
__JF_BUILD_VERSION__: 'readonly',
__PACKAGE_JSON_NAME__: 'readonly',
__PACKAGE_JSON_VERSION__: 'readonly',

View File

@@ -1,22 +1,20 @@
name: Automation
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
name: 'Automation'
on:
push:
branches:
- master
pull_request_target:
types:
- synchronize
jobs:
conflicts:
name: Merge conflict labeling
triage:
name: 'Merge conflict labeling'
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
- 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

@@ -7,7 +7,7 @@ concurrency:
on:
push:
branches: [ master, release* ]
pull_request_target:
pull_request:
branches: [ master, release* ]
workflow_dispatch:
@@ -18,12 +18,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -43,87 +41,8 @@ jobs:
mv dist/config.tmp.json dist/config.json
- name: Upload artifact
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: jellyfin-web__prod
path: dist
publish:
name: Deploy to Cloudflare Pages
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
needs:
- run-build-prod
permissions:
contents: read
deployments: write
steps:
- name: Add comment
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
if: ${{ github.event_name == 'pull_request_target' }}
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | 🔄 Deploying... |
| **Preview URL** | Not available |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate
- name: Download workflow artifact
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with:
name: jellyfin-web__prod
path: dist
- name: Publish to Cloudflare
id: cf
uses: cloudflare/wrangler-action@f84a562284fc78278ff9052435d9526f9c718361 # v3.7.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=jellyfin-web --branch=${{
(github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository)
&& (github.event.pull_request.head.ref || github.ref_name)
|| format('{0}/{1}', github.event.pull_request.head.repo.full_name, github.event.pull_request.head.ref)
}} --commit-hash=${{ github.event.pull_request.head.sha || github.sha }}
- name: Update status comment (Success)
if: ${{ github.event_name == 'pull_request_target' && success() }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | ✅ Deployed! |
| **Preview URL** | ${{ steps.cf.outputs.deployment-url != '' && steps.cf.outputs.deployment-url || 'Not available' }} |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate
- name: Update status comment (Failure)
if: ${{ github.event_name == 'pull_request_target' && failure() }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: |
## Cloudflare Pages deployment
| **Latest commit** | <code>${{ github.event.pull_request.head.sha || github.sha }}</code> |
|-------------------|:-:|
| **Status** | ❌ Failure. Check workflow logs for details |
| **Preview URL** | Not available |
| **Type** | 🔀 Preview |
pr_number: ${{ github.event.pull_request.number }}
comment_tag: CFPages-deployment
mode: recreate
path: |
dist

View File

@@ -19,16 +19,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Initialize CodeQL
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with:
languages: javascript
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0

65
.github/workflows/job-messages.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Job messages
on:
workflow_call:
inputs:
branch:
required: false
type: string
commit:
required: true
type: string
preview_url:
required: false
type: string
build_workflow_run_id:
required: false
type: number
commenting_workflow_run_id:
required: true
type: string
in_progress:
required: true
type: boolean
outputs:
msg:
description: The composed message
value: ${{ jobs.msg.outputs.msg }}
marker:
description: Hidden marker to detect PR comments composed by the bot
value: "CFPages-deployment"
jobs:
msg:
name: Deployment status
runs-on: ubuntu-latest
outputs:
msg: ${{ env.msg }}
steps:
- name: Compose message
if: ${{ always() }}
id: compose
env:
COMMIT: ${{ inputs.commit }}
PREVIEW_URL: ${{ inputs.preview_url != '' && (inputs.branch != 'master' && inputs.preview_url || format('https://jellyfin-web.pages.dev ({0})', inputs.preview_url)) || 'Not available' }}
DEPLOY_STATUS: ${{ inputs.in_progress && '🔄 Deploying...' || (inputs.preview_url != '' && '✅ Deployed!' || '❌ Failure. Check workflow logs for details') }}
DEPLOYMENT_TYPE: ${{ inputs.branch != 'master' && '🔀 Preview' || '⚙️ Production' }}
BUILD_WORKFLOW_RUN: ${{ !inputs.in_progress && format('**[View build logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.build_workflow_run_id) || '' }}
COMMENTING_WORKFLOW_RUN: ${{ format('**[View bot logs](https://github.com/{0}/actions/runs/{1})**', 'jellyfin/jellyfin-web', inputs.commenting_workflow_run_id) }}
# EOF is needed for multiline environment variables in a GitHub Actions context
run: |
echo "## Cloudflare Pages deployment" > $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| **Latest commit** | <code>${COMMIT::7}</code> |" >> $GITHUB_STEP_SUMMARY
echo "|------------------------- |:----------------------------: |" >> $GITHUB_STEP_SUMMARY
echo "| **Status** | $DEPLOY_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| **Preview URL** | $PREVIEW_URL |" >> $GITHUB_STEP_SUMMARY
echo "| **Type** | $DEPLOYMENT_TYPE |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$BUILD_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
echo "$COMMENTING_WORKFLOW_RUN" >> $GITHUB_STEP_SUMMARY
COMPOSED_MSG=$(cat $GITHUB_STEP_SUMMARY)
echo "msg<<EOF" >> $GITHUB_ENV
echo "$COMPOSED_MSG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV

View File

@@ -7,6 +7,8 @@ concurrency:
on:
pull_request_target:
branches: [ master, release* ]
types:
- synchronize
jobs:
run-eslint:
@@ -15,12 +17,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -31,6 +33,6 @@ jobs:
- name: Run eslint
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
uses: CatChen/eslint-suggestion-action@bc82950fa97bb3e46d9cca16a8bf2ad3e3c010fc # v4.1.5
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

84
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Publish
on:
workflow_run:
workflows:
- Build
types:
- completed
jobs:
publish:
name: Deploy to Cloudflare Pages
if: ${{ always() }}
runs-on: ubuntu-latest
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
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with:
run_id: ${{ github.event.workflow_run.id }}
name: jellyfin-web__prod
path: dist
- name: Publish
id: cf
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # 1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: jellyfin-web
branch: ${{ env.TARGET_BRANCH }}
directory: dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
compose-comment:
name: Compose comment
if: ${{ always() }}
uses: ./.github/workflows/job-messages.yml
needs:
- publish
with:
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 }}
in_progress: false
comment-status:
name: Create comment status
if: |
always() &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.pull_requests[0].number != ''
runs-on: ubuntu-latest
needs:
- compose-comment
steps:
- name: Update job summary in PR comment
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
message: ${{ needs.compose-comment.outputs.msg }}
pr_number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment_tag: ${{ needs.compose-comment.outputs.marker }}
mode: recreate

View File

@@ -17,10 +17,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -41,10 +41,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -62,10 +62,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -86,10 +86,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -107,10 +107,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true

View File

@@ -16,13 +16,13 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: master
token: ${{ secrets.JF_BOT_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
@@ -35,7 +35,7 @@ jobs:
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Open a pull request
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
commit-message: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}

9
.gitignore vendored
View File

@@ -3,9 +3,6 @@ dist
web
node_modules
# test coverage
coverage
# config
config.json
@@ -13,6 +10,12 @@ config.json
.idea
.vs
# log
yarn-error.log
# vim
*.sw?
# build artifacts
fedora/jellyfin-web-*.src.rpm
fedora/jellyfin-web-*.tar.gz

View File

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

View File

@@ -85,11 +85,8 @@ Jellyfin Web is the frontend used for most of the clients available for end user
├── controllers # Legacy page views and controllers 🧹
├── elements # Basic webcomponents and React wrappers 🧹
├── hooks # Custom React hooks
├── lib # Reusable libraries
│   ├── globalize # Custom localization library
│   ├── legacy # Polyfills for legacy browsers
│   ├── navdrawer # Navigation drawer library for classic layout
│   └── scroller # Content scrolling library
├── legacy # Polyfills for legacy browsers
├── libraries # Third party libraries 🧹
├── plugins # Client plugins
├── scripts # Random assortment of visual components and utilities 🐉
├── strings # Translation files

View File

@@ -15,8 +15,8 @@ module.exports = {
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-class-properties',
'@babel/plugin-transform-private-methods',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-private-methods',
'babel-plugin-dynamic-import-polyfill'
]
};

10846
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,26 @@
{
"name": "jellyfin-web",
"version": "10.10.0",
"version": "10.9.11",
"description": "Web interface for Jellyfin",
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@babel/core": "7.24.9",
"@babel/plugin-transform-class-properties": "7.24.7",
"@babel/plugin-transform-modules-umd": "7.24.7",
"@babel/plugin-transform-private-methods": "7.24.7",
"@babel/preset-env": "7.24.8",
"@babel/preset-react": "7.24.7",
"@types/dompurify": "3.0.5",
"@babel/core": "7.24.3",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-private-methods": "7.18.6",
"@babel/plugin-transform-modules-umd": "7.24.1",
"@babel/preset-env": "7.24.3",
"@babel/preset-react": "7.24.1",
"@types/escape-html": "1.0.4",
"@types/loadable__component": "5.13.9",
"@types/lodash-es": "4.17.12",
"@types/markdown-it": "14.1.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/markdown-it": "13.0.7",
"@types/react": "17.0.79",
"@types/react-dom": "17.0.25",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"@vitest/coverage-v8": "2.0.5",
"autoprefixer": "10.4.19",
"babel-loader": "9.1.3",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
@@ -30,70 +28,71 @@
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"cssnano": "7.0.4",
"es-check": "7.2.1",
"css-loader": "6.10.0",
"cssnano": "6.1.2",
"es-check": "7.1.1",
"eslint": "8.57.0",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-sonarjs": "0.25.1",
"expose-loader": "5.0.0",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-sonarjs": "0.24.0",
"expose-loader": "4.1.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"html-loader": "5.1.0",
"html-loader": "4.2.0",
"html-webpack-plugin": "5.6.0",
"jsdom": "24.1.1",
"mini-css-extract-plugin": "2.9.0",
"postcss": "8.4.40",
"postcss-loader": "8.1.1",
"postcss-preset-env": "9.6.0",
"jsdom": "23.2.0",
"mini-css-extract-plugin": "2.8.1",
"postcss": "8.4.38",
"postcss-loader": "7.3.4",
"postcss-preset-env": "9.5.2",
"postcss-scss": "4.0.9",
"sass": "1.77.8",
"sass-loader": "15.0.0",
"source-map-loader": "5.0.0",
"sass": "1.72.0",
"sass-loader": "13.3.3",
"source-map-loader": "4.0.2",
"speed-measure-webpack-plugin": "1.5.0",
"style-loader": "4.0.0",
"style-loader": "3.3.4",
"stylelint": "15.11.0",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.3.0",
"stylelint-order": "6.0.4",
"stylelint-scss": "5.3.2",
"ts-loader": "9.5.1",
"typescript": "5.5.4",
"vitest": "2.0.5",
"webpack": "5.93.0",
"webpack-bundle-analyzer": "4.10.2",
"typescript": "5.4.3",
"vitest": "1.4.0",
"webpack": "5.91.0",
"webpack-bundle-analyzer": "4.10.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"webpack-merge": "6.0.1",
"webpack-dev-server": "4.15.2",
"webpack-merge": "5.10.0",
"worker-loader": "3.0.8"
},
"dependencies": {
"@emotion/react": "11.13.0",
"@emotion/styled": "11.13.0",
"@fontsource/noto-sans": "5.0.22",
"@fontsource/noto-sans-hk": "5.0.20",
"@fontsource/noto-sans-jp": "5.0.19",
"@fontsource/noto-sans-kr": "5.0.19",
"@fontsource/noto-sans-sc": "5.0.20",
"@fontsource/noto-sans-tc": "5.0.20",
"@jellyfin/libass-wasm": "4.2.2",
"@jellyfin/sdk": "0.0.0-unstable.202408050429",
"@mui/icons-material": "5.15.19",
"@mui/material": "5.15.19",
"@mui/x-data-grid": "7.6.1",
"@react-hook/resize-observer": "2.0.1",
"@tanstack/react-query": "5.51.11",
"@tanstack/react-query-devtools": "5.51.11",
"@types/react-lazy-load-image-component": "1.6.4",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
"@fontsource/noto-sans": "5.0.21",
"@fontsource/noto-sans-hk": "5.0.18",
"@fontsource/noto-sans-jp": "5.0.18",
"@fontsource/noto-sans-kr": "5.0.18",
"@fontsource/noto-sans-sc": "5.0.18",
"@fontsource/noto-sans-tc": "5.0.18",
"@jellyfin/libass-wasm": "4.2.1",
"@jellyfin/sdk": "0.9.0",
"@loadable/component": "5.16.3",
"@mui/icons-material": "5.15.11",
"@mui/material": "5.15.11",
"@mui/x-data-grid": "6.19.5",
"@react-hook/resize-observer": "1.2.6",
"@tanstack/react-query": "4.36.1",
"@tanstack/react-query-devtools": "4.36.1",
"@types/react-lazy-load-image-component": "1.6.3",
"abortcontroller-polyfill": "1.7.5",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.5.1",
"core-js": "3.37.1",
"core-js": "3.36.1",
"date-fns": "2.30.0",
"dompurify": "3.0.1",
"epubjs": "0.3.93",
@@ -102,7 +101,7 @@
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"history": "5.3.0",
"hls.js": "1.5.13",
"hls.js": "1.5.7",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1",
@@ -113,16 +112,16 @@
"material-design-icons-iconfont": "6.7.0",
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"react": "18.3.1",
"react": "17.0.2",
"react-blurhash": "0.3.0",
"react-dom": "18.3.1",
"react-lazy-load-image-component": "1.6.2",
"react-router-dom": "6.25.1",
"react-dom": "17.0.2",
"react-lazy-load-image-component": "1.6.0",
"react-router-dom": "6.22.3",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",
"sortablejs": "1.15.2",
"swiper": "11.1.7",
"usehooks-ts": "3.1.0",
"swiper": "11.0.7",
"usehooks-ts": "2.16.0",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.20"
},

View File

@@ -6,17 +6,19 @@ import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import { queryClient } from 'utils/query/queryClient';
import RootAppRouter from 'RootAppRouter';
import RootAppRouter from './RootAppRouter';
const RootApp = () => (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<RootAppRouter />
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
const RootApp = () => {
return (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<RootAppRouter />
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default RootApp;

View File

@@ -13,20 +13,23 @@ import { useApi } from 'hooks/useApi';
import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import { DASHBOARD_APP_PATHS } from './routes/routes';
import './AppOverrides.scss';
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
interface AppLayoutProps {
drawerlessPaths: string[]
}
export const Component: FC = () => {
const AppLayout: FC<AppLayoutProps> = ({
drawerlessPaths
}) => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const location = useLocation();
const { user } = useApi();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isDrawerAvailable = Boolean(user)
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
&& !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
@@ -92,3 +95,5 @@ export const Component: FC = () => {
</Box>
);
};
export default AppLayout;

View File

@@ -2,7 +2,7 @@ import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import Chip from '@mui/material/Chip';
import React from 'react';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const LogLevelChip = ({ level }: { level: LogLevel }) => {
let color: 'info' | 'warning' | 'error' | undefined;

View File

@@ -29,8 +29,8 @@ const AppDrawer: FC<ResponsiveDrawerProps> = ({
<ServerDrawerSection />
<DevicesDrawerSection />
<LiveTvDrawerSection />
<PluginDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</ResponsiveDrawer>
);

View File

@@ -1,18 +1,36 @@
import Article from '@mui/icons-material/Article';
import EditNotifications from '@mui/icons-material/EditNotifications';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Extension from '@mui/icons-material/Extension';
import Lan from '@mui/icons-material/Lan';
import Schedule from '@mui/icons-material/Schedule';
import VpnKey from '@mui/icons-material/VpnKey';
import Collapse from '@mui/material/Collapse';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const PLUGIN_PATHS = [
'/dashboard/plugins',
'/dashboard/plugins/catalog',
'/dashboard/plugins/repositories',
'/dashboard/plugins/add',
'/configurationpage'
];
const AdvancedDrawerSection = () => {
const location = useLocation();
const isPluginSectionOpen = PLUGIN_PATHS.includes(location.pathname);
return (
<List
aria-labelledby='advanced-subheader'
@@ -46,6 +64,36 @@ const AdvancedDrawerSection = () => {
<ListItemText primary={globalize.translate('TabLogs')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/notifications'>
<ListItemIcon>
<EditNotifications />
</ListItemIcon>
<ListItemText primary={globalize.translate('Notifications')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/plugins' selected={false}>
<ListItemIcon>
<Extension />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabPlugins')} />
{isPluginSectionOpen ? <ExpandLess /> : <ExpandMore />}
</ListItemLink>
</ListItem>
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding>
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabCatalog')} />
</ListItemLink>
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabRepositories')} />
</ListItemLink>
</List>
</Collapse>
<ListItem disablePadding>
<ListItemLink to='/dashboard/tasks'>
<ListItemIcon>

View File

@@ -1,4 +1,4 @@
import { Devices, Analytics } from '@mui/icons-material';
import { Devices, Analytics, Input } from '@mui/icons-material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
@@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const DevicesDrawerSection = () => {
return (
@@ -35,6 +35,14 @@ const DevicesDrawerSection = () => {
<ListItemText primary={globalize.translate('HeaderActivity')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/dlna'>
<ListItemIcon>
<Input />
</ListItemIcon>
<ListItemText primary={'DLNA'} />
</ListItemLink>
</ListItem>
</List>
);
};

View File

@@ -7,7 +7,7 @@ import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const LiveTvDrawerSection = () => {
return (

View File

@@ -1,26 +1,41 @@
import Extension from '@mui/icons-material/Extension';
import Folder from '@mui/icons-material/Folder';
import Public from '@mui/icons-material/Public';
import { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client';
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
import { Folder } from '@mui/icons-material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import Dashboard from 'utils/dashboard';
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
const PluginDrawerSection = () => {
const {
data: pagesInfo,
error
} = useConfigurationPages({ enableInMainMenu: true });
const { api } = useApi();
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
useEffect(() => {
if (error) console.error('[PluginDrawerSection] unable to fetch plugin config pages', error);
}, [ error ]);
const fetchPluginPages = async () => {
if (!api) return;
const pagesResponse = await getDashboardApi(api)
.getConfigurationPages({ enableInMainMenu: true });
setPagesInfo(pagesResponse.data);
};
fetchPluginPages()
.catch(err => {
console.error('[PluginDrawerSection] unable to fetch plugin config pages', err);
});
}, [ api ]);
if (!api || pagesInfo.length < 1) {
return null;
}
return (
<List
@@ -31,39 +46,19 @@ const PluginDrawerSection = () => {
</ListSubheader>
}
>
<ListItemLink
to='/dashboard/plugins'
includePaths={[ '/configurationpage' ]}
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
>
<ListItemIcon>
<Extension />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink
to='/dashboard/plugins/catalog'
includePaths={[ '/dashboard/plugins/repositories' ]}
>
<ListItemIcon>
<Public />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabCatalog')} />
</ListItemLink>
{pagesInfo?.map(pageInfo => (
<ListItemLink
key={pageInfo.PluginId}
to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}
>
<ListItemIcon>
{/* TODO: Support different icons? */}
<Folder />
</ListItemIcon>
<ListItemText primary={pageInfo.DisplayName} />
</ListItemLink>
))}
{
pagesInfo.map(pageInfo => (
<ListItem key={pageInfo.PluginId} disablePadding>
<ListItemLink to={`/${Dashboard.getPluginUrl(pageInfo.Name)}`}>
<ListItemIcon>
{/* TODO: Support different icons? */}
<Folder />
</ListItemIcon>
<ListItemText primary={pageInfo.DisplayName} />
</ListItemLink>
</ListItem>
))
}
</List>
);
};

View File

@@ -9,7 +9,7 @@ import React from 'react';
import { useLocation } from 'react-router-dom';
import ListItemLink from 'components/ListItemLink';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const LIBRARY_PATHS = [
'/dashboard/libraries',

View File

@@ -1,21 +0,0 @@
import type { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client/models/configuration-page-info';
export const findBestConfigurationPage = (
configurationPages: ConfigurationPageInfo[],
pluginId: string
) => {
// Find candidates matching the plugin id
const candidates = configurationPages.filter(c => c.PluginId === pluginId);
// If none are found, return undefined
if (candidates.length === 0) return;
// If only one is found, return it
if (candidates.length === 1) return candidates[0];
// Prefer the first candidate with the EnableInMainMenu flag for consistency
const menuCandidate = candidates.find(c => !!c.EnableInMainMenu);
if (menuCandidate) return menuCandidate;
// Fallback to the first match
return candidates[0];
};

View File

@@ -1,25 +0,0 @@
import type { PluginInfo } from '@jellyfin/sdk/lib/generated-client/models/plugin-info';
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
/**
* HACK: The Plugins API is returning garbage data in some cases,
* so we need to try to find the "best" match if multiple exist.
*/
export const findBestPluginInfo = (
pluginId: string,
plugins?: PluginInfo[]
) => {
if (!plugins) return;
// Find all plugin entries with a matching ID
const matches = plugins.filter(p => p.Id === pluginId);
// Get the first match (or undefined if none)
const firstMatch = matches?.[0];
if (matches.length > 1) {
return matches.find(p => p.Status === PluginStatus.Disabled) // Disabled entries take priority
|| matches.find(p => p.Status === PluginStatus.Restart) // Then entries specifying restart is needed
|| firstMatch; // Fallback to the first match
}
return firstMatch;
};

View File

@@ -1,5 +0,0 @@
export enum QueryKey {
ConfigurationPages = 'ConfigurationPages',
PackageInfo = 'PackageInfo',
Plugins = 'Plugins'
}

View File

@@ -1,40 +0,0 @@
import type { Api } from '@jellyfin/sdk';
import type { DashboardApiGetConfigurationPagesRequest } from '@jellyfin/sdk/lib/generated-client/api/dashboard-api';
import { getDashboardApi } from '@jellyfin/sdk/lib/utils/api/dashboard-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchConfigurationPages = async (
api?: Api,
params?: DashboardApiGetConfigurationPagesRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchConfigurationPages] No API instance available');
return [];
}
const response = await getDashboardApi(api)
.getConfigurationPages(params, options);
return response.data;
};
const getConfigurationPagesQuery = (
api?: Api,
params?: DashboardApiGetConfigurationPagesRequest
) => queryOptions({
queryKey: [ QueryKey.ConfigurationPages, params?.enableInMainMenu ],
queryFn: ({ signal }) => fetchConfigurationPages(api, params, { signal }),
enabled: !!api
});
export const useConfigurationPages = (
params?: DashboardApiGetConfigurationPagesRequest
) => {
const { api } = useApi();
return useQuery(getConfigurationPagesQuery(api, params));
};

View File

@@ -1,24 +0,0 @@
import type { PluginsApiDisablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useDisablePlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiDisablePluginRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.disablePlugin(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,24 +0,0 @@
import type { PluginsApiEnablePluginRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useEnablePlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiEnablePluginRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.enablePlugin(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,27 +0,0 @@
import type { PackageApiInstallPackageRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useInstallPackage = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PackageApiInstallPackageRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPackageApi(api!)
.installPackage(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.ConfigurationPages ]
});
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
}
});
};

View File

@@ -1,47 +0,0 @@
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { Api } from '@jellyfin/sdk';
import type { PackageApiGetPackageInfoRequest } from '@jellyfin/sdk/lib/generated-client/api/package-api';
import { getPackageApi } from '@jellyfin/sdk/lib/utils/api/package-api';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchPackageInfo = async (
api?: Api,
params?: PackageApiGetPackageInfoRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchPackageInfo] No API instance available');
return;
}
if (!params) {
console.warn('[fetchPackageInfo] Missing request params');
return;
}
const response = await getPackageApi(api)
.getPackageInfo(params, options);
return response.data;
};
const getPackageInfoQuery = (
api?: Api,
params?: PackageApiGetPackageInfoRequest
) => queryOptions({
// Don't retry since requests for plugins not available in repos fail
retry: false,
queryKey: [ QueryKey.PackageInfo, params?.name, params?.assemblyGuid ],
queryFn: ({ signal }) => fetchPackageInfo(api, params, { signal }),
enabled: !!api && !!params?.name
});
export const usePackageInfo = (
params?: PackageApiGetPackageInfoRequest
) => {
const { api } = useApi();
return useQuery(getPackageInfoQuery(api, params));
};

View File

@@ -1,36 +0,0 @@
import type { Api } from '@jellyfin/sdk';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
import { QueryKey } from './queryKey';
const fetchPlugins = async (
api?: Api,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchPlugins] No API instance available');
return [];
}
const response = await getPluginsApi(api)
.getPlugins(options);
return response.data;
};
const getPluginsQuery = (
api?: Api
) => queryOptions({
queryKey: [ QueryKey.Plugins ],
queryFn: ({ signal }) => fetchPlugins(api, { signal }),
enabled: !!api
});
export const usePlugins = () => {
const { api } = useApi();
return useQuery(getPluginsQuery(api));
};

View File

@@ -1,27 +0,0 @@
import type { PluginsApiUninstallPluginByVersionRequest } from '@jellyfin/sdk/lib/generated-client/api/plugins-api';
import { getPluginsApi } from '@jellyfin/sdk/lib/utils/api/plugins-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QueryKey } from './queryKey';
export const useUninstallPlugin = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: PluginsApiUninstallPluginByVersionRequest) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getPluginsApi(api!)
.uninstallPluginByVersion(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QueryKey.Plugins ]
});
void queryClient.invalidateQueries({
queryKey: [ QueryKey.ConfigurationPages ]
});
}
});
};

View File

@@ -1,94 +0,0 @@
import Link from '@mui/material/Link/Link';
import Paper, { type PaperProps } from '@mui/material/Paper/Paper';
import Skeleton from '@mui/material/Skeleton/Skeleton';
import Table from '@mui/material/Table/Table';
import TableBody from '@mui/material/TableBody/TableBody';
import TableCell from '@mui/material/TableCell/TableCell';
import TableContainer from '@mui/material/TableContainer/TableContainer';
import TableRow from '@mui/material/TableRow/TableRow';
import React, { FC } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import globalize from 'lib/globalize';
import type { PluginDetails } from '../types/PluginDetails';
interface PluginDetailsTableProps extends PaperProps {
isPluginLoading: boolean
isRepositoryLoading: boolean
pluginDetails?: PluginDetails
}
const PluginDetailsTable: FC<PluginDetailsTableProps> = ({
isPluginLoading,
isRepositoryLoading,
pluginDetails,
...paperProps
}) => (
<TableContainer component={Paper} {...paperProps}>
<Table>
<TableBody>
<TableRow>
<TableCell variant='head'>
{globalize.translate('LabelStatus')}
</TableCell>
<TableCell>
{
(isPluginLoading && <Skeleton />)
|| pluginDetails?.status
|| globalize.translate('LabelNotInstalled')
}
</TableCell>
</TableRow>
<TableRow>
<TableCell variant='head'>
{globalize.translate('LabelVersion')}
</TableCell>
<TableCell>
{
(isPluginLoading && <Skeleton />)
|| pluginDetails?.version?.version
}
</TableCell>
</TableRow>
<TableRow>
<TableCell variant='head'>
{globalize.translate('LabelDeveloper')}
</TableCell>
<TableCell>
{
(isRepositoryLoading && <Skeleton />)
|| pluginDetails?.owner
|| globalize.translate('Unknown')
}
</TableCell>
</TableRow>
<TableRow
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell variant='head'>
{globalize.translate('LabelRepository')}
</TableCell>
<TableCell>
{
(isRepositoryLoading && <Skeleton />)
|| (pluginDetails?.version?.repositoryUrl && (
<Link
component={RouterLink}
to={pluginDetails.version.repositoryUrl}
target='_blank'
rel='noopener noreferrer'
>
{pluginDetails.version.repositoryName}
</Link>
))
|| globalize.translate('Unknown')
}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
export default PluginDetailsTable;

View File

@@ -1,34 +0,0 @@
import Paper from '@mui/material/Paper/Paper';
import Skeleton from '@mui/material/Skeleton/Skeleton';
import React, { type FC } from 'react';
interface PluginImageProps {
isLoading: boolean
alt?: string
url?: string
}
const PluginImage: FC<PluginImageProps> = ({
isLoading,
alt,
url
}) => (
<Paper sx={{ width: '100%', aspectRatio: 16 / 9, overflow: 'hidden' }}>
{isLoading && (
<Skeleton
variant='rectangular'
width='100%'
height='100%'
/>
)}
{url && (
<img
src={url}
alt={alt}
width='100%'
/>
)}
</Paper>
);
export default PluginImage;

View File

@@ -1,67 +0,0 @@
import Download from '@mui/icons-material/Download';
import DownloadDone from '@mui/icons-material/DownloadDone';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Accordion from '@mui/material/Accordion/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary/AccordionSummary';
import Button from '@mui/material/Button/Button';
import Stack from '@mui/material/Stack/Stack';
import React, { type FC } from 'react';
import MarkdownBox from 'components/MarkdownBox';
import { parseISO8601Date, toLocaleString } from 'scripts/datetime';
import globalize from 'lib/globalize';
import type { PluginDetails } from '../types/PluginDetails';
import { VersionInfo } from '@jellyfin/sdk/lib/generated-client';
interface PluginRevisionsProps {
pluginDetails?: PluginDetails,
onInstall: (version?: VersionInfo) => () => void
}
const PluginRevisions: FC<PluginRevisionsProps> = ({
pluginDetails,
onInstall
}) => (
pluginDetails?.versions?.map(version => (
<Accordion key={version.checksum}>
<AccordionSummary
expandIcon={<ExpandMore />}
>
{version.version}
{version.timestamp && (<>
&nbsp;&mdash;&nbsp;
{toLocaleString(parseISO8601Date(version.timestamp))}
</>)}
</AccordionSummary>
<AccordionDetails>
<Stack spacing={2}>
<MarkdownBox
fallback={globalize.translate('LabelNoChangelog')}
markdown={version.changelog}
/>
{pluginDetails.status && version.version === pluginDetails.version?.version ? (
<Button
disabled
startIcon={<DownloadDone />}
variant='outlined'
>
{globalize.translate('LabelInstalled')}
</Button>
) : (
<Button
startIcon={<Download />}
variant='outlined'
onClick={onInstall(version)}
>
{globalize.translate('HeaderInstall')}
</Button>
)}
</Stack>
</AccordionDetails>
</Accordion>
))
);
export default PluginRevisions;

View File

@@ -1,15 +0,0 @@
import type { ConfigurationPageInfo, PluginStatus, VersionInfo } from '@jellyfin/sdk/lib/generated-client';
export interface PluginDetails {
canUninstall: boolean
description?: string
id: string
imageUrl?: string
isEnabled: boolean
name?: string
owner?: string
configurationPage?: ConfigurationPageInfo
status?: PluginStatus
version?: VersionInfo
versions: VersionInfo[]
}

View File

@@ -2,12 +2,13 @@ import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity', type: AsyncRouteType.Dashboard },
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard },
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AsyncRouteType.Dashboard },
{ path: 'dlna', type: AsyncRouteType.Dashboard },
{ path: 'notifications', type: AsyncRouteType.Dashboard },
{ path: 'users', type: AsyncRouteType.Dashboard },
{ path: 'users/access', type: AsyncRouteType.Dashboard },
{ path: 'users/add', type: AsyncRouteType.Dashboard },
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
{ path: 'users/password', type: AsyncRouteType.Dashboard },
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
{ path: 'users/profile', type: AsyncRouteType.Dashboard },
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard }
];

View File

@@ -31,6 +31,12 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/devices/device',
view: 'dashboard/devices/device.html'
}
}, {
path: 'plugins/add',
pageProps: {
controller: 'dashboard/plugins/add/index',
view: 'dashboard/plugins/add/index.html'
}
}, {
path: 'libraries',
pageProps: {

View File

@@ -1,12 +1,16 @@
import type { Redirect } from 'components/router/Redirect';
export const REDIRECTS: Redirect[] = [
{ from: 'addplugin.html', to: '/dashboard/plugins/add' },
{ from: 'apikeys.html', to: '/dashboard/keys' },
{ from: 'availableplugins.html', to: '/dashboard/plugins/catalog' },
{ from: 'dashboard.html', to: '/dashboard' },
{ from: 'dashboardgeneral.html', to: '/dashboard/settings' },
{ from: 'device.html', to: '/dashboard/devices/edit' },
{ from: 'devices.html', to: '/dashboard/devices' },
{ from: 'dlnaprofile.html', to: '/dashboard/dlna' },
{ from: 'dlnaprofiles.html', to: '/dashboard/dlna' },
{ from: 'dlnasettings.html', to: '/dashboard/dlna' },
{ from: 'edititemmetadata.html', to: '/metadata' },
{ from: 'encodingsettings.html', to: '/dashboard/playback/transcoding' },
{ from: 'installedplugins.html', to: '/dashboard/plugins' },
@@ -20,6 +24,7 @@ export const REDIRECTS: Redirect[] = [
{ from: 'metadataimages.html', to: '/dashboard/libraries/metadata' },
{ from: 'metadatanfo.html', to: '/dashboard/libraries/nfo' },
{ from: 'networking.html', to: '/dashboard/networking' },
{ from: 'notificationsettings.html', to: '/dashboard/notifications' },
{ from: 'playbackconfiguration.html', to: '/dashboard/playback/resume' },
{ from: 'repositories.html', to: '/dashboard/plugins/repositories' },
{ from: 'scheduledtask.html', to: '/dashboard/tasks/edit' },

View File

@@ -16,7 +16,7 @@ import Page from 'components/Page';
import UserAvatar from 'components/UserAvatar';
import { useApi } from 'hooks/useApi';
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { toBoolean } from 'utils/string';
import LogLevelChip from '../components/activityTable/LogLevelChip';
@@ -60,7 +60,7 @@ const Activity = () => {
field: 'User',
headerName: globalize.translate('LabelUser'),
width: 60,
valueGetter: ( value, row ) => users[row.UserId]?.Name,
valueGetter: ({ row }) => users[row.UserId]?.Name,
renderCell: ({ row }) => (
<IconButton
size='large'
@@ -82,16 +82,16 @@ const Activity = () => {
headerName: globalize.translate('LabelDate'),
width: 90,
type: 'date',
valueGetter: ( value ) => parseISO8601Date(value),
valueFormatter: ( value ) => toLocaleDateString(value)
valueGetter: ({ value }) => parseISO8601Date(value),
valueFormatter: ({ value }) => toLocaleDateString(value)
},
{
field: 'Time',
headerName: globalize.translate('LabelTime'),
width: 100,
type: 'dateTime',
valueGetter: ( value, row ) => parseISO8601Date(row.Date),
valueFormatter: ( value ) => toLocaleTimeString(value)
valueGetter: ({ row }) => parseISO8601Date(row.Date),
valueFormatter: ({ value }) => toLocaleTimeString(value)
},
{
field: 'Severity',
@@ -113,7 +113,7 @@ const Activity = () => {
field: 'Overview',
headerName: globalize.translate('LabelOverview'),
width: 200,
valueGetter: ( value, row ) => row.ShortOverview ?? row.Overview,
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
renderCell: ({ row }) => (
<OverviewCell {...row} />
)
@@ -147,7 +147,7 @@ const Activity = () => {
}
];
const onViewChange = useCallback((_e: React.MouseEvent<HTMLElement, MouseEvent>, newView: ActivityView | null) => {
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
if (newView !== null) {
setActivityView(newView);
}

View File

@@ -0,0 +1,33 @@
import Alert from '@mui/material/Alert/Alert';
import Box from '@mui/material/Box/Box';
import Button from '@mui/material/Button/Button';
import React from 'react';
import { Link } from 'react-router-dom';
import Page from 'components/Page';
import globalize from 'scripts/globalize';
const DlnaPage = () => (
<Page
id='dlnaSettingsPage'
title='DLNA'
className='mainAnimatedPage type-interior'
>
<div className='content-primary'>
<h2>DLNA</h2>
<Alert severity='info'>
<Box sx={{ marginBottom: 2 }}>
{globalize.translate('DlnaMovedMessage')}
</Box>
<Button
component={Link}
to='/dashboard/plugins/add?name=DLNA&guid=33eba9cd7da14720967fdd7dae7b74a1'
>
{globalize.translate('GetThePlugin')}
</Button>
</Alert>
</div>
</Page>
);
export default DlnaPage;

View File

@@ -0,0 +1,34 @@
import Alert from '@mui/material/Alert/Alert';
import Box from '@mui/material/Box/Box';
import Button from '@mui/material/Button/Button';
import React from 'react';
import { Link } from 'react-router-dom';
import Page from 'components/Page';
import globalize from 'scripts/globalize';
const NotificationsPage = () => (
<Page
id='notificationSettingPage'
title={globalize.translate('Notifications')}
className='mainAnimatedPage type-interior'
>
<div className='content-primary'>
<h2>{globalize.translate('Notifications')}</h2>
<Alert severity='info'>
<Box sx={{ marginBottom: 2 }}>
{globalize.translate('NotificationsMovedMessage')}
</Box>
<Button
component={Link}
to='/dashboard/plugins/add?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
>
{globalize.translate('GetThePlugin')}
</Button>
</Alert>
</div>
</Page>
);
export default NotificationsPage;

View File

@@ -1,9 +1,7 @@
import type { ServerConfiguration } from '@jellyfin/sdk/lib/generated-client/models/server-configuration';
import { TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client/models/trickplay-scan-behavior';
import { ProcessPriorityClass } from '@jellyfin/sdk/lib/generated-client/models/process-priority-class';
import React, { type FC, useCallback, useEffect, useRef } from 'react';
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
import React, { type FunctionComponent, useCallback, useEffect, useRef } from 'react';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import Page from '../../../../components/Page';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import ButtonElement from '../../../../elements/ButtonElement';
@@ -19,10 +17,10 @@ function onSaveComplete() {
toast(globalize.translate('SettingsSaved'));
}
const PlaybackTrickplay: FC = () => {
const PlaybackTrickplay: FunctionComponent = () => {
const element = useRef<HTMLDivElement>(null);
const loadConfig = useCallback((config: ServerConfiguration) => {
const loadConfig = useCallback((config) => {
const page = element.current;
const options = config.TrickplayOptions;
@@ -31,17 +29,17 @@ const PlaybackTrickplay: FC = () => {
return;
}
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options?.EnableHwAcceleration || false;
(page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked = options?.EnableHwEncoding || false;
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = (options?.ScanBehavior || TrickplayScanBehavior.NonBlocking);
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = (options?.ProcessPriority || ProcessPriorityClass.Normal);
(page.querySelector('#txtInterval') as HTMLInputElement).value = options?.Interval?.toString() || '10000';
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options?.WidthResolutions?.join(',') || '';
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options?.TileWidth?.toString() || '10';
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options?.TileHeight?.toString() || '10';
(page.querySelector('#txtQscale') as HTMLInputElement).value = options?.Qscale?.toString() || '4';
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options?.JpegQuality?.toString() || '90';
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options?.ProcessThreads?.toString() || '1';
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options.EnableHwAcceleration;
(page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked = options.EnableHwEncoding;
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = options.ScanBehavior;
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = options.ProcessPriority;
(page.querySelector('#txtInterval') as HTMLInputElement).value = options.Interval;
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options.WidthResolutions.join(',');
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options.TileWidth;
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options.TileHeight;
(page.querySelector('#txtQscale') as HTMLInputElement).value = options.Qscale;
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options.JpegQuality;
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options.ProcessThreads;
loading.hide();
}, []);

View File

@@ -1,443 +0,0 @@
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
import type { VersionInfo } from '@jellyfin/sdk/lib/generated-client/models/version-info';
import Alert from '@mui/material/Alert/Alert';
import Button from '@mui/material/Button/Button';
import Container from '@mui/material/Container/Container';
import FormControlLabel from '@mui/material/FormControlLabel/FormControlLabel';
import FormGroup from '@mui/material/FormGroup/FormGroup';
import Grid from '@mui/material/Grid/Grid';
import Skeleton from '@mui/material/Skeleton/Skeleton';
import Stack from '@mui/material/Stack/Stack';
import Switch from '@mui/material/Switch/Switch';
import Typography from '@mui/material/Typography/Typography';
import Delete from '@mui/icons-material/Delete';
import Download from '@mui/icons-material/Download';
import Settings from '@mui/icons-material/Settings';
import React, { type FC, useState, useCallback, useMemo } from 'react';
import { useSearchParams, Link as RouterLink, useParams } from 'react-router-dom';
import { findBestConfigurationPage } from 'apps/dashboard/features/plugins/api/configurationPage';
import { findBestPluginInfo } from 'apps/dashboard/features/plugins/api/pluginInfo';
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
import { useDisablePlugin } from 'apps/dashboard/features/plugins/api/useDisablePlugin';
import { useEnablePlugin } from 'apps/dashboard/features/plugins/api/useEnablePlugin';
import { useInstallPackage } from 'apps/dashboard/features/plugins/api/useInstallPackage';
import { usePackageInfo } from 'apps/dashboard/features/plugins/api/usePackageInfo';
import { usePlugins } from 'apps/dashboard/features/plugins/api/usePlugins';
import { useUninstallPlugin } from 'apps/dashboard/features/plugins/api/useUninstallPlugin';
import PluginImage from 'apps/dashboard/features/plugins/components/PluginImage';
import PluginDetailsTable from 'apps/dashboard/features/plugins/components/PluginDetailsTable';
import PluginRevisions from 'apps/dashboard/features/plugins/components/PluginRevisions';
import type { PluginDetails } from 'apps/dashboard/features/plugins/types/PluginDetails';
import ConfirmDialog from 'components/ConfirmDialog';
import Page from 'components/Page';
import { useApi } from 'hooks/useApi';
import globalize from 'lib/globalize';
import { getPluginUrl } from 'utils/dashboard';
import { getUri } from 'utils/api';
interface AlertMessage {
severity?: 'success' | 'info' | 'warning' | 'error'
messageKey: string
}
// Plugins from this url will be trusted and not prompt for confirmation when installing
const TRUSTED_REPO_URL = 'https://repo.jellyfin.org/';
const PluginPage: FC = () => {
const { api } = useApi();
const { pluginId } = useParams();
const [ searchParams ] = useSearchParams();
const disablePlugin = useDisablePlugin();
const enablePlugin = useEnablePlugin();
const installPlugin = useInstallPackage();
const uninstallPlugin = useUninstallPlugin();
const [ isEnabledOverride, setIsEnabledOverride ] = useState<boolean>();
const [ isInstallConfirmOpen, setIsInstallConfirmOpen ] = useState(false);
const [ isUninstallConfirmOpen, setIsUninstallConfirmOpen ] = useState(false);
const [ pendingInstallVersion, setPendingInstallVersion ] = useState<VersionInfo>();
const pluginName = searchParams.get('name') ?? undefined;
const {
data: configurationPages,
isError: isConfigurationPagesError,
isLoading: isConfigurationPagesLoading
} = useConfigurationPages();
const {
data: packageInfo,
isError: isPackageInfoError,
isLoading: isPackageInfoLoading
} = usePackageInfo(pluginName ? {
name: pluginName,
assemblyGuid: pluginId
} : undefined);
const {
data: plugins,
isLoading: isPluginsLoading,
isError: isPluginsError
} = usePlugins();
const isLoading =
isConfigurationPagesLoading || isPackageInfoLoading || isPluginsLoading;
const pluginDetails = useMemo<PluginDetails | undefined>(() => {
if (pluginId && !isPluginsLoading) {
const pluginInfo = findBestPluginInfo(pluginId, plugins);
let version;
if (pluginInfo) {
// Find the installed version
const repoVersion = packageInfo?.versions?.find(v => v.version === pluginInfo.Version);
version = repoVersion || {
version: pluginInfo.Version,
VersionNumber: pluginInfo.Version
};
} else {
// Use the latest version
version = packageInfo?.versions?.[0];
}
let imageUrl;
if (pluginInfo?.HasImage) {
imageUrl = getUri(`/Plugins/${pluginInfo.Id}/${pluginInfo.Version}/Image`, api);
}
return {
canUninstall: !!pluginInfo?.CanUninstall,
description: pluginInfo?.Description || packageInfo?.description || packageInfo?.overview,
id: pluginId,
imageUrl: imageUrl || packageInfo?.imageUrl || undefined,
isEnabled: (isEnabledOverride && pluginInfo?.Status === PluginStatus.Restart)
?? pluginInfo?.Status !== PluginStatus.Disabled,
name: pluginName || pluginInfo?.Name || packageInfo?.name,
owner: packageInfo?.owner,
status: pluginInfo?.Status,
configurationPage: findBestConfigurationPage(configurationPages || [], pluginId),
version,
versions: packageInfo?.versions || []
};
}
}, [
api,
configurationPages,
isEnabledOverride,
isPluginsLoading,
packageInfo?.description,
packageInfo?.imageUrl,
packageInfo?.name,
packageInfo?.overview,
packageInfo?.owner,
packageInfo?.versions,
pluginId,
pluginName,
plugins
]);
const alertMessages = useMemo(() => {
const alerts: AlertMessage[] = [];
if (disablePlugin.isError) {
alerts.push({ messageKey: 'PluginDisableError' });
}
if (enablePlugin.isError) {
alerts.push({ messageKey: 'PluginEnableError' });
}
if (installPlugin.isSuccess) {
alerts.push({
severity: 'success',
messageKey: 'MessagePluginInstalled'
});
}
if (installPlugin.isError) {
alerts.push({ messageKey: 'MessagePluginInstallError' });
}
if (uninstallPlugin.isError) {
alerts.push({ messageKey: 'PluginUninstallError' });
}
if (isConfigurationPagesError) {
alerts.push({ messageKey: 'PluginLoadConfigError' });
}
if (isPackageInfoError) {
alerts.push({
severity: 'warning',
messageKey: 'PluginLoadRepoError'
});
}
if (isPluginsError) {
alerts.push({ messageKey: 'MessageGetInstalledPluginsError' });
}
return alerts;
}, [
disablePlugin.isError,
enablePlugin.isError,
installPlugin.isError,
installPlugin.isSuccess,
isConfigurationPagesError,
isPackageInfoError,
isPluginsError,
uninstallPlugin.isError
]);
/** Enable/disable the plugin */
const toggleEnabled = useCallback(() => {
if (!pluginDetails?.version?.version) return;
console.debug('[PluginPage] %s plugin', pluginDetails.isEnabled ? 'disabling' : 'enabling', pluginDetails);
if (pluginDetails.isEnabled) {
disablePlugin.mutate({
pluginId: pluginDetails.id,
version: pluginDetails.version.version
}, {
onSuccess: () => {
setIsEnabledOverride(false);
},
onSettled: () => {
installPlugin.reset();
enablePlugin.reset();
uninstallPlugin.reset();
}
});
} else {
enablePlugin.mutate({
pluginId: pluginDetails.id,
version: pluginDetails.version.version
}, {
onSuccess: () => {
setIsEnabledOverride(true);
},
onSettled: () => {
installPlugin.reset();
disablePlugin.reset();
uninstallPlugin.reset();
}
});
}
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
/** Install the plugin or prompt for confirmation if untrusted */
const onInstall = useCallback((version?: VersionInfo, isConfirmed = false) => () => {
if (!pluginDetails?.name) return;
const installVersion = version || pluginDetails.version;
if (!installVersion) return;
if (!isConfirmed && !installVersion.repositoryUrl?.startsWith(TRUSTED_REPO_URL)) {
console.debug('[PluginPage] plugin install needs confirmed', installVersion);
setPendingInstallVersion(installVersion);
setIsInstallConfirmOpen(true);
return;
}
console.debug('[PluginPage] installing plugin', installVersion);
installPlugin.mutate({
name: pluginDetails.name,
assemblyGuid: pluginDetails.id,
version: installVersion.version,
repositoryUrl: installVersion.repositoryUrl
}, {
onSettled: () => {
setPendingInstallVersion(undefined);
disablePlugin.reset();
enablePlugin.reset();
uninstallPlugin.reset();
}
});
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
/** Confirm and install the plugin */
const onConfirmInstall = useCallback(() => {
console.debug('[PluginPage] confirmed installing plugin', pendingInstallVersion);
setIsInstallConfirmOpen(false);
onInstall(pendingInstallVersion, true)();
}, [ onInstall, pendingInstallVersion ]);
/** Close the install confirmation dialog */
const onCloseInstallConfirmDialog = useCallback(() => {
setPendingInstallVersion(undefined);
setIsInstallConfirmOpen(false);
}, []);
/** Show the uninstall confirmation dialog */
const onConfirmUninstall = useCallback(() => {
setIsUninstallConfirmOpen(true);
}, []);
/** Uninstall the plugin */
const onUninstall = useCallback(() => {
if (!pluginDetails?.version?.version) return;
console.debug('[PluginPage] uninstalling plugin', pluginDetails);
setIsUninstallConfirmOpen(false);
uninstallPlugin.mutate({
pluginId: pluginDetails.id,
version: pluginDetails.version.version
}, {
onSettled: () => {
disablePlugin.reset();
enablePlugin.reset();
installPlugin.reset();
}
});
}, [ disablePlugin, enablePlugin, installPlugin, pluginDetails, uninstallPlugin ]);
/** Close the uninstall confirmation dialog */
const onCloseUninstallConfirmDialog = useCallback(() => {
setIsUninstallConfirmOpen(false);
}, []);
return (
<Page
id='addPluginPage'
className='mainAnimatedPage type-interior'
>
<Container className='content-primary'>
{alertMessages.map(({ severity = 'error', messageKey }) => (
<Alert key={messageKey} severity={severity}>
{globalize.translate(messageKey)}
</Alert>
))}
<Grid container spacing={2} sx={{ marginTop: 0 }}>
<Grid item xs={12} lg={8}>
<Stack spacing={2}>
<Typography variant='h1'>
{pluginDetails?.name || pluginName}
</Typography>
<Typography sx={{ maxWidth: '80ch' }}>
{isLoading && !pluginDetails?.description ? (
<Skeleton />
) : (
pluginDetails?.description
)}
</Typography>
</Stack>
</Grid>
<Grid item lg={4} sx={{ display: { xs: 'none', lg: 'initial' } }}>
<PluginImage
isLoading={isLoading}
alt={pluginDetails?.name}
url={pluginDetails?.imageUrl}
/>
</Grid>
<Grid item xs={12} lg={8} sx={{ order: { xs: 1, lg: 'initial' } }}>
{!!pluginDetails?.versions.length && (
<>
<Typography variant='h3' sx={{ marginBottom: 2 }}>
{globalize.translate('HeaderRevisionHistory')}
</Typography>
<PluginRevisions
pluginDetails={pluginDetails}
onInstall={onInstall}
/>
</>
)}
</Grid>
<Grid item xs={12} lg={4}>
<Stack spacing={2} direction={{ xs: 'column', sm: 'row-reverse', lg: 'column' }}>
<Stack spacing={1} sx={{ flexBasis: '50%' }}>
{!isLoading && !pluginDetails?.status && (
<>
<Alert severity='info'>
{globalize.translate('ServerRestartNeededAfterPluginInstall')}
</Alert>
<Button
startIcon={<Download />}
onClick={onInstall()}
>
{globalize.translate('HeaderInstall')}
</Button>
</>
)}
{!isLoading && pluginDetails?.canUninstall && (
<FormGroup>
<FormControlLabel
control={
<Switch
checked={pluginDetails.isEnabled}
onChange={toggleEnabled}
disabled={pluginDetails.status === PluginStatus.Restart}
/>
}
label={globalize.translate('LabelEnablePlugin')}
/>
</FormGroup>
)}
{!isLoading && pluginDetails?.configurationPage?.Name && (
<Button
component={RouterLink}
to={`/${getPluginUrl(pluginDetails.configurationPage.Name)}`}
startIcon={<Settings />}
>
{globalize.translate('Settings')}
</Button>
)}
{!isLoading && pluginDetails?.canUninstall && (
<Button
color='error'
startIcon={<Delete />}
onClick={onConfirmUninstall}
>
{globalize.translate('ButtonUninstall')}
</Button>
)}
</Stack>
<PluginDetailsTable
isPluginLoading={isPluginsLoading}
isRepositoryLoading={isPackageInfoLoading}
pluginDetails={pluginDetails}
sx={{ flexBasis: '50%' }}
/>
</Stack>
</Grid>
</Grid>
</Container>
<ConfirmDialog
open={isInstallConfirmOpen}
title={globalize.translate('HeaderConfirmPluginInstallation')}
text={globalize.translate('MessagePluginInstallDisclaimer')}
onCancel={onCloseInstallConfirmDialog}
onConfirm={onConfirmInstall}
confirmButtonText={globalize.translate('HeaderInstall')}
/>
<ConfirmDialog
open={isUninstallConfirmOpen}
title={globalize.translate('HeaderUninstallPlugin')}
text={globalize.translate('UninstallPluginConfirmation', pluginName || '')}
onCancel={onCloseUninstallConfirmDialog}
onConfirm={onUninstall}
confirmButtonColor='error'
confirmButtonText={globalize.translate('ButtonUninstall')}
/>
</Page>
);
};
export default PluginPage;

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { RouteObject } from 'react-router-dom';
import AppLayout from '../AppLayout';
import ConnectionRequired from 'components/ConnectionRequired';
import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { LEGACY_ADMIN_ROUTES } from './_legacyRoutes';
import ServerContentPage from 'components/ServerContentPage';
import ErrorBoundary from 'components/router/ErrorBoundary';
export const DASHBOARD_APP_PATHS = {
Dashboard: 'dashboard',
@@ -19,15 +19,14 @@ export const DASHBOARD_APP_ROUTES: RouteObject[] = [
element: <ConnectionRequired isAdminRequired />,
children: [
{
lazy: () => import('../AppLayout'),
element: <AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />,
children: [
{
path: DASHBOARD_APP_PATHS.Dashboard,
children: [
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
],
errorElement: <ErrorBoundary pageClasses={[ 'type-interior' ]} />
]
},
/* NOTE: The metadata editor might deserve a dedicated app in the future */

View File

@@ -1,10 +1,10 @@
import type { BaseItemDto, DeviceInfo, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { useCallback, useEffect, useState, useRef } from 'react';
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';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import toast from '../../../../components/toast/toast';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import ButtonElement from '../../../../elements/ButtonElement';
@@ -14,13 +14,13 @@ import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../../../components/Page';
type ItemsArr = {
Name?: string | null;
Id?: string | null;
AppName?: string | null;
Name?: string;
Id?: string;
AppName?: string;
checkedAttribute?: string
};
const UserLibraryAccess = () => {
const UserLibraryAccess: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
@@ -35,7 +35,7 @@ const UserLibraryAccess = () => {
select.dispatchEvent(evt);
};
const loadMediaFolders = useCallback((user: UserDto, mediaFolders: BaseItemDto[]) => {
const loadMediaFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
@@ -46,7 +46,7 @@ const UserLibraryAccess = () => {
const itemsArr: ItemsArr[] = [];
for (const folder of mediaFolders) {
const isChecked = user.Policy?.EnableAllFolders || user.Policy?.EnabledFolders?.indexOf(folder.Id || '') != -1;
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
@@ -58,11 +58,11 @@ const UserLibraryAccess = () => {
setMediaFoldersItems(itemsArr);
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
chkEnableAllFolders.checked = Boolean(user.Policy?.EnableAllFolders);
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}, []);
const loadChannels = useCallback((user: UserDto, channels: BaseItemDto[]) => {
const loadChannels = useCallback((user, channels) => {
const page = element.current;
if (!page) {
@@ -73,7 +73,7 @@ const UserLibraryAccess = () => {
const itemsArr: ItemsArr[] = [];
for (const folder of channels) {
const isChecked = user.Policy?.EnableAllChannels || user.Policy?.EnabledChannels?.indexOf(folder.Id || '') != -1;
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
@@ -91,11 +91,11 @@ const UserLibraryAccess = () => {
}
const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
chkEnableAllChannels.checked = Boolean(user.Policy?.EnableAllChannels);
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels);
}, []);
const loadDevices = useCallback((user: UserDto, devices: DeviceInfo[]) => {
const loadDevices = useCallback((user, devices) => {
const page = element.current;
if (!page) {
@@ -106,7 +106,7 @@ const UserLibraryAccess = () => {
const itemsArr: ItemsArr[] = [];
for (const device of devices) {
const isChecked = user.Policy?.EnableAllDevices || user.Policy?.EnabledDevices?.indexOf(device.Id || '') != -1;
const isChecked = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: device.Id,
@@ -119,18 +119,18 @@ const UserLibraryAccess = () => {
setDevicesItems(itemsArr);
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
chkEnableAllDevices.checked = Boolean(user.Policy?.EnableAllDevices);
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy?.IsAdministrator) {
if (user.Policy.IsAdministrator) {
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
}
}, []);
const loadUser = useCallback((user: UserDto, mediaFolders: BaseItemDto[], channels: BaseItemDto[], devices: DeviceInfo[]) => {
setUserName(user.Name || '');
const loadUser = useCallback((user, mediaFolders, channels, devices) => {
setUserName(user.Name);
libraryMenu.setTitle(user.Name);
loadChannels(user, channels);
loadMediaFolders(user, mediaFolders);

View File

@@ -1,8 +1,7 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
@@ -18,16 +17,16 @@ type userInput = {
};
type ItemsArr = {
Name?: string | null;
Name?: string;
Id?: string;
};
const UserNew = () => {
const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
const getItemsResult = (items: BaseItemDto[]) => {
const getItemsResult = (items: ItemsArr[]) => {
return items.map(item =>
({
Id: item.Id,
@@ -36,7 +35,7 @@ const UserNew = () => {
);
};
const loadMediaFolders = useCallback((result: BaseItemDto[]) => {
const loadMediaFolders = useCallback((result) => {
const page = element.current;
if (!page) {
@@ -54,7 +53,7 @@ const UserNew = () => {
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
}, []);
const loadChannels = useCallback((result: BaseItemDto[]) => {
const loadChannels = useCallback((result) => {
const page = element.current;
if (!page) {

View File

@@ -1,8 +1,8 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { useEffect, useState, useRef } from 'react';
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import loading from '../../../../components/loading/loading';
import dom from '../../../../scripts/dom';
import confirm from '../../../../components/confirm/confirm';
@@ -21,7 +21,7 @@ type MenuEntry = {
icon?: string;
};
const UserProfiles = () => {
const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);

View File

@@ -1,11 +1,10 @@
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
import { UnratedItem } from '@jellyfin/sdk/lib/generated-client/models/unrated-item';
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import escapeHTML from 'escape-html';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu';
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
import TagList from '../../../../components/dashboard/users/TagList';
@@ -20,12 +19,9 @@ import Page from '../../../../components/Page';
import prompt from '../../../../components/prompt/prompt';
import ServerConnections from 'components/ServerConnections';
type NamedItem = {
type UnratedItem = {
name: string;
value: UnratedItem;
};
type UnratedNamedItem = NamedItem & {
value: string;
checkedAttribute: string
};
@@ -60,19 +56,19 @@ function handleSaveUser(
};
}
const UserParentalControl = () => {
const UserParentalControl: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedNamedItem[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => {
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: ParentalRating[] = [];
@@ -97,7 +93,7 @@ const UserParentalControl = () => {
setParentalRatings(ratings);
}, []);
const loadUnratedItems = useCallback((user: UserDto) => {
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
@@ -105,42 +101,42 @@ const UserParentalControl = () => {
return;
}
const items: NamedItem[] = [{
const items = [{
name: globalize.translate('Books'),
value: UnratedItem.Book
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: UnratedItem.ChannelContent
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: UnratedItem.LiveTvChannel
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: UnratedItem.Movie
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: UnratedItem.Music
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: UnratedItem.Trailer
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: UnratedItem.Series
value: 'Series'
}];
const unratedNamedItem: UnratedNamedItem[] = [];
const itemsArr: UnratedItem[] = [];
for (const item of items) {
const isChecked = user.Policy?.BlockUnratedItems?.indexOf(item.value) != -1;
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
unratedNamedItem.push({
itemsArr.push({
value: item.value,
name: item.name,
checkedAttribute: checkedAttribute
});
}
setUnratedItems(unratedNamedItem);
setUnratedItems(itemsArr);
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
@@ -188,7 +184,7 @@ const UserParentalControl = () => {
}
}, []);
const renderAccessSchedule = useCallback((schedules: AccessSchedule[]) => {
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
@@ -204,7 +200,7 @@ const UserParentalControl = () => {
btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter((_, i) => i != index);
const newindex = schedules.filter((i: number) => i != index);
renderAccessSchedule(newindex);
});
}
@@ -235,7 +231,7 @@ const UserParentalControl = () => {
});
}
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = String(ratingValue);
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy?.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
@@ -7,7 +7,7 @@ import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Page from '../../../../components/Page';
import loading from '../../../../components/loading/loading';
const UserPassword = () => {
const UserPassword: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');

View File

@@ -1,10 +1,10 @@
import type { BaseItemDto, NameIdPair, SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import escapeHTML from 'escape-html';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu';
import ButtonElement from '../../../../elements/ButtonElement';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
@@ -17,10 +17,15 @@ import toast from '../../../../components/toast/toast';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
type ResetProvider = BaseItemDto & {
type ResetProvider = AuthProvider & {
checkedAttribute: string
};
type AuthProvider = {
Name?: string;
Id?: string;
};
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
Array.prototype.filter.call(elements, e => e.checked)
.map(e => e.getAttribute('data-id'))
@@ -35,13 +40,13 @@ function onSaveComplete() {
toast(globalize.translate('SettingsSaved'));
}
const UserEdit = () => {
const UserEdit: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<NameIdPair[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<NameIdPair[]>([]);
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
@@ -58,27 +63,48 @@ const UserEdit = () => {
return window.ApiClient.getUser(userId);
};
const loadAuthProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => {
const loadAuthProviders = useCallback((user, providers) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
setAuthProviders(providers);
const currentProviderId = user.Policy?.AuthenticationProviderId || '';
const currentProviderId = user.Policy.AuthenticationProviderId;
setAuthenticationProviderId(currentProviderId);
}, []);
const loadPasswordResetProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => {
const loadPasswordResetProviders = useCallback((user, providers) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
setPasswordResetProviders(providers);
const currentProviderId = user.Policy?.PasswordResetProviderId || '';
const currentProviderId = user.Policy.PasswordResetProviderId;
setPasswordResetProviderId(currentProviderId);
}, []);
const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, mediaFolders: BaseItemDto[]) => {
const loadDeleteFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
@@ -86,20 +112,22 @@ const UserEdit = () => {
let checkedAttribute;
const itemsArr: ResetProvider[] = [];
for (const mediaFolder of mediaFolders) {
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1;
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
...mediaFolder,
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
for (const channel of channelsResult.Items) {
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(channel.Id || '') != -1;
for (const folder of channelsResult.Items) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
...channel,
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
@@ -107,14 +135,14 @@ const UserEdit = () => {
setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders);
}).catch(err => {
console.error('[useredit] failed to fetch channels', err);
});
}, []);
const loadUser = useCallback((user: UserDto) => {
const loadUser = useCallback((user) => {
const page = element.current;
if (!page) {
@@ -123,25 +151,25 @@ const UserEdit = () => {
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(page, user, providers);
loadAuthProviders(user, providers);
}).catch(err => {
console.error('[useredit] failed to fetch auth providers', err);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(page, user, providers);
loadPasswordResetProviders(user, providers);
}).catch(err => {
console.error('[useredit] failed to fetch password reset providers', err);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
})).then(function (folders) {
loadDeleteFolders(page, user, folders.Items);
loadDeleteFolders(user, folders.Items);
}).catch(err => {
console.error('[useredit] failed to fetch media folders', err);
});
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled);
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false;
@@ -150,30 +178,30 @@ const UserEdit = () => {
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
LibraryMenu.setTitle(user.Name);
setUserName(user.Name || '');
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || '';
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled;
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = !!user.Policy?.IsHidden;
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = !!user.Policy?.EnableCollectionManagement;
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = !!user.Policy?.EnableSubtitleManagement;
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = !!user.Policy?.EnableSharedDeviceControl;
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = !!user.Policy?.EnableRemoteControlOfOtherUsers;
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = !!user.Policy?.EnableContentDownloading;
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvManagement;
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvAccess;
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = !!user.Policy?.EnableMediaPlayback;
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableAudioPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableVideoPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = !!user.Policy?.EnablePlaybackRemuxing;
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = !!user.Policy?.ForceRemoteSourceTranscoding;
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy?.EnableRemoteAccess == null || user.Policy?.EnableRemoteAccess;
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy?.RemoteClientBitrateLimit && user.Policy?.RemoteClientBitrateLimit > 0 ?
(user.Policy?.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.SyncPlayAccess) || '0';
setUserName(user.Name);
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
(page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = user.Policy.EnableSubtitleManagement;
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) {
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess);
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);

View File

@@ -15,7 +15,7 @@ import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
import './AppOverrides.scss';
export const Component = () => {
const AppLayout = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const { user } = useApi();
const location = useLocation();
@@ -29,7 +29,7 @@ export const Component = () => {
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ position: 'relative', display: 'flex' }}>
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
@@ -76,3 +76,5 @@ export const Component = () => {
</Box>
);
};
export default AppLayout;

View File

@@ -7,6 +7,10 @@ $mui-bp-xl: 1536px;
$drawer-width: 240px;
#reactRoot {
height: 100%;
}
// Fix main pages layout to work with drawer
.mainAnimatedPage {
@media all and (min-width: $mui-bp-md) {

View File

@@ -8,7 +8,7 @@ import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import Events from 'utils/events';
import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
@@ -33,7 +33,7 @@ const RemotePlayButton = () => {
const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl);
const onRemotePlayButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
const onRemotePlayButtonClick = useCallback((event) => {
setRemotePlayMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayMenuAnchorEl ]);
@@ -44,7 +44,7 @@ const RemotePlayButton = () => {
const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl);
const onRemotePlayActiveButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
const onRemotePlayActiveButtonClick = useCallback((event) => {
setRemotePlayActiveMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayActiveMenuAnchorEl ]);

View File

@@ -1,62 +0,0 @@
import React, { type FC } from 'react';
import {
Link,
URLSearchParamsInit,
createSearchParams,
useLocation,
useSearchParams
} from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import globalize from 'lib/globalize';
const getUrlParams = (searchParams: URLSearchParams) => {
const parentId =
searchParams.get('parentId') || searchParams.get('topParentId');
const collectionType = searchParams.get('collectionType');
const params: URLSearchParamsInit = {};
if (parentId) {
params.parentId = parentId;
}
if (collectionType) {
params.collectionType = collectionType;
}
return params;
};
interface SearchButtonProps {
isTabsAvailable: boolean;
}
const SearchButton: FC<SearchButtonProps> = ({ isTabsAvailable }) => {
const location = useLocation();
const [searchParams] = useSearchParams();
const isSearchPath = location.pathname === '/search.html';
const createSearchLink = isTabsAvailable ?
{
pathname: '/search.html',
search: `?${createSearchParams(getUrlParams(searchParams))}`
} :
'/search.html';
return (
<Tooltip title={globalize.translate('Search')}>
<IconButton
size='large'
aria-label={globalize.translate('Search')}
color='inherit'
component={Link}
disabled={isSearchPath}
to={createSearchLink}
>
<SearchIcon />
</IconButton>
</Tooltip>
);
};
export default SearchButton;

View File

@@ -6,7 +6,7 @@ import React, { useCallback, useState } from 'react';
import { pluginManager } from 'components/pluginManager';
import { useApi } from 'hooks/useApi';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { PluginType } from 'types/plugin';
import AppSyncPlayMenu, { ID } from './menus/SyncPlayMenu';
@@ -17,7 +17,7 @@ const SyncPlayButton = () => {
const [ syncPlayMenuAnchorEl, setSyncPlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isSyncPlayMenuOpen = Boolean(syncPlayMenuAnchorEl);
const onSyncPlayButtonClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
const onSyncPlayButtonClick = useCallback((event) => {
setSyncPlayMenuAnchorEl(event.currentTarget);
}, [ setSyncPlayMenuAnchorEl ]);

View File

@@ -1,10 +1,15 @@
import React, { type FC } from 'react';
import { useLocation } from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import React, { FC } from 'react';
import { Link, useLocation } from 'react-router-dom';
import AppToolbar from 'components/toolbar/AppToolbar';
import globalize from 'scripts/globalize';
import AppTabs from '../tabs/AppTabs';
import RemotePlayButton from './RemotePlayButton';
import SyncPlayButton from './SyncPlayButton';
import SearchButton from './SearchButton';
import { isTabPath } from '../tabs/tabRoutes';
interface AppToolbarProps {
@@ -40,7 +45,18 @@ const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
<>
<SyncPlayButton />
<RemotePlayButton />
<SearchButton isTabsAvailable={isTabsAvailable} />
<Tooltip title={globalize.translate('Search')}>
<IconButton
size='large'
aria-label={globalize.translate('Search')}
color='inherit'
component={Link}
to='/search.html'
>
<SearchIcon />
</IconButton>
</Tooltip>
</>
)}
isDrawerAvailable={isDrawerAvailable}

View File

@@ -13,7 +13,7 @@ import React, { FC, useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import { enable, isEnabled } from 'scripts/autocast';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
interface RemotePlayActiveMenuProps extends MenuProps {
onMenuClose: () => void

View File

@@ -6,7 +6,7 @@ import Menu, { type MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import React, { FC, useEffect, useState } from 'react';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { playbackManager } from 'components/playback/playbackmanager';
import { pluginManager } from 'components/pluginManager';
import type { PlayTarget } from 'types/playTarget';

View File

@@ -20,9 +20,9 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
import { pluginManager } from 'components/pluginManager';
import { useApi } from 'hooks/useApi';
import { useSyncPlayGroups } from 'hooks/useSyncPlayGroups';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { PluginType } from 'types/plugin';
import Events, { Event } from 'utils/events';
import Events from 'utils/events';
export const ID = 'app-sync-play-menu';
@@ -136,7 +136,7 @@ const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
}
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
const updateSyncPlayGroup = useCallback((_e: Event, enabled: boolean) => {
const updateSyncPlayGroup = useCallback((_e, enabled) => {
if (syncPlay && enabled) {
setCurrentGroup(syncPlay.Manager.getGroupInfo() ?? undefined);
} else {

View File

@@ -18,7 +18,7 @@ import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import { useUserViews } from 'hooks/useUserViews';
import { useWebConfig } from 'hooks/useWebConfig';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import LibraryIcon from '../LibraryIcon';
import DrawerHeaderLink from './DrawerHeaderLink';

View File

@@ -2,7 +2,7 @@ import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/bas
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import React, { FC } from 'react';
import { useGetGenres } from 'hooks/useFetchItems';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import Loading from 'components/loading/LoadingComponent';
import GenresSectionContainer from './GenresSectionContainer';
import type { ParentId } from 'types/library';

View File

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import { ButtonGroup, IconButton } from '@mui/material';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import ViewListIcon from '@mui/icons-material/ViewList';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { LibraryViewSettings, ViewMode } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
import ViewSettingsButton from './ViewSettingsButton';

View File

@@ -72,7 +72,7 @@ const ItemsView: FC<ItemsViewProps> = ({
const {
isLoading,
data: itemsResult,
isPlaceholderData,
isPreviousData,
refetch
} = useGetItemsViewByType(
viewType,
@@ -228,7 +228,7 @@ const ItemsView: FC<ItemsViewProps> = ({
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPlaceholderData={isPlaceholderData}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
)}
@@ -312,7 +312,7 @@ const ItemsView: FC<ItemsViewProps> = ({
<Pagination
totalRecordCount={totalRecordCount}
libraryViewSettings={libraryViewSettings}
isPlaceholderData={isPlaceholderData}
isPreviousData={isPreviousData}
setLibraryViewSettings={setLibraryViewSettings}
/>
</Box>

View File

@@ -1,7 +1,7 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
const NewCollectionButton: FC = () => {
const showCollectionEditor = useCallback(() => {

View File

@@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
import ButtonGroup from '@mui/material/ButtonGroup';
import IconButton from '@mui/material/IconButton';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import * as userSettings from 'scripts/settings/userSettings';
import { LibraryViewSettings } from 'types/library';
@@ -13,14 +13,14 @@ interface PaginationProps {
libraryViewSettings: LibraryViewSettings;
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
totalRecordCount: number;
isPlaceholderData: boolean
isPreviousData: boolean
}
const Pagination: FC<PaginationProps> = ({
libraryViewSettings,
setLibraryViewSettings,
totalRecordCount,
isPlaceholderData
isPreviousData
}) => {
const limit = userSettings.libraryPageSize(undefined);
const startIndex = libraryViewSettings.StartIndex ?? 0;
@@ -65,7 +65,7 @@ const Pagination: FC<PaginationProps> = ({
<IconButton
title={globalize.translate('Previous')}
className='paper-icon-button-light btnPreviousPage autoSize'
disabled={startIndex == 0 || isPlaceholderData}
disabled={startIndex == 0 || isPreviousData}
onClick={onPreviousPageClick}
>
<ArrowBackIcon />
@@ -74,7 +74,7 @@ const Pagination: FC<PaginationProps> = ({
<IconButton
title={globalize.translate('Next')}
className='paper-icon-button-light btnNextPage autoSize'
disabled={startIndex + limit >= totalRecordCount || isPlaceholderData }
disabled={startIndex + limit >= totalRecordCount || isPreviousData }
onClick={onNextPageClick}
>
<ArrowForwardIcon />

View File

@@ -4,7 +4,7 @@ import { IconButton } from '@mui/material';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { getFiltersQuery } from 'utils/items';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';

View File

@@ -1,7 +1,7 @@
import React, { type FC } from 'react';
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
import { appRouter } from 'components/router/appRouter';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import Loading from 'components/loading/LoadingComponent';
import SectionContainer from './SectionContainer';
import { CardShape } from 'utils/card';

View File

@@ -4,7 +4,7 @@ import { IconButton } from '@mui/material';
import QueueIcon from '@mui/icons-material/Queue';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
interface QueueButtonProps {
item: BaseItemDto | undefined

View File

@@ -5,7 +5,7 @@ import { IconButton } from '@mui/material';
import ShuffleIcon from '@mui/icons-material/Shuffle';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { getFiltersQuery } from 'utils/items';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';

View File

@@ -10,7 +10,7 @@ import FormControl from '@mui/material/FormControl';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import SortByAlphaIcon from '@mui/icons-material/SortByAlpha';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
@@ -36,12 +36,6 @@ const movieOrFavoriteOptions = [
{ label: 'Runtime', value: ItemSortBy.Runtime }
];
const photosOrPhotoAlbumsOptions = [
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionRandom', value: ItemSortBy.Random },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated }
];
const sortOptionsMapping: SortOptionsMapping = {
[LibraryTab.Movies]: movieOrFavoriteOptions,
[LibraryTab.Trailers]: [
@@ -95,16 +89,6 @@ const sortOptionsMapping: SortOptionsMapping = {
{ label: 'OptionReleaseDate', value: ItemSortBy.PremiereDate },
{ label: 'Runtime', value: ItemSortBy.Runtime },
{ label: 'OptionRandom', value: ItemSortBy.Random }
],
[LibraryTab.PhotoAlbums]: photosOrPhotoAlbumsOptions,
[LibraryTab.Photos]: photosOrPhotoAlbumsOptions,
[LibraryTab.Videos]: [
{ label: 'Name', value: ItemSortBy.SortName },
{ label: 'OptionDateAdded', value: ItemSortBy.DateCreated },
{ label: 'OptionDatePlayed', value: ItemSortBy.DatePlayed },
{ label: 'OptionPlayCount', value: ItemSortBy.PlayCount },
{ label: 'Runtime', value: ItemSortBy.Runtime },
{ label: 'OptionRandom', value: ItemSortBy.Random }
]
};

View File

@@ -8,7 +8,7 @@ import {
useGetSuggestionSectionsWithItems
} from 'hooks/useFetchItems';
import { appRouter } from 'components/router/appRouter';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import Loading from 'components/loading/LoadingComponent';
import SectionContainer from './SectionContainer';
import { CardShape } from 'utils/card';

View File

@@ -2,7 +2,7 @@ import React, { type FC } from 'react';
import Box from '@mui/material/Box';
import { useGetGroupsUpcomingEpisodes } from 'hooks/useFetchItems';
import Loading from 'components/loading/LoadingComponent';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import SectionContainer from './SectionContainer';
import { CardShape } from 'utils/card';
import type { LibraryViewProps } from 'types/library';

View File

@@ -15,7 +15,7 @@ import Select, { SelectChangeEvent } from '@mui/material/Select';
import Popover from '@mui/material/Popover';
import ViewComfyIcon from '@mui/icons-material/ViewComfy';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { LibraryTab } from 'types/libraryTab';

View File

@@ -15,7 +15,7 @@ import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import { useGetQueryFiltersLegacy, useGetStudios } from 'hooks/useFetchItems';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import FiltersFeatures from './FiltersFeatures';
import FiltersGenres from './FiltersGenres';

View File

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { EpisodeFilter, LibraryViewSettings } from 'types/library';
const episodeFilterOptions = [

View File

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { FeatureFilters, LibraryViewSettings } from 'types/library';
const featuresOptions = [

View File

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client';

View File

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { LibraryViewSettings } from 'types/library';
import { ItemFilter } from '@jellyfin/sdk/lib/generated-client';
import { LibraryTab } from 'types/libraryTab';
@@ -56,8 +56,6 @@ const FiltersStatus: FC<FiltersStatusProps> = ({
&& viewType !== LibraryTab.AlbumArtists
&& viewType !== LibraryTab.Songs
&& viewType !== LibraryTab.Channels
&& viewType !== LibraryTab.PhotoAlbums
&& viewType !== LibraryTab.Photos
) {
visibleFiltersStatus.push(ItemFilter.IsUnplayed);
visibleFiltersStatus.push(ItemFilter.IsPlayed);

View File

@@ -1,4 +1,4 @@
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import * as userSettings from 'scripts/settings/userSettings';
import { LibraryTab } from 'types/libraryTab';
@@ -184,28 +184,6 @@ const TabRoutes: TabRoute[] = [
value: LibraryTab.Episodes
}
]
},
{
path: '/homevideos.html',
tabs: [
{
index: 0,
label: globalize.translate('Photos'),
value: LibraryTab.Photos,
isDefault: true
},
{
index: 1,
label: globalize.translate('HeaderPhotoAlbums'),
value: LibraryTab.PhotoAlbums,
isDefault: true
},
{
index: 2,
label: globalize.translate('HeaderVideos'),
value: LibraryTab.Videos
}
]
}
];

View File

@@ -1,15 +1,13 @@
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
{ path: 'quickconnect', page: 'quickConnect' },
{ path: 'search.html', page: 'search' },
{ path: 'userprofile.html', page: 'user/userprofile' },
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental },
{ path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental },
{ path: 'music.html', page: 'music', type: AsyncRouteType.Experimental },
{ path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental },
{ path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental },
{ path: 'homevideos.html', page: 'homevideos', type: AsyncRouteType.Experimental }
{ path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental }
];

View File

@@ -1,14 +1,15 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import globalize from '../../../lib/globalize';
import globalize from '../../../scripts/globalize';
import LibraryMenu from '../../../scripts/libraryMenu';
import { clearBackdrop } from '../../../components/backdrop/backdrop';
import layoutManager from '../../../components/layoutManager';
import Page from '../../../components/Page';
import * as mainTabsManager from '../../../components/maintabsmanager';
import '../../../elements/emby-tabs/emby-tabs';
import '../../../elements/emby-button/emby-button';
import '../../../elements/emby-scroller/emby-scroller';
import Page from '../../../components/Page';
type OnResumeOptions = {
autoFocus?: boolean;
@@ -24,18 +25,16 @@ type ControllerProps = {
destroy: () => void;
};
const Home = () => {
const Home: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const initialTabIndex = parseInt(searchParams.get('tab') ?? '0', 10);
const libraryMenu = useMemo(async () => ((await import('../../../scripts/libraryMenu')).default), []);
const mainTabsManager = useMemo(() => import('../../../components/maintabsmanager'), []);
const tabController = useRef<ControllerProps | null>();
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
const element = useRef<HTMLDivElement>(null);
const setTitle = async () => {
(await libraryMenu).setTitle(null);
const setTitle = () => {
LibraryMenu.setTitle(null);
};
const getTabs = () => {
@@ -79,6 +78,18 @@ const Home = () => {
});
}, [ tabControllers ]);
const onViewDestroy = useCallback(() => {
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
}
tabController.current = null;
}, [ tabControllers ]);
const loadTab = useCallback((index: number, previousIndex: number | null) => {
getTabController(index).then((controller) => {
const refresh = !controller.refreshed;
@@ -107,23 +118,19 @@ const Home = () => {
loadTab(newIndex, previousIndex);
}, [ loadTab, tabControllers ]);
const onSetTabs = useCallback(async () => {
(await mainTabsManager).setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
}, [ initialTabIndex, mainTabsManager, onTabChange ]);
const onResume = useCallback(async () => {
void setTitle();
const onResume = useCallback(() => {
setTitle();
clearBackdrop();
const currentTabController = tabController.current;
if (!currentTabController) {
(await mainTabsManager).selectedTabIndex(initialTabIndex);
mainTabsManager.selectedTabIndex(initialTabIndex);
} else if (currentTabController?.onResume) {
currentTabController.onResume({});
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
}, [ initialTabIndex, mainTabsManager ]);
}, [ initialTabIndex ]);
const onPause = useCallback(() => {
const currentTabController = tabController.current;
@@ -134,13 +141,13 @@ const Home = () => {
}, []);
useEffect(() => {
void onSetTabs();
mainTabsManager.setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
void onResume();
onResume();
return () => {
onPause();
};
}, [ onPause, onResume, onSetTabs ]);
}, [ initialTabIndex, onPause, onResume, onTabChange, onViewDestroy ]);
return (
<div ref={element}>

View File

@@ -1,59 +0,0 @@
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import React, { type FC } from 'react';
import useCurrentTab from 'hooks/useCurrentTab';
import Page from 'components/Page';
import PageTabContent from '../../components/library/PageTabContent';
import { LibraryTab } from 'types/libraryTab';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { LibraryTabContent, LibraryTabMapping } from 'types/libraryTabContent';
const photosTabContent: LibraryTabContent = {
viewType: LibraryTab.Photos,
collectionType: CollectionType.Homevideos,
isBtnPlayAllEnabled: true,
isBtnShuffleEnabled: true,
itemType: [BaseItemKind.Photo]
};
const photoAlbumsTabContent: LibraryTabContent = {
viewType: LibraryTab.PhotoAlbums,
collectionType: CollectionType.Homevideos,
isBtnPlayAllEnabled: true,
isBtnShuffleEnabled: true,
itemType: [BaseItemKind.PhotoAlbum]
};
const videosTabContent: LibraryTabContent = {
viewType: LibraryTab.Videos,
collectionType: CollectionType.Homevideos,
isBtnPlayAllEnabled: true,
isBtnShuffleEnabled: true,
itemType: [BaseItemKind.Video]
};
const homevideosTabMapping: LibraryTabMapping = {
0: photosTabContent,
1: photoAlbumsTabContent,
2: videosTabContent
};
const HomeVideos: FC = () => {
const { libraryId, activeTab } = useCurrentTab();
const currentTab = homevideosTabMapping[activeTab];
return (
<Page
id='homevideos'
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
backDropType='video, photo'
>
<PageTabContent
key={`${currentTab.viewType} - ${libraryId}`}
currentTab={currentTab}
parentId={libraryId}
/>
</Page>
);
};
export default HomeVideos;

View File

@@ -4,19 +4,21 @@ 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 BangRedirect from 'components/router/BangRedirect';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import ErrorBoundary from 'components/router/ErrorBoundary';
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[] = [
{
path: '/*',
lazy: () => import('../AppLayout'),
element: <AppLayout />,
children: [
{
/* User routes: Any child route of this layout is authenticated */
@@ -30,8 +32,7 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
path: 'video',
element: <VideoPage />
}
],
ErrorBoundary
]
},
/* Public routes */

View File

@@ -13,7 +13,7 @@ import React, { Fragment } from 'react';
import { appHost } from 'components/apphost';
import { useApi } from 'hooks/useApi';
import { useThemes } from 'hooks/useThemes';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
import { useScreensavers } from './hooks/useScreensavers';

View File

@@ -6,7 +6,7 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import React from 'react';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
interface ItemDetailPreferencesProps {

View File

@@ -7,7 +7,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import React from 'react';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
interface LibraryPreferencesProps {

View File

@@ -10,7 +10,7 @@ import React from 'react';
import { appHost } from 'components/apphost';
import datetime from 'scripts/datetime';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants';
import { DisplaySettingsValues } from './types';

View File

@@ -7,7 +7,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import React from 'react';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
interface NextUpPreferencesProps {

View File

@@ -1,4 +1,4 @@
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
export const LANGUAGE_OPTIONS = [
{ value: 'auto', label: globalize.translate('Auto') },

View File

@@ -2,15 +2,10 @@ import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import toast from 'components/toast/toast';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from '../types';
import { useDisplaySettings } from './useDisplaySettings';
type UpdateField = {
name: keyof DisplaySettingsValues;
value: string | boolean;
};
export function useDisplaySettingForm() {
const [urlParams] = useSearchParams();
const {
@@ -26,7 +21,7 @@ export function useDisplaySettingForm() {
}
}, [formValues, loading, displaySettings]);
const updateField = useCallback(({ name, value }: UpdateField) => {
const updateField = useCallback(({ name, value }) => {
if (formValues) {
setFormValues({
...formValues,

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { pluginManager } from 'components/pluginManager';
import { Plugin, PluginType } from 'types/plugin';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
export function useScreensavers() {
const screensavers = useMemo<Plugin[]>(() => {

View File

@@ -4,7 +4,7 @@ import Stack from '@mui/material/Stack';
import React, { useCallback } from 'react';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import { DisplayPreferences } from './DisplayPreferences';
import { ItemDetailPreferences } from './ItemDetailPreferences';
import { LibraryPreferences } from './LibraryPreferences';

View File

@@ -3,7 +3,7 @@ import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import globalize from 'scripts/globalize';
import InputElement from 'elements/InputElement';
import ButtonElement from 'elements/ButtonElement';
import { useApi } from 'hooks/useApi';

View File

@@ -5,7 +5,6 @@ import ConnectionRequired from 'components/ConnectionRequired';
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import ErrorBoundary from 'components/router/ErrorBoundary';
import AppLayout from '../AppLayout';
@@ -17,7 +16,7 @@ import BangRedirect from 'components/router/BangRedirect';
export const STABLE_APP_ROUTES: RouteObject[] = [
{
path: '/*',
Component: AppLayout,
element: <AppLayout />,
children: [
{
/* User routes */
@@ -25,8 +24,7 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
children: [
...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
],
ErrorBoundary
]
},
/* Public routes */

View File

@@ -1,25 +1,24 @@
import React, { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useDebounceValue } from 'usehooks-ts';
import { usePrevious } from 'hooks/usePrevious';
import globalize from 'lib/globalize';
import Page from 'components/Page';
import SearchFields from 'components/search/SearchFields';
import SearchSuggestions from 'components/search/SearchSuggestions';
import SearchResults from 'components/search/SearchResults';
import SearchSuggestions from 'components/search/SearchSuggestions';
import LiveTVSearchResults from 'components/search/LiveTVSearchResults';
import { usePrevious } from 'hooks/usePrevious';
import globalize from 'scripts/globalize';
const COLLECTION_TYPE_PARAM = 'collectionType';
const PARENT_ID_PARAM = 'parentId';
const QUERY_PARAM = 'query';
const SERVER_ID_PARAM = 'serverId';
const Search: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
const collectionTypeQuery = searchParams.get(COLLECTION_TYPE_PARAM) || undefined;
const [ searchParams, setSearchParams ] = useSearchParams();
const urlQuery = searchParams.get(QUERY_PARAM) || '';
const [query, setQuery] = useState(urlQuery);
const [ query, setQuery ] = useState(urlQuery);
const prevQuery = usePrevious(query, '');
const [debouncedQuery] = useDebounceValue(query, 500);
useEffect(() => {
if (query !== prevQuery) {
@@ -50,17 +49,23 @@ const Search: FC = () => {
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
>
<SearchFields query={query} onSearch={setQuery} />
{!query ? (
<SearchSuggestions
parentId={parentIdQuery}
{!query
&& <SearchSuggestions
parentId={searchParams.get(PARENT_ID_PARAM)}
/>
) : (
<SearchResults
parentId={parentIdQuery}
collectionType={collectionTypeQuery}
query={debouncedQuery}
/>
)}
}
<SearchResults
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
parentId={searchParams.get(PARENT_ID_PARAM)}
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
query={query}
/>
<LiveTVSearchResults
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
parentId={searchParams.get(PARENT_ID_PARAM)}
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
query={query}
/>
</Page>
);
};

View File

@@ -4,7 +4,7 @@ import React, { FunctionComponent, useEffect, useState, useRef, useCallback } fr
import { useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu';
import { appHost } from '../../../../components/apphost';
import confirm from '../../../../components/confirm/confirm';

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