Compare commits

..

277 Commits

Author SHA1 Message Date
Joshua M. Boniface
8e37078b60 Bump version to 10.8.13
Some checks failed
Lint / Run eslint (push) Has been cancelled
Lint / Run stylelint (css) (push) Has been cancelled
Lint / Run stylelint (scss) (push) Has been cancelled
2023-11-28 22:22:14 -05:00
Joshua M. Boniface
a5184bb843 Merge pull request #5019 from joshuaboniface/set-ffmpeg-disabled
Set FFmpeg path disabled
2023-11-28 22:19:29 -05:00
Joshua M. Boniface
b9ee65e49d Set FFmpeg path disabled 2023-11-27 12:04:19 -05:00
Bill Thornton
d5ebc64fcb Merge pull request #4992 from rafma0/release-10.8.z 2023-11-14 18:36:22 -05:00
Bill Thornton
f4c49427fd Merge pull request #4993 from rafma0/backport-fix-tizen-flac 2023-11-14 18:35:52 -05:00
rafma0
6c97b7a6d8 remove video audio flac on tizen 2023-11-12 23:43:10 -03:00
rafma0
0859d4d881 fix jittering in checkboxes on tv ui 2023-11-12 23:17:29 -03:00
Joshua M. Boniface
4b6bbcfe26 Bump version to 10.8.12 2023-11-04 14:42:47 -04:00
Bill Thornton
1a1735340f Merge pull request #4912 from dmitrylyzo/fix-imports
Fix imports
2023-10-24 13:13:10 -04:00
Dmitry Lyzo
5dad4b4486 Fix imports in Playback/Queue controller
According to the template.
2023-10-24 14:10:33 +03:00
Dmitry Lyzo
201aec56c6 Fix imports in RemoteControl 2023-10-24 14:10:33 +03:00
Bill Thornton
d274d7f741 Merge pull request #4892 from thornbill/backport-4860 2023-10-19 23:04:38 -04:00
cbe
b270d4051b Fix language/subtitle switcher when using gamepad
Raising the event code `13` (Enter) should be a lot more stable since `0`
is just the default and not assigned to any actual key [1]. This keycode
also has been standardized early enough to change it at this crucial part
of gamepad handling in my opinion.

[1] https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode

Fixes #3755
2023-10-19 19:18:29 -04:00
Bill Thornton
aa09fa710e Merge pull request #4893 from thornbill/backport-4754 2023-10-19 19:01:52 -04:00
Marcus Nilsson
c33c2b1e26 Change Subtitle Sync slider to go from -300 to 300
This patch changes the subtitle sync from using a procentage to a
'slider value' that ranges from -300 to 300. The reasons for this is that
WebOS doesn't jump in 0.1 increments but instead jumps 1.0 increments in
the slider, which results in subtitle sync jumping 0.6s per increment.
Using a value from -300 to 300 makes LG WebOS jump 0.1s instead.
2023-10-19 12:53:23 -04:00
Bill Thornton
eb7fb6b39d Merge pull request #4837 from dmitrylyzo/fix-noitem-play 2023-10-04 08:04:12 -04:00
Dmitry Lyzo
5fd5292f6d Fix playing empty item set 2023-10-04 13:49:23 +03:00
Bill Thornton
1250c76567 Merge pull request #4813 from dmitrylyzo/backport/slider-force-change 2023-09-29 07:55:45 -04:00
Bill Thornton
1ba6bde32a Merge pull request #4797 from Mauroq/release-10.8.z 2023-09-29 07:54:50 -04:00
Dmitry Lyzo
3c80bf7b19 Handle pressing Enter to finish keyboard dragging of slider
(cherry picked from commit 9024ebea39)
2023-09-29 13:19:48 +03:00
Joshua M. Boniface
1e1af1c67f Merge pull request #4787 from dmitrylyzo/fix-legacy 2023-09-23 21:49:36 -04:00
Joshua M. Boniface
8d1c34f80e Bump version to 10.8.11 2023-09-23 21:41:43 -04:00
Dmitry Lyzo
d3e3bc7282 Fix homesections in legacy browser 2023-09-23 23:26:48 +03:00
Dennis de Lange
0ad87f3b87 fix: Remove h264-10bit support from Tizen 2023-09-22 17:41:26 +02:00
Bill Thornton
9e871d43ec Merge pull request #4632 from dmitrylyzo/multi-purpose-keys 2023-09-21 12:23:04 -04:00
Dmitry Lyzo
11ae2ff43f Add KeyboardEvent constructor polyfill 2023-09-14 01:28:19 +03:00
Dmitry Lyzo
76c55116ce Change behavior of arrow keys and Enter when OSD is hidden 2023-09-14 01:28:19 +03:00
Dmitry Lyzo
884ce171ea Focus on corresponding button 2023-09-14 01:26:55 +03:00
Dmitry Lyzo
51bd2bef1a Don't show OSD for Fullscreen and Mute 2023-09-14 01:26:55 +03:00
Bill Thornton
0bfe91b0fa Merge pull request #4757 from dmitrylyzo/fix-samsung_tv-dolbyvision
Remove Dolby Vision support on Samsung TV (Tizen)
2023-09-08 16:56:26 -04:00
Bill Thornton
a5feddb48b Merge pull request #4758 from dmitrylyzo/fix-slider-step 2023-09-08 07:52:20 -04:00
Dmitry Lyzo
7d27596d6b Fix slider step
Use the value of the `step` attribute if no keyboard steps are specified.
2023-08-24 00:23:11 +03:00
Dmitry Lyzo
e0cb79088b fix: Remove Dolby Vision support on Samsung TV 2023-08-23 00:18:25 +03:00
Bill Thornton
b2f3720282 Merge pull request #4709 from thornbill/backport-4692
Fix scheduled task time limit key
2023-07-07 20:16:52 -04:00
hikaps
b67f4eccfb Fix scheduled task time limit key 2023-07-07 19:42:53 -04:00
Bill Thornton
331fa87216 Merge pull request #4654 from joshuaboniface/additionalPluginVerification
Add confirmation for 3rd party repos
2023-07-01 01:32:05 -04:00
Joshua M. Boniface
59813ee0ea Merge branch 'release-10.8.z' into additionalPluginVerification 2023-06-28 23:01:40 -04:00
Joshua M. Boniface
eaae0f3c55 Add translation of "unknown" repo details 2023-06-28 22:49:58 -04:00
Bill Thornton
6304e27940 Merge pull request #4688 from dmitrylyzo/fix-macos-alac 2023-06-14 13:06:50 -04:00
Dmitry Lyzo
7ada8796a7 Disable ALAC on MacOS in non-Safari browsers 2023-06-14 11:30:22 +03:00
Bill Thornton
eab36f9934 Merge pull request #4685 from hurani/backport-directory-viewer-parent-fix 2023-06-12 16:39:52 -04:00
Bill Thornton
b044bc25de Merge pull request #4171 from nielsvanvelzen/directory-browser-go-up
Fix going to parent folder in directory browser
2023-06-11 22:34:23 -07:00
Bill Thornton
5cc91f2ee0 Merge pull request #4657 from thornbill/subs-xss
Fix xss in custom subtitles element
2023-06-01 02:13:28 -04:00
Bill Thornton
2ffb833daf Fix xss in custom subtitles element 2023-06-01 01:33:59 -04:00
Joshua M. Boniface
93d63330fd Fix conditional formatting and add fallback 2023-05-30 10:39:27 -04:00
Joshua M. Boniface
f1b0b504dd Check for pkg.versions.length
Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
2023-05-30 10:35:46 -04:00
Joshua M. Boniface
919be18c84 Fix syntax error in HTML 2023-05-30 10:13:21 -04:00
Joshua M. Boniface
cf530b30d5 Fix linting errors
* Remove superfluous variable
* Remove extra random spaces from editor
* Use single-quotes around text
2023-05-30 09:35:23 -04:00
Joshua M. Boniface
dd004ec06b Add labels to i8n 2023-05-30 09:25:32 -04:00
Joshua M. Boniface
a11d74ae68 Use camelCase variable name 2023-05-30 09:21:41 -04:00
Joshua M. Boniface
da9eece6c0 Remove extra console.log 2023-05-29 11:19:13 -04:00
Joshua M. Boniface
509cbabedb Add confirmation for 3rd party repos
Adds a confirmation similar to the one performed during plugin
installation, when adding a 3rd party repository.

The safe domain is hardcoded to be "repo.jellyfin.org" as this is very
stable and we have no plans to change it. Individual mirrors don't need
to be specified since this is user-input content and they should be
using the main URL not the URL of a specific mirror.

The confirmation message makes explicit mention of the possibility of
malicious code from 3rd party repositories as well as updates that may
bring it in, and suggests only adding 3rd parties from trusted people.

The plugin install confirmation is also modified to use the same
conditional and an altered message similar to the above, again to
emphasize the potential security risks of 3rd party plugins.

Finally, some additional information is added to the Developer Info
section of the plugin page; specifically, the name of the repository the
plugin is sourced from as well as its URL. How this is obtained is a
hack, since these should probably be part of the main information about
the plugin and not each specific version, but this is worked around by
only showing the information from the first (i.e. newest) version.
2023-05-29 10:59:21 -04:00
Bill Thornton
62246fe0a9 Merge pull request #4628 from dmitrylyzo/fix-volume-slider 2023-05-21 12:32:19 -04:00
Bill Thornton
35a7dfbed6 Merge pull request #4627 from dmitrylyzo/fix-slider-bubble 2023-05-21 01:43:51 -04:00
Dmitry Lyzo
b93221a9b2 Fix initial state of slider 2023-05-20 23:36:20 +03:00
Dmitry Lyzo
1a858d9dda Fix overlap of slider bubble 2023-05-20 23:04:14 +03:00
Bill Thornton
07ce5c44a1 Merge pull request #4591 from thornbill/fix-es-quick-connect 2023-05-12 10:07:58 -04:00
Bill Thornton
2eda12ba8f Fix QuickConnect code not displaying in Spanish 2023-05-12 08:31:04 -04:00
Bill Thornton
9266e51aaf Merge pull request #4589 from thornbill/fix-api-key-xss 2023-05-11 17:10:22 -04:00
Bill Thornton
069ea049eb Fix xss in api key page 2023-05-11 16:48:06 -04:00
Bill Thornton
56af039fb9 Merge pull request #4561 from dmitrylyzo/fix-bottom-hide
Fix bottom video controls don't auto-hide
2023-05-02 16:54:10 -04:00
Dmitry Lyzo
1fb5c4d95d Fix query selector target 2023-05-02 22:36:51 +03:00
Bill Thornton
828fa340d5 Merge pull request #4553 from dmitrylyzo/fix-tv-episode-autoplay 2023-05-02 00:53:01 -04:00
Dmitry Lyzo
a77a8c7aec fix: Unhide PlayNextEpisodeAutomatically on TV 2023-05-01 02:17:50 +03:00
Joshua M. Boniface
55714d5341 Bump version to 10.8.10 2023-04-23 11:01:37 -04:00
Joshua M. Boniface
b88a5951e1 Merge pull request from GHSA-89hp-h43h-r5pq
Escape device id in raw HTML
2023-04-23 11:00:06 -04:00
Ian Walton
bd480aa1db Escape device id in raw HTML. 2023-04-22 10:12:58 -04:00
Bill Thornton
cf0cf93e47 Merge pull request #4492 from nyanmisaka/tonemap-mode-options
Add the tonemap mode options
2023-04-16 23:18:43 -04:00
nyanmisaka
f942072e53 Update translations for tonemapping
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2023-04-14 18:55:08 +08:00
nyanmisaka
f28db6699d Add tonemap mode options
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2023-04-14 18:50:24 +08:00
Bill Thornton
a447786a24 Merge pull request #4487 from thornbill/backport-4485
Fix dead documentation link
2023-04-12 11:01:56 -04:00
Bill Thornton
c9e5b35e42 Merge pull request #4385 from nyanmisaka/drop-progressive
Drop progressive transcoding in web client
2023-04-12 10:15:52 -04:00
Brett Petch
a2ffa9dfaf Update src/controllers/dashboard/encodingsettings.html
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2023-04-12 10:08:25 -04:00
Brett Petch
3c8c6ad469 fix: dead link 2023-04-12 10:08:11 -04:00
nyanmisaka
62dbf0d106 Drop progressive transcoding in web client
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2023-03-14 20:38:08 +08:00
Bill Thornton
22d1f40587 Merge pull request #4395 from thornbill/fix-installed-plugin-version
Fix installed plugin version html
2023-03-07 16:27:42 -05:00
Bill Thornton
909f03460c Fix installed plugin version html 2023-03-06 16:27:35 -05:00
Bill Thornton
7e99e3ec51 Merge pull request #4330 from dmitrylyzo/fix-navigation-input
Fix navigation for some types of INPUT
2023-02-27 10:33:01 -05:00
Bill Thornton
af27e084d5 Merge pull request #4362 from dmitrylyzo/fix-reset-subtitleoffset
Fix subtitle offset reset when seeking progressive stream
2023-02-23 16:46:26 -05:00
Bill Thornton
8f75a42669 Merge pull request #4356 from dmitrylyzo/babel-libass 2023-02-18 22:10:44 -05:00
Dmitry Lyzo
1040685c1e Fix subtitle offset reset when seeking progressive stream 2023-02-19 01:41:23 +03:00
Dmitry Lyzo
38aebf4e42 Babelify @jellyfin/libass-wasm 2023-02-14 23:13:03 +03:00
Dmitry Lyzo
62d0354fe0 Simplify adding modules to babel-loader 2023-02-14 23:13:03 +03:00
Dmitry Lyzo
e580f0c869 Fix navigation for some types of INPUT
Regression from 36fce00270
2023-02-06 15:27:42 +03:00
Bill Thornton
551f12fdfb Merge pull request #4312 from dmitrylyzo/backport-4150
Backport PR #4150 to 10.8.z branch
2023-01-31 12:20:44 -05:00
Bill Thornton
00d59c546d Merge pull request #4310 from jsayol/patch-1
Backport PR #4147 to 10.8.z branch
2023-01-31 12:19:23 -05:00
Dmitry Lyzo
0864432105 Fix file name escaping
(cherry picked from commit 45542a67a4)
2023-01-28 23:30:07 +03:00
Dmitry Lyzo
0c3c47b8b4 Fix Subtitle Uploder navigation in TV mode
(cherry picked from commit 78bbac8ca7)
2023-01-28 23:30:07 +03:00
Josep Sayol
5f7d2659dc Backport PR #4147 to 10.8.z branch 2023-01-28 08:22:38 +01:00
Joshua M. Boniface
72c66e91ed Bump version to 10.8.9 2023-01-22 14:09:15 -05:00
Bill Thornton
acb6519ef9 Merge pull request #4288 from dmitrylyzo/fix-es-419
Fix loading Spanish (Latin America) (es-419)
2023-01-19 15:40:35 -05:00
Joshua M. Boniface
0ff86d9ea0 Merge pull request #4274 from dmitrylyzo/fix-hisense-back
Fix back action on Hisense TV
2023-01-19 15:32:51 -05:00
Dmitry Lyzo
b94d14399f fix loading Spanish (Latin America) (es-419) 2023-01-15 20:15:39 +03:00
Dmitry Lyzo
f4c8dd6b1f fix back action on Hisense TV 2023-01-13 14:05:25 +03:00
Bill Thornton
9139153d16 Merge pull request #4263 from dmitrylyzo/fix-change-track 2023-01-12 17:39:02 -05:00
Dmitry Lyzo
0ff3cf321c fix change audio track during playback 2023-01-11 20:17:24 +03:00
Dmitry Lyzo
fe65e0c3b3 refactor: decrease the number of iterations 2023-01-10 23:13:37 +03:00
Dmitry Lyzo
a7bd7e30c6 fix filtering of supported audio tracks 2023-01-10 23:13:37 +03:00
Bill Thornton
ea79d2651a Merge pull request #4267 from dmitrylyzo/fix-audiocontext-limit
Fix AudioContext limit exceeded
2023-01-10 10:00:55 -05:00
Joshua M. Boniface
21a3bae204 Merge pull request #4269 from thornbill/fix-plugin-xss
Fix XSS vulnerability in plugin repo pages
2023-01-09 12:15:18 -05:00
Bill Thornton
4bc0eebee0 Fix XSS vulnerability in plugin repo pages 2023-01-09 11:11:33 -05:00
Dmitry Lyzo
21cf0f5f8e fix AudioContext limit exceeded 2023-01-07 22:36:54 +03:00
Bill Thornton
96234eafb7 Merge pull request #4240 from dmitrylyzo/fix-audiotracks
Fix detection of SecondaryAudio support
2023-01-05 11:19:08 -05:00
Bill Thornton
ae907cd8a6 Merge pull request #4243 from dmitrylyzo/fix-escape-html 2023-01-04 19:41:39 -05:00
Dmitry Lyzo
0ce839c4a8 Fix HTML escaping in MediaSession and on remote page 2023-01-05 01:52:47 +03:00
Dmitry Lyzo
a3e64088b1 Fix detection of SecondaryAudio support 2023-01-05 01:05:27 +03:00
Bill Thornton
255df81375 Merge pull request #4238 from thornbill/fix-router-xss
Fix XSS vulnerability in router
2023-01-04 16:34:45 -05:00
Bill Thornton
5bfffd6209 Merge pull request #4182 from nyanmisaka/fix-progressive-mp4
Fix the progressive mp4 transcoding profile
2023-01-04 16:33:44 -05:00
Bill Thornton
8877e2f758 Fix XSS vulnerability in router 2023-01-04 13:55:25 -05:00
Joshua M. Boniface
b909369127 Bump version to 10.8.8 2022-11-29 13:43:09 -05:00
nyanmisaka
869cbef571 Fix the progressive mp4 transcoding profile
mp4 should be used instead of webm for the maximum compatibility.

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2022-11-20 20:10:53 +08:00
Bill Thornton
ff4e6a6778 Merge pull request #4172 from thornbill/fix-items-banner-alignment
Fix item details banner image alignment
2022-11-16 22:56:16 -05:00
Bill Thornton
fda6d3c969 Fix item details banner image alignment on mobile 2022-11-15 13:22:52 -05:00
Bill Thornton
8a549eb45b Fix item details banner image alignment 2022-11-15 11:38:34 -05:00
Bill Thornton
4f3ac34739 Merge pull request #4166 from dmitrylyzo/fix-input-navigation 2022-11-10 18:09:08 -05:00
Dmitry Lyzo
36fce00270 Fix keyboard navigation for INPUT and TEXTAREA 2022-11-10 16:31:31 +03:00
Joshua M. Boniface
de8ee44b22 Bump version to 10.8.7 2022-10-31 23:06:37 -04:00
Joshua M. Boniface
1b33cb6f9b Bump version to 10.8.6 2022-10-28 22:44:19 -04:00
Bill Thornton
eb47e5c374 Merge pull request #4117 from thornbill/backport-4094
Backport PR #4094 to 10.8 release
2022-10-28 16:04:23 -04:00
CrispyBaguette
aca8b0ed18 Append new contributor 2022-10-28 15:52:20 -04:00
CrispyBaguette
e516014dc0 Accept arbitrary precision for community rating 2022-10-28 15:52:10 -04:00
Bill Thornton
4c99480c42 Merge pull request #4069 from peterspenler/fix/lazyload-image-intersection 2022-10-26 17:34:47 -04:00
peterspenler
c200a7d2c6 Use isIntersecting instead of intersectionRatio 2022-10-26 23:56:45 +03:00
Bill Thornton
aa009091d5 Merge pull request #4089 from dmitrylyzo/fix-escape-html 2022-10-22 15:38:31 -04:00
Dmitry Lyzo
f81f0ef9d7 fix double escape HTML 2022-10-22 22:31:18 +03:00
Bill Thornton
66baa9e069 Merge pull request #4047 from thornbill/backport-4028 2022-10-14 11:17:51 -04:00
Urtzi Odriozola
375bf86a22 Update displaySettings.template.html
"Euskara" is the native way of naming Basque
2022-10-13 10:31:38 -04:00
Bill Thornton
fb39a56700 Merge pull request #4039 from thornbill/fix-details-card-crop
Fix card cropping on item details page
2022-10-13 10:17:53 -04:00
Bill Thornton
0b9415a041 Fix card cropping on item details page 2022-10-13 00:47:06 -04:00
Bill Thornton
b14d76a3d4 Merge pull request #4005 from dmitrylyzo/fix-last-seen
Fix locale with suffix
2022-10-06 17:04:16 -04:00
Bill Thornton
1263468d49 Merge pull request #4004 from cvium/add_basque_10.8
chore: add Basque display language option
2022-10-06 11:26:41 -04:00
Dmitry Lyzo
fe4ee0c101 fix locale with suffix
"Locale with suffix" is constructed only once with the initial
(browser) locale.
2022-10-06 11:48:15 +03:00
Claus Vium
30df221bbb Update dfnshelper.js 2022-10-06 10:07:23 +02:00
cvium
dfa9b33949 chore: add Basque display language option 2022-10-06 09:42:04 +02:00
Joshua M. Boniface
354157c003 Bump version to 10.8.5 2022-09-24 22:02:30 -04:00
Bill Thornton
72d538e902 Merge pull request #3877 from Callum17/bugfix/release-fix_itemcontextmenu_fails_to_update_for_items_with_no_image_metadata 2022-09-23 23:39:54 -04:00
callum
f1574e0f42 Merge branch 'bugfix/release-fix_itemcontextmenu_fails_to_update_for_items_with_no_image_metadata' of github.com:Callum17/jellyfin-web into bugfix/release-fix_itemcontextmenu_fails_to_update_for_items_with_no_image_metadata 2022-09-10 20:21:46 +01:00
callum
ebfd28d396 Avoid unnecessary DOM update if concurrent played items lack image metadata. 2022-09-10 20:21:37 +01:00
Callum
1861605958 Merge branch 'release-10.8.z' into bugfix/release-fix_itemcontextmenu_fails_to_update_for_items_with_no_image_metadata 2022-09-06 23:35:29 +01:00
Joshua M. Boniface
f85f7d2fe6 Merge pull request #3878 from thornbill/fix-sdk-imports 2022-09-06 01:20:16 -04:00
Bill Thornton
03ce4210af Fix sdk imports to improve build size 2022-09-06 00:43:08 -04:00
callum
689a65cc92 Style. Prefer to use nowPlayingImageUrl to load image in order to make intent clear. 2022-09-04 08:50:20 +01:00
callum
44a5d7bb8d Ensure nowPlayingBar updates correctly when navigating from a song with no image back to a previous song with an image. 2022-09-04 08:50:20 +01:00
callum
072e20b585 Fixed: itemContextMenu opened from bottom media control bar applying operations to the wrong item when items have no image metadata. Can manifest as the wrong song being added to a playlist, deleted, or receiving user supplied metadata, etc. 2022-09-04 08:50:20 +01:00
Bill Thornton
89ec4f4e8a Merge pull request #3849 from dmitrylyzo/fix-cursor-hide 2022-08-24 12:58:33 -04:00
Bill Thornton
89d92e738f Merge pull request #3848 from dmitrylyzo/fix-screensaver 2022-08-24 12:56:45 -04:00
Dmitry Lyzo
c9f4e3c301 Fix unexpected cursor hiding
Hide cursor in case of:
- TV layout
- Active playback
- Active screensaver
2022-08-24 15:56:27 +03:00
Dmitry Lyzo
a80fa25a68 Fix idle function call 2022-08-24 15:55:58 +03:00
Joshua Boniface
bb040b90d1 Bump version to 10.8.4 2022-08-13 21:52:06 -04:00
Bill Thornton
747f7beae7 Merge pull request #3789 from thornbill/fix-cards-xss
Fix XSS in card aria labels
2022-08-02 14:36:36 -04:00
Bill Thornton
eb4159788d Fix XSS in card aria labels 2022-08-02 13:51:20 -04:00
Joshua Boniface
2feaff3648 Bump version to 10.8.3 2022-08-01 20:22:02 -04:00
Joshua M. Boniface
45fe89c26f Bump version to 10.8.2 2022-08-01 14:27:58 -04:00
Bill Thornton
b167bf2d37 Merge pull request #3781 from yahuli/fix-type-error 2022-07-29 18:32:10 -04:00
Bill Thornton
6fe43e45e9 Merge pull request #3778 from nielsvanvelzen/itemdetails-download-missing-args
Add title and filename to download request in item details
2022-07-27 09:41:40 -04:00
dumbfox
d23aa6ada4 Fix TypeError 2022-07-27 10:49:13 +00:00
Niels van Velzen
7936502047 Fix serverId in item details download request
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2022-07-27 08:59:47 +02:00
Bill Thornton
79c53c6458 Merge pull request #3775 from daullmer/remote-user-config
Fix user specific remote access
2022-07-27 00:15:35 -04:00
Niels van Velzen
5489f34b92 Add title and filename to download request in item details 2022-07-26 22:16:06 +02:00
David Ullmer
5f659b0ef6 Fix user specific remote access 2022-07-23 14:30:41 +02:00
Bill Thornton
db346e4c05 Merge pull request #3760 from nielsvanvelzen/fix-logs-submit
Fix saving log settings not working
2022-07-11 14:28:24 -04:00
Niels van Velzen
b05c7e3309 Fix saving log settings not working 2022-07-10 17:21:59 +02:00
Bill Thornton
8e7a3045dd Merge pull request #3751 from nielsvanvelzen/splashscreen-config-fix
Fix splash screen checkbox always unchecked
2022-07-06 16:28:23 -04:00
Niels van Velzen
7c33579260 Fix splash screen checkbox always unchecked 2022-07-06 13:34:56 +02:00
Bill Thornton
a38d0bbacc Merge pull request #3730 from SenorSmartyPants/CardbuilderEpisodeWithNoName
CardBuilder: Test episode number in addition to episode name
2022-07-02 00:30:31 -04:00
SenorSmartyPants
f4ee7076dd Test episode number in addition to episode name
Display episode number on card if present. Some episodes can have no name, but are still numbered (in my EPG data)
2022-06-28 14:16:09 -05:00
Bill Thornton
42bec6c11e Merge pull request #3729 from SenorSmartyPants/SeriesImageDownload
Save series images under season, when browsing parent images for season
2022-06-28 13:43:36 -04:00
SenorSmartyPants
b93c244e2d Save series images under season, when browsing parent images for season 2022-06-27 14:41:53 -05:00
Joshua M. Boniface
e1ed816a13 Bump version to 10.8.1 2022-06-26 21:00:43 -04:00
Joshua M. Boniface
bc48691738 Merge pull request #3724 from samcon/fix_resume_webos 2022-06-26 20:58:23 -04:00
Joshua M. Boniface
ae83d1d356 Merge pull request #3720 from Shadowghost/device-logo-fix 2022-06-26 20:56:17 -04:00
Joshua M. Boniface
66b86044a9 Merge pull request #3719 from nyanmisaka/dovi-meta 2022-06-26 20:55:44 -04:00
Joshua M. Boniface
d967ce860c Merge pull request #3721 from Shadowghost/fix-stream-autoselect 2022-06-26 20:54:51 -04:00
Joshua M. Boniface
d11b51d0f1 Update src/plugins/htmlVideoPlayer/plugin.js
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2022-06-26 20:54:12 -04:00
Joshua M. Boniface
0e0dd46c1b Merge pull request #3722 from nyanmisaka/fix-audio-ch 2022-06-26 20:53:47 -04:00
Nyanmisaka
5978d157e7 Remove unnecessary check 2022-06-27 06:23:00 +08:00
Nyanmisaka
ba34384b71 Apply suggestion from code review 2022-06-27 05:54:36 +08:00
Shadowghost
a792737add Use stream defaults in stream auto selection if previous source had no stream of that kind 2022-06-26 23:30:42 +02:00
Shadowghost
22a77ce54e Apply suggestions from code review
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2022-06-26 22:40:28 +02:00
Samcon
cab6e34390 Fix setCurrentTimeIfNeeded calc + change preload to auto to resume in WebOS 2022-06-26 14:37:04 +03:00
Cody Robibero
c20243c8bf Merge pull request #3571 from mihawk90/fedora-spec-rework
Cleanup and standardise Fedora build (web)
2022-06-23 08:06:21 -06:00
nyanmisaka
54e3276ba2 Fix 6ch audio is disabled on AC3 supported browsers 2022-06-23 21:44:18 +08:00
Joshua M. Boniface
f1ff81884a Merge pull request #3717 from dmitrylyzo/resolution 2022-06-20 10:01:01 -04:00
Shadowghost
2b0091eca2 Fix stream selection remembering 2022-06-19 17:42:00 +02:00
Shadowghost
37b4203967 Fix device logos for various clients 2022-06-19 16:34:25 +02:00
Tarulia
08d77664a7 Add make in Fedora Docker install
Fedora 36 doesn't seem to ship make, so add it manually.
2022-06-18 20:29:32 +02:00
nyanmisaka
b0ab4d3e63 Expose DoVi metadata and VideoRangeType 2022-06-18 22:16:10 +08:00
Dmitry Lyzo
f06cd961d5 Add maximum allowed video resolution selector 2022-06-17 22:14:14 +03:00
Bill Thornton
c8590d37ed Merge pull request #3713 from nyanmisaka/video-range-condition
Add VideoRangeType condition for web client
2022-06-17 13:10:36 -04:00
Nyanmisaka
8544bf08ac Merge branch 'release-10.8.z' into video-range-condition 2022-06-18 00:58:08 +08:00
Bill Thornton
6142283e99 Merge pull request #3714 from nyanmisaka/vpp-tm-configs
Expose VPP TM brightness/contrast gain options
2022-06-17 12:52:15 -04:00
Bill Thornton
721bc54dbd Merge pull request #3716 from Orhideous/fix/mimetype
Recognize MIME for transcoded audio in streams
2022-06-17 10:05:22 -04:00
Andriy Kushnir (Orhideous)
f2a826bb5d Left old MIME type for backward compatibility 2022-06-17 13:38:43 +03:00
nyanmisaka
8660f72915 Add translate 2022-06-17 17:06:37 +08:00
Andriy Kushnir (Orhideous)
8d531976a1 Recognize MIME for transcoded audio in streams
Fixes: #3663
See: jellyfin/jellyfin#6941
2022-06-16 22:40:31 +03:00
nyanmisaka
cbfa0acfb3 Expose VPP TM brightness/contrast gain options 2022-06-16 23:55:55 +08:00
nyanmisaka
7195e8a15d Add VideoRangeType condition for HDR content 2022-06-16 21:34:52 +08:00
Tarulia
26ec0e8e4a Use Fedora 36 image in Fedora Docker builds
* fixes #2351
2022-06-16 03:30:47 +02:00
Tarulia
5d5be25008 Move web-files to default location
* when running Jellyfin as a user from a terminal without passing
  arguments, it would not find the web-files. This moves them to the
  expected/default location.
* fixes #2059
2022-06-16 03:29:49 +02:00
Tarulia
390a0edf70 Standardise Fedora spec to packaging guidelines
* move actual building process to %build
* remove AutoReqProv as the package purely contains text files and
  fonts. There's no dependencies to begin with. This feature is also
  intended as sort of a "last resort" and we don't need this here.
* define LICENSE as %license, which automatically puts it in a
  standardised directory
2022-06-16 03:29:49 +02:00
Tarulia
2896cfbdb2 Adjust license in Fedora Spec according to LICENSE 2022-06-16 03:29:49 +02:00
Tarulia
2ff0a67e10 Rewrite Fedora build version detection
Rewrite so we don't need to constantly update with every new Fedora
release. This is especially useful when Fedora and Jellyfin release
cycles don't line up.

Version selection is as follows:
* TARGET environment variable, which is currently used already
* Currently running Fedora version
* Hardcoded Fallback version that can be updated occasionally
2022-06-16 03:29:49 +02:00
Bill Thornton
0bd774dd45 Merge pull request #3694 from crobibero/disable-splashscreen
Add the ability to enable/disable the splashscreen
2022-06-14 10:28:33 -04:00
Bill Thornton
4fdfcde018 Merge pull request #3701 from nyanmisaka/patch-1
Remove MPEG4 hwaccel from AMF
2022-06-14 10:14:00 -04:00
Bill Thornton
eaefc7bea1 Merge pull request #3704 from zhuangzhuang/release-10.8.z
Fix empty  avatar when upload same profile image
2022-06-14 10:12:22 -04:00
zhuangzhuang1988
427bdb2203 Fix empty avatar when upload same profile image
(cherry picked from commit 9cb5b03edc605fb62d0b664dee26df27540ae532)
2022-06-14 09:57:30 +08:00
Nyanmisaka
e8e531ffb2 Remove MPEG4 hwaccel from AMF 2022-06-14 02:19:09 +08:00
Cody Robibero
a3f9f45c54 Add strings 2022-06-12 09:22:58 -06:00
Cody Robibero
801d656d48 Add the ability to enable/disable the splashscreen 2022-06-12 08:50:49 -06:00
Joshua M. Boniface
e727eed1d1 Bump version to 10.8.0 2022-06-10 22:17:34 -04:00
Joshua M. Boniface
0701c4dff3 Merge pull request #3668 from dmitrylyzo/show-play-settings 2022-06-06 11:52:20 -04:00
Bill Thornton
57312e5cd5 Merge pull request #3662 from 1337joe/fix-tv-guide-search-2
Support searching for tv programs
2022-05-25 11:46:16 -04:00
Joe Rogers
07d2537de6 Restrict series/programs to uncategorized items 2022-05-24 18:43:06 +02:00
Bill Thornton
46f31b3f15 Merge pull request #3664 from dmitrylyzo/fix-memory-leak 2022-05-23 08:03:05 -04:00
Dmitry Lyzo
abbc5963f2 Show PlaySettings button even if transcoding isn't supported 2022-05-22 14:55:59 +03:00
Cody Robibero
f1274041ce Merge pull request #3667 from dmitrylyzo/release-lint-job
Enable Lint job on release branches
2022-05-22 05:27:54 -06:00
grafixeyehero
f2c747ce19 Fix lint 2022-05-22 13:57:22 +03:00
Dmitry Lyzo
8f4e87dd1f Enable Lint job on release branches 2022-05-22 13:51:24 +03:00
Dmitry Lyzo
3df8bd8be8 Bump @jellyfin/libass-wasm from 4.1.0 to 4.1.1
Bumps [@jellyfin/libass-wasm](https://github.com/jellyfin/JavascriptSubtitlesOctopus) from 4.1.0 to 4.1.1.
    - [Release notes](https://github.com/jellyfin/JavascriptSubtitlesOctopus/releases)
    - [Commits](https://github.com/jellyfin/JavascriptSubtitlesOctopus/compare/v4.1.0...v4.1.1)
2022-05-21 19:24:49 +03:00
Joe Rogers
46daea5238 Support searching for tv programs 2022-05-21 12:26:35 +02:00
Bill Thornton
1be3d30027 Merge pull request #3658 from thornbill/fix-channel-cards
Add workaround for channel card images
2022-05-20 15:34:01 -04:00
Bill Thornton
6b362fb591 Merge pull request #3657 from dmitrylyzo/unhighlight-play
Remove play button highlighting
2022-05-20 13:33:53 -04:00
Bill Thornton
cd31ae7afe Add workaround for channel card images 2022-05-20 13:26:57 -04:00
Dmitry Lyzo
a3fd4ba62f Remove play button highlighting 2022-05-20 10:32:14 +03:00
Bill Thornton
9b8507706c Merge pull request #3647 from 1337joe/disable_auto_collection
Disable "Automatically add to collection" by default
2022-05-17 11:38:34 -04:00
Joe Rogers
ea0161d132 Disable "Automatically add to collection" by default 2022-05-16 16:47:24 +02:00
Joshua M. Boniface
a36f515b30 Merge pull request #3644 from dmitrylyzo/bump-jso 2022-05-15 22:10:42 -04:00
Joshua M. Boniface
3ed1a9d098 Merge branch 'release-10.8.z' into bump-jso 2022-05-15 20:29:25 -04:00
Joshua M. Boniface
a07d5b7bd1 Merge pull request #3642 from taku0/font-family-by-language 2022-05-15 20:27:54 -04:00
taku0
88e129f793 Change CSS font-family by language preference
Fixes https://github.com/jellyfin/jellyfin-web/issues/913.

- Update `lang` attribute of `html` element on user preference change.
- Choose appropriate `font-family` depending on `lang` attribute using
  attribute selectors in CSS.
- Add `Noto Sans TC`, the Traditional Chinese (Taiwan variant).
- Fix event listener registration in `globalize.js`.
2022-05-16 09:18:06 +09:00
Joshua M. Boniface
4582e6185a Bump version to 10.8.0-beta3 2022-05-15 20:17:00 -04:00
Dmitry Lyzo
3ef4622772 Migrate to @jellyfin/libass-wasm@4.1.0 2022-05-16 01:51:47 +03:00
Bill Thornton
0895163344 Merge pull request #3643 from dmitrylyzo/fix-tizen5 2022-05-15 15:20:46 -04:00
Dmitry Lyzo
44b990e331 Resolve worker URLs
Worker in Tizen 5 doesn't resolve relative path with async request.
2022-05-15 18:10:55 +03:00
Bill Thornton
ae32ece346 Merge pull request #3637 from MinecraftPlaye/add-webp 2022-05-14 10:43:29 -04:00
Patrick Farwick
c929b6946d Add WebP to the page detection list for comics
The WebP image format can be used to store comic pages.
2022-05-14 11:09:19 +00:00
Bill Thornton
ff95eba35f Merge pull request #3639 from rhld16/unused-languages
Add missing languages
2022-05-14 02:26:29 -04:00
Bill Thornton
38ef0e2bf2 Merge pull request #3635 from dmitrylyzo/fix-ssa-font
Fix SSA/ASS missing font
2022-05-14 02:19:13 -04:00
rhld16
a8c20ca35b Add other date-fns valid languages 2022-05-13 19:30:19 +00:00
rhld16
790b2a0b14 Add Welsh (Cymraeg) to Web 2022-05-13 19:30:09 +00:00
Dmitry Lyzo
cf7a93cd80 Add font MIME type 2022-05-13 17:15:06 +03:00
Bill Thornton
d606a2aad5 Merge pull request #3628 from thornbill/actually-fix-artist-albums 2022-05-11 23:13:02 -04:00
Bill Thornton
91c9a26e4d Fix artists album lists 2022-05-11 12:40:42 -04:00
Bill Thornton
add924e35e Merge pull request #3622 from thornbill/restore-external-links
Restore external links on mobile
2022-05-09 10:00:48 -04:00
Bill Thornton
3d901d3680 Restore external links on mobile 2022-05-09 09:30:46 -04:00
Bill Thornton
f2226ee745 Merge pull request #3604 from thornbill/fix-custom-elements
Fix custom element creation
2022-05-06 13:30:10 -04:00
Bill Thornton
f2c27dc1b5 Merge pull request #3618 from thornbill/fix-backdrop-mobile
Fix backdrop being used on large screens in mobile layout
2022-05-05 16:01:34 -04:00
Bill Thornton
d43418bf05 Merge pull request #3617 from thornbill/fix-ipados-icon
Fix iPadOS icon
2022-05-05 16:00:27 -04:00
Bill Thornton
d0ee66c2ce Fix backdrop being used on large screens in mobile layout 2022-05-05 10:38:45 -04:00
Bill Thornton
bd25a4bdab Merge pull request #3616 from thornbill/revert-translations
Revert some poor translations
2022-05-05 10:19:13 -04:00
Bill Thornton
a088f6afb3 Fix iPad app icon 2022-05-05 10:07:28 -04:00
Bill Thornton
bc5c7817a8 Fix custom element creation 2022-05-05 02:12:38 -04:00
Bill Thornton
76ca94094b Merge pull request #3614 from dmitrylyzo/fix-undefined-streaminfo-url
Check undefined streamInfo.url
2022-05-05 01:51:08 -04:00
Bill Thornton
191c6dd678 Revert some poor translations 2022-05-05 01:38:35 -04:00
Dmitry Lyzo
a5163d0be4 fix: Check undefined streamInfo.url
When remuxing and transcoding are disabled and the media
cannot be played direct, `streamInfo.url` is not set.
2022-05-03 21:47:52 +03:00
Bill Thornton
4792631f06 Merge pull request #3543 from Shadowghost/strm-display-fix 2022-04-29 14:09:05 -04:00
Bill Thornton
f7e2f07c05 Merge pull request #3597 from nielsvanvelzen/qc-text-input
Use text input in Quick Connect page
2022-04-27 15:23:47 -04:00
Joshua M. Boniface
bc0288e57f Merge pull request #3601 from thornbill/fix-repositories-xss 2022-04-27 12:37:16 -04:00
Bill Thornton
ee3c4a2681 Fix XSS in repositories list 2022-04-27 10:19:14 -04:00
Niels van Velzen
b00e93dd3d Require at least 6 characters in Quick Connect input 2022-04-27 10:25:00 +02:00
Niels van Velzen
d03aed23c8 Use text input in Quick Connect page 2022-04-26 19:35:57 +02:00
Bill Thornton
9b697ce832 Merge pull request #3577 from thornbill/fix-sd-filter-backport
Fix SD filter state
2022-04-25 10:37:29 -04:00
Bill Thornton
f2b9dc3aaa Fix SD filter state 2022-04-20 00:14:17 -04:00
Joshua M. Boniface
9c9b2721c5 Bump version to 10.8.0-beta2 2022-04-17 15:53:31 -04:00
Bill Thornton
dace55907f Merge pull request #3535 from dmitrylyzo/fix-poster-resume
Fix 'resume' when clicking on item details poster
2022-04-12 16:08:42 -04:00
Bill Thornton
5ede3c8e47 Merge pull request #3547 from thornbill/touching-books
Fix touch events in epub player
2022-04-12 16:08:00 -04:00
Shadowghost
b69b9227c4 Add track sorting to mediainfo and player track selection 2022-04-07 11:37:57 +02:00
Shadowghost
dc956eb48c Restore sort order after jellyfin/jellyfin#7529, allow subtitle selector display whithout video stream 2022-04-06 23:53:42 +02:00
Bill Thornton
5820416ede Fix touch events in epub player 2022-04-05 17:02:17 -04:00
Dmitry Lyzo
e7b80b7fa2 fix: Fix 'resume' when clicking on item details poster
Find the element with 'action'.
2022-03-31 21:05:01 +03:00
Bill Thornton
33b1f039ea Merge pull request #3527 from thornbill/fix-rewatching-next-up
Fix rewatching next up status
2022-03-31 13:58:40 -04:00
Bill Thornton
a514d168bf Fix rewatching in next up checked when disabled 2022-03-31 11:56:55 -04:00
Bill Thornton
847a81afd3 Merge pull request #3525 from whiteowl3/patch-3
Correct Typo
2022-03-31 11:43:25 -04:00
Bill Thornton
ef811e699c Merge pull request #3519 from dmitrylyzo/fix-escapehtml
Escape HTML
2022-03-31 11:42:34 -04:00
whiteowl3
d13ea90c23 Correct Typo 2022-03-29 06:43:12 -04:00
Dmitry Lyzo
9338dd082b fix: Escape HTML 2022-03-29 02:25:54 +03:00
Joshua M. Boniface
b4fce063b0 Bump packaging version to 10.8.0~beta1 2022-03-27 12:12:46 -04:00
899 changed files with 71233 additions and 133801 deletions

View File

@@ -0,0 +1,57 @@
jobs:
- job: Build
displayName: 'Build'
strategy:
matrix:
Development:
BuildConfiguration: development
Production:
BuildConfiguration: production
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
displayName: 'Install Node'
inputs:
versionSpec: '12.x'
- task: Cache@2
displayName: 'Cache node_modules'
inputs:
key: 'npm | package-lock.json'
path: 'node_modules'
- script: 'npm ci --no-audit'
displayName: 'Install Dependencies'
env:
SKIP_PREPARE: 'true'
- script: 'npm run build:development'
displayName: 'Build Development'
condition: eq(variables['BuildConfiguration'], 'development')
- script: 'npm run build:production'
displayName: 'Build Production'
condition: eq(variables['BuildConfiguration'], 'production')
- script: 'test -d dist'
displayName: 'Check Build'
- script: 'mv dist jellyfin-web'
displayName: 'Rename Directory'
- task: ArchiveFiles@2
displayName: 'Archive Directory'
inputs:
rootFolderOrFile: 'jellyfin-web'
includeRootFolder: true
archiveFile: 'jellyfin-web-$(BuildConfiguration)'
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
inputs:
targetPath: '$(Build.SourcesDirectory)/jellyfin-web-$(BuildConfiguration).zip'
artifactName: 'jellyfin-web-$(BuildConfiguration)'

View File

@@ -0,0 +1,126 @@
jobs:
- job: BuildPackage
displayName: 'Build Packages'
strategy:
matrix:
CentOS:
BuildConfiguration: centos
Debian:
BuildConfiguration: debian
Fedora:
BuildConfiguration: fedora
Portable:
BuildConfiguration: portable
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-web-$(BuildConfiguration) deployment'
displayName: 'Build Dockerfile'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
displayName: 'Run Dockerfile (unstable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-web-$(BuildConfiguration)'
displayName: 'Run Dockerfile (stable)'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: PublishPipelineArtifact@1
displayName: 'Publish Release'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
targetPath: '$(Build.SourcesDirectory)/deployment/dist'
artifactName: 'jellyfin-web-$(BuildConfiguration)'
- task: SSH@0
displayName: 'Create target directory on repository server'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
inputs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: BuildDocker
displayName: 'Build Docker'
pool:
vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: 0.0.0
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
repository: 'jellyfin/jellyfin-web'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker'
containerRegistry: Docker Hub
tags: |
unstable-$(Build.BuildNumber)
unstable
- task: Docker@2
displayName: 'Push Stable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
repository: 'jellyfin/jellyfin-web'
command: buildAndPush
buildContext: '.'
Dockerfile: 'deployment/Dockerfile.docker'
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)
$(JellyfinVersion)
- job: CollectArtifacts
displayName: 'Collect Artifacts'
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: SSH@0
displayName: 'Update Unstable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
- task: SSH@0
displayName: 'Update Stable Repository'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch)'

16
.ci/azure-pipelines.yml Normal file
View File

@@ -0,0 +1,16 @@
trigger:
batch: true
branches:
include:
- '*'
tags:
include:
- '*'
pr:
branches:
include:
- '*'
jobs:
- template: azure-pipelines-build.yml
- template: azure-pipelines-package.yml

1
.copr Symbolic link
View File

@@ -0,0 +1 @@
fedora/

View File

@@ -1,23 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node
{
"name": "Node.js",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
//https://github.com/microsoft/vscode-dev-containers/issues/559
"postCreateCommand": "source $NVM_DIR/nvm.sh && nvm install 20"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -8,5 +8,5 @@ trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
[*.{json,yaml,yml}]
[*.json]
indent_size = 2

View File

@@ -1,11 +0,0 @@
{
"ecmaVersion": "es5",
"modules": "false",
"files": "./dist/**/*.js",
"not": [
"./dist/libraries/pdf.worker.js",
"./dist/libraries/worker-bundle.js",
"./dist/libraries/wasm-gen/libarchive.js",
"./dist/serviceworker.js"
]
}

View File

@@ -2,13 +2,12 @@ const restrictedGlobals = require('confusing-browser-globals');
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'@babel',
'react',
'promise',
'import',
'eslint-comments',
'sonarjs'
'eslint-comments'
],
env: {
node: true,
@@ -16,110 +15,64 @@ module.exports = {
es2017: true,
es2020: true
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
jsx: true
}
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
// 'plugin:promise/recommended',
'plugin:import/errors',
'plugin:eslint-comments/recommended',
'plugin:compat/recommended',
'plugin:sonarjs/recommended'
'plugin:compat/recommended'
],
rules: {
'array-callback-return': ['error', { 'checkForEach': true }],
'array-callback-return': ['error'],
'block-spacing': ['error'],
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
'comma-dangle': ['error', 'never'],
'comma-spacing': ['error'],
'curly': ['error', 'multi-line', 'consistent'],
'default-case-last': ['error'],
'eol-last': ['error'],
'indent': ['error', 4, { 'SwitchCase': 1 }],
'jsx-quotes': ['error', 'prefer-single'],
'keyword-spacing': ['error'],
'max-statements-per-line': ['error'],
'max-params': ['error', 7],
'new-cap': [
'error',
{
'capIsNewExceptions': ['jQuery.Deferred'],
'newIsCapExceptionPattern': '\\.default$'
}
],
'no-duplicate-imports': ['error'],
'no-empty-function': ['error'],
'no-extend-native': ['error'],
'no-floating-decimal': ['error'],
'no-lonely-if': ['error'],
'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }],
'no-nested-ternary': ['error'],
'no-redeclare': ['off'],
'@typescript-eslint/no-redeclare': ['error', { builtinGlobals: false }],
'no-restricted-globals': ['error'].concat(restrictedGlobals),
'no-return-assign': ['error'],
'no-return-await': ['error'],
'no-sequences': ['error', { 'allowInParentheses': false }],
'no-shadow': ['off'],
'@typescript-eslint/no-shadow': ['error'],
'no-throw-literal': ['error'],
'no-trailing-spaces': ['error'],
'no-undef-init': ['error'],
'no-unneeded-ternary': ['error'],
'no-unused-expressions': ['off'],
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
'no-unused-private-class-members': ['error'],
'no-useless-rename': ['error'],
'no-useless-constructor': ['off'],
'@typescript-eslint/no-useless-constructor': ['error'],
'no-var': ['error'],
'no-void': ['error', { 'allowAsStatement': true }],
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
'object-curly-spacing': ['error', 'always'],
'@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
'one-var': ['error', 'never'],
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'@typescript-eslint/prefer-for-of': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'prefer-const': ['error', {'destructuring': 'all'}],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'radix': ['error'],
'@typescript-eslint/semi': ['error'],
'@babel/semi': ['error'],
'no-var': ['error'],
'space-before-blocks': ['error'],
'space-infix-ops': 'error',
'yoda': 'error',
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
'react/jsx-no-bind': ['error'],
'react/jsx-no-useless-fragment': ['error'],
'react/jsx-no-constructed-context-values': ['error'],
'react/no-array-index-key': ['error'],
'sonarjs/no-inverted-boolean-check': ['error'],
// TODO: Enable the following rules and fix issues
'sonarjs/cognitive-complexity': ['off'],
'sonarjs/no-duplicate-string': ['off']
'yoda': 'error'
},
settings: {
react: {
version: 'detect'
},
'import/extensions': [
'.js',
'.ts',
'.jsx',
'.tsx'
],
'import/parsers': {
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
},
'import/resolver': {
node: {
extensions: [
'.js',
'.ts',
'.jsx',
'.tsx'
],
moduleDirectory: [
'node_modules',
'src'
]
}
},
polyfills: [
// Native Promises Only
'Promise',
@@ -207,24 +160,12 @@ module.exports = {
]
},
overrides: [
// Config files and development scripts
{
files: [
'./babel.config.js',
'./.eslintrc.js',
'./postcss.config.js',
'./webpack.*.js',
'./scripts/**/*.js'
]
},
// JavaScript source files
{
files: [
'./src/**/*.{js,jsx,ts,tsx}'
'./src/**/*.js',
'./src/**/*.ts'
],
parserOptions: {
project: ['./tsconfig.json']
},
parser: '@babel/eslint-parser',
env: {
node: false,
amd: true,
@@ -251,33 +192,32 @@ module.exports = {
'DlnaProfilePage': 'writable',
'DashboardPage': 'writable',
'Emby': 'readonly',
'getParameterByName': 'writable',
'getWindowLocationSearch': 'writable',
'Globalize': 'writable',
'Hls': 'writable',
'dfnshelper': 'writable',
'LibraryMenu': 'writable',
'LinkParser': 'writable',
'LiveTvHelpers': 'writable',
'Loading': 'writable',
'MetadataEditor': 'writable',
'PlaylistViewer': 'writable',
'ServerNotifications': 'writable',
'TaskButton': 'writable',
'UserParentalControlPage': 'writable',
'Windows': 'readonly',
// Build time definitions
__JF_BUILD_VERSION__: 'readonly',
__PACKAGE_JSON_NAME__: 'readonly',
__PACKAGE_JSON_VERSION__: 'readonly',
__USE_SYSTEM_FONTS__: 'readonly',
__WEBPACK_SERVE__: 'readonly'
'Windows': 'readonly'
},
rules: {
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
}
},
// TypeScript source files
{
files: [
'./src/**/*.{ts,tsx}'
'./src/**/*.ts',
'./src/**/*.tsx'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:import/typescript',
@@ -286,13 +226,7 @@ module.exports = {
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended'
],
rules: {
'@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-unused-vars': ['error'],
'sonarjs/cognitive-complexity': ['error']
}
]
}
]
};

7
.github/CODEOWNERS vendored
View File

@@ -1 +1,6 @@
* @jellyfin/web
.ci @dkanada @EraYaN
.github @jellyfin/core
fedora @joshuaboniface
debian @joshuaboniface
.copr @joshuaboniface
deployment @joshuaboniface

2
.github/SUPPORT.md vendored
View File

@@ -7,7 +7,7 @@ When looking for support or information, please first search for your
question in these venues:
* [Jellyfin Forum](https://forum.jellyfin.org)
* [Jellyfin Documentation](https://jellyfin.org/docs/)
* [Jellyfin Documentation](https://docs.jellyfin.org)
* [Open or **closed** issues in the organization](https://github.com/issues?q=sort%3Aupdated-desc+org%3Ajellyfin+is%3Aissue+)
If you didn't find an answer in the resources above, contributors and other

View File

@@ -1,6 +1,6 @@
<!--
Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y).
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.org/docs/general/contributing/issues page.
For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://docs.jellyfin.org/general/contributing/issues.html page.
-->
**Changes**

55
.github/renovate.json vendored
View File

@@ -1,8 +1,51 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>jellyfin/.github//renovate-presets/nodejs",
":semanticCommitsDisabled",
":dependencyDashboard"
]
"packageRules": [
{
"matchManagers": ["npm"],
"addLabels": ["javascript"]
},
{
"description": "Adds label to dev dependency updates",
"matchDepTypes": ["devDependencies"],
"addLabels": ["dev-deps"]
},
{
"description": "Collects and groups dev dependency updates",
"matchDepTypes": ["devDependencies"],
"groupName": "development dependencies",
"groupSlug": "dev-deps"
},
{
"description": "Collects and groups npm dependency updates",
"matchDepTypes": ["dependencies"],
"groupName": "dependencies",
"groupSlug": "deps"
},
{
"description": "Collects and groups GitHub Action dependency updates",
"matchDepTypes": ["action"],
"addLabels": ["github_actions"],
"groupName": "CI dependencies",
"groupSlug": "ci-deps"
},
{
"description": "Disables HLS.js major updates",
"matchPackageNames": ["hls.js"],
"matchUpdateTypes": "major",
"enabled": false
}
],
"vulnerabilityAlerts": {
"addLabels": ["security"]
},
"dependencyDashboard": false,
"ignoreDeps": ["npm", "node"],
"lockFileMaintenance": {
"enabled": false
},
"enabledManagers": ["npm", "github-actions"],
"labels": ["dependencies"],
"prHourlyLimit": 2,
"rebaseWhen": "conflicted",
"rangeStrategy": "pin"
}

View File

@@ -14,8 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
- uses: eps1lon/actions-label-merge-conflict@v2.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.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -1,48 +0,0 @@
name: Build
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
workflow_dispatch:
jobs:
run-build-prod:
name: Run production build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run a production build
env:
JELLYFIN_VERSION: ${{ github.event.pull_request.head.sha || github.sha }}
run: npm run build:production
- name: Update config.json for testing
run: |
jq '.multiserver=true | .servers=["https://demo.jellyfin.org/unstable"]' dist/config.json > dist/config.tmp.json
mv dist/config.tmp.json dist/config.json
- name: Upload artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: jellyfin-web__prod
path: |
dist

31
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '30 7 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,34 +0,0 @@
name: CodeQL
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
schedule:
- cron: '30 7 * * 6'
jobs:
codeql:
name: Run CodeQL
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Initialize CodeQL
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
with:
languages: javascript
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6

View File

@@ -12,25 +12,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
uses: peter-evans/create-or-update-comment@v1.4.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@v3.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- name: Comment on failure
if: failure()
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
I'm sorry @${{ github.event.comment.user.login }}, I'm afraid I can't do that.

View File

@@ -1,65 +0,0 @@
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

119
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,119 @@
name: Lint
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
jobs:
run-eslint:
name: Run eslint
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v3.0.0
with:
node-version: 12
check-latest: true
- name: Get npm cache directory path
id: npm-cache-dir-path
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.0
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install Node.js dependencies
run: npm ci --no-audit
env:
SKIP_PREPARE: true
- name: Run eslint
run: npm run lint
run-stylelint-css:
name: Run stylelint (css)
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v3.0.0
with:
node-version: 12
check-latest: true
- name: Get npm cache directory path
id: npm-cache-dir-path
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.0
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@v1
- name: Install Node.js dependencies
run: npm ci --no-audit
env:
SKIP_PREPARE: true
- name: Run stylelint
run: npm run stylelint:css
run-stylelint-scss:
name: Run stylelint (scss)
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup node environment
uses: actions/setup-node@v3.0.0
with:
node-version: 12
check-latest: true
- name: Get npm cache directory path
id: npm-cache-dir-path
run: echo "::set-output name=dir::$(npm config get cache)"
- name: Cache node_modules
uses: actions/cache@v3.0.0
id: npm-cache
with:
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@v1
- name: Install Node.js dependencies
run: npm ci --no-audit
env:
SKIP_PREPARE: true
- name: Run stylelint
run: npm run stylelint:scss

View File

@@ -1,38 +0,0 @@
name: PR suggestions
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.id || github.run_id }}
cancel-in-progress: true
on:
pull_request_target:
branches: [ master, release* ]
types:
- synchronize
jobs:
run-eslint:
name: Run eslint suggestions
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run eslint
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
uses: CatChen/eslint-suggestion-action@b110ac684564c7b73e47cc223eb7a5266ec83fd3 # v4.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,84 +0,0 @@
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

@@ -1,123 +0,0 @@
name: Quality checks
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
branches: [ master, release* ]
pull_request:
branches: [ master, release* ]
jobs:
run-escheck:
name: Run es-check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run a production build
run: npm run build:production
- name: Run es-check
run: npm run escheck
run-eslint:
name: Run eslint
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run eslint
run: npx eslint --quiet "."
run-stylelint:
name: Run stylelint
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Set up stylelint matcher
uses: xt0rted/stylelint-problem-matcher@34db1b874c0452909f0696aedef70b723870a583 # tag=v1
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run stylelint
run: npm run stylelint
run-tsc:
name: Run TypeScript build check
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run tsc
run: npm run build:check
run-test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup node environment
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install Node.js dependencies
run: npm ci --no-audit
- name: Run test suite
run: npm run test

View File

@@ -1,24 +1,18 @@
name: Stale Check
name: Issue Stale Check
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
issues:
name: Check issues
stale:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
- uses: actions/stale@v5.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
@@ -27,25 +21,7 @@ jobs:
stale-issue-label: stale
stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).

View File

@@ -1,52 +0,0 @@
name: Update the Jellyfin SDK
on:
schedule:
- cron: '0 7 * * *'
workflow_dispatch:
concurrency:
group: unstable-sdk-pr
cancel-in-progress: true
jobs:
update:
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin-web' }}
steps:
- name: Check out Git repository
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: master
token: ${{ secrets.JF_BOT_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
check-latest: true
cache: npm
- name: Install latest unstable SDK
run: |
npm i --save @jellyfin/sdk@unstable
VERSION=$(jq -r '.dependencies["@jellyfin/sdk"]' package.json)
echo "JF_SDK_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Open a pull request
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}}
committer: jellyfin-bot <team@jellyfin.org>
author: jellyfin-bot <team@jellyfin.org>
branch: update-jf-sdk
delete-branch: true
title: Update @jellyfin/sdk to ${{env.JF_SDK_VERSION}}
body: |
**Changes**
Updates to the latest unstable @jellyfin/sdk build
labels: |
dependencies
npm

2
.gitignore vendored
View File

@@ -8,7 +8,7 @@ config.json
# ide
.idea
.vs
.vscode
# log
yarn-error.log

1
.npmrc
View File

@@ -1,3 +1,2 @@
engine-strict=true
fund=false
save-exact=true

View File

@@ -1,3 +0,0 @@
# Exclude test files from Sonar sources
# See: https://docs.sonarcloud.io/advanced-setup/analysis-scope/#file-exclusion-and-inclusion
sonar.exclusions=src/**/*.test.js,src/**/*.test.ts

View File

@@ -1,7 +1,7 @@
{
"plugins": [
"stylelint-no-browser-hacks/lib"
],
"stylelint-no-browser-hacks/lib"
],
"rules": {
"at-rule-empty-line-before": [ "always", {
"except": [
@@ -13,7 +13,6 @@
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": true,
"at-rule-no-vendor-prefix": true,
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
@@ -78,7 +77,6 @@
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-name-no-vendor-prefix": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
@@ -105,7 +103,6 @@
]
}
],
"property-no-vendor-prefix": true,
"rule-empty-line-before": [ "always-multi-line", {
"except": ["first-nested"],
"ignore": ["after-comment"]
@@ -119,7 +116,6 @@
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-no-vendor-prefix": true,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
@@ -138,25 +134,9 @@
"string-no-newline": true,
"unit-case": "lower",
"unit-no-unknown": true,
"value-no-vendor-prefix": true,
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0
},
"overrides": [
{
"files": [
"*.scss",
"**/*.scss"
],
"customSyntax": "postcss-scss",
"plugins": [ "stylelint-scss" ],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true,
"plugin/no-browser-hacks": null
}
}
]
}
}

10
.stylelintrc.scss.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": [ "./.stylelintrc.json" ],
"customSyntax": "postcss-scss",
"plugins": [ "stylelint-scss" ],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true,
"plugin/no-browser-hacks": null
}
}

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

View File

@@ -1,7 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.format.enable": true,
"editor.formatOnSave": false
}

View File

@@ -1,160 +1,121 @@
# Jellyfin Contributors
- [JoshuaBoniface](https://github.com/joshuaboniface)
- [nvllsvm](https://github.com/nvllsvm)
- [JustAMan](https://github.com/JustAMan)
- [dcrdev](https://github.com/dcrdev)
- [EraYaN](https://github.com/EraYaN)
- [flemse](https://github.com/flemse)
- [bfayers](https://github.com/bfayers)
- [Bond_009](https://github.com/Bond-009)
- [AnthonyLavado](https://github.com/anthonylavado)
- [dkanada](https://github.com/dkanada)
- [sparky8251](https://github.com/sparky8251)
- [LeoVerto](https://github.com/LeoVerto)
- [cvium](https://github.com/cvium)
- [grafixeyehero](https://github.com/grafixeyehero)
- [Drago96](https://github.com/drago-96)
- [ViXXoR](https://github.com/ViXXoR)
- [nkmerrill](https://github.com/nkmerrill)
- [TtheCreator](https://github.com/Tthecreator)
- [RazeLighter777](https://github.com/RazeLighter777)
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
- [thornbill](https://github.com/thornbill)
- [redSpoutnik](https://github.com/redSpoutnik)
- [DrPandemic](https://github.com/drpandemic)
- [Oddstr13](https://github.com/oddstr13)
- [petermcneil](https://github.com/petermcneil)
- [lewazo](https://github.com/lewazo)
- [Raghu Saxena](https://github.com/ckcr4lyf)
- [Nickbert7](https://github.com/Nickbert7)
- [ferferga](https://github.com/ferferga)
- [bilde2910](https://github.com/bilde2910)
- [Daniel Hartung](https://github.com/dhartung)
- [Ryan Hartzell](https://github.com/ryan-hartzell)
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
- [MrTimscampi](https://github.com/MrTimscampi)
- [artiume](https://github.com/Artiume)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [Sarab Singh](https://github.com/sarab97)
- [DesertCookie](https://github.com/desertcookie)
- [GuilhermeHideki](https://github.com/GuilhermeHideki)
- [Andrei Oanca](https://github.com/OancaAndrei)
- [Cromefire_](https://github.com/cromefire)
- [Orry Verducci](https://github.com/orryverducci)
- [Camc314](https://github.com/camc314)
- [danieladov](https://github.com/danieladov)
- [Stephane Senart](https://github.com/ssenart)
- [imchasingshadows](https://github.com/imchasingshadows)
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
- [Keegan Dahm](https://github.com/keegandahm)
- [GodTamIt](https://github.com/GodTamIt)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [taku0](https://github.com/taku0)
- [Viperinius](https://github.com/Viperinius)
- [is343](https://github.com/is343)
- [Meet Pandya](https://github.com/meet-k-pandya)
- [Peter Spenler](https://github.com/peterspenler)
- [jomp16](https://github.com/jomp16)
- [Leon de Klerk](https://github.com/leondeklerk)
- [CrispyBaguette](https://github.com/CrispyBaguette)
- [Vankerkom](https://github.com/vankerkom)
- [edvwib](https://github.com/edvwib)
- [Rob Farraher](https://github.com/farraherbg)
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
- [Anantharaju S](https://github.com/Anantharajus)
- [Merlin Sievers](https://github.com/dann-merlin)
- [Fishbigger](https://github.com/fishbigger)
- [sleepycatcoding](https://github.com/sleepycatcoding)
- [TheMelmacian](https://github.com/TheMelmacian)
- [v0idMrK](https://github.com/v0idMrK)
- [tehciolo](https://github.com/tehciolo)
- [scampower3](https://github.com/scampower3)
- [LittleBigOwI](https://github.com/LittleBigOwI/)
- [Nate G](https://github.com/GGProGaming)
- [Grady Hallenbeck](https://github.com/grhallenbeck)
- [DinuD](https://github.com/DinuD)
- [Kevin Tan (Valius)](https://github.com/valius)
- [Rasmus Krämer](https://github.com/rasmuslos)
- [ntarelix](https://github.com/ntarelix)
- [btopherjohnson](https://github.com/btopherjohnson)
- [András Maróy](https://github.com/andrasmaroy)
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
- [Vedant](https://github.com/viktory36)
- [GeorgeH005](https://github.com/GeorgeH005)
- [JPUC1143](https://github.com/Jpuc1143)
- [David Angel](https://github.com/davidangel)
- [Pithaya](https://github.com/Pithaya)
- [Chaitanya Shahare](https://github.com/Chaitanya-Shahare)
- [Connor Smith](https://github.com/ConnorS1110)
- [Venkat Karasani](https://github.com/venkat-karasani)
- [JoshuaBoniface](https://github.com/joshuaboniface)
- [nvllsvm](https://github.com/nvllsvm)
- [JustAMan](https://github.com/JustAMan)
- [dcrdev](https://github.com/dcrdev)
- [EraYaN](https://github.com/EraYaN)
- [flemse](https://github.com/flemse)
- [bfayers](https://github.com/bfayers)
- [Bond_009](https://github.com/Bond-009)
- [AnthonyLavado](https://github.com/anthonylavado)
- [dkanada](https://github.com/dkanada)
- [sparky8251](https://github.com/sparky8251)
- [LeoVerto](https://github.com/LeoVerto)
- [cvium](https://github.com/cvium)
- [grafixeyehero](https://github.com/grafixeyehero)
- [Drago96](https://github.com/drago-96)
- [ViXXoR](https://github.com/ViXXoR)
- [nkmerrill](https://github.com/nkmerrill)
- [TtheCreator](https://github.com/Tthecreator)
- [RazeLighter777](https://github.com/RazeLighter777)
- [LogicalPhallacy](https://github.com/LogicalPhallacy)
- [thornbill](https://github.com/thornbill)
- [redSpoutnik](https://github.com/redSpoutnik)
- [DrPandemic](https://github.com/drpandemic)
- [Oddstr13](https://github.com/oddstr13)
- [petermcneil](https://github.com/petermcneil)
- [lewazo](https://github.com/lewazo)
- [Raghu Saxena](https://github.com/ckcr4lyf)
- [Nickbert7](https://github.com/Nickbert7)
- [ferferga](https://github.com/ferferga)
- [bilde2910](https://github.com/bilde2910)
- [Daniel Hartung](https://github.com/dhartung)
- [Ryan Hartzell](https://github.com/ryan-hartzell)
- [Thibault Nocchi](https://github.com/ThibaultNocchi)
- [MrTimscampi](https://github.com/MrTimscampi)
- [artiume](https://github.com/Artiume)
- [ConfusedPolarBear](https://github.com/ConfusedPolarBear)
- [Sarab Singh](https://github.com/sarab97)
- [DesertCookie](https://github.com/desertcookie)
- [GuilhermeHideki](https://github.com/GuilhermeHideki)
- [Andrei Oanca](https://github.com/OancaAndrei)
- [Cromefire_](https://github.com/cromefire)
- [Orry Verducci](https://github.com/orryverducci)
- [Camc314](https://github.com/camc314)
- [danieladov](https://github.com/danieladov)
- [Stephane Senart](https://github.com/ssenart)
- [imchasingshadows](https://github.com/imchasingshadows)
- [Ömer Erdinç Yağmurlu](https://github.com/omeryagmurlu)
- [Keegan Dahm](https://github.com/keegandahm)
- [GodTamIt](https://github.com/GodTamIt)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [taku0](https://github.com/taku0)
- [Peter Spenler](https://github.com/peterspenler)
## Emby Contributors
# Emby Contributors
- [LukePulverenti](https://github.com/LukePulverenti)
- [ebr11](https://github.com/ebr11)
- [lalmanzar](https://github.com/lalmanzar)
- [schneifu](https://github.com/schneifu)
- [Mark2xv](https://github.com/Mark2xv)
- [ScottRapsey](https://github.com/ScottRapsey)
- [skynet600](https://github.com/skynet600)
- [Cheesegeezer](https://githum.com/Cheesegeezer)
- [Radeon](https://github.com/radeonorama)
- [gcw07](https://github.com/gcw07)
- [SivaramAdhiappan](https://github.com/shivaram1190)
- [CWatkinsNash](https://github.com/CWatkinsNash)
- [sfnetwork](https://github.com/sfnetwork)
- [Logos302](https://github.com/Logos302)
- [TheWorkz](https://github.com/TheWorkz)
- [mboehler](https://github.com/mboehler)
- [KaHooli](https://github.com/KaHooli)
- [xzener](https://github.com/xzener)
- [CBers](https://github.com/CBers)
- [Sagaia](https://github.com/Sagaia)
- [JHawk111](https://github.com/JHawk111)
- [David3663](https://github.com/david3663)
- [Smyken](https://github.com/Smyken)
- [doron1](https://github.com/doron1)
- [brainfryd](https://github.com/brainfryd)
- [DGMayor](http://github.com/DGMayor)
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
- [aspdend](https://github.com/aspdend)
- [RedshirtMB](https://github.com/RedshirtMB)
- [thealienamongus](https://github.com/thealienamongus)
- [brocass](https://github.com/brocass)
- [pjrollo2000](https://github.com/pjrollo2000)
- [abobader](https://github.com/abobader)
- [milli260876](https://github.com/milli260876)
- [vileboy](https://github.com/vileboy)
- [starkadius](https://github.com/starkadius)
- [wraslor](https://github.com/wraslor)
- [mrwebsmith](https://github.com/mrwebsmith)
- [rickster53](https://github.com/rickster53)
- [Tharnax](https://github.com/Tharnax)
- [0sm0](https://github.com/0sm0)
- [swhitmore](https://github.com/swhitmore)
- [DigiTM](https://github.com/DigiTM)
- [crisliv / xliv](https://github.com/crisliv)
- [Yogi](https://github.com/yogi12)
- [madFloyd](https://github.com/madFloyd)
- [yardameus](https://github.com/yardameus)
- [rrb008](https://github.com/rrb008)
- [Toonguy](https://github.com/Toonguy)
- [Alwin Hummels](https://github.com/AlwinHummels)
- [trooper11](https://github.com/trooper11)
- [danlotfy](https://github.com/danlotfy)
- [jordy1955](https://github.com/jordy1955)
- [JoshFink](https://github.com/JoshFink)
- [Detector1](https://github.com/Detector1)
- [BlackIce013](https://github.com/blackice013)
- [mporcas](https://github.com/mporcas)
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
<!--
NOTE: This is the end of the list of past Emby Contributors.
New Jellyfin contributors should add their name to the end
of the list of Jellyfin Contributors above. NOT HERE ;)
-->
- [LukePulverenti](https://github.com/LukePulverenti)
- [ebr11](https://github.com/ebr11)
- [lalmanzar](https://github.com/lalmanzar)
- [schneifu](https://github.com/schneifu)
- [Mark2xv](https://github.com/Mark2xv)
- [ScottRapsey](https://github.com/ScottRapsey)
- [skynet600](https://github.com/skynet600)
- [Cheesegeezer](https://githum.com/Cheesegeezer)
- [Radeon](https://github.com/radeonorama)
- [gcw07](https://github.com/gcw07)
- [SivaramAdhiappan](https://github.com/shivaram1190)
- [CWatkinsNash](https://github.com/CWatkinsNash)
- [sfnetwork](https://github.com/sfnetwork)
- [Logos302](https://github.com/Logos302)
- [TheWorkz](https://github.com/TheWorkz)
- [mboehler](https://github.com/mboehler)
- [KaHooli](https://github.com/KaHooli)
- [xzener](https://github.com/xzener)
- [CBers](https://github.com/CBers)
- [Sagaia](https://github.com/Sagaia)
- [JHawk111](https://github.com/JHawk111)
- [David3663](https://github.com/david3663)
- [Smyken](https://github.com/Smyken)
- [doron1](https://github.com/doron1)
- [brainfryd](https://github.com/brainfryd)
- [DGMayor](http://github.com/DGMayor)
- [Jon-theHTPC](https://github.com/Jon-theHTPC)
- [aspdend](https://github.com/aspdend)
- [RedshirtMB](https://github.com/RedshirtMB)
- [thealienamongus](https://github.com/thealienamongus)
- [brocass](https://github.com/brocass)
- [pjrollo2000](https://github.com/pjrollo2000)
- [abobader](https://github.com/abobader)
- [milli260876](https://github.com/milli260876)
- [vileboy](https://github.com/vileboy)
- [starkadius](https://github.com/starkadius)
- [wraslor](https://github.com/wraslor)
- [mrwebsmith](https://github.com/mrwebsmith)
- [rickster53](https://github.com/rickster53)
- [Tharnax](https://github.com/Tharnax)
- [0sm0](https://github.com/0sm0)
- [swhitmore](https://github.com/swhitmore)
- [DigiTM](https://github.com/DigiTM)
- [crisliv / xliv](https://github.com/crisliv)
- [Yogi](https://github.com/yogi12)
- [madFloyd](https://github.com/madFloyd)
- [yardameus](https://github.com/yardameus)
- [rrb008](https://github.com/rrb008)
- [Toonguy](https://github.com/Toonguy)
- [Alwin Hummels](https://github.com/AlwinHummels)
- [trooper11](https://github.com/trooper11)
- [danlotfy](https://github.com/danlotfy)
- [jordy1955](https://github.com/jordy1955)
- [JoshFink](https://github.com/JoshFink)
- [Detector1](https://github.com/Detector1)
- [BlackIce013](https://github.com/blackice013)
- [mporcas](https://github.com/mporcas)
- [tikuf](https://github.com/tikuf/)
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [jomp16](https://github.com/jomp16)
- [Leon de Klerk](https://github.com/leondeklerk)
- [CrispyBaguette](https://github.com/CrispyBaguette)

View File

@@ -70,31 +70,3 @@ Jellyfin Web is the frontend used for most of the clients available for end user
```sh
npm run build:development
```
## Directory Structure
```
.
└── src
├── apps
│   ├── dashboard # Admin dashboard app layout and routes
│   ├── experimental # New experimental app layout and routes
│   └── stable # Classic (stable) app layout and routes
├── assets # Static assets
├── components # Higher order visual components and React components
├── controllers # Legacy page views and controllers 🧹
├── elements # Basic webcomponents and React wrappers 🧹
├── hooks # Custom React hooks
├── legacy # Polyfills for legacy browsers
├── libraries # Third party libraries 🧹
├── plugins # Client plugins
├── scripts # Random assortment of visual components and utilities 🐉
├── strings # Translation files
├── styles # Common app Sass stylesheets
├── themes # CSS themes
├── types # Common TypeScript interfaces/types
└── utils # Utility functions
```
- 🧹 &mdash; Needs cleanup
- 🐉 &mdash; Serious mess (Here be dragons)

View File

@@ -12,7 +12,14 @@ module.exports = {
corejs: 3
}
],
'@babel/preset-react'
'@babel/preset-react',
[
'@babel/preset-typescript',
{
isTSX: true,
allExtensions: true
}
]
],
plugins: [
'@babel/plugin-proposal-class-properties',

110
build.sh Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# build.sh - Build Jellyfin binary packages
# Part of the Jellyfin Project
set -o errexit
set -o pipefail
usage() {
echo -e "build.sh - Build Jellyfin binary packages"
echo -e "Usage:"
echo -e " $0 -t/--type <BUILD_TYPE> -p/--platform <PLATFORM> [-k/--keep-artifacts] [-l/--list-platforms]"
echo -e "Notes:"
echo -e " * BUILD_TYPE can be one of: [native, docker] and must be specified"
echo -e " * native: Build using the build script in the host OS"
echo -e " * docker: Build using the build script in a standardized Docker container"
echo -e " * PLATFORM can be any platform shown by -l/--list-platforms and must be specified"
echo -e " * If -k/--keep-artifacts is specified, transient artifacts (e.g. Docker containers) will be"
echo -e " retained after the build is finished; the source directory will still be cleaned"
echo -e " * If -l/--list-platforms is specified, all other arguments are ignored; the script will print"
echo -e " the list of supported platforms and exit"
}
list_platforms() {
declare -a platforms
platforms=(
$( find deployment -maxdepth 1 -mindepth 1 -name "build.*" | awk -F'.' '{ $1=""; printf $2; if ($3 != ""){ printf "." $3; }; if ($4 != ""){ printf "." $4; }; print ""; }' | sort )
)
echo -e "Valid platforms:"
echo
for platform in ${platforms[@]}; do
echo -e "* ${platform} : $( grep '^#=' deployment/build.${platform} | sed 's/^#= //' )"
done
}
do_build_native() {
export IS_DOCKER=NO
deployment/build.${PLATFORM}
}
do_build_docker() {
if ! [ $(uname -m) = "x86_64" ]; then
echo "Docker-based builds only support amd64-based cross-building; use a 'native' build instead."
exit 1
fi
if [[ ! -f deployment/Dockerfile.${PLATFORM} ]]; then
echo "Missing Dockerfile for platform ${PLATFORM}"
exit 1
fi
if [[ ${KEEP_ARTIFACTS} == YES ]]; then
docker_args=""
else
docker_args="--rm"
fi
docker build . -t "jellyfin-builder.${PLATFORM}" -f deployment/Dockerfile.${PLATFORM}
mkdir -p ${ARTIFACT_DIR}
docker run $docker_args -v "${SOURCE_DIR}:/jellyfin" -v "${ARTIFACT_DIR}:/dist" "jellyfin-builder.${PLATFORM}"
}
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-t|--type)
BUILD_TYPE="$2"
shift
shift
;;
-p|--platform)
PLATFORM="$2"
shift
shift
;;
-k|--keep-artifacts)
KEEP_ARTIFACTS=YES
shift
;;
-l|--list-platforms)
list_platforms
exit 0
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option $1"
usage
exit 1
;;
esac
done
if [[ -z ${BUILD_TYPE} || -z ${PLATFORM} ]]; then
usage
exit 1
fi
export SOURCE_DIR="$( pwd )"
export ARTIFACT_DIR="${SOURCE_DIR}/../bin/${PLATFORM}"
# Determine build type
case ${BUILD_TYPE} in
native)
do_build_native
;;
docker)
do_build_docker
;;
esac

9
build.yaml Normal file
View File

@@ -0,0 +1,9 @@
---
# We just wrap `build` so this is really it
name: "jellyfin-web"
version: "10.8.13"
packages:
- debian.all
- fedora.all
- centos.all
- portable

View File

@@ -7,7 +7,7 @@ set -o pipefail
set -o xtrace
usage() {
echo -e "bump_version - increase the shared version"
echo -e "bump_version - increase the shared version and generate changelogs"
echo -e ""
echo -e "Usage:"
echo -e " $ bump_version <new_version>"
@@ -18,12 +18,75 @@ if [[ -z $1 ]]; then
exit 1
fi
build_file="./build.yaml"
package_file="./package*.json"
new_version="$1"
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
old_version="$(
grep "version:" ${build_file} \
| sed -E 's/version: "([0-9\.]+[-a-z0-9]*)"/\1/'
)"
echo "Old version: ${old_version}"
# Bump the NPM version
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
npm --no-git-tag-version --allow-same-version version v${new_version_sed}
# Set the build.yaml version to the specified new_version
old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
if [[ ${new_version} == *"-"* ]]; then
new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
new_version_deb_sup=""
else
new_version_pkg="${new_version}"
new_version_deb_sup="-1"
fi
# Write out a temporary Debian changelog with our new stuff appended and some templated formatting
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
echo -e "jellyfin-web (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
" >> ${debian_changelog_temp}
cat ${debian_changelog_file} >> ${debian_changelog_temp}
# Move into place
mv ${debian_changelog_temp} ${debian_changelog_file}
# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting
fedora_spec_file="fedora/jellyfin-web.spec"
fedora_changelog_temp="$( mktemp )"
fedora_spec_temp_dir="$( mktemp -d )"
fedora_spec_temp="${fedora_spec_temp_dir}/jellyfin-web.spec.tmp"
# Make a copy of our spec file for hacking
cp ${fedora_spec_file} ${fedora_spec_temp_dir}/
pushd ${fedora_spec_temp_dir}
# Split out the stuff before and after changelog
csplit jellyfin-web.spec "/^%changelog/" # produces xx00 xx01
# Update the version in xx00
sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
# Remove the header from xx01
sed -i '/^%changelog/d' xx01
# Create new temp file with our changelog
echo -e "%changelog
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v${new_version}" >> ${fedora_changelog_temp}
cat xx01 >> ${fedora_changelog_temp}
# Reassembble
cat xx00 ${fedora_changelog_temp} > ${fedora_spec_temp}
popd
# Move into place
mv ${fedora_spec_temp} ${fedora_spec_file}
# Clean up
rm -rf ${fedora_spec_temp_dir}
# Stage the changed files for commit
git add .
git status -v

View File

@@ -1,10 +0,0 @@
module.exports = {
preset: [
'default',
// Turn off `mergeLonghand` because it combines `padding-*` and `margin-*`,
// breaking fallback styles.
// https://github.com/cssnano/cssnano/issues/1163
// https://github.com/cssnano/cssnano/issues/1192
{ mergeLonghand: false }
]
};

95
debian/changelog vendored Normal file
View File

@@ -0,0 +1,95 @@
jellyfin-web (10.8.13-1) unstable; urgency=medium
* New upstream version 10.8.13; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.13
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 28 Nov 2023 22:21:29 -0500
jellyfin-web (10.8.12-1) unstable; urgency=medium
* New upstream version 10.8.12; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.12
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 04 Nov 2023 14:42:41 -0400
jellyfin-web (10.8.11-1) unstable; urgency=medium
* New upstream version 10.8.11; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.11
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 23 Sep 2023 21:41:40 -0400
jellyfin-web (10.8.10-1) unstable; urgency=medium
* New upstream version 10.8.10; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.10
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 23 Apr 2023 11:01:33 -0400
jellyfin-web (10.8.9-1) unstable; urgency=medium
* New upstream version 10.8.9; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.9
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 22 Jan 2023 14:09:13 -0500
jellyfin-web (10.8.8-1) unstable; urgency=medium
* New upstream version 10.8.8; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.8
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 29 Nov 2022 13:42:54 -0500
jellyfin-web (10.8.7-1) unstable; urgency=medium
* New upstream version 10.8.7; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.7
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 31 Oct 2022 23:06:34 -0400
jellyfin-web (10.8.6-1) unstable; urgency=medium
* New upstream version 10.8.6; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.6
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 28 Oct 2022 22:44:15 -0400
jellyfin-web (10.8.5-1) unstable; urgency=medium
* New upstream version 10.8.5; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.5
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 24 Sep 2022 22:02:26 -0400
jellyfin-web (10.8.4-1) unstable; urgency=medium
* New upstream version 10.8.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.4
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 13 Aug 2022 21:52:04 -0400
jellyfin-web (10.8.3-1) unstable; urgency=medium
* New upstream version 10.8.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.3
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 01 Aug 2022 20:22:00 -0400
jellyfin-web (10.8.2-1) unstable; urgency=medium
* New upstream version 10.8.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 01 Aug 2022 14:27:56 -0400
jellyfin-web (10.8.1-1) unstable; urgency=medium
* New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 26 Jun 2022 20:59:39 -0400
jellyfin-web (10.8.0-1) unstable; urgency=medium
* New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 10 Jun 2022 22:16:40 -0400
jellyfin-web (10.7.0-1) unstable; urgency=medium
* New upstream version 10.7.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.7.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:13:31 -0400
jellyfin-web (10.6.0-1) unstable; urgency=medium
* New upstream version 10.6.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 16 Mar 2020 11:15:00 -0400

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
8

1
debian/conffiles vendored Normal file
View File

@@ -0,0 +1 @@
/usr/share/jellyfin/web/config.json

16
debian/control vendored Normal file
View File

@@ -0,0 +1,16 @@
Source: jellyfin-web
Section: misc
Priority: optional
Maintainer: Jellyfin Team <team@jellyfin.org>
Build-Depends: debhelper (>= 9),
npm | nodejs
Standards-Version: 3.9.4
Homepage: https://jellyfin.org/
Vcs-Git: https://github.org/jellyfin/jellyfin-web.git
Vcs-Browser: https://github.org/jellyfin/jellyfin-web
Package: jellyfin-web
Recommends: jellyfin-server
Architecture: all
Description: Jellyfin is the Free Software Media System.
This package provides the Jellyfin web client.

28
debian/copyright vendored Normal file
View File

@@ -0,0 +1,28 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: jellyfin-web
Source: https://github.com/jellyfin/jellyfin-web
Files: *
Copyright: 2018-2020 Jellyfin Team
License: GPL-3.0
Files: debian/*
Copyright: 2020 Joshua Boniface <joshua@boniface.me>
License: GPL-3.0
License: GPL-3.0
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".

6
debian/gbp.conf vendored Normal file
View File

@@ -0,0 +1,6 @@
[DEFAULT]
pristine-tar = False
cleaner = fakeroot debian/rules clean
[import-orig]
filter = [ ".git*", ".hg*", ".vs*", ".vscode*" ]

1
debian/install vendored Normal file
View File

@@ -0,0 +1 @@
web usr/share/jellyfin/

1
debian/po/POTFILES.in vendored Normal file
View File

@@ -0,0 +1 @@
[type: gettext/rfc822deb] templates

57
debian/po/templates.pot vendored Normal file
View File

@@ -0,0 +1,57 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: jellyfin-server\n"
"Report-Msgid-Bugs-To: jellyfin-server@packages.debian.org\n"
"POT-Creation-Date: 2015-06-12 20:51-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#. Type: note
#. Description
#: ../templates:1001
msgid "Jellyfin permission info:"
msgstr ""
#. Type: note
#. Description
#: ../templates:1001
msgid ""
"Jellyfin by default runs under a user named \"jellyfin\". Please ensure that the "
"user jellyfin has read and write access to any folders you wish to add to your "
"library. Otherwise please run jellyfin under a different user."
msgstr ""
#. Type: string
#. Description
#: ../templates:2001
msgid "Username to run Jellyfin as:"
msgstr ""
#. Type: string
#. Description
#: ../templates:2001
msgid "The user that jellyfin will run as."
msgstr ""
#. Type: note
#. Description
#: ../templates:3001
msgid "Jellyfin still running"
msgstr ""
#. Type: note
#. Description
#: ../templates:3001
msgid "Jellyfin is currently running. Please close it and try again."
msgstr ""

20
debian/rules vendored Executable file
View File

@@ -0,0 +1,20 @@
#! /usr/bin/make -f
export DH_VERBOSE=1
%:
dh $@
# disable "make check"
override_dh_auto_test:
# disable stripping debugging symbols
override_dh_clistrip:
override_dh_auto_build:
npm ci --no-audit --unsafe-perm
mv $(CURDIR)/dist $(CURDIR)/web
override_dh_auto_clean:
test -d $(CURDIR)/dist && rm -rf '$(CURDIR)/dist' || true
test -d $(CURDIR)/web && rm -rf '$(CURDIR)/web' || true
test -d $(CURDIR)/node_modules && rm -rf '$(CURDIR)/node_modules' || true

1
debian/source/format vendored Normal file
View File

@@ -0,0 +1 @@
1.0

7
debian/source/options vendored Normal file
View File

@@ -0,0 +1,7 @@
tar-ignore='.git*'
tar-ignore='**/.git'
tar-ignore='**/.hg'
tar-ignore='**/.vs'
tar-ignore='**/.vscode'
tar-ignore='deployment'
tar-ignore='*.deb'

View File

@@ -0,0 +1,26 @@
FROM centos:7
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare CentOS environment
RUN yum update -y \
&& yum install -y epel-release \
&& yum install -y @buildsys-build rpmdevtools git yum-plugins-core autoconf automake glibc-devel gcc-c++ make \
&& curl -fsSL https://rpm.nodesource.com/setup_12.x | bash - \
&& yum install -y nodejs
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.centos /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,27 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV DEB_BUILD_OPTIONS=noddebs
ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y debhelper mmv git curl \
&& curl -fsSL https://deb.nodesource.com/setup_12.x | bash - \
&& apt-get install -y nodejs
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.debian /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,11 @@
FROM node:lts-alpine
ARG SOURCE_DIR=/src
ARG ARTIFACT_DIR=/jellyfin-web
RUN apk add autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3
WORKDIR ${SOURCE_DIR}
COPY . .
RUN npm ci --no-audit --unsafe-perm && mv dist ${ARTIFACT_DIR}

View File

@@ -0,0 +1,23 @@
FROM fedora:36
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare Fedora environment
RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

View File

@@ -0,0 +1,25 @@
FROM debian:10
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist
# Docker run environment
ENV SOURCE_DIR=/jellyfin
ENV ARTIFACT_DIR=/dist
ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update \
&& apt-get install -y mmv curl git \
&& curl -fsSL https://deb.nodesource.com/setup_12.x | bash - \
&& apt-get install -y nodejs
# Link to build script
RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh
VOLUME ${SOURCE_DIR}
VOLUME ${ARTIFACT_DIR}
ENTRYPOINT ["/build.sh"]

41
deployment/build.centos Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a package-lock.json /tmp/package-lock.json
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
sed -i "/%changelog/q" jellyfin-web.spec
cat <<EOF >>jellyfin-web.spec
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
EOF
popd
fi
# build rpm
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
# move the artifacts
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
rm -f fedora/jellyfin*.tar.gz
cp -a /tmp/package-lock.json package-lock.json
popd

39
deployment/build.debian Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a package-lock.json /tmp/package-lock.json
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd debian
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
cat <<EOF >changelog
jellyfin-web (${BUILD_ID}-unstable) unstable; urgency=medium
* Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
-- Jellyfin Packaging Team <packaging@jellyfin.org> $( date --rfc-2822 )
EOF
popd
fi
# build deb
dpkg-buildpackage -us -uc --pre-clean --post-clean
mkdir -p ${ARTIFACT_DIR}
mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}
cp -a /tmp/package-lock.json package-lock.json
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
popd

41
deployment/build.fedora Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
cp -a package-lock.json /tmp/package-lock.json
# modify changelog to unstable configuration if IS_UNSTABLE
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
pushd fedora
PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
sed -i "s/Version:.*/Version: ${BUILD_ID}/" jellyfin-web.spec
sed -i "/%changelog/q" jellyfin-web.spec
cat <<EOF >>jellyfin-web.spec
* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
- Jellyfin Web unstable build ${BUILD_ID} for merged PR #${PR_ID}
EOF
popd
fi
# build rpm
make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
# move the artifacts
mv /root/rpmbuild/RPMS/noarch/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
rm -f fedora/jellyfin*.tar.gz
cp -a /tmp/package-lock.json package-lock.json
popd

30
deployment/build.portable Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -o errexit
set -o xtrace
# move to source directory
pushd ${SOURCE_DIR}
# get version
if [[ ${IS_UNSTABLE} == 'yes' ]]; then
version="${BUILD_ID}"
else
version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
fi
# build archives
npm ci --no-audit --unsafe-perm
mv dist jellyfin-web_${version}
tar -czf jellyfin-web_${version}_portable.tar.gz jellyfin-web_${version}
rm -rf dist
# move the artifacts
mkdir -p ${ARTIFACT_DIR}
mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}
if [[ ${IS_DOCKER} == YES ]]; then
chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
fi
popd

48
fedora/Makefile Normal file
View File

@@ -0,0 +1,48 @@
DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# install git and npm
$(info $(shell set -x; if [ "$$(id -u)" = "0" ]; then echo "Installing git"; dnf -y install git npm; fi))
NAME := jellyfin-web
VERSION := $(shell set -x; sed -ne '/^Version:/s/.* *//p' $(DIR)/$(NAME).spec)
RELEASE := $(shell set -x; sed -ne '/^Release:/s/.* *\(.*\)%{.*}.*/\1/p' $(DIR)/$(NAME).spec)
SRPM := jellyfin-web-$(subst -,~,$(VERSION))-$(RELEASE)$(shell rpm --eval %dist).src.rpm
TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/
fed_ver := $(shell rpm -E %fedora)
# fallback when not running on Fedora
fed_ver ?= 36
TARGET ?= fedora-$(fed_ver)-x86_64
outdir ?= $(PWD)/$(DIR)/
srpm: $(DIR)/$(SRPM)
tarball: $(DIR)/$(TARBALL)
$(DIR)/$(TARBALL):
cd $(DIR)/; \
SOURCE_DIR=.. \
WORKDIR="$${PWD}"; \
version=$(VERSION); \
tar \
--transform "s,^\.,$(NAME)-$(subst -,~,$(VERSION))," \
--exclude='.git*' \
--exclude='**/.git' \
--exclude='**/.hg' \
--exclude=deployment \
--exclude='*.deb' \
--exclude='*.rpm' \
--exclude=$(notdir $@) \
-czf $(notdir $@) \
-C $${SOURCE_DIR} ./
$(DIR)/$(SRPM): $(DIR)/$(TARBALL) $(DIR)/jellyfin-web.spec
cd $(DIR)/; \
rpmbuild -bs $(NAME).spec \
--define "_sourcedir $$PWD/" \
--define "_srcrpmdir $(outdir)"
rpms: $(DIR)/$(SRPM)
mock $(addprefix --addrepo=, $($(TARGET)_repos)) \
--enable-network \
-r $(TARGET) $<

76
fedora/jellyfin-web.spec Normal file
View File

@@ -0,0 +1,76 @@
%global debug_package %{nil}
Name: jellyfin-web
Version: 10.8.13
Release: 1%{?dist}
Summary: The Free Software Media System web client
License: GPLv2
URL: https://jellyfin.org
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
Source0: jellyfin-web-%{version}.tar.gz
BuildArch: noarch
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
BuildRequires: nodejs
%else
BuildRequires: git
BuildRequires: npm
%endif
%description
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
%prep
%autosetup -n jellyfin-web-%{version} -b 0
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
# Required for CentOS build
chown root:root -R .
%endif
%build
npm ci --no-audit --unsafe-perm
%install
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin/jellyfin-web
%{__cp} -r dist/* %{buildroot}%{_libdir}/jellyfin/jellyfin-web
%files
%defattr(644,root,root,755)
%{_libdir}/jellyfin/jellyfin-web
%license LICENSE
%changelog
* Tue Nov 28 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.13; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.13
* Sat Nov 04 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.12; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.12
* Sat Sep 23 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.11; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.11
* Sun Apr 23 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.10; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.10
* Sun Jan 22 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.9; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.9
* Tue Nov 29 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.8; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.8
* Mon Oct 31 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.7; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.7
* Fri Oct 28 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.6; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.6
* Sat Sep 24 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.5; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.5
* Sat Aug 13 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.4
* Mon Aug 01 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.3
* Mon Aug 01 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.2
* Sun Jun 26 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.1
* Fri Jun 10 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.0

37573
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,129 +1,109 @@
{
"name": "jellyfin-web",
"version": "10.9.11",
"version": "10.8.13",
"description": "Web interface for Jellyfin",
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@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": "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",
"@babel/core": "7.17.7",
"@babel/eslint-parser": "7.17.0",
"@babel/eslint-plugin": "7.17.7",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-private-methods": "7.16.11",
"@babel/plugin-transform-modules-umd": "7.16.7",
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@thornbill/jellyfin-sdk": "0.4.1",
"@types/escape-html": "1.0.1",
"@types/lodash-es": "4.17.6",
"@types/react": "17.0.40",
"@types/react-dom": "17.0.13",
"@typescript-eslint/eslint-plugin": "5.15.0",
"@typescript-eslint/parser": "5.15.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.19",
"babel-loader": "9.1.3",
"autoprefixer": "10.4.4",
"babel-loader": "8.2.3",
"babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "6.10.0",
"cssnano": "6.1.2",
"es-check": "7.1.1",
"eslint": "8.57.0",
"eslint-plugin-compat": "4.2.0",
"copy-webpack-plugin": "10.2.4",
"css-loader": "6.7.1",
"cssnano": "5.1.4",
"eslint": "8.11.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.29.1",
"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": "4.2.0",
"html-webpack-plugin": "5.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.72.0",
"sass-loader": "13.3.3",
"source-map-loader": "4.0.2",
"speed-measure-webpack-plugin": "1.5.0",
"style-loader": "3.3.4",
"stylelint": "15.11.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-promise": "6.0.0",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"expose-loader": "3.1.0",
"html-loader": "3.1.0",
"html-webpack-plugin": "5.5.0",
"postcss": "8.4.12",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.4.2",
"postcss-scss": "4.0.3",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"source-map-loader": "3.0.1",
"style-loader": "3.3.1",
"stylelint": "14.6.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.4.3",
"vitest": "1.4.0",
"webpack": "5.91.0",
"webpack-bundle-analyzer": "4.10.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.2",
"webpack-merge": "5.10.0",
"stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "5.0.0",
"stylelint-scss": "4.2.0",
"ts-loader": "9.2.8",
"typescript": "4.6.2",
"webpack": "5.70.0",
"webpack-cli": "4.9.2",
"webpack-dev-server": "4.7.4",
"webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.1",
"worker-loader": "3.0.8"
},
"dependencies": {
"@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",
"@fontsource/noto-sans": "4.5.1",
"@fontsource/noto-sans-hk": "4.5.2",
"@fontsource/noto-sans-jp": "4.5.2",
"@fontsource/noto-sans-kr": "4.5.2",
"@fontsource/noto-sans-sc": "4.5.2",
"@fontsource/noto-sans-tc": "4.5.2",
"@jellyfin/libass-wasm": "4.1.1",
"blurhash": "1.1.4",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.5.1",
"core-js": "3.36.1",
"date-fns": "2.30.0",
"dompurify": "3.0.1",
"classnames": "2.3.1",
"core-js": "3.20.2",
"date-fns": "2.28.0",
"dompurify": "2.3.4",
"epubjs": "0.3.93",
"escape-html": "1.0.3",
"fast-text-encoding": "1.0.6",
"fast-text-encoding": "1.0.3",
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"history": "5.3.0",
"hls.js": "1.5.7",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1",
"jstree": "3.3.16",
"hls.js": "0.14.17",
"intersection-observer": "0.12.0",
"jellyfin-apiclient": "1.10.0",
"jquery": "3.6.0",
"jstree": "3.3.12",
"libarchive.js": "1.3.0",
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0",
"marked": "4.0.10",
"material-design-icons-iconfont": "6.1.1",
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"page": "1.11.6",
"pdfjs-dist": "2.12.313",
"react": "17.0.2",
"react-blurhash": "0.3.0",
"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.0.7",
"usehooks-ts": "2.16.0",
"screenfull": "6.0.0",
"sortablejs": "1.14.0",
"swiper": "6.8.4",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.20"
"whatwg-fetch": "3.6.2",
"workbox-core": "6.2.4",
"workbox-precaching": "6.2.4"
},
"browserslist": [
"last 2 Firefox versions",
@@ -144,19 +124,15 @@
"scripts": {
"start": "npm run serve",
"serve": "webpack serve --config webpack.dev.js",
"build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js",
"prepare": "node ./scripts/prepare.js",
"build:development": "webpack --config webpack.dev.js",
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
"build:check": "tsc --noEmit",
"escheck": "es-check",
"lint": "eslint \"./\"",
"test": "vitest --watch=false --config vite.config.ts",
"test:watch": "vitest --config vite.config.ts",
"stylelint": "stylelint \"src/**/*.{css,scss}\""
"build:production": "webpack --config webpack.prod.js",
"lint": "eslint \"src/\"",
"stylelint": "npm run stylelint:css && npm run stylelint:scss",
"stylelint:css": "stylelint \"src/**/*.css\"",
"stylelint:scss": "stylelint --config=\".stylelintrc.scss.json\" \"src/**/*.scss\""
},
"engines": {
"node": ">=20.0.0",
"npm": ">=9.6.4",
"yarn": "YARN NO LONGER USED - use npm instead."
}
}

View File

@@ -7,8 +7,8 @@ const config = () => ({
plugins: [
// Explicitly specify browserslist to override ones from node_modules
// For example, Swiper has it in its package.json
postcssPresetEnv({ browsers: packageConfig.browserslist }),
autoprefixer({ overrideBrowserslist: packageConfig.browserslist }),
postcssPresetEnv({browsers: packageConfig.browserslist}),
autoprefixer({overrideBrowserslist: packageConfig.browserslist}),
cssnano()
]
});

12
scripts/prepare.js Executable file
View File

@@ -0,0 +1,12 @@
const { execSync } = require('child_process');
/**
* The npm `prepare` script needs to run a build to support installing
* a package from git repositories (this is dumb but a limitation of how
* npm behaves). We don't want to run these in CI though because
* building is slow so this script will skip the build when the
* `SKIP_PREPARE` environment variable has been set.
*/
if (!process.env.SKIP_PREPARE) {
execSync('webpack --config webpack.prod.js', { stdio: 'inherit' });
}

View File

@@ -1,24 +0,0 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import { queryClient } from 'utils/query/queryClient';
import RootAppRouter from './RootAppRouter';
const RootApp = () => {
return (
<QueryClientProvider client={queryClient}>
<ApiProvider>
<WebConfigProvider>
<RootAppRouter />
</WebConfigProvider>
</ApiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default RootApp;

View File

@@ -1,54 +0,0 @@
import React from 'react';
import {
RouterProvider,
createHashRouter,
Outlet,
useLocation
} from 'react-router-dom';
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { createRouterHistory } from 'components/router/routerHistory';
import UserThemeProvider from 'themes/UserThemeProvider';
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const router = createHashRouter([
{
element: <RootAppLayout />,
children: [
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
...DASHBOARD_APP_ROUTES
]
}
]);
export const history = createRouterHistory(router);
export default function RootAppRouter() {
return <RouterProvider router={router} />;
}
/**
* Layout component that renders legacy components required on all pages.
* NOTE: The app will crash if these get removed from the DOM.
*/
function RootAppLayout() {
const location = useLocation();
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
.some(path => location.pathname.startsWith(`/${path}`));
return (
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
<Outlet />
</UserThemeProvider>
);
}

15
src/apiclient.d.ts vendored
View File

@@ -67,8 +67,7 @@ declare module 'jellyfin-apiclient' {
UserPolicy,
UtcTimeResponse,
VirtualFolderInfo
} from '@jellyfin/sdk/lib/generated-client';
import { ConnectionState } from './utils/jellyfin-apiclient/ConnectionState';
} from '@thornbill/jellyfin-sdk/dist/generated-client';
class ApiClient {
constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string);
@@ -76,7 +75,6 @@ declare module 'jellyfin-apiclient' {
accessToken(): string;
addMediaPath(virtualFolderName: string, mediaPath: string, networkSharePath: string, refreshLibrary?: boolean): Promise<void>;
addVirtualFolder(name: string, type?: string, refreshLibrary?: boolean, libraryOptions?: any): Promise<void>;
ajax(request: any): Promise<any>;
appName(): string;
appVersion(): string;
authenticateUserByName(name: string, password: string): Promise<AuthenticationResult>;
@@ -118,7 +116,6 @@ declare module 'jellyfin-apiclient' {
getCountries(): Promise<CountryInfo[]>;
getCriticReviews(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getCultures(): Promise<CultureDto[]>;
getCurrentUser(cache?: boolean): Promise<UserDto>;
getCurrentUserId(): string;
getDateParamValue(date: Date): string;
getDefaultImageQuality(imageType: ImageType): number;
@@ -269,7 +266,7 @@ declare module 'jellyfin-apiclient' {
sendWebSocketMessage(name: string, data: any): void;
serverAddress(val?: string): string;
serverId(): string;
serverVersion(): string;
serverVersion(): string
setAuthenticationInfo(accessKey?: string, userId?: string): void;
setRequestHeaders(headers: any): void;
setSystemInfo(info: SystemInfo): void;
@@ -313,18 +310,12 @@ declare module 'jellyfin-apiclient' {
setItem(name: string, value: string): void;
}
interface ConnectResponse {
ApiClient: ApiClient
Servers: any[]
State: ConnectionState
}
class ConnectionManager {
constructor(credentialProvider: Credentials, appName: string, appVersion: string, deviceName: string, deviceId: string, capabilities: ClientCapabilities);
addApiClient(apiClient: ApiClient): void;
clearData(): void;
connect(options?: any): Promise<ConnectResponse>;
connect(options?: any): Promise<any>;
connectToAddress(address: string, options?: any): Promise<any>;
connectToServer(server: any, options?: any): Promise<any>;
connectToServers(servers: any[], options?: any): Promise<any>;

View File

@@ -1,99 +0,0 @@
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { type Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import './AppOverrides.scss';
interface AppLayoutProps {
drawerlessPaths: string[]
}
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)
&& !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
setIsDrawerActive(!isDrawerActive);
}, [ isDrawerActive, setIsDrawerActive ]);
// Update body class
useEffect(() => {
document.body.classList.add('dashboardDocument');
return () => {
document.body.classList.remove('dashboardDocument');
};
}, []);
return (
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
>
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
</AppBar>
</ElevationScroll>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};
export default AppLayout;

View File

@@ -1,34 +0,0 @@
// Default MUI breakpoints
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
$mui-bp-sm: 600px;
$mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
$drawer-width: 240px;
// Fix dashboard pages layout to work with drawer
.dashboardDocument {
.mainAnimatedPage {
@media all and (min-width: $mui-bp-md) {
left: $drawer-width;
}
}
.skinBody {
position: unset !important;
}
// Fix the padding of dashboard pages
.content-primary {
padding-top: 3.25rem;
}
// Tabbed pages
.withTabs .content-primary {
padding-top: 6.5rem;
@media all and (min-width: $mui-bp-lg) {
padding-top: 3.25rem;
}
}
}

View File

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

View File

@@ -1,34 +0,0 @@
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import Chip from '@mui/material/Chip';
import React from 'react';
import globalize from 'scripts/globalize';
const LogLevelChip = ({ level }: { level: LogLevel }) => {
let color: 'info' | 'warning' | 'error' | undefined;
switch (level) {
case LogLevel.Information:
color = 'info';
break;
case LogLevel.Warning:
color = 'warning';
break;
case LogLevel.Error:
case LogLevel.Critical:
color = 'error';
break;
}
const levelText = globalize.translate(`LogLevel.${level}`);
return (
<Chip
size='small'
color={color}
label={levelText}
title={levelText}
/>
);
};
export default LogLevelChip;

View File

@@ -1,64 +0,0 @@
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
import Info from '@mui/icons-material/Info';
import Box from '@mui/material/Box';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import React, { FC, useCallback, useState } from 'react';
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
const displayValue = ShortOverview ?? Overview;
const [ open, setOpen ] = useState(false);
const onTooltipClose = useCallback(() => {
setOpen(false);
}, []);
const onTooltipOpen = useCallback(() => {
setOpen(true);
}, []);
if (!displayValue) return null;
return (
<Box
sx={{
display: 'flex',
width: '100%',
alignItems: 'center'
}}
>
<Box
sx={{
flexGrow: 1,
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
component='div'
title={displayValue}
>
{displayValue}
</Box>
{ShortOverview && Overview && (
<ClickAwayListener onClickAway={onTooltipClose}>
<Tooltip
title={Overview}
placement='top'
arrow
onClose={onTooltipClose}
open={open}
disableFocusListener
disableHoverListener
disableTouchListener
>
<IconButton onClick={onTooltipOpen}>
<Info />
</IconButton>
</Tooltip>
</ClickAwayListener>
)}
</Box>
);
};
export default OverviewCell;

View File

@@ -1,17 +0,0 @@
import React, { type RefAttributes } from 'react';
import { Link } from 'react-router-dom';
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
/**
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
* @see https://github.com/mui/mui-x/issues/4654
*/
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
<Link to={to}>
<GridActionsCellItem {...props} />
</Link>
);
export default GridActionsCellLink;

View File

@@ -1,37 +0,0 @@
import ListItem from '@mui/material/ListItem';
import List from '@mui/material/List';
import React, { FC } from 'react';
import DrawerHeaderLink from 'apps/experimental/components/drawers/DrawerHeaderLink';
import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';
import ServerDrawerSection from './sections/ServerDrawerSection';
import DevicesDrawerSection from './sections/DevicesDrawerSection';
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
import PluginDrawerSection from './sections/PluginDrawerSection';
const AppDrawer: FC<ResponsiveDrawerProps> = ({
open = false,
onClose,
onOpen
}) => (
<ResponsiveDrawer
open={open}
onClose={onClose}
onOpen={onOpen}
>
<List disablePadding>
<ListItem disablePadding>
<DrawerHeaderLink />
</ListItem>
</List>
<ServerDrawerSection />
<DevicesDrawerSection />
<LiveTvDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</ResponsiveDrawer>
);
export default AppDrawer;

View File

@@ -1,109 +0,0 @@
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 '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'
subheader={
<ListSubheader component='div' id='advanced-subheader'>
{globalize.translate('TabAdvanced')}
</ListSubheader>
}
>
<ListItem disablePadding>
<ListItemLink to='/dashboard/networking'>
<ListItemIcon>
<Lan />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabNetworking')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/keys'>
<ListItemIcon>
<VpnKey />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/logs'>
<ListItemIcon>
<Article />
</ListItemIcon>
<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>
<Schedule />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabScheduledTasks')} />
</ListItemLink>
</ListItem>
</List>
);
};
export default AdvancedDrawerSection;

View File

@@ -1,50 +0,0 @@
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';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
const DevicesDrawerSection = () => {
return (
<List
aria-labelledby='devices-subheader'
subheader={
<ListSubheader component='div' id='devices-subheader'>
{globalize.translate('HeaderDevices')}
</ListSubheader>
}
>
<ListItem disablePadding>
<ListItemLink to='/dashboard/devices'>
<ListItemIcon>
<Devices />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderDevices')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/activity'>
<ListItemIcon>
<Analytics />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderActivity')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/dlna'>
<ListItemIcon>
<Input />
</ListItemIcon>
<ListItemText primary={'DLNA'} />
</ListItemLink>
</ListItem>
</List>
);
};
export default DevicesDrawerSection;

View File

@@ -1,42 +0,0 @@
import { Dvr, LiveTv } 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 from 'react';
import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';
const LiveTvDrawerSection = () => {
return (
<List
aria-labelledby='livetv-subheader'
subheader={
<ListSubheader component='div' id='livetv-subheader'>
{globalize.translate('LiveTV')}
</ListSubheader>
}
>
<ListItem disablePadding>
<ListItemLink to='/dashboard/livetv'>
<ListItemIcon>
<LiveTv />
</ListItemIcon>
<ListItemText primary={globalize.translate('LiveTV')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/recordings'>
<ListItemIcon>
<Dvr />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderDVR')} />
</ListItemLink>
</ListItem>
</List>
);
};
export default LiveTvDrawerSection;

View File

@@ -1,66 +0,0 @@
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, useState } from 'react';
import ListItemLink from 'components/ListItemLink';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import Dashboard from 'utils/dashboard';
const PluginDrawerSection = () => {
const { api } = useApi();
const [ pagesInfo, setPagesInfo ] = useState<ConfigurationPageInfo[]>([]);
useEffect(() => {
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
aria-labelledby='plugins-subheader'
subheader={
<ListSubheader component='div' id='plugins-subheader'>
{globalize.translate('TabPlugins')}
</ListSubheader>
}
>
{
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>
);
};
export default PluginDrawerSection;

View File

@@ -1,121 +0,0 @@
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
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 'scripts/globalize';
const LIBRARY_PATHS = [
'/dashboard/libraries',
'/dashboard/libraries/display',
'/dashboard/libraries/metadata',
'/dashboard/libraries/nfo'
];
const PLAYBACK_PATHS = [
'/dashboard/playback/transcoding',
'/dashboard/playback/resume',
'/dashboard/playback/streaming',
'/dashboard/playback/trickplay'
];
const ServerDrawerSection = () => {
const location = useLocation();
const isLibrarySectionOpen = LIBRARY_PATHS.includes(location.pathname);
const isPlaybackSectionOpen = PLAYBACK_PATHS.includes(location.pathname);
return (
<List
aria-labelledby='server-subheader'
subheader={
<ListSubheader component='div' id='server-subheader'>
{globalize.translate('TabServer')}
</ListSubheader>
}
>
<ListItem disablePadding>
<ListItemLink to='/dashboard'>
<ListItemIcon>
<Dashboard />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabDashboard')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/settings'>
<ListItemIcon>
<Settings />
</ListItemIcon>
<ListItemText primary={globalize.translate('General')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/users'>
<ListItemIcon>
<People />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderUsers')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/libraries' selected={false}>
<ListItemIcon>
<LibraryAdd />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderLibraries')} />
{isLibrarySectionOpen ? <ExpandLess /> : <ExpandMore />}
</ListItemLink>
</ListItem>
<Collapse in={isLibrarySectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding>
<ListItemLink to='/dashboard/libraries' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('HeaderLibraries')} />
</ListItemLink>
<ListItemLink to='/dashboard/libraries/display' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Display')} />
</ListItemLink>
<ListItemLink to='/dashboard/libraries/metadata' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Metadata')} />
</ListItemLink>
<ListItemLink to='/dashboard/libraries/nfo' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabNfoSettings')} />
</ListItemLink>
</List>
</Collapse>
<ListItem disablePadding>
<ListItemLink to='/dashboard/playback/transcoding' selected={false}>
<ListItemIcon>
<PlayCircle />
</ListItemIcon>
<ListItemText primary={globalize.translate('TitlePlayback')} />
{isPlaybackSectionOpen ? <ExpandLess /> : <ExpandMore />}
</ListItemLink>
</ListItem>
<Collapse in={isPlaybackSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding>
<ListItemLink to='/dashboard/playback/transcoding' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Transcoding')} />
</ListItemLink>
<ListItemLink to='/dashboard/playback/resume' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('ButtonResume')} />
</ListItemLink>
<ListItemLink to='/dashboard/playback/streaming' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabStreaming')} />
</ListItemLink>
<ListItemLink to='/dashboard/playback/trickplay' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('Trickplay')} />
</ListItemLink>
</List>
</Collapse>
</List>
);
};
export default ServerDrawerSection;

View File

@@ -1,14 +0,0 @@
import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity', 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: 'playback/trickplay', type: AsyncRouteType.Dashboard }
];

View File

@@ -1,149 +0,0 @@
import type { LegacyRoute } from 'components/router/LegacyRoute';
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
{
path: '/dashboard',
pageProps: {
controller: 'dashboard/dashboard',
view: 'dashboard/dashboard.html'
}
}, {
path: 'settings',
pageProps: {
controller: 'dashboard/general',
view: 'dashboard/general.html'
}
}, {
path: 'networking',
pageProps: {
controller: 'dashboard/networking',
view: 'dashboard/networking.html'
}
}, {
path: 'devices',
pageProps: {
controller: 'dashboard/devices/devices',
view: 'dashboard/devices/devices.html'
}
}, {
path: 'devices/edit',
pageProps: {
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: {
controller: 'dashboard/library',
view: 'dashboard/library.html'
}
}, {
path: 'libraries/display',
pageProps: {
controller: 'dashboard/librarydisplay',
view: 'dashboard/librarydisplay.html'
}
}, {
path: 'playback/transcoding',
pageProps: {
controller: 'dashboard/encodingsettings',
view: 'dashboard/encodingsettings.html'
}
}, {
path: 'logs',
pageProps: {
controller: 'dashboard/logs',
view: 'dashboard/logs.html'
}
}, {
path: 'libraries/metadata',
pageProps: {
controller: 'dashboard/metadataImages',
view: 'dashboard/metadataimages.html'
}
}, {
path: 'libraries/nfo',
pageProps: {
controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html'
}
}, {
path: 'playback/resume',
pageProps: {
controller: 'dashboard/playback',
view: 'dashboard/playback.html'
}
}, {
path: 'plugins/catalog',
pageProps: {
controller: 'dashboard/plugins/available/index',
view: 'dashboard/plugins/available/index.html'
}
}, {
path: 'plugins/repositories',
pageProps: {
controller: 'dashboard/plugins/repositories/index',
view: 'dashboard/plugins/repositories/index.html'
}
}, {
path: 'livetv/guide',
pageProps: {
controller: 'livetvguideprovider',
view: 'livetvguideprovider.html'
}
}, {
path: 'recordings',
pageProps: {
controller: 'livetvsettings',
view: 'livetvsettings.html'
}
}, {
path: 'livetv',
pageProps: {
controller: 'livetvstatus',
view: 'livetvstatus.html'
}
}, {
path: 'livetv/tuner',
pageProps: {
controller: 'livetvtuner',
view: 'livetvtuner.html'
}
}, {
path: 'plugins',
pageProps: {
controller: 'dashboard/plugins/installed/index',
view: 'dashboard/plugins/installed/index.html'
}
}, {
path: 'tasks/edit',
pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtask',
view: 'dashboard/scheduledtasks/scheduledtask.html'
}
}, {
path: 'tasks',
pageProps: {
controller: 'dashboard/scheduledtasks/scheduledtasks',
view: 'dashboard/scheduledtasks/scheduledtasks.html'
}
}, {
path: 'keys',
pageProps: {
controller: 'dashboard/apikeys',
view: 'dashboard/apikeys.html'
}
}, {
path: 'playback/streaming',
pageProps: {
view: 'dashboard/streaming.html',
controller: 'dashboard/streaming'
}
}
];

View File

@@ -1,40 +0,0 @@
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' },
{ from: 'library.html', to: '/dashboard/libraries' },
{ from: 'librarydisplay.html', to: '/dashboard/libraries/display' },
{ from: 'livetvguideprovider.html', to: '/dashboard/livetv/guide' },
{ from: 'livetvsettings.html', to: '/dashboard/recordings' },
{ from: 'livetvstatus.html', to: '/dashboard/livetv' },
{ from: 'livetvtuner.html', to: '/dashboard/livetv/tuner' },
{ from: 'log.html', to: '/dashboard/logs' },
{ 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' },
{ from: 'scheduledtasks.html', to: '/dashboard/tasks' },
{ from: 'serveractivity.html', to: '/dashboard/activity' },
{ from: 'streamingsettings.html', to: '/dashboard/playback/streaming' },
{ from: 'useredit.html', to: '/dashboard/users/profile' },
{ from: 'userlibraryaccess.html', to: '/dashboard/users/access' },
{ from: 'usernew.html', to: '/dashboard/users/add' },
{ from: 'userparentalcontrol.html', to: '/dashboard/users/parentalcontrol' },
{ from: 'userpassword.html', to: '/dashboard/users/password' },
{ from: 'userprofiles.html', to: '/dashboard/users' }
];

View File

@@ -1,274 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
import PermMedia from '@mui/icons-material/PermMedia';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Typography from '@mui/material/Typography';
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
import { Link, useSearchParams } from 'react-router-dom';
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 'scripts/globalize';
import { toBoolean } from 'utils/string';
import LogLevelChip from '../components/activityTable/LogLevelChip';
import OverviewCell from '../components/activityTable/OverviewCell';
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
const DEFAULT_PAGE_SIZE = 25;
const VIEW_PARAM = 'useractivity';
const enum ActivityView {
All,
User,
System
}
const getActivityView = (param: string | null) => {
if (param === null) return ActivityView.All;
if (toBoolean(param)) return ActivityView.User;
return ActivityView.System;
};
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
const Activity = () => {
const { api } = useApi();
const [ searchParams, setSearchParams ] = useSearchParams();
const [ activityView, setActivityView ] = useState(
getActivityView(searchParams.get(VIEW_PARAM)));
const [ isLoading, setIsLoading ] = useState(true);
const [ paginationModel, setPaginationModel ] = useState({
page: 0,
pageSize: DEFAULT_PAGE_SIZE
});
const [ rowCount, setRowCount ] = useState(0);
const [ rows, setRows ] = useState<ActivityLogEntry[]>([]);
const [ users, setUsers ] = useState<Record<string, UserDto>>({});
const userColDef: GridColDef[] = activityView !== ActivityView.System ? [
{
field: 'User',
headerName: globalize.translate('LabelUser'),
width: 60,
valueGetter: ({ row }) => users[row.UserId]?.Name,
renderCell: ({ row }) => (
<IconButton
size='large'
color='inherit'
sx={{ padding: 0 }}
title={users[row.UserId]?.Name ?? undefined}
component={Link}
to={`/dashboard/users/profile?userId=${row.UserId}`}
>
<UserAvatar user={users[row.UserId]} />
</IconButton>
)
}
] : [];
const columns: GridColDef[] = [
{
field: 'Date',
headerName: globalize.translate('LabelDate'),
width: 90,
type: 'date',
valueGetter: ({ value }) => parseISO8601Date(value),
valueFormatter: ({ value }) => toLocaleDateString(value)
},
{
field: 'Time',
headerName: globalize.translate('LabelTime'),
width: 100,
type: 'dateTime',
valueGetter: ({ row }) => parseISO8601Date(row.Date),
valueFormatter: ({ value }) => toLocaleTimeString(value)
},
{
field: 'Severity',
headerName: globalize.translate('LabelLevel'),
width: 110,
renderCell: ({ value }) => (
value ? (
<LogLevelChip level={value} />
) : undefined
)
},
...userColDef,
{
field: 'Name',
headerName: globalize.translate('LabelName'),
width: 300
},
{
field: 'Overview',
headerName: globalize.translate('LabelOverview'),
width: 200,
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
renderCell: ({ row }) => (
<OverviewCell {...row} />
)
},
{
field: 'Type',
headerName: globalize.translate('LabelType'),
width: 180
},
{
field: 'actions',
type: 'actions',
width: 50,
getActions: ({ row }) => {
const actions = [];
if (row.ItemId) {
actions.push(
<GridActionsCellLink
size='large'
icon={<PermMedia />}
label={globalize.translate('LabelMediaDetails')}
title={globalize.translate('LabelMediaDetails')}
to={`/details?id=${row.ItemId}`}
/>
);
}
return actions;
}
}
];
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
if (newView !== null) {
setActivityView(newView);
}
}, []);
useEffect(() => {
if (api) {
const fetchUsers = async () => {
const { data } = await getUserApi(api).getUsers();
const usersById: Record<string, UserDto> = {};
data.forEach(user => {
if (user.Id) {
usersById[user.Id] = user;
}
});
setUsers(usersById);
};
fetchUsers()
.catch(err => {
console.error('[activity] failed to fetch users', err);
});
}
}, [ api ]);
useEffect(() => {
if (api) {
const fetchActivity = async () => {
const params: {
startIndex: number,
limit: number,
hasUserId?: boolean
} = {
startIndex: paginationModel.page * paginationModel.pageSize,
limit: paginationModel.pageSize
};
if (activityView !== ActivityView.All) {
params.hasUserId = activityView === ActivityView.User;
}
const { data } = await getActivityLogApi(api)
.getLogEntries(params);
setRowCount(data.TotalRecordCount ?? 0);
setRows(data.Items ?? []);
setIsLoading(false);
};
setIsLoading(true);
fetchActivity()
.catch(err => {
console.error('[activity] failed to fetch activity log entries', err);
});
}
}, [ activityView, api, paginationModel ]);
useEffect(() => {
const currentViewParam = getActivityView(searchParams.get(VIEW_PARAM));
if (currentViewParam !== activityView) {
if (activityView === ActivityView.All) {
searchParams.delete(VIEW_PARAM);
} else {
searchParams.set(VIEW_PARAM, `${activityView === ActivityView.User}`);
}
setSearchParams(searchParams);
}
}, [ activityView, searchParams, setSearchParams ]);
return (
<Page
id='serverActivityPage'
title={globalize.translate('HeaderActivity')}
className='mainAnimatedPage type-interior'
>
<div className='content-primary'>
<Box
sx={{
display: 'flex',
alignItems: 'baseline',
marginY: 2
}}
>
<Box sx={{ flexGrow: 1 }}>
<Typography variant='h2'>
{globalize.translate('HeaderActivity')}
</Typography>
</Box>
<ToggleButtonGroup
value={activityView}
onChange={onViewChange}
exclusive
>
<ToggleButton value={ActivityView.All}>
{globalize.translate('All')}
</ToggleButton>
<ToggleButton value={ActivityView.User}>
{globalize.translate('LabelUser')}
</ToggleButton>
<ToggleButton value={ActivityView.System}>
{globalize.translate('LabelSystem')}
</ToggleButton>
</ToggleButtonGroup>
</Box>
<DataGrid
columns={columns}
rows={rows}
pageSizeOptions={[ 10, 25, 50, 100 ]}
paginationMode='server'
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
rowCount={rowCount}
getRowId={getRowId}
loading={isLoading}
sx={{
minHeight: 500
}}
/>
</div>
</Page>
);
};
export default Activity;

View File

@@ -1,33 +0,0 @@
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

@@ -1,34 +0,0 @@
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,311 +0,0 @@
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
import React, { type FunctionComponent, useCallback, useEffect, useRef } from 'react';
import globalize from '../../../../scripts/globalize';
import Page from '../../../../components/Page';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import ButtonElement from '../../../../elements/ButtonElement';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import InputElement from '../../../../elements/InputElement';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import ServerConnections from '../../../../components/ServerConnections';
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const PlaybackTrickplay: FunctionComponent = () => {
const element = useRef<HTMLDivElement>(null);
const loadConfig = useCallback((config) => {
const page = element.current;
const options = config.TrickplayOptions;
if (!page) {
console.error('Unexpected null reference');
return;
}
(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();
}, []);
const loadData = useCallback(() => {
loading.show();
ServerConnections.currentApiClient()?.getServerConfiguration().then(function (config) {
loadConfig(config);
}).catch(err => {
console.error('[PlaybackTrickplay] failed to fetch server config', err);
});
}, [loadConfig]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const saveConfig = (config: ServerConfiguration) => {
const apiClient = ServerConnections.currentApiClient();
if (!apiClient) {
console.error('[PlaybackTrickplay] No current apiclient instance');
return;
}
if (!config.TrickplayOptions) {
throw new Error('Unexpected null TrickplayOptions');
}
const options = config.TrickplayOptions;
options.EnableHwAcceleration = (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked;
options.EnableHwEncoding = (page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked;
options.ScanBehavior = (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value as TrickplayScanBehavior;
options.ProcessPriority = (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value as ProcessPriorityClass;
options.Interval = Math.max(1, parseInt((page.querySelector('#txtInterval') as HTMLInputElement).value || '10000', 10));
options.WidthResolutions = (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value.replace(' ', '').split(',').map(Number);
options.TileWidth = Math.max(1, parseInt((page.querySelector('#txtTileWidth') as HTMLInputElement).value || '10', 10));
options.TileHeight = Math.max(1, parseInt((page.querySelector('#txtTileHeight') as HTMLInputElement).value || '10', 10));
options.Qscale = Math.min(31, parseInt((page.querySelector('#txtQscale') as HTMLInputElement).value || '4', 10));
options.JpegQuality = Math.min(100, parseInt((page.querySelector('#txtJpegQuality') as HTMLInputElement).value || '90', 10));
options.ProcessThreads = parseInt((page.querySelector('#txtProcessThreads') as HTMLInputElement).value || '1', 10);
apiClient.updateServerConfiguration(config).then(() => {
onSaveComplete();
}).catch(err => {
console.error('[PlaybackTrickplay] failed to update config', err);
});
};
const onSubmit = (e: Event) => {
const apiClient = ServerConnections.currentApiClient();
if (!apiClient) {
console.error('[PlaybackTrickplay] No current apiclient instance');
return;
}
loading.show();
apiClient.getServerConfiguration().then(function (config) {
saveConfig(config);
}).catch(err => {
console.error('[PlaybackTrickplay] failed to fetch server config', err);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.trickplayConfigurationForm') as HTMLFormElement).addEventListener('submit', onSubmit);
loadData();
}, [loadData]);
const optionScanBehavior = () => {
let content = '';
content += `<option value='NonBlocking'>${globalize.translate('NonBlockingScan')}</option>`;
content += `<option value='Blocking'>${globalize.translate('BlockingScan')}</option>`;
return content;
};
const optionProcessPriority = () => {
let content = '';
content += `<option value='High'>${globalize.translate('PriorityHigh')}</option>`;
content += `<option value='AboveNormal'>${globalize.translate('PriorityAboveNormal')}</option>`;
content += `<option value='Normal'>${globalize.translate('PriorityNormal')}</option>`;
content += `<option value='BelowNormal'>${globalize.translate('PriorityBelowNormal')}</option>`;
content += `<option value='Idle'>${globalize.translate('PriorityIdle')}</option>`;
return content;
};
return (
<Page
id='trickplayConfigurationPage'
className='mainAnimatedPage type-interior playbackConfigurationPage'
title={globalize.translate('Trickplay')}
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('Trickplay')}
isLinkVisible={false}
/>
</div>
<form className='trickplayConfigurationForm'>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableHwAcceleration'
title='LabelTrickplayAccel'
/>
</div>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableHwEncoding'
title='LabelTrickplayAccelEncoding'
/>
<div className='fieldDescription checkboxFieldDescription'>
<div className='fieldDescription'>
{globalize.translate('LabelTrickplayAccelEncodingHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectScanBehavior'>
<SelectElement
id='selectScanBehavior'
label='LabelScanBehavior'
>
{optionScanBehavior()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('LabelScanBehaviorHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectProcessPriority'>
<SelectElement
id='selectProcessPriority'
label='LabelProcessPriority'
>
{optionProcessPriority()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('LabelProcessPriorityHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtInterval'
label='LabelImageInterval'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelImageIntervalHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='text'
id='txtWidthResolutions'
label='LabelWidthResolutions'
options={'required pattern="[0-9,]*"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelWidthResolutionsHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtTileWidth'
label='LabelTileWidth'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTileWidthHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtTileHeight'
label='LabelTileHeight'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTileHeightHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtJpegQuality'
label='LabelJpegQuality'
options={'required inputMode="numeric" pattern="[0-9]*" min="1" max="100"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelJpegQualityHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtQscale'
label='LabelQscale'
options={'required inputMode="numeric" pattern="[0-9]*" min="2" max="31"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelQscaleHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtProcessThreads'
label='LabelTrickplayThreads'
options={'required inputMode="numeric" pattern="[0-9]*" min="0"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTrickplayThreadsHelp')}
</div>
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</Page>
);
};
export default PlaybackTrickplay;

View File

@@ -1,49 +0,0 @@
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';
export const DASHBOARD_APP_PATHS = {
Dashboard: 'dashboard',
MetadataManager: 'metadata',
PluginConfig: 'configurationpage'
};
export const DASHBOARD_APP_ROUTES: RouteObject[] = [
{
element: <ConnectionRequired isAdminRequired />,
children: [
{
element: <AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />,
children: [
{
path: DASHBOARD_APP_PATHS.Dashboard,
children: [
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
]
},
/* NOTE: The metadata editor might deserve a dedicated app in the future */
toViewManagerPageRoute({
path: DASHBOARD_APP_PATHS.MetadataManager,
pageProps: {
controller: 'edititemmetadata',
view: 'edititemmetadata.html'
}
}),
{
path: DASHBOARD_APP_PATHS.PluginConfig,
element: <ServerContentPage view='/web/configurationpage' />
}
]
}
]
}
];

View File

@@ -1,188 +0,0 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../scripts/globalize';
import loading from '../../../../components/loading/loading';
import dom from '../../../../scripts/dom';
import confirm from '../../../../components/confirm/confirm';
import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import '../../../../elements/emby-button/emby-button';
import '../../../../elements/emby-button/paper-icon-button-light';
import '../../../../components/cardbuilder/card.scss';
import '../../../../components/indicators/indicators.scss';
import '../../../../styles/flexstyles.scss';
import Page from '../../../../components/Page';
type MenuEntry = {
name?: string;
id?: string;
icon?: string;
};
const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);
const loadData = () => {
loading.show();
window.ApiClient.getUsers().then(function (result) {
setUsers(result);
loading.hide();
}).catch(err => {
console.error('[userprofiles] failed to fetch users', err);
});
};
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card?.getAttribute('data-userid');
const username = card?.getAttribute('data-username');
if (!userId) {
console.error('Unexpected null user id');
return;
}
const menuItems: MenuEntry[] = [];
menuItems.push({
name: globalize.translate('ButtonEditUser'),
id: 'open',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ButtonLibraryAccess'),
id: 'access',
icon: 'lock'
});
menuItems.push({
name: globalize.translate('ButtonParentalControl'),
id: 'parentalcontrol',
icon: 'person'
});
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id: string) {
switch (id) {
case 'open':
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to user edit page', err);
});
break;
case 'access':
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to user library page', err);
});
break;
case 'parentalcontrol':
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to parental control page', err);
});
break;
case 'delete':
deleteUser(userId, username);
}
}
}).catch(() => {
// action sheet closed
});
}).catch(err => {
console.error('[userprofiles] failed to load action sheet', err);
});
};
const deleteUser = (id: string, username?: string | null) => {
const title = username ? globalize.translate('DeleteName', username) : globalize.translate('DeleteUser');
const text = globalize.translate('DeleteUserConfirmation');
confirm({
title,
text,
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
window.ApiClient.deleteUser(id).then(function () {
loadData();
}).catch(err => {
console.error('[userprofiles] failed to delete user', err);
});
}).catch(() => {
// confirm dialog closed
});
};
page.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('/dashboard/users/add')
.catch(err => {
console.error('[userprofiles] failed to navigate to new user page', err);
});
});
}, []);
return (
<Page
id='userProfilesPage'
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
title={globalize.translate('HeaderUsers')}
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
btnId='btnAddUser'
btnClassName='fab submit sectionTitleButton'
btnTitle='ButtonAddUser'
btnIcon='add'
url='https://jellyfin.org/docs/general/server/users/adding-managing-users'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
return <UserCardBox key={user.Id} user={user} />;
})}
</div>
</div>
</Page>
);
};
export default UserProfiles;

View File

@@ -1,528 +0,0 @@
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import escapeHTML from 'escape-html';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import globalize from '../../../../scripts/globalize';
import LibraryMenu from '../../../../scripts/libraryMenu';
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
import TagList from '../../../../components/dashboard/users/TagList';
import ButtonElement from '../../../../elements/ButtonElement';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
import prompt from '../../../../components/prompt/prompt';
import ServerConnections from 'components/ServerConnections';
type UnratedItem = {
name: string;
value: string;
checkedAttribute: string
};
function handleSaveUser(
page: HTMLDivElement,
getSchedulesFromPage: () => AccessSchedule[],
getAllowedTagsFromPage: () => string[],
getBlockedTagsFromPage: () => string[],
onSaveComplete: () => void
) {
return (user: UserDto) => {
const userId = user.Id;
const userPolicy = user.Policy;
if (!userId || !userPolicy) {
throw new Error('Unexpected null user id or policy');
}
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
userPolicy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
userPolicy.BlockUnratedItems = Array.prototype.filter
.call(page.querySelectorAll('.chkUnratedItem'), i => i.checked)
.map(i => i.getAttribute('data-itemtype'));
userPolicy.AccessSchedules = getSchedulesFromPage();
userPolicy.AllowedTags = getAllowedTagsFromPage();
userPolicy.BlockedTags = getBlockedTagsFromPage();
ServerConnections.getCurrentApiClientAsync()
.then(apiClient => apiClient.updateUserPolicy(userId, userPolicy))
.then(() => onSaveComplete())
.catch(err => {
console.error('[userparentalcontrol] failed to update user policy', err);
});
};
}
const UserParentalControl: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
setParentalRatings(ratings);
}, []);
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
const itemsArr: UnratedItem[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
value: item.value,
name: item.name,
checkedAttribute: checkedAttribute
});
}
setUnratedItems(itemsArr);
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
}, []);
const loadAllowedTags = useCallback((tags: string[]) => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
setAllowedTags(tags);
const allowedTagsElem = page.querySelector('.allowedTags') as HTMLDivElement;
for (const btnDeleteTag of allowedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(t => t !== tag);
loadAllowedTags(newTags);
});
}
}, []);
const loadBlockedTags = useCallback((tags: string[]) => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
setBlockedTags(tags);
const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement;
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(t => t !== tag);
loadBlockedTags(newTags);
});
}
}, []);
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
setAccessSchedules(schedules);
const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement;
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') ?? '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter((i: number) => i != index);
renderAccessSchedule(newindex);
});
}
}, []);
const loadUser = useCallback((user: UserDto, allParentalRatings: ParentalRating[]) => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
setUserName(user.Name || '');
LibraryMenu.setTitle(user.Name);
loadUnratedItems(user);
loadAllowedTags(user.Policy?.AllowedTags || []);
loadBlockedTags(user.Policy?.BlockedTags || []);
populateRatings(allParentalRatings);
let ratingValue = '';
if (user.Policy?.MaxParentalRating) {
allParentalRatings.forEach(rating => {
if (rating.Value && user.Policy?.MaxParentalRating && user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = `${rating.Value}`;
}
});
}
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy?.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
}
renderAccessSchedule(user.Policy?.AccessSchedules || []);
loading.hide();
}, [loadAllowedTags, loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => {
if (!userId) {
console.error('[userparentalcontrol.loadData] missing user id');
return;
}
loading.show();
const promise1 = window.ApiClient.getUser(userId);
const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(responses[0], responses[1]);
}).catch(err => {
console.error('[userparentalcontrol] failed to load data', err);
});
}, [loadUser, userId]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('[userparentalcontrol] Unexpected null page reference');
return;
}
loadData();
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {};
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage();
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(schedules);
}).catch(() => {
// access schedule closed
});
}).catch(err => {
console.error('[userparentalcontrol] failed to load access schedule', err);
});
};
const getSchedulesFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.liSchedule'), function (elem) {
return {
DayOfWeek: elem.getAttribute('data-day'),
StartHour: elem.getAttribute('data-start'),
EndHour: elem.getAttribute('data-end')
};
}) as AccessSchedule[];
};
const getAllowedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.allowedTag'), function (elem) {
return elem.getAttribute('data-tag');
}) as string[];
};
const showAllowedTagPopup = () => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getAllowedTagsFromPage();
if (tags.indexOf(value) == -1) {
tags.push(value);
loadAllowedTags(tags);
}
}).catch(() => {
// prompt closed
});
};
const getBlockedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag');
}) as string[];
};
const showBlockedTagPopup = () => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage();
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(tags);
}
}).catch(() => {
// prompt closed
});
};
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
const onSubmit = (e: Event) => {
if (!userId) {
console.error('[userparentalcontrol.onSubmit] missing user id');
return;
}
loading.show();
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
}).catch(err => {
console.error('[userparentalcontrol] failed to fetch user', err);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
StartHour: 0,
EndHour: 0
}, -1);
});
(page.querySelector('#btnAddAllowedTag') as HTMLButtonElement).addEventListener('click', function () {
showAllowedTagPopup();
});
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadAllowedTags, loadBlockedTags, loadData, renderAccessSchedule]);
const optionMaxParentalRating = () => {
let content = '';
content += '<option value=\'\'></option>';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
}
return content;
};
return (
<Page
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</div>
<br />
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelAllowContentWithTags')}
isBtnVisible={true}
btnId='btnAddAllowedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='fieldDescription'>
{globalize.translate('AllowContentWithTagsHelp')}
</div>
<div className='allowedTags' style={{ marginTop: '.5em' }}>
{allowedTags?.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='allowedTag'
/>;
})}
</div>
</div>
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='fieldDescription'>
{globalize.translate('BlockContentWithTagsHelp')}
</div>
<div className='blockedTags' style={{ marginTop: '.5em' }}>
{blockedTags.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='blockedTag'
/>;
})}
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={accessSchedule.Id}
index={index}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
/>;
})}
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</Page>
);
};
export default UserParentalControl;

View File

@@ -1,60 +0,0 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Page from '../../../../components/Page';
import loading from '../../../../components/loading/loading';
const UserPassword: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => {
if (!userId) {
console.error('[userpassword] missing user id');
return;
}
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
setUserName(user.Name);
loading.hide();
}).catch(err => {
console.error('[userpassword] failed to fetch user', err);
});
}, [userId]);
useEffect(() => {
loadUser();
}, [loadUser]);
return (
<Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
userId={userId}
/>
</div>
</div>
</Page>
);
};
export default UserPassword;

View File

@@ -1,80 +0,0 @@
import React, { useCallback, useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { type Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import AppToolbar from './components/AppToolbar';
import AppDrawer, { isDrawerPath } from './components/drawers/AppDrawer';
import './AppOverrides.scss';
const AppLayout = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const { user } = useApi();
const location = useLocation();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isDrawerAvailable = isDrawerPath(location.pathname) && Boolean(user);
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
setIsDrawerActive(!isDrawerActive);
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};
export default AppLayout;

View File

@@ -1,51 +0,0 @@
// Default MUI breakpoints
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
$mui-bp-sm: 600px;
$mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;
$drawer-width: 240px;
#reactRoot {
height: 100%;
}
// Fix main pages layout to work with drawer
.mainAnimatedPage {
@media all and (min-width: $mui-bp-md) {
left: $drawer-width;
}
}
// Hide some items from the user "settings" page that are in the drawer
#myPreferencesMenuPage {
.lnkQuickConnectPreferences,
.adminSection,
.userSection {
display: none !important;
}
}
// Fix the padding of some pages
.homePage.libraryPage.withTabs, // Home page
// Library pages excluding the item details page and tabbed pages
.libraryPage:not(
.itemDetailPage,
.withTabs
) {
padding-top: 3.25rem !important;
}
// Tabbed library pages
.libraryPage.withTabs {
padding-top: 6.5rem !important;
@media all and (min-width: $mui-bp-lg) {
padding-top: 3.25rem !important;
}
}
// Fix backdrop position on mobile item details page
.layout-mobile .itemBackdrop {
margin-top: 0 !important;
}

View File

@@ -1,112 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import CastConnected from '@mui/icons-material/CastConnected';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Cast from '@mui/icons-material/Cast';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
import Events from 'utils/events';
import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu';
const RemotePlayButton = () => {
const theme = useTheme();
const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo());
const updatePlayerInfo = useCallback(() => {
setPlayerInfo(playbackManager.getPlayerInfo());
}, [ setPlayerInfo ]);
useEffect(() => {
Events.on(playbackManager, 'playerchange', updatePlayerInfo);
return () => {
Events.off(playbackManager, 'playerchange', updatePlayerInfo);
};
}, [ updatePlayerInfo ]);
const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl);
const onRemotePlayButtonClick = useCallback((event) => {
setRemotePlayMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayMenuAnchorEl ]);
const onRemotePlayMenuClose = useCallback(() => {
setRemotePlayMenuAnchorEl(null);
}, [ setRemotePlayMenuAnchorEl ]);
const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl);
const onRemotePlayActiveButtonClick = useCallback((event) => {
setRemotePlayActiveMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayActiveMenuAnchorEl ]);
const onRemotePlayActiveMenuClose = useCallback(() => {
setRemotePlayActiveMenuAnchorEl(null);
}, [ setRemotePlayActiveMenuAnchorEl ]);
return (
<>
{(playerInfo && !playerInfo.isLocalPlayer) ? (
<Box
sx={{
alignSelf: 'center'
}}
>
<Tooltip title={globalize.translate('ButtonCast')}>
<Button
variant='text'
size='large'
startIcon={<CastConnected />}
aria-label={globalize.translate('ButtonCast')}
aria-controls={ACTIVE_ID}
aria-haspopup='true'
onClick={onRemotePlayActiveButtonClick}
color='inherit'
sx={{
color: theme.palette.primary.main
}}
>
{playerInfo.deviceName || playerInfo.name}
</Button>
</Tooltip>
</Box>
) : (
<Tooltip title={globalize.translate('ButtonCast')}>
<IconButton
size='large'
aria-label={globalize.translate('ButtonCast')}
aria-controls={ID}
aria-haspopup='true'
onClick={onRemotePlayButtonClick}
color='inherit'
>
<Cast />
</IconButton>
</Tooltip>
)}
<RemotePlayMenu
open={isRemotePlayMenuOpen}
anchorEl={remotePlayMenuAnchorEl}
onMenuClose={onRemotePlayMenuClose}
/>
<RemotePlayActiveMenu
open={isRemotePlayActiveMenuOpen}
anchorEl={remotePlayActiveMenuAnchorEl}
onMenuClose={onRemotePlayActiveMenuClose}
playerInfo={playerInfo}
/>
</>
);
};
export default RemotePlayButton;

View File

@@ -1,61 +0,0 @@
import { SyncPlayUserAccessType } from '@jellyfin/sdk/lib/generated-client/models/sync-play-user-access-type';
import Groups from '@mui/icons-material/Groups';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import React, { useCallback, useState } from 'react';
import { pluginManager } from 'components/pluginManager';
import { useApi } from 'hooks/useApi';
import globalize from 'scripts/globalize';
import { PluginType } from 'types/plugin';
import AppSyncPlayMenu, { ID } from './menus/SyncPlayMenu';
const SyncPlayButton = () => {
const { user } = useApi();
const [ syncPlayMenuAnchorEl, setSyncPlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isSyncPlayMenuOpen = Boolean(syncPlayMenuAnchorEl);
const onSyncPlayButtonClick = useCallback((event) => {
setSyncPlayMenuAnchorEl(event.currentTarget);
}, [ setSyncPlayMenuAnchorEl ]);
const onSyncPlayMenuClose = useCallback(() => {
setSyncPlayMenuAnchorEl(null);
}, [ setSyncPlayMenuAnchorEl ]);
if (
// SyncPlay not enabled for user
(user?.Policy && user.Policy.SyncPlayAccess === SyncPlayUserAccessType.None)
// SyncPlay plugin is not loaded
|| pluginManager.ofType(PluginType.SyncPlay).length === 0
) {
return null;
}
return (
<>
<Tooltip title={globalize.translate('ButtonSyncPlay')}>
<IconButton
size='large'
aria-label={globalize.translate('ButtonSyncPlay')}
aria-controls={ID}
aria-haspopup='true'
onClick={onSyncPlayButtonClick}
color='inherit'
>
<Groups />
</IconButton>
</Tooltip>
<AppSyncPlayMenu
open={isSyncPlayMenuOpen}
anchorEl={syncPlayMenuAnchorEl}
onMenuClose={onSyncPlayMenuClose}
/>
</>
);
};
export default SyncPlayButton;

View File

@@ -1,72 +0,0 @@
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 { isTabPath } from '../tabs/tabRoutes';
interface AppToolbarProps {
isDrawerAvailable: boolean
isDrawerOpen: boolean
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
}
const PUBLIC_PATHS = [
'/addserver.html',
'/selectserver.html',
'/login.html',
'/forgotpassword.html',
'/forgotpasswordpin.html'
];
const ExperimentalAppToolbar: FC<AppToolbarProps> = ({
isDrawerAvailable,
isDrawerOpen,
onDrawerButtonClick
}) => {
const location = useLocation();
// The video osd does not show the standard toolbar
if (location.pathname === '/video') return null;
const isTabsAvailable = isTabPath(location.pathname);
const isPublicPath = PUBLIC_PATHS.includes(location.pathname);
return (
<AppToolbar
buttons={!isPublicPath && (
<>
<SyncPlayButton />
<RemotePlayButton />
<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}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onDrawerButtonClick}
isUserMenuAvailable={!isPublicPath}
>
{isTabsAvailable && (<AppTabs isDrawerOpen={isDrawerOpen} />)}
</AppToolbar>
);
};
export default ExperimentalAppToolbar;

View File

@@ -1,157 +0,0 @@
import Check from '@mui/icons-material/Check';
import Close from '@mui/icons-material/Close';
import SettingsRemote from '@mui/icons-material/SettingsRemote';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Menu, { MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import dialog from 'components/dialog/dialog';
import { playbackManager } from 'components/playback/playbackmanager';
import React, { FC, useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import { enable, isEnabled } from 'scripts/autocast';
import globalize from 'scripts/globalize';
interface RemotePlayActiveMenuProps extends MenuProps {
onMenuClose: () => void
playerInfo: {
name: string
isLocalPlayer: boolean
id?: string
deviceName?: string
playableMediaTypes?: string[]
supportedCommands?: string[]
} | null
}
export const ID = 'app-remote-play-active-menu';
const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
anchorEl,
open,
onMenuClose,
playerInfo
}) => {
const [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ] = useState(playbackManager.enableDisplayMirroring());
const isDisplayMirrorSupported = playerInfo?.supportedCommands && playerInfo.supportedCommands.indexOf('DisplayContent') !== -1;
const toggleDisplayMirror = useCallback(() => {
playbackManager.enableDisplayMirroring(!isDisplayMirrorEnabled);
setIsDisplayMirrorEnabled(!isDisplayMirrorEnabled);
}, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]);
const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled());
const toggleAutoCast = useCallback(() => {
enable(!isAutoCastEnabled);
setIsAutoCastEnabled(!isAutoCastEnabled);
}, [ isAutoCastEnabled ]);
const remotePlayerName = playerInfo?.deviceName || playerInfo?.name;
const disconnectRemotePlayer = useCallback(() => {
if (playbackManager.getSupportedCommands().indexOf('EndSession') !== -1) {
dialog.show({
buttons: [
{
name: globalize.translate('Yes'),
id: 'yes'
}, {
name: globalize.translate('No'),
id: 'no'
}
],
text: globalize.translate('ConfirmEndPlayerSession', remotePlayerName)
}).then(id => {
onMenuClose();
if (id === 'yes') {
playbackManager.getCurrentPlayer().endSession();
}
playbackManager.setDefaultPlayerActive();
}).catch(() => {
// Dialog closed
});
} else {
onMenuClose();
playbackManager.setDefaultPlayerActive();
}
}, [ onMenuClose, remotePlayerName ]);
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
id={ID}
keepMounted
open={open}
onClose={onMenuClose}
MenuListProps={{
'aria-labelledby': 'remote-play-active-subheader',
subheader: (
<ListSubheader component='div' id='remote-play-active-subheader'>
{remotePlayerName}
</ListSubheader>
)
}}
>
{isDisplayMirrorSupported && (
<MenuItem onClick={toggleDisplayMirror}>
{isDisplayMirrorEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isDisplayMirrorEnabled}>
{globalize.translate('EnableDisplayMirroring')}
</ListItemText>
</MenuItem>
)}
<MenuItem onClick={toggleAutoCast}>
{isAutoCastEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isAutoCastEnabled}>
{globalize.translate('EnableAutoCast')}
</ListItemText>
</MenuItem>
<Divider />
<MenuItem
component={Link}
to='/queue'
onClick={onMenuClose}
>
<ListItemIcon>
<SettingsRemote />
</ListItemIcon>
<ListItemText>
{globalize.translate('HeaderRemoteControl')}
</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={disconnectRemotePlayer}>
<ListItemIcon>
<Close />
</ListItemIcon>
<ListItemText>
{globalize.translate('Disconnect')}
</ListItemText>
</MenuItem>
</Menu>
);
};
export default RemotePlayActiveMenu;

View File

@@ -1,100 +0,0 @@
import Warning from '@mui/icons-material/Warning';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Menu, { type MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import React, { FC, useEffect, useState } from 'react';
import globalize from 'scripts/globalize';
import { playbackManager } from 'components/playback/playbackmanager';
import { pluginManager } from 'components/pluginManager';
import type { PlayTarget } from 'types/playTarget';
import PlayTargetIcon from '../../PlayTargetIcon';
interface RemotePlayMenuProps extends MenuProps {
onMenuClose: () => void
}
export const ID = 'app-remote-play-menu';
const RemotePlayMenu: FC<RemotePlayMenuProps> = ({
anchorEl,
open,
onMenuClose
}) => {
// TODO: Add other checks for support (Android app, secure context, etc)
const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast');
const [ playbackTargets, setPlaybackTargets ] = useState<PlayTarget[]>([]);
const onPlayTargetClick = (target: PlayTarget) => {
playbackManager.trySetActivePlayer(target.playerName, target);
onMenuClose();
};
useEffect(() => {
const fetchPlaybackTargets = async () => {
setPlaybackTargets(
await playbackManager.getTargets()
);
};
if (open) {
fetchPlaybackTargets()
.catch(err => {
console.error('[AppRemotePlayMenu] unable to get playback targets', err);
});
}
}, [ open, setPlaybackTargets ]);
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
id={ID}
keepMounted
open={open}
onClose={onMenuClose}
>
{!isChromecastPluginLoaded && ([
<MenuItem key='cast-unsupported-item' disabled>
<ListItemIcon>
<Warning />
</ListItemIcon>
<ListItemText>
{globalize.translate('GoogleCastUnsupported')}
</ListItemText>
</MenuItem>,
<Divider key='cast-unsupported-divider' />
])}
{playbackTargets.map(target => (
<MenuItem
key={target.id}
// Since we are looping over targets there is no good way to avoid creating a new function here
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onPlayTargetClick(target)}
>
<ListItemIcon>
<PlayTargetIcon target={target} />
</ListItemIcon>
<ListItemText
primary={ target.appName ? `${target.name} - ${target.appName}` : target.name }
secondary={ target.user?.Name }
/>
</MenuItem>
))}
</Menu>
);
};
export default RemotePlayMenu;

View File

@@ -1,297 +0,0 @@
import type { GroupInfoDto } from '@jellyfin/sdk/lib/generated-client/models/group-info-dto';
import { SyncPlayUserAccessType } from '@jellyfin/sdk/lib/generated-client/models/sync-play-user-access-type';
import { getSyncPlayApi } from '@jellyfin/sdk/lib/utils/api/sync-play-api';
import GroupAdd from '@mui/icons-material/GroupAdd';
import PersonAdd from '@mui/icons-material/PersonAdd';
import PersonOff from '@mui/icons-material/PersonOff';
import PersonRemove from '@mui/icons-material/PersonRemove';
import PlayCircle from '@mui/icons-material/PlayCircle';
import StopCircle from '@mui/icons-material/StopCircle';
import Tune from '@mui/icons-material/Tune';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Menu, { MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import type { ApiClient } from 'jellyfin-apiclient';
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 'scripts/globalize';
import { PluginType } from 'types/plugin';
import Events from 'utils/events';
export const ID = 'app-sync-play-menu';
interface SyncPlayMenuProps extends MenuProps {
onMenuClose: () => void
}
interface SyncPlayInstance {
Manager: {
getGroupInfo: () => GroupInfoDto | null | undefined
getTimeSyncCore: () => object
isPlaybackActive: () => boolean
isPlaylistEmpty: () => boolean
haltGroupPlayback: (apiClient: ApiClient) => void
resumeGroupPlayback: (apiClient: ApiClient) => void
}
}
const SyncPlayMenu: FC<SyncPlayMenuProps> = ({
anchorEl,
open,
onMenuClose
}) => {
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
const { __legacyApiClient__, api, user } = useApi();
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
const isSyncPlayEnabled = Boolean(currentGroup);
useEffect(() => {
setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance);
}, []);
const { data: groups } = useSyncPlayGroups();
const onGroupAddClick = useCallback(() => {
if (api && user) {
getSyncPlayApi(api)
.syncPlayCreateGroup({
newGroupRequestDto: {
GroupName: globalize.translate('SyncPlayGroupDefaultTitle', user.Name)
}
})
.catch(err => {
console.error('[SyncPlayMenu] failed to create a SyncPlay group', err);
});
onMenuClose();
}
}, [ api, onMenuClose, user ]);
const onGroupLeaveClick = useCallback(() => {
if (api) {
getSyncPlayApi(api)
.syncPlayLeaveGroup()
.catch(err => {
console.error('[SyncPlayMenu] failed to leave SyncPlay group', err);
});
onMenuClose();
}
}, [ api, onMenuClose ]);
const onGroupJoinClick = useCallback((GroupId: string) => {
if (api) {
getSyncPlayApi(api)
.syncPlayJoinGroup({
joinGroupRequestDto: {
GroupId
}
})
.catch(err => {
console.error('[SyncPlayMenu] failed to join SyncPlay group', err);
});
onMenuClose();
}
}, [ api, onMenuClose ]);
const onGroupSettingsClick = useCallback(async () => {
if (!syncPlay) return;
// TODO: Rewrite settings UI
const SyncPlaySettingsEditor = (await import('../../../../../plugins/syncPlay/ui/settings/SettingsEditor')).default;
new SyncPlaySettingsEditor(
__legacyApiClient__,
syncPlay.Manager.getTimeSyncCore(),
{
groupInfo: currentGroup
})
.embed()
.catch(err => {
if (err) {
console.error('[SyncPlayMenu] Error creating SyncPlay settings editor', err);
}
});
onMenuClose();
}, [ __legacyApiClient__, currentGroup, onMenuClose, syncPlay ]);
const onStartGroupPlaybackClick = useCallback(() => {
if (__legacyApiClient__) {
syncPlay?.Manager.resumeGroupPlayback(__legacyApiClient__);
onMenuClose();
}
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
const onStopGroupPlaybackClick = useCallback(() => {
if (__legacyApiClient__) {
syncPlay?.Manager.haltGroupPlayback(__legacyApiClient__);
onMenuClose();
}
}, [ __legacyApiClient__, onMenuClose, syncPlay ]);
const updateSyncPlayGroup = useCallback((_e, enabled) => {
if (syncPlay && enabled) {
setCurrentGroup(syncPlay.Manager.getGroupInfo() ?? undefined);
} else {
setCurrentGroup(undefined);
}
}, [ syncPlay ]);
useEffect(() => {
if (!syncPlay) return;
Events.on(syncPlay.Manager, 'enabled', updateSyncPlayGroup);
return () => {
Events.off(syncPlay.Manager, 'enabled', updateSyncPlayGroup);
};
}, [ updateSyncPlayGroup, syncPlay ]);
const menuItems = [];
if (isSyncPlayEnabled) {
if (!syncPlay?.Manager.isPlaylistEmpty() && !syncPlay?.Manager.isPlaybackActive()) {
menuItems.push(
<MenuItem
key='sync-play-start-playback'
onClick={onStartGroupPlaybackClick}
>
<ListItemIcon>
<PlayCircle />
</ListItemIcon>
<ListItemText primary={globalize.translate('LabelSyncPlayResumePlayback')} />
</MenuItem>
);
} else if (syncPlay?.Manager.isPlaybackActive()) {
menuItems.push(
<MenuItem
key='sync-play-stop-playback'
onClick={onStopGroupPlaybackClick}
>
<ListItemIcon>
<StopCircle />
</ListItemIcon>
<ListItemText primary={globalize.translate('LabelSyncPlayHaltPlayback')} />
</MenuItem>
);
}
menuItems.push(
<MenuItem
key='sync-play-settings'
onClick={onGroupSettingsClick}
>
<ListItemIcon>
<Tune />
</ListItemIcon>
<ListItemText
primary={globalize.translate('Settings')}
/>
</MenuItem>
);
menuItems.push(
<Divider key='sync-play-controls-divider' />
);
menuItems.push(
<MenuItem
key='sync-play-exit'
onClick={onGroupLeaveClick}
>
<ListItemIcon>
<PersonRemove />
</ListItemIcon>
<ListItemText
primary={globalize.translate('LabelSyncPlayLeaveGroup')}
/>
</MenuItem>
);
} else if (!groups?.length && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
menuItems.push(
<MenuItem key='sync-play-unavailable' disabled>
<ListItemIcon>
<PersonOff />
</ListItemIcon>
<ListItemText primary={globalize.translate('LabelSyncPlayNoGroups')} />
</MenuItem>
);
} else {
if (groups && groups.length > 0) {
groups.forEach(group => {
menuItems.push(
<MenuItem
key={group.GroupId}
// Since we are looping over groups there is no good way to avoid creating a new function here
// eslint-disable-next-line react/jsx-no-bind
onClick={() => group.GroupId && onGroupJoinClick(group.GroupId)}
>
<ListItemIcon>
<PersonAdd />
</ListItemIcon>
<ListItemText
primary={group.GroupName}
secondary={group.Participants?.join(', ')}
/>
</MenuItem>
);
});
menuItems.push(
<Divider key='sync-play-groups-divider' />
);
}
if (user?.Policy?.SyncPlayAccess === SyncPlayUserAccessType.CreateAndJoinGroups) {
menuItems.push(
<MenuItem
key='sync-play-new-group'
onClick={onGroupAddClick}
>
<ListItemIcon>
<GroupAdd />
</ListItemIcon>
<ListItemText primary={globalize.translate('LabelSyncPlayNewGroupDescription')} />
</MenuItem>
);
}
}
const MenuListProps = isSyncPlayEnabled ? {
'aria-labelledby': 'sync-play-active-subheader',
subheader: (
<ListSubheader component='div' id='sync-play-active-subheader'>
{currentGroup?.GroupName}
</ListSubheader>
)
} : undefined;
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
id={ID}
keepMounted
open={open}
onClose={onMenuClose}
MenuListProps={MenuListProps}
>
{menuItems}
</Menu>
);
};
export default SyncPlayMenu;

View File

@@ -1,50 +0,0 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import Movie from '@mui/icons-material/Movie';
import MusicNote from '@mui/icons-material/MusicNote';
import Photo from '@mui/icons-material/Photo';
import LiveTv from '@mui/icons-material/LiveTv';
import Tv from '@mui/icons-material/Tv';
import Theaters from '@mui/icons-material/Theaters';
import MusicVideo from '@mui/icons-material/MusicVideo';
import Book from '@mui/icons-material/Book';
import Collections from '@mui/icons-material/Collections';
import Queue from '@mui/icons-material/Queue';
import Folder from '@mui/icons-material/Folder';
import React, { FC } from 'react';
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
interface LibraryIconProps {
item: BaseItemDto
}
const LibraryIcon: FC<LibraryIconProps> = ({
item
}) => {
switch (item.CollectionType) {
case CollectionType.Movies:
return <Movie />;
case CollectionType.Music:
return <MusicNote />;
case CollectionType.Homevideos:
case CollectionType.Photos:
return <Photo />;
case CollectionType.Livetv:
return <LiveTv />;
case CollectionType.Tvshows:
return <Tv />;
case CollectionType.Trailers:
return <Theaters />;
case CollectionType.Musicvideos:
return <MusicVideo />;
case CollectionType.Books:
return <Book />;
case CollectionType.Boxsets:
return <Collections />;
case CollectionType.Playlists:
return <Queue />;
default:
return <Folder />;
}
};
export default LibraryIcon;

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