Compare commits

..

181 Commits

Author SHA1 Message Date
Jellyfin Release Bot
f4b8aa0ed4 Bump version to 10.10.7
Some checks failed
Push & Release 🌍 / Automation 🎛️ (push) Has been cancelled
Push & Release 🌍 / Unstable release 🚀⚠️ (push) Has been cancelled
Push & Release 🌍 / Quality checks 👌🧪 (push) Has been cancelled
Push & Release 🌍 / GitHub CodeQL 🔬 (push) Has been cancelled
Push & Release 🌍 / Deploy 🚀 (push) Has been cancelled
2025-04-05 15:14:58 -04:00
rlauuzo
2413566327 Fix skip button not displaying correctly with OSD (#6583) 2025-04-03 10:32:50 -04:00
Bill Thornton
c3c598e1f4 Merge pull request #6510 from dmitrylyzo/fix-focus
Fix re-focusing on pause button when displaying OSD
2025-03-25 16:04:53 -04:00
Bill Thornton
78a8642d4a Merge pull request #6661 from dmitrylyzo/tizen-version 2025-03-25 10:53:18 -04:00
Dmitry Lyzo
76911509bc Fix parsing minor version of Tizen 2025-03-25 15:16:17 +03:00
Dmitry Lyzo
fb47403b72 Fix re-focusing on pause button when displaying OSD
When focus is debounced, `document.activeElement` is not updated
immediately, and someone (skipsegment.ts) might read it too early.

Remove focus debounce to immediately update `document.activeElement`.

This (undebounced focus) seems to work in webOS 1.2, webOS 5,
Firefox 134, Chrome 132. So the timeout was probably for
Internet Explorer.
2025-03-24 14:00:45 +03:00
Dmitry Lyzo
2ab61b6d7b Add container for skip button to make it focusable
FocusManager.isCurrentlyFocusableInternal doesn't work with
fixed elements.
2025-03-24 12:09:57 +03:00
Bill Thornton
16a084b009 Merge pull request #6570 from nielsvanvelzen/plugin-page-name
Fix catalog plugin page not setting page title
2025-02-25 13:50:18 -05:00
Niels van Velzen
489731863b Fix catalog plugin page not setting page title 2025-02-25 17:30:51 +01:00
Jellyfin Release Bot
c335a3024e Bump version to 10.10.6 2025-02-16 16:55:44 -05:00
Bill Thornton
d3054985a5 Merge pull request #6509 from dmitrylyzo/fix-nav-regression 2025-02-09 06:28:48 +00:00
Dmitry Lyzo
0f598073a8 Fix navigation for emby-select element
Regression 84563176a1
If we prevent default action, keyboard navigation won't work.
2025-02-02 21:49:45 +03:00
Bill Thornton
5d8ab6a99b Merge pull request #6496 from dmitrylyzo/skip-prevented-keyboard-event 2025-01-31 10:18:30 -05:00
Dmitry Lyzo
84563176a1 Skip prevented keyboard event 2025-01-31 01:52:37 +03:00
Bill Thornton
4793223f5d Merge pull request #6396 from Kevinjil/fix-mediasourceid
Remove invalid item id usage as media source id
2025-01-29 14:41:03 -05:00
Kevin Jilissen
8f64beec30 Remove invalid item id usage as media source id
Looking at the change history, an ` || item.Id` was introduced in
4c31742cc5 to query for the item, but
this workaround is only needed for track selection in some cases and
breaks playback in others. Only apply it when a track is selected.
2025-01-28 18:08:29 +01:00
Jellyfin Release Bot
e8e4ff0ca9 Bump version to 10.10.5 2025-01-25 14:14:30 -05:00
Bill Thornton
9b3fc622c9 Merge pull request #6462 from dmitrylyzo/fix-target-fps 2025-01-22 19:49:55 -05:00
Jellyfin Release Bot
8994299490 Bump version to 10.10.4 2025-01-21 21:20:14 -05:00
Dmitry Lyzo
36aa4dcd88 Use reference framerate as sampling rate (target FPS) for SubtitlesOctopus 2025-01-21 22:43:37 +03:00
Dmitry Lyzo
2f6781a7c2 Use average framerate as sampling rate (target FPS) for SubtitlesOctopus 2025-01-21 13:31:00 +03:00
Bill Thornton
7a07a79b12 Merge pull request #6453 from thornbill/fix-player-inset 2025-01-18 02:45:30 -05:00
Bill Thornton
88cc991fa5 Remove safe area padding on youtube container 2025-01-17 17:58:38 -05:00
Bill Thornton
8710de09d4 Merge pull request #6450 from thornbill/fix-menu-expand 2025-01-17 16:43:48 -05:00
Bill Thornton
14ff6474f3 Remove safe area padding on video container 2025-01-16 15:29:51 -05:00
Bill Thornton
bfa53b57f4 Fix menu closing when expanding section 2025-01-16 11:50:02 -05:00
rlauuzo
4f17cebc02 Prevent Focus Loss When Skip Button is Pressed (#6413)
* Prevent Focus Loss When Skip Button is Pressed

* Implement the suggested changes

* Update index.js

* Ensures focus shifts to the pause button

* Apply suggested changes

---------

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
2025-01-13 10:55:08 -05:00
Bill Thornton
700e72b409 Merge pull request #6433 from thornbill/fix-home-tabs-crash 2025-01-09 22:10:55 -05:00
dantesbr
cc011feffb Fix focus loss on popups in Samsung Tizen 8 Smart TVs (#6408) 2025-01-09 20:32:42 -05:00
Bill Thornton
d4b55ec67a Merge pull request #6425 from bernarden/bugfix/6422-unsupported-max-css-function-in-chrome-76 2025-01-09 20:29:14 -05:00
Bill Thornton
d1aa2f3685 Fix main tab manager crash in experimental layout 2025-01-09 17:11:20 -05:00
Victor Usoltsev
321822c57f Fix css fallback of max function in chrome 76. 2025-01-08 18:28:30 +13:00
Bill Thornton
e1deddcba1 Merge pull request #6421 from viown/fix-metadata-outer-click
Fix metadata dialog backdrop click to exit
2025-01-03 16:49:01 -05:00
Bill Thornton
b797ca4e1e Merge pull request #6352 from viown/fix-parental-control-select
Fix maximum allowed parental rating not showing up
2025-01-03 14:37:50 -05:00
Bill Thornton
74a209ed63 Merge pull request #6324 from carsso/fix-spinner-direction
Fix spinner direction (should be clockwise) in @keyframes spin
2025-01-03 13:56:26 -05:00
viown
a553ef54f6 Fix metadata outer click 2025-01-02 13:45:35 +03:00
Bill Thornton
5b4cfbf410 Merge pull request #6362 from nyanmisaka/sw-tonemap-by-default 2024-12-29 15:32:47 -05:00
Bill Thornton
e15d700d40 Merge pull request #6317 from dmitrylyzo/tizen8-no-secondary-audio 2024-12-29 15:29:37 -05:00
Bill Thornton
3d20694109 Merge pull request #6388 from viown/fix-profile-refresh 2024-12-29 15:24:31 -05:00
Bill Thornton
469abcc517 Merge pull request #6373 from gnattu/fix-safari-opus-stereo 2024-12-29 14:24:51 -05:00
viown
26df03b64c Enable TV Support for 'Ask to Skip' (#6295) 2024-12-29 14:21:06 -05:00
gnattu
8f7974d5c6 Remove redundant channel limit 2024-12-22 01:58:56 +08:00
viown
206f70cf34 Fix async route page refresh 2024-12-15 14:07:22 +03:00
gnattu
92caea08af Prevent opus more than 2 channels being remux on Safari
Safari only supports stereo Opus, which requires a similar workaround as we are applying on WebOS for FLAC.
2024-12-11 14:50:57 +08:00
nyanmisaka
a3872ffa25 Enable software tone-mapping by default
Signed-off-by: nyanmisaka <nst7999610810@gmail.com>
2024-12-03 22:19:15 +08:00
viown
63834e164a Remove redundant check 2024-11-28 22:48:26 +03:00
viown
9e4e3b0106 Fix maximum allowed parental rating not showing up 2024-11-27 23:00:25 +03:00
Dmitry Lyzo
a91e44828b Don't use AudioTrack API on Tizen 8+ 2024-11-27 15:21:40 +03:00
Bill Thornton
92e8821003 Merge pull request #6338 from solidsnake1298/tdp-embedded-checkbox 2024-11-23 23:02:22 -05:00
Germain Carré
306390179b Fixing spinner direction (should be clockwise) and removing duplicate animation @keyframes spin 2024-11-23 15:37:03 +01:00
TheDreadPirate
4fb33badef Correcting type for Prefer embedded episode info checkbox 2024-11-22 14:12:32 -05:00
Bill Thornton
1496542381 Merge pull request #6329 from nielsvanvelzen/fix-lyric-sort 2024-11-19 19:03:34 -05:00
Niels van Velzen
e22187247b Fix changing lyric fetcher order not changeable in library options 2024-11-19 22:23:46 +01:00
Jellyfin Release Bot
b95dad4ba2 Bump version to 10.10.3 2024-11-18 22:38:44 -05:00
Bill Thornton
298399802a Merge pull request #6326 from thornbill/10.10.3-translations
Backport translations for 10.10.3
2024-11-18 16:05:28 -05:00
burghy86
53a91227d7 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2024-11-18 15:41:34 -05:00
koreapyj
d5ccc0ea37 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2024-11-18 15:41:34 -05:00
koreapyj
c190f7b770 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2024-11-18 15:41:34 -05:00
Jellyfin Release Bot
d69468a95a Bump version to 10.10.2 2024-11-16 14:59:27 -05:00
Joshua M. Boniface
0c61dff5c5 Merge pull request #6321 from thornbill/10.10.2-translations
Backport translations for 10.10.2 again
2024-11-16 13:26:32 -05:00
Jamoliddin Rakhmonberdiev
5479d18082 Translated using Weblate (Uzbek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uz/
2024-11-15 12:40:36 -05:00
Jamoliddin Rakhmonberdiev
e13668530d Translated using Weblate (Uzbek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uz/
2024-11-15 12:40:36 -05:00
NickSkier
32d7a962ff Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2024-11-15 12:40:36 -05:00
Johan Dixelius
f01b45fb04 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2024-11-15 12:40:36 -05:00
GolanGitHub
38d9b6b528 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2024-11-15 12:40:36 -05:00
Simon-Pierre Corriveau
77ebe19f40 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2024-11-15 12:40:35 -05:00
nextlooper42
6095248f6e Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2024-11-15 12:40:35 -05:00
George Vella
5725554085 Translated using Weblate (Maltese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mt/
2024-11-15 12:40:35 -05:00
koreapyj
b98d74de33 Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2024-11-15 12:40:35 -05:00
l00d3r
bfceb43602 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2024-11-15 12:40:35 -05:00
Blackspirits
e54b19e2d4 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-15 12:40:35 -05:00
darkabella
2d459c75dc Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2024-11-15 12:40:35 -05:00
Aindriú Mac Giolla Eoin
8a2789e316 Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2024-11-15 12:40:35 -05:00
Aindriú Mac Giolla Eoin
99edacc08d Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2024-11-15 12:40:35 -05:00
koreapyj
377c0e3bdb Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2024-11-15 12:40:35 -05:00
Bill Thornton
d56ff77308 Merge pull request #6311 from thornbill/10.10.2-translations 2024-11-10 09:44:37 -05:00
MisterCry
6391ccac1b Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2024-11-10 02:33:15 -05:00
Kristian
08f7477813 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2024-11-10 02:33:15 -05:00
Frederik Palmø
2f7e751359 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2024-11-10 02:33:15 -05:00
Rainer Geiger
8a569d3692 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2024-11-10 02:33:15 -05:00
Jose Riha
df79405af0 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2024-11-10 02:33:15 -05:00
Quang Minh Nguyen
be2c8a0afc Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2024-11-10 02:33:15 -05:00
Kamran Imami
6c588dcb8f Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2024-11-10 02:33:15 -05:00
electreo
00c7999ad4 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2024-11-10 02:33:15 -05:00
Kristyan Petrov Georgiev
1cdda2f8f6 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2024-11-10 02:33:15 -05:00
saleem0915
ef42d9c8b4 Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ja/
2024-11-10 02:33:15 -05:00
Andi Chandler
8acc33705c Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2024-11-10 02:33:15 -05:00
kimpig
7769137bff Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2024-11-10 02:33:15 -05:00
Rafael Morine
5728b5e5d4 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2024-11-10 02:33:15 -05:00
p0358
13318e805f Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2024-11-10 02:33:15 -05:00
Nyanmisaka
132c6ca858 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2024-11-10 02:33:15 -05:00
stanol
f8e109bbc3 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/
2024-11-10 02:33:15 -05:00
Frederik Palmø
1562606b28 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2024-11-10 02:33:15 -05:00
Roi Gabay
9fa5e438d9 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Lukáš Kucharczyk
b683becaf7 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2024-11-10 02:33:15 -05:00
Robin Lavigne
9cbab78cc1 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2024-11-10 02:33:15 -05:00
Tomi
05e5aa744d Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2024-11-10 02:33:15 -05:00
hoanghuy309
2e4986c497 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2024-11-10 02:33:15 -05:00
Kityn
c42d7b3f5d Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2024-11-10 02:33:15 -05:00
Justin
c60a0190ff Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2024-11-10 02:33:15 -05:00
hoanghuy309
b7bbadb2df Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2024-11-10 02:33:15 -05:00
Frostslayer
837884c337 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2024-11-10 02:33:15 -05:00
Bas
3143a08a33 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2024-11-10 02:33:15 -05:00
Roi Gabay
bdcf6186ce Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Roi Gabay
3d9d1f5ae3 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Roi Gabay
bd0c43d6c8 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Roi Gabay
c5934d08de Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Roi Gabay
dec3d2ac19 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
Roi Gabay
25354caf8f Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-10 02:33:15 -05:00
jh
3897704a40 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2024-11-10 02:33:15 -05:00
Bill Thornton
133273a3e8 Merge pull request #6303 from Arcus92/pgs-clear-fix 2024-11-07 15:18:07 -05:00
David Schulte
67996d3a96 Updated dependency libpgs to v0.8.1. Fixed incomplete subtitle clearing. 2024-11-07 20:01:14 +01:00
Bill Thornton
048d3f1e2c Merge pull request #6298 from viown/fix-pdf-display
Fix large PDF Display
2024-11-05 10:39:48 -05:00
viown
e8f9bfcf57 Fix PDF Display 2024-11-04 20:27:43 +03:00
Bill Thornton
22ae941a9a Merge pull request #6291 from Arcus92/pgs-fix-position
Update dependency libpgs to v0.8.0
2024-11-04 12:24:10 -05:00
Bill Thornton
5c55e458dd Merge pull request #6297 from thornbill/include-theme-images
Fix missing theme images
2024-11-04 11:04:39 -05:00
Bill Thornton
f35893d0a1 Fix missing theme images 2024-11-04 10:25:36 -05:00
Jellyfin Release Bot
2f2844c33f Bump version to 10.10.1 2024-11-03 10:57:49 -05:00
Bill Thornton
026893e78b Merge pull request #6285 from thornbill/10.10.1-translations 2024-11-03 10:30:12 -05:00
David Schulte
1ab22fc258 Updated dependency libpgs to v0.8.0. Fixed subtitle positioning, implemented custom aspect-ratio and cropping. 2024-11-03 12:54:28 +01:00
viown
ed3671a536 Always show enabled filters (#6286) 2024-11-01 17:22:14 -04:00
Roi Gabay
f43402d284 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Roi Gabay
bfd1e9123a Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Roi Gabay
13e6766e09 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Blackspirits
ca45ddfd18 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:30 -04:00
Blackspirits
02f9d28423 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-01 14:21:30 -04:00
Bas
3c287626f2 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2024-11-01 14:21:30 -04:00
millallo
a1b78b3557 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2024-11-01 14:21:30 -04:00
Andi Chandler
ef68930e52 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2024-11-01 14:21:30 -04:00
Roi Gabay
4e18e36fa0 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Roi Gabay
cee58c742e Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Roi Gabay
caa677b643 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Gabriel Santana
9ab418c69d Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2024-11-01 14:21:30 -04:00
Roi Gabay
d037fc12c3 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Spiros Vita
281298b55d Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2024-11-01 14:21:30 -04:00
Daniel
cafbd93ecd Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2024-11-01 14:21:30 -04:00
Roi Gabay
6f24b334f9 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Roi Gabay
9c18db984c Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Blackspirits
30871d0a21 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:30 -04:00
Blackspirits
bd98a7ae15 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-01 14:21:30 -04:00
Roi Gabay
be5a12a263 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2024-11-01 14:21:30 -04:00
Blackspirits
35dd9a1970 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:30 -04:00
Blackspirits
0271bf4c82 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-01 14:21:30 -04:00
Frostslayer
2b80c59ab7 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:30 -04:00
Frostslayer
928663c3e8 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2024-11-01 14:21:30 -04:00
Antonios
ae76e82f7d Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2024-11-01 14:21:30 -04:00
Blackspirits
e69dcbfa03 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:30 -04:00
Blackspirits
66e641199e Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-01 14:21:29 -04:00
gendelo3
c54a203128 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2024-11-01 14:21:29 -04:00
Blackspirits
5a9b511a16 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2024-11-01 14:21:29 -04:00
Blackspirits
b012884a8e Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2024-11-01 14:21:29 -04:00
millallo
ef24aa319f Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2024-11-01 14:21:29 -04:00
hoanghuy309
828a49f6bd Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2024-11-01 14:21:29 -04:00
Frederik Palmø
81fdf371f1 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2024-11-01 14:21:29 -04:00
Frederik Palmø
f3ffd327fc Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2024-11-01 14:21:29 -04:00
Lukáš Kucharczyk
43c9f853e3 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2024-11-01 14:21:29 -04:00
NOV
965182e538 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2024-11-01 14:21:29 -04:00
Andrejs
a6307eb8c8 Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/
2024-11-01 14:21:29 -04:00
queeup
7de923ba74 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2024-11-01 14:21:29 -04:00
Bill Thornton
c5cc093fba Merge pull request #6284 from thornbill/exclude-files
Exclude unneeded files from build output
2024-11-01 14:20:12 -04:00
Marissa
6d37cfcd1b Fix missing transcoding speed info (#6199)
* Fix missing transcoding speed info https://github.com/jellyfin/jellyfin-web/issues/6198

* Use ReferenceFrameRate by default

---------

Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-11-01 14:19:31 -04:00
Bill Thornton
9ff9f05a26 Merge pull request #6276 from thornbill/plugin-categories
Update plugin category labels
2024-11-01 14:19:05 -04:00
Bill Thornton
2ed3965197 Merge pull request #6278 from dmitrylyzo/fix-tizen-secondary-subtitles
Fix displaying secondary subtitles on Tizen 5
2024-11-01 11:56:02 -04:00
Bill Thornton
ad8868a996 Merge pull request #6274 from viown/make-filtering-local
Make filtering local to each device
2024-11-01 11:54:50 -04:00
Bill Thornton
1761dd1cfc Exclude unneeded files from build output 2024-11-01 10:33:09 -04:00
Bill Thornton
d365dd1b98 Merge pull request #6282 from viown/add-hash-to-filenames
Add content hash to CSS files
2024-11-01 10:31:18 -04:00
Bill Thornton
05de692675 Update plugin category labels 2024-11-01 10:02:19 -04:00
viown
89b5e44870 Add content hash to CSS files 2024-11-01 11:35:25 +03:00
Dmitry Lyzo
be324cca22 Fix displaying secondary subtitles on Tizen 5 2024-10-31 11:18:33 +03:00
rlauuzo
3311a1407f Show Skip Button When 'Next Video Info Overlay' is Disabled (#6258)
* Show Skip Button When 'Next Video Info Overlay' is Disabled

* Update skipsegment.ts

* Apply Suggestions

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

* Apply Suggestions

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

---------

Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2024-10-30 13:18:33 -04:00
viown
d4eae7bde6 Add missing episode filter settings 2024-10-30 18:50:48 +03:00
viown
c0a9f8b544 Make filtering local to device 2024-10-29 21:28:50 +03:00
viown
18ea570ea1 Save filtering and sort fields only 2024-10-29 12:07:18 +03:00
Bill Thornton
7ccc494a5b Merge pull request #6266 from viown/make-ask-to-skip-default
Make ask to skip the default for 'Intro' and 'Outro' segments
2024-10-28 12:56:35 -04:00
Bill Thornton
71ab6fea5d Merge pull request #6265 from thornbill/safe-plugin-dates
Add support for plugin revisions with bad timestamps
2024-10-28 12:53:08 -04:00
viown
acde0685b5 Use a constant for defaults 2024-10-28 17:36:10 +03:00
Bill Thornton
3062f0f38c Add support for plugin revisions with bad timestamps 2024-10-28 10:12:33 -04:00
viown
37be617523 Make 'ask to skip' the default for intro and outro segments 2024-10-28 17:12:07 +03:00
viown
6f7ece6592 Remove letter filter from loadQuerySettings 2024-10-28 14:51:57 +03:00
viown
ae5afd9ea7 Don't persist letter filter when querying 2024-10-28 14:35:08 +03:00
Jellyfin Release Bot
d4d84d0a18 Bump version to 10.10.0 2024-10-26 13:32:54 -04:00
871 changed files with 32450 additions and 51143 deletions

View File

@@ -1,5 +1,6 @@
{
"ecmaVersion": "es5",
"modules": "false",
"files": "./dist/**/*.js",
"not": [
"./dist/libraries/pdf.worker.js",

5
.eslintignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules
coverage
dist
.idea
.vscode

301
.eslintrc.js Normal file
View File

@@ -0,0 +1,301 @@
const restrictedGlobals = require('confusing-browser-globals');
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@stylistic',
'@typescript-eslint',
'react',
'import',
'sonarjs'
],
env: {
node: true,
es6: true,
es2017: true,
es2020: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:import/errors',
'plugin:@eslint-community/eslint-comments/recommended',
'plugin:compat/recommended',
'plugin:sonarjs/recommended'
],
rules: {
'array-callback-return': ['error', { 'checkForEach': true }],
'curly': ['error', 'multi-line', 'consistent'],
'default-case-last': ['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-lonely-if': ['error'],
'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-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'] }],
'one-var': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
'@typescript-eslint/prefer-for-of': ['error'],
'@typescript-eslint/prefer-optional-chain': ['error'],
'radix': ['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'],
'@stylistic/block-spacing': ['error'],
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/comma-spacing': ['error'],
'@stylistic/eol-last': ['error'],
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
'@stylistic/keyword-spacing': ['error'],
'@stylistic/max-statements-per-line': ['error'],
'@stylistic/no-floating-decimal': ['error'],
'@stylistic/no-multi-spaces': ['error'],
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1 }],
'@stylistic/no-trailing-spaces': ['error'],
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'@stylistic/padded-blocks': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'@stylistic/semi': ['error'],
'@stylistic/space-before-blocks': ['error'],
'@stylistic/space-infix-ops': ['error']
},
settings: {
react: {
version: 'detect'
},
'import/parsers': {
'@typescript-eslint/parser': [ '.ts', '.tsx' ]
},
'import/resolver': {
node: {
extensions: [
'.js',
'.ts',
'.jsx',
'.tsx'
],
moduleDirectory: [
'node_modules',
'src'
]
}
},
polyfills: [
// Native Promises Only
'Promise',
// whatwg-fetch
'fetch',
// document-register-element
'document.registerElement',
// resize-observer-polyfill
'ResizeObserver',
// fast-text-encoding
'TextEncoder',
// intersection-observer
'IntersectionObserver',
// Core-js
'Object.assign',
'Object.is',
'Object.setPrototypeOf',
'Object.toString',
'Object.freeze',
'Object.seal',
'Object.preventExtensions',
'Object.isFrozen',
'Object.isSealed',
'Object.isExtensible',
'Object.getOwnPropertyDescriptor',
'Object.getPrototypeOf',
'Object.keys',
'Object.entries',
'Object.getOwnPropertyNames',
'Function.name',
'Function.hasInstance',
'Array.from',
'Array.arrayOf',
'Array.copyWithin',
'Array.fill',
'Array.find',
'Array.findIndex',
'Array.iterator',
'String.fromCodePoint',
'String.raw',
'String.iterator',
'String.codePointAt',
'String.endsWith',
'String.includes',
'String.repeat',
'String.startsWith',
'String.trim',
'String.anchor',
'String.big',
'String.blink',
'String.bold',
'String.fixed',
'String.fontcolor',
'String.fontsize',
'String.italics',
'String.link',
'String.small',
'String.strike',
'String.sub',
'String.sup',
'RegExp',
'Number',
'Math',
'Date',
'async',
'Symbol',
'Map',
'Set',
'WeakMap',
'WeakSet',
'ArrayBuffer',
'DataView',
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'Reflect',
// Temporary while eslint-compat-plugin is buggy
'document.querySelector'
]
},
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}'
],
parserOptions: {
project: ['./tsconfig.json']
},
env: {
node: false,
amd: true,
browser: true,
es6: true,
es2017: true,
es2020: true
},
globals: {
// Browser globals
'MediaMetadata': 'readonly',
// Tizen globals
'tizen': 'readonly',
'webapis': 'readonly',
// WebOS globals
'webOS': 'readonly',
// Dependency globals
'$': 'readonly',
'jQuery': 'readonly',
// Jellyfin globals
'ApiClient': 'writable',
'Events': 'writable',
'chrome': 'writable',
'DlnaProfilePage': 'writable',
'DashboardPage': 'writable',
'Emby': 'readonly',
'Globalize': 'writable',
'Hls': 'writable',
'LibraryMenu': 'writable',
'LinkParser': 'writable',
'LiveTvHelpers': 'writable',
'Loading': 'writable',
'MetadataEditor': 'writable',
'ServerNotifications': 'writable',
'TaskButton': 'writable',
'UserParentalControlPage': 'writable',
'Windows': 'readonly',
// Build time definitions
__COMMIT_SHA__: 'readonly',
__JF_BUILD_VERSION__: 'readonly',
__PACKAGE_JSON_NAME__: 'readonly',
__PACKAGE_JSON_VERSION__: 'readonly',
__USE_SYSTEM_FONTS__: 'readonly',
__WEBPACK_SERVE__: 'readonly'
},
rules: {
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
}
},
// TypeScript source files
{
files: [
'./src/**/*.{ts,tsx}'
],
extends: [
'eslint:recommended',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:@eslint-community/eslint-comments/recommended',
'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']
}
}
]
};

4
.github/CODEOWNERS vendored
View File

@@ -1,5 +1 @@
* @jellyfin/web
# Joshua must review all changes to bump_version
bump_version @joshuaboniface
# Core must approve all changes within the repo config
.github/ @jellyfin/core

32
.github/ISSUE_TEMPLATE/1-bug-report.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Bug Report
about: You have noticed a general issue or regression, and would like to report it
labels: bug
---
**Describe The Bug**
<!-- A clear and concise description of what the bug is. -->
**Steps To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected Behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Logs**
<!-- Please paste any log errors. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**System (please complete the following information):**
- Platform: [e.g. Linux, Windows, iPhone, Tizen]
- Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.6.0]
**Additional Context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,122 +0,0 @@
name: Bug Report
description: You have noticed a general issue or regression, and would like to report it
labels:
- bug
body:
- type: checkboxes
id: before-posting
attributes:
label: "This issue respects the following points:"
description: All conditions are **required**.
options:
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
required: true
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
required: true
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
required: true
- type: markdown
attributes:
value: |
## Bug information
- type: textarea
id: description
attributes:
label: Describe the bug
description: |
A clear and concise description of the bug.
You can also attach screenshots or screen recordings here to help explain your issue.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction Steps
description: |
Steps to reproduce the behavior:
placeholder: |
1. Go to …
2. Click on …
3. Scroll down to …
4. See error / the app crashes
validations:
required: true
- type: textarea
id: behaviour
attributes:
label: Expected/Actual behaviour
description: |
Describe the behavior you were expecting versus what actually occurred.
placeholder: |
I expected the app to... However, the actual behavior was that...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: |
Please paste any log errors.
placeholder: Paste logs…
- type: markdown
attributes:
value: |
## Environment
- type: markdown
attributes:
value: |
### Server
You will find these values in your Admin Dashboard
- type: input
id: server-version
attributes:
label: Server version
placeholder: 10.10.2
validations:
required: true
- type: input
id: web-version
attributes:
label: Web version
placeholder: 10.10.2
validations:
required: true
- type: input
id: build-version
attributes:
label: Build version
placeholder: 10.10.2
validations:
required: true
- type: markdown
attributes:
value: |
### Client
Information about the device you are seeing the issue on
- type: input
id: platform
attributes:
label: Platform
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
placeholder: e.g. Linux, Windows, iPhone, Tizen
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
placeholder: e.g. Firefox, Chrome, Safari
validations:
required: true
- type: markdown
attributes:
value: |
## Additional
- type: textarea
attributes:
label: Additional information
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
placeholder: Add any additional context here.
validations:
required: false

View File

@@ -0,0 +1,22 @@
---
name: Playback Issue
about: You have playback issues with some files
labels: playback
---
**Describe The Bug**
<!-- A clear and concise description of what the bug is. -->
**Media Information**
<!-- Please paste any ffprobe or MediaInfo logs. -->
**Screenshots**
<!-- Add screenshots from the Playback Data and Media Info. -->
**System (please complete the following information):**
- Platform: [e.g. Linux, Windows, iPhone, Tizen]
- Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.6.0]
**Additional Context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,145 +0,0 @@
name: Playback Issue
description: Create a bug report related to media playback
labels:
- bug
- playback
body:
- type: checkboxes
id: before-posting
attributes:
label: "This issue respects the following points:"
description: All conditions are **required**.
options:
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin-web/issues?q=is%3Aissue) _(I've searched it)_.
required: true
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
required: true
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
required: true
- type: markdown
attributes:
value: |
## Bug information
- type: textarea
id: description
attributes:
label: Describe the bug
description: |
A clear and concise description of the bug.
You can also attach screenshots or screen recordings here to help explain your issue.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction Steps
description: |
Steps to reproduce the behavior:
placeholder: |
1. Go to …
2. Click on …
3. Scroll down to …
4. See error / the app crashes
validations:
required: true
- type: textarea
id: behaviour
attributes:
label: Expected/Actual behaviour
description: |
Describe the behavior you were expecting versus what actually occurred.
placeholder: |
I expected the app to... However, the actual behavior was that...
validations:
required: true
- type: textarea
id: mediainfo
attributes:
label: Media info of the file
description: |
Please share the media information for the file causing issues. You can use a variety of tools to retrieve this information.
- Use ffprobe (`ffprobe ./file.mp4`)
- Copy the media info from the web interface
placeholder: Paste media info…
render: shell
- type: markdown
attributes:
value: |
## Logs
- type: textarea
id: logs
attributes:
label: Logs
description: |
Please paste your logs here if applicable.
placeholder: Paste logs…
- type: textarea
id: logs-ffmpeg
attributes:
label: FFmpeg logs
description: |
Please paste your FFmpeg logs here if available. You can find these in your servers dashboard under "logs".
placeholder: Paste logs…
render: shell
- type: markdown
attributes:
value: |
## Environment
- type: markdown
attributes:
value: |
### Server
You will find these values in your Admin Dashboard
- type: input
id: server-version
attributes:
label: Server version
placeholder: 10.10.2
validations:
required: true
- type: input
id: web-version
attributes:
label: Web version
placeholder: 10.10.2
validations:
required: true
- type: input
id: build-version
attributes:
label: Build version
placeholder: 10.10.2
validations:
required: true
- type: markdown
attributes:
value: |
### Client
Information about the device you are seeing the issue on
- type: input
id: platform
attributes:
label: Platform
description: Specify the operating system or device where the issue occurs. If relevant, include details like version or model.
placeholder: e.g. Linux, Windows, iPhone, Tizen
validations:
required: true
- type: input
id: browser
attributes:
label: Browser
description: Indicate which browser you're using when encountering the issue. If possible, mention the browser version as well.
placeholder: e.g. Firefox, Chrome, Safari
validations:
required: true
- type: markdown
attributes:
value: |
## Additional
- type: textarea
attributes:
label: Additional information
description: Include any relevant details, resources, or screenshots that might help in understanding or implementing the request.
placeholder: Add any additional context here.
validations:
required: false

View File

@@ -0,0 +1,13 @@
---
name: Technical Discussion
about: You want to discuss technical aspects of changes you intend to make
labels: enhancement
---
<!-- Explain the change and the motivations behind it.
For example, if you plan to rely on a new dependency, explain why and what
it brings to the project.
If you plan to make significant changes, go roughly over the steps you intend
to take and how you would divide the change in PRs of a manageable size. -->

View File

@@ -0,0 +1,9 @@
---
name: Meta Issue
about: You want to track a number of other issues as part of a larger project
labels: meta
---
* [ ] Issue 1 [#123]
* [ ] Issue 2 [#456]
* [ ] ...

12
.github/renovate.json vendored
View File

@@ -4,25 +4,17 @@
"github>jellyfin/.github//renovate-presets/nodejs",
":dependencyDashboard"
],
"lockFileMaintenance": {
"enabled": false
},
"packageRules": [
{
"matchPackageNames": [ "@jellyfin/sdk" ],
"followTag": "unstable",
"minimumReleaseAge": null,
"prPriority": 5,
"schedule": [ "after 7:00 am" ]
},
{
"matchPackageNames": [ "dompurify" ],
"matchUpdateTypes": [ "major" ],
"matchPackageNames": ["dompurify"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": [ "hls.js" ],
"prPriority": 5
}
]
}

View File

@@ -8,7 +8,7 @@ jobs:
name: Merge conflict labeling 🏷️
runs-on: ubuntu-latest
steps:
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'

View File

@@ -20,21 +20,21 @@ jobs:
steps:
- name: Checkout repository ⬇️
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit }}
show-progress: false
- name: Initialize CodeQL 🛠️
uses: github/codeql-action/init@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
queries: security-and-quality
languages: ${{ matrix.language }}
- name: Autobuild 📦
uses: github/codeql-action/autobuild@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
- name: Perform CodeQL Analysis 🧪
uses: github/codeql-action/analyze@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3.31.4
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
category: '/language:${{matrix.language}}'

View File

@@ -29,13 +29,13 @@ jobs:
steps:
- name: Download workflow artifact ⬇️
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: ${{ inputs.artifact_name }}
path: dist
- name: Publish to Cloudflare Pages 📃
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3.11.0
id: cf
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@@ -57,7 +57,7 @@ jobs:
echo "EOF" >> $GITHUB_ENV
- name: Push comment to Pull Request 🔼
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0
if: ${{ inputs.comment && steps.compose.conclusion == 'success' }}
with:
github-token: ${{ secrets.JF_BOT_TOKEN }}

View File

@@ -14,12 +14,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit || github.sha }}
- name: Setup node environment
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: npm
@@ -39,7 +39,7 @@ jobs:
mv dist/config.tmp.json dist/config.json
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: frontend
path: dist

View File

@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit }}
show-progress: false
- name: Scan
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5
with:
## Workaround from https://github.com/actions/dependency-review-action/issues/456
## TODO: Remove when necessary
@@ -42,13 +42,13 @@ jobs:
steps:
- name: Checkout ⬇️
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.commit }}
show-progress: false
- name: Setup node environment ⚙️
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: npm

View File

@@ -80,12 +80,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup node environment
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: npm
@@ -95,6 +95,6 @@ jobs:
run: npm ci --no-audit
- name: Run eslint
uses: CatChen/eslint-suggestion-action@4ee415529307a8ca0260b4a3775484802523e5af # v4.1.19
uses: CatChen/eslint-suggestion-action@9c12109c4943f26f0676b71c9c10e456748872cf # v4.1.7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -55,4 +55,4 @@ jobs:
secrets: inherit
with:
branch: ${{ github.ref_name }}
comment: false
comment:

View File

@@ -10,17 +10,18 @@ permissions:
jobs:
issues:
name: Check stale issues and PRs
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# Issues receive a stale warning after 120 days and close after an additional 21 days
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
@@ -29,10 +30,21 @@ jobs:
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 are closed after having unresolved merge conflicts for 90 days
days-before-pr-stale: 0
days-before-pr-close: 90
only-pr-labels: merge conflict
stale-pr-label: stale
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.

5
.gitignore vendored
View File

@@ -16,8 +16,3 @@ config.json
# vim
*.sw?
# direnv
.direnv/
# environment related
.envrc

View File

@@ -1,6 +1,5 @@
{
"plugins": [
"@stylistic/stylelint-plugin",
"stylelint-no-browser-hacks/lib"
],
"rules": {
@@ -11,20 +10,20 @@
],
"ignore": ["after-comment"]
} ],
"@stylistic/at-rule-name-case": "lower",
"@stylistic/at-rule-name-space-after": "always-single-line",
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": true,
"at-rule-no-vendor-prefix": true,
"@stylistic/at-rule-semicolon-newline-after": "always",
"@stylistic/block-closing-brace-empty-line-before": "never",
"@stylistic/block-closing-brace-newline-after": "always",
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
"@stylistic/block-closing-brace-space-before": "always-single-line",
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
"@stylistic/block-opening-brace-space-after": "always-single-line",
"@stylistic/block-opening-brace-space-before": "always",
"@stylistic/color-hex-case": "lower",
"block-opening-brace-newline-after": "always-multi-line",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [ "always", {
@@ -43,8 +42,8 @@
"inside-single-line-block"
]
} ],
"@stylistic/declaration-bang-space-after": "never",
"@stylistic/declaration-bang-space-before": "always",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [
true,
{
@@ -52,52 +51,52 @@
}
],
"declaration-block-no-shorthand-property-overrides": true,
"@stylistic/declaration-block-semicolon-newline-after": "always-multi-line",
"@stylistic/declaration-block-semicolon-space-after": "always-single-line",
"@stylistic/declaration-block-semicolon-space-before": "never",
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1,
"@stylistic/declaration-block-trailing-semicolon": "always",
"@stylistic/declaration-colon-newline-after": "always-multi-line",
"@stylistic/declaration-colon-space-after": "always-single-line",
"@stylistic/declaration-colon-space-before": "never",
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"@stylistic/function-comma-newline-after": "always-multi-line",
"@stylistic/function-comma-space-after": "always-single-line",
"@stylistic/function-comma-space-before": "never",
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"@stylistic/function-max-empty-lines": 0,
"function-max-empty-lines": 0,
"function-name-case": "lower",
"@stylistic/function-parentheses-newline-inside": "always-multi-line",
"@stylistic/function-parentheses-space-inside": "never-single-line",
"@stylistic/function-whitespace-after": "always",
"@stylistic/indentation": 4,
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-whitespace-after": "always",
"indentation": 4,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"@stylistic/max-empty-lines": 1,
"@stylistic/media-feature-colon-space-after": "always",
"@stylistic/media-feature-colon-space-before": "never",
"@stylistic/media-feature-name-case": "lower",
"max-empty-lines": 1,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-name-no-vendor-prefix": true,
"@stylistic/media-feature-parentheses-space-inside": "never",
"@stylistic/media-feature-range-operator-space-after": "always",
"@stylistic/media-feature-range-operator-space-before": "always",
"@stylistic/media-query-list-comma-newline-after": "always-multi-line",
"@stylistic/media-query-list-comma-space-after": "always-single-line",
"@stylistic/media-query-list-comma-space-before": "never",
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"@stylistic/no-eol-whitespace": true,
"@stylistic/no-extra-semicolons": true,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"@stylistic/no-missing-end-of-source-newline": true,
"@stylistic/number-leading-zero": "always",
"@stylistic/number-no-trailing-zeros": true,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"plugin/no-browser-hacks": true,
"@stylistic/property-case": "lower",
"property-case": "lower",
"property-no-unknown": [
true,
{
@@ -111,20 +110,20 @@
"except": ["first-nested"],
"ignore": ["after-comment"]
} ],
"@stylistic/selector-attribute-brackets-space-inside": "never",
"@stylistic/selector-attribute-operator-space-after": "never",
"@stylistic/selector-attribute-operator-space-before": "never",
"@stylistic/selector-combinator-space-after": "always",
"@stylistic/selector-combinator-space-before": "always",
"@stylistic/selector-descendant-combinator-no-non-space": true,
"@stylistic/selector-list-comma-newline-after": "always",
"@stylistic/selector-list-comma-space-before": "never",
"@stylistic/selector-max-empty-lines": 0,
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-no-vendor-prefix": true,
"@stylistic/selector-pseudo-class-case": "lower",
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"@stylistic/selector-pseudo-class-parentheses-space-inside": "never",
"@stylistic/selector-pseudo-element-case": "lower",
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": [
true,
@@ -137,13 +136,13 @@
"selector-type-case": "lower",
"selector-type-no-unknown": true,
"string-no-newline": true,
"@stylistic/unit-case": "lower",
"unit-case": "lower",
"unit-no-unknown": true,
"value-no-vendor-prefix": true,
"@stylistic/value-list-comma-newline-after": "always-multi-line",
"@stylistic/value-list-comma-space-after": "always-single-line",
"@stylistic/value-list-comma-space-before": "never",
"@stylistic/value-list-max-empty-lines": 0
"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": [
{

View File

@@ -1,7 +1,4 @@
{
"[json][typescript][typescriptreact][javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},

View File

@@ -93,16 +93,6 @@
- [Connor Smith](https://github.com/ConnorS1110)
- [iFraan](https://github.com/iFraan)
- [Ali](https://github.com/bu3alwa)
- [K. Kyle Puchkov](https://github.com/kepper104)
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
- [Jxiced](https://github.com/Jxiced)
- [Derek Huber](https://github.com/Derek4aty1)
- [StableCrimson](https://github.com/StableCrimson)
- [diegoeche](https://github.com/diegoeche)
- [Free O'Toole](https://github.com/freeotoole)
- [TheBosZ](https://github.com/thebosz)
- [qm3jp](https://github.com/qm3jp)
- [johnnyg](https://github.com/johnnyg)
## Emby Contributors

View File

@@ -73,39 +73,31 @@ Jellyfin Web is the frontend used for most of the clients available for end user
## Directory Structure
> [!NOTE]
> We are in the process of refactoring to a [new structure](https://forum.jellyfin.org/t-proposed-update-to-the-structure-of-jellyfin-web) based on [Bulletproof React](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md) architecture guidelines.
> Most new code should be organized under the appropriate app directory unless it is common/shared.
```
.
└── src
├── apps
│   ├── dashboard # Admin dashboard app
│   ├── experimental # New experimental app
│   ── stable # Classic (stable) app
│   └── wizard # Startup wizard app
├── assets # Static assets
├── components # Higher order visual components and React components
├── constants # Common constant values
├── controllers # Legacy page views and controllers 🧹 ❌
├── elements # Basic webcomponents and React equivalents 🧹
├── hooks # Custom React hooks
├── lib # Reusable libraries
│   ├── globalize # Custom localization library
│   ── jellyfin-apiclient # Supporting code for the deprecated apiclient package
│   ├── legacy # Polyfills for legacy browsers
│   ├── navdrawer # Navigation drawer library for classic layout
│   └── scroller # Content scrolling library
├── plugins # Client plugins (features dynamically loaded at runtime)
├── scripts # Random assortment of visual components and utilities 🐉 ❌
├── strings # Translation files (only commit changes to en-us.json)
── styles # Common app Sass stylesheets
├── themes # Sass and MUI themes
├── types # Common TypeScript interfaces/types
└── utils # Utility functions
│   ├── 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
├── lib # Reusable libraries
│   ├── globalize # Custom localization library
│   ├── legacy # Polyfills for legacy browsers
│   ├── navdrawer # Navigation drawer library for classic layout
│   ── scroller # Content scrolling library
├── 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; Deprecated, do **not** create new files here
- 🧹 &mdash; Needs cleanup
- 🐉 &mdash; Serious mess (Here be dragons)

View File

@@ -1,417 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
import compat from 'eslint-plugin-compat';
import globals from 'globals';
// @ts-expect-error Missing type definition
import importPlugin from 'eslint-plugin-import';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import restrictedGlobals from 'confusing-browser-globals';
import sonarjs from 'eslint-plugin-sonarjs';
import stylistic from '@stylistic/eslint-plugin';
// eslint-disable-next-line import/no-unresolved
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
// @ts-expect-error Harmless type mismatch in dependency
comments.recommended,
compat.configs['flat/recommended'],
importPlugin.flatConfigs.errors,
sonarjs.configs.recommended,
reactPlugin.configs.flat.recommended,
{
settings: {
react: {
version: 'detect'
}
}
},
jsxA11y.flatConfigs.recommended,
// Global ignores
{
ignores: [
'node_modules',
'coverage',
'dist',
'.idea',
'.vscode'
]
},
// Global style rules
{
plugins: {
'@stylistic': stylistic
},
extends: [ importPlugin.flatConfigs.typescript ],
rules: {
'array-callback-return': ['error', { 'checkForEach': true }],
'curly': ['error', 'multi-line', 'consistent'],
'default-case-last': '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-lonely-if': 'error',
'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-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',
'@typescript-eslint/no-unused-vars': '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': ['hack', 'xxx'] }],
'one-var': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }],
'prefer-promise-reject-errors': ['warn', { 'allowEmptyReject': true }],
'@typescript-eslint/prefer-for-of': 'error',
'radix': 'error',
'yoda': 'error',
'sonarjs/fixme-tag': 'warn',
'sonarjs/todo-tag': 'off',
'sonarjs/deprecation': 'off',
'sonarjs/no-alphabetical-sort': 'warn',
'sonarjs/no-inverted-boolean-check': 'error',
'sonarjs/no-selector-parameter': 'off',
'sonarjs/pseudo-random': 'warn',
// TODO: Enable the following sonarjs rules and fix issues
'sonarjs/no-duplicate-string': 'off',
'sonarjs/no-nested-functions': 'warn',
// TODO: Replace with stylistic.configs.customize()
'@stylistic/block-spacing': 'error',
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/comma-spacing': 'error',
'@stylistic/eol-last': 'error',
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
'@stylistic/keyword-spacing': 'error',
'@stylistic/max-statements-per-line': 'error',
'@stylistic/no-floating-decimal': 'error',
'@stylistic/no-mixed-spaces-and-tabs': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1 }],
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'@stylistic/padded-blocks': ['error', 'never'],
'@stylistic/quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
'@stylistic/semi': 'error',
'@stylistic/space-before-blocks': 'error',
'@stylistic/space-infix-ops': 'error',
'@typescript-eslint/no-restricted-imports': [
'error',
{
paths: [
{
name: '@jellyfin/sdk/lib/generated-client',
message: 'Use direct file imports for tree-shaking',
allowTypeImports: true
},
{
name: '@jellyfin/sdk/lib/generated-client/api',
message: 'Use direct file imports for tree-shaking',
allowTypeImports: true
},
{
name: '@jellyfin/sdk/lib/generated-client/models',
message: 'Use direct file imports for tree-shaking',
allowTypeImports: true
},
{
name: '@mui/icons-material',
message: 'Use direct file imports for tree-shaking',
allowTypeImports: true
},
{
name: '@mui/material',
message: 'Use direct file imports for tree-shaking',
allowTypeImports: true
}
]
}
]
}
},
// Config files use node globals
{
ignores: [ 'src' ],
languageOptions: {
globals: {
...globals.node
}
}
},
// Config files are commonjs by default
{
files: [ '**/*.{cjs,js}' ],
ignores: [ 'src' ],
languageOptions: {
sourceType: 'commonjs'
},
rules: {
'@typescript-eslint/no-require-imports': 'off'
}
},
// App files
{
files: [
'src/**/*.{js,jsx,ts,tsx}'
],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
},
globals: {
...globals.browser,
// Tizen globals
'tizen': false,
'webapis': false,
// WebOS globals
'webOS': false,
// Dependency globals
'$': false,
'jQuery': false,
// Jellyfin globals
'ApiClient': true,
'Events': true,
'chrome': true,
'Emby': false,
'Hls': true,
'LibraryMenu': true,
'Windows': false,
// Build time definitions
__COMMIT_SHA__: false,
__JF_BUILD_VERSION__: false,
__PACKAGE_JSON_NAME__: false,
__PACKAGE_JSON_VERSION__: false,
__USE_SYSTEM_FONTS__: false,
__WEBPACK_SERVE__: false
}
},
settings: {
'import/resolver': {
node: {
extensions: [
'.js',
'.ts',
'.jsx',
'.tsx'
],
moduleDirectory: [
'node_modules',
'src'
]
}
},
polyfills: [
'Promise',
// whatwg-fetch
'fetch',
// document-register-element
'document.registerElement',
// resize-observer-polyfill
'ResizeObserver',
// fast-text-encoding
'TextEncoder',
// intersection-observer
'IntersectionObserver',
// Core-js
'Object.assign',
'Object.is',
'Object.setPrototypeOf',
'Object.toString',
'Object.freeze',
'Object.seal',
'Object.preventExtensions',
'Object.isFrozen',
'Object.isSealed',
'Object.isExtensible',
'Object.getOwnPropertyDescriptor',
'Object.getPrototypeOf',
'Object.keys',
'Object.entries',
'Object.getOwnPropertyNames',
'Function.name',
'Function.hasInstance',
'Array.from',
'Array.arrayOf',
'Array.copyWithin',
'Array.fill',
'Array.find',
'Array.findIndex',
'Array.iterator',
'String.fromCodePoint',
'String.raw',
'String.iterator',
'String.codePointAt',
'String.endsWith',
'String.includes',
'String.repeat',
'String.startsWith',
'String.trim',
'String.anchor',
'String.big',
'String.blink',
'String.bold',
'String.fixed',
'String.fontcolor',
'String.fontsize',
'String.italics',
'String.link',
'String.small',
'String.strike',
'String.sub',
'String.sup',
'RegExp',
'Number',
'Math',
'Date',
'async',
'Symbol',
'Map',
'Set',
'WeakMap',
'WeakSet',
'ArrayBuffer',
'DataView',
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'Reflect'
]
},
rules: {
// TODO: Add typescript recommended typed rules
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: [ 'camelCase', 'PascalCase' ],
leadingUnderscore: 'allow'
},
{
selector: 'variable',
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble'
},
{
selector: 'typeLike',
format: [ 'PascalCase' ]
},
{
selector: 'enumMember',
format: [ 'PascalCase', 'UPPER_CASE' ]
},
{
selector: [ 'objectLiteralProperty', 'typeProperty' ],
format: [ 'camelCase', 'PascalCase' ],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble'
},
// Ignore numbers, locale strings (en-us), aria/data attributes and CSS selectors
{
selector: [ 'objectLiteralProperty', 'typeProperty' ],
format: null,
filter: {
regex: '[ &\\-]|^([0-9]+)$',
match: true
}
}
],
'@typescript-eslint/no-deprecated': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error'
}
},
// React files
{
files: [ 'src/**/*.{jsx,tsx}' ],
plugins: {
'react-hooks': reactHooks
},
rules: {
'react/jsx-filename-extension': ['error', { 'extensions': ['.jsx', '.tsx'] }],
'react/jsx-no-bind': 'error',
'react/jsx-no-useless-fragment': 'error',
'react/no-array-index-key': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
},
// Service worker
{
files: [ 'src/serviceworker.js' ],
languageOptions: {
globals: {
...globals.serviceworker
}
}
},
// Legacy JS (less strict)
{
files: [ 'src/**/*.{js,jsx}' ],
rules: {
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-this-alias': 'off',
'sonarjs/public-static-readonly': 'off',
// TODO: Enable the following rules and fix issues
'sonarjs/cognitive-complexity': 'off',
'sonarjs/constructor-for-side-effects': 'off',
'sonarjs/function-return-type': 'off',
'sonarjs/no-async-constructor': 'off',
'sonarjs/no-duplicate-string': 'off',
'sonarjs/no-ignored-exceptions': 'off',
'sonarjs/no-invariant-returns': 'warn',
'sonarjs/no-nested-functions': 'off',
'sonarjs/void-use': 'off'
}
}
);

60
flake.lock generated
View File

@@ -1,60 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1739874174,
"narHash": "sha256-XGxSVtojlwjYRYGvGXex0Cw+/363EVJlbY9TPX9bARk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d2ab2691c798f6b633be91d74b1626980ddaff30",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,34 +0,0 @@
{
description = "jellyfin-web nix flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {
inherit system;
};
in {
devShell = with pkgs;
mkShell rec {
buildInputs = [
nodejs_20
];
shellHook = ''
# Also see: https://github.com/sass/embedded-host-node/issues/334
echo "Removing sass-embedded from node-modules as its broken on NixOS."
rm -rf node_modules/sass-embedded*
'';
};
}
);
}

21852
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,113 +1,106 @@
{
"name": "jellyfin-web",
"version": "10.12.0",
"version": "10.10.7",
"description": "Web interface for Jellyfin",
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@babel/core": "7.27.4",
"@babel/plugin-transform-modules-umd": "7.27.1",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@eslint/js": "9.30.1",
"@stylistic/eslint-plugin": "4.4.1",
"@stylistic/stylelint-plugin": "3.1.3",
"@babel/core": "7.25.8",
"@babel/plugin-transform-modules-umd": "7.25.7",
"@babel/preset-env": "7.25.8",
"@babel/preset-react": "7.25.7",
"@eslint-community/eslint-plugin-eslint-comments": "4.4.0",
"@stylistic/eslint-plugin": "2.9.0",
"@types/dompurify": "3.0.5",
"@types/escape-html": "1.0.4",
"@types/loadable__component": "5.13.9",
"@types/lodash-es": "4.17.12",
"@types/markdown-it": "14.1.2",
"@types/react": "18.3.23",
"@types/react-dom": "18.3.7",
"@types/react-lazy-load-image-component": "1.6.4",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/parser": "8.35.1",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@uupaa/dynamic-import-polyfill": "1.0.2",
"@vitest/coverage-v8": "3.2.4",
"autoprefixer": "10.4.21",
"babel-loader": "10.0.0",
"@vitest/coverage-v8": "2.1.3",
"autoprefixer": "10.4.20",
"babel-loader": "9.2.1",
"clean-webpack-plugin": "4.0.0",
"confusing-browser-globals": "1.0.11",
"copy-webpack-plugin": "13.0.0",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"cssnano": "7.0.7",
"es-check": "9.1.4",
"eslint": "9.30.1",
"eslint-plugin-compat": "6.0.2",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-sonarjs": "3.0.4",
"expose-loader": "5.0.1",
"fast-glob": "3.3.3",
"fork-ts-checker-webpack-plugin": "9.1.0",
"globals": "16.2.0",
"cssnano": "7.0.6",
"es-check": "7.2.1",
"eslint": "8.57.1",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-react": "7.37.1",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-sonarjs": "0.25.1",
"expose-loader": "5.0.0",
"fork-ts-checker-webpack-plugin": "9.0.2",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3",
"jsdom": "26.1.0",
"mini-css-extract-plugin": "2.9.2",
"postcss": "8.5.6",
"html-webpack-plugin": "5.6.0",
"jsdom": "25.0.1",
"mini-css-extract-plugin": "2.9.1",
"postcss": "8.4.47",
"postcss-loader": "8.1.1",
"postcss-preset-env": "10.2.3",
"postcss-preset-env": "10.0.7",
"postcss-scss": "4.0.9",
"sass": "1.89.2",
"sass-loader": "16.0.5",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"source-map-loader": "5.0.0",
"speed-measure-webpack-plugin": "1.5.0",
"style-loader": "4.0.0",
"stylelint": "16.21.0",
"stylelint": "15.11.0",
"stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "2.0.0",
"stylelint-order": "7.0.0",
"stylelint-scss": "6.12.1",
"ts-loader": "9.5.2",
"typescript": "5.8.3",
"typescript-eslint": "8.35.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack": "5.99.9",
"stylelint-no-browser-hacks": "1.3.0",
"stylelint-order": "6.0.4",
"stylelint-scss": "5.3.2",
"ts-loader": "9.5.1",
"typescript": "5.6.3",
"vitest": "2.1.3",
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-merge": "6.0.1",
"worker-loader": "3.0.8"
},
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@fontsource/noto-sans": "5.2.7",
"@fontsource/noto-sans-hk": "5.2.6",
"@fontsource/noto-sans-jp": "5.2.6",
"@fontsource/noto-sans-kr": "5.2.6",
"@fontsource/noto-sans-sc": "5.2.6",
"@fontsource/noto-sans-tc": "5.2.6",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@fontsource/noto-sans": "5.1.0",
"@fontsource/noto-sans-hk": "5.1.0",
"@fontsource/noto-sans-jp": "5.1.0",
"@fontsource/noto-sans-kr": "5.1.0",
"@fontsource/noto-sans-sc": "5.1.0",
"@fontsource/noto-sans-tc": "5.1.0",
"@jellyfin/libass-wasm": "4.2.3",
"@jellyfin/sdk": "0.0.0-unstable.202512091852",
"@jellyfin/ux-web": "1.0.0",
"@mui/icons-material": "6.4.12",
"@mui/material": "6.4.12",
"@mui/x-date-pickers": "7.29.4",
"@jellyfin/sdk": "0.0.0-unstable.202410250501",
"@mui/icons-material": "5.16.7",
"@mui/material": "5.16.7",
"@mui/x-date-pickers": "7.20.0",
"@react-hook/resize-observer": "2.0.2",
"@tanstack/react-query": "5.80.10",
"@tanstack/react-query-devtools": "5.80.10",
"abortcontroller-polyfill": "1.7.8",
"@tanstack/react-query": "5.59.13",
"@tanstack/react-query-devtools": "5.59.13",
"@types/react-lazy-load-image-component": "1.6.4",
"abortcontroller-polyfill": "1.7.5",
"blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.5.1",
"core-js": "3.43.0",
"core-js": "3.38.1",
"date-fns": "2.30.0",
"dompurify": "2.5.8",
"element-closest-polyfill": "1.0.7",
"dompurify": "2.5.7",
"epubjs": "0.3.93",
"escape-html": "1.0.3",
"fast-text-encoding": "1.0.6",
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"history": "5.3.0",
"hls.js": "1.6.13",
"hls.js": "1.5.16",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1",
@@ -117,25 +110,24 @@
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"material-design-icons-iconfont": "6.7.0",
"material-react-table": "3.2.1",
"material-react-table": "2.13.3",
"native-promise-only": "0.8.1",
"pdfjs-dist": "3.11.174",
"proxy-polyfill": "0.3.2",
"react": "18.3.1",
"react-blurhash": "0.3.0",
"react-dom": "18.3.1",
"react-lazy-load-image-component": "1.6.3",
"react-router-dom": "6.30.1",
"react-lazy-load-image-component": "1.6.2",
"react-router-dom": "6.27.0",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2",
"sortablejs": "1.15.6",
"swiper": "11.2.8",
"usehooks-ts": "3.1.1",
"sortablejs": "1.15.3",
"swiper": "11.1.14",
"usehooks-ts": "3.1.0",
"webcomponents.js": "0.7.24",
"whatwg-fetch": "3.6.20"
},
"optionalDependencies": {
"sass-embedded": "1.89.2"
"sass-embedded": "1.79.5"
},
"browserslist": [
"last 2 Firefox versions",
@@ -162,14 +154,14 @@
"build:check": "tsc --noEmit",
"build:es-check": "npm run build:production && npm run escheck",
"escheck": "es-check",
"lint": "eslint",
"lint": "eslint \"./\"",
"test": "vitest --watch=false --config vite.config.ts",
"test:watch": "vitest --config vite.config.ts",
"stylelint": "stylelint \"src/**/*.{css,scss}\""
},
"engines": {
"node": ">=20.0.0",
"npm": ">=9.6.4 <11.0.0",
"npm": ">=9.6.4",
"yarn": "YARN NO LONGER USED - use npm instead."
}
}

View File

@@ -1,4 +1,4 @@
import { ThemeProvider } from '@mui/material/styles';
import React from 'react';
import {
RouterProvider,
@@ -10,19 +10,14 @@ import {
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 { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { SETTING_KEY as LAYOUT_SETTING_KEY } from 'components/layoutManager';
import BangRedirect from 'components/router/BangRedirect';
import { createRouterHistory } from 'components/router/routerHistory';
import { LayoutMode } from 'constants/layoutMode';
import browser from 'scripts/browser';
import appTheme from 'themes';
import { ThemeStorageManager } from 'themes/themeStorageManager';
import UserThemeProvider from 'themes/UserThemeProvider';
const layoutMode = browser.tv ? LayoutMode.Tv : localStorage.getItem(LAYOUT_SETTING_KEY);
const isExperimentalLayout = !layoutMode || layoutMode === LayoutMode.Experimental;
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const router = createHashRouter([
{
@@ -30,7 +25,6 @@ const router = createHashRouter([
children: [
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
...DASHBOARD_APP_ROUTES,
...WIZARD_APP_ROUTES,
{
path: '!/*',
Component: BangRedirect
@@ -55,15 +49,11 @@ function RootAppLayout() {
.some(path => location.pathname.startsWith(`/${path}`));
return (
<ThemeProvider
theme={appTheme}
defaultMode='dark'
storageManager={ThemeStorageManager}
>
<UserThemeProvider>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
<Outlet />
</ThemeProvider>
</UserThemeProvider>
);
}

5
src/apiclient.d.ts vendored
View File

@@ -1,3 +1,4 @@
// TODO: Move to jellyfin-apiclient
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module 'jellyfin-apiclient' {
import type {
@@ -67,7 +68,7 @@ declare module 'jellyfin-apiclient' {
UtcTimeResponse,
VirtualFolderInfo
} from '@jellyfin/sdk/lib/generated-client';
import type { ConnectionState } from 'lib/jellyfin-apiclient';
import { ConnectionState } from './utils/jellyfin-apiclient/ConnectionState';
class ApiClient {
constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string);
@@ -136,7 +137,6 @@ declare module 'jellyfin-apiclient' {
getInstantMixFromItem(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getIntros(itemId: string): Promise<BaseItemDtoQueryResult>;
getItemCounts(userId?: string): Promise<ItemCounts>;
/** @deprecated This function returns a URL with a legacy auth parameter.*/
getItemDownloadUrl(itemId: string): string;
getItemImageInfos(itemId: string): Promise<ImageInfo[]>;
getItems(userId: string, options?: any): Promise<BaseItemDtoQueryResult>;
@@ -337,7 +337,6 @@ declare module 'jellyfin-apiclient' {
handleMessageReceived(msg: any): void;
logout(): Promise<void>;
minServerVersion(val?: string): string;
updateSavedServerId(server: any): Promise<void>;
user(apiClient: ApiClient): Promise<any>;
}

View File

@@ -9,21 +9,19 @@ import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ServerButton from 'components/toolbar/ServerButton';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { appRouter } from 'components/router/appRouter';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
import { useLocale } from 'hooks/useLocale';
import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import HelpButton from './components/toolbar/HelpButton';
import { DASHBOARD_APP_PATHS } from './routes/routes';
import './AppOverrides.scss';
const DRAWERLESS_PATHS = [ DASHBOARD_APP_PATHS.MetadataManager ];
export const Component: FC = () => {
const [ isDrawerActive, setIsDrawerActive ] = useState(false);
const location = useLocation();
@@ -31,8 +29,8 @@ export const Component: FC = () => {
const { dateFnsLocale } = useLocale();
const isMediumScreen = useMediaQuery((t: Theme) => t.breakpoints.up('md'));
const isMetadataManager = location.pathname.startsWith(`/${DASHBOARD_APP_PATHS.MetadataManager}`);
const isDrawerAvailable = Boolean(user) && !isMetadataManager;
const isDrawerAvailable = Boolean(user)
&& !DRAWERLESS_PATHS.some(path => location.pathname.startsWith(`/${path}`));
const isDrawerOpen = isDrawerActive && isDrawerAvailable;
const onToggleDrawer = useCallback(() => {
@@ -67,18 +65,10 @@ export const Component: FC = () => {
}}
>
<AppToolbar
isBackButtonAvailable={appRouter.canGoBack()}
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
buttons={
<HelpButton />
}
>
{isMetadataManager && (
<ServerButton />
)}
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
</AppBar>
@@ -107,7 +97,6 @@ export const Component: FC = () => {
</AppBody>
</Box>
</Box>
<ThemeCss dashboard />
</LocalizationProvider>
);
};

View File

@@ -7,7 +7,7 @@ import isEqual from 'lodash-es/isEqual';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { EventType } from 'constants/eventType';
import { EventType } from 'types/eventType';
import Events, { type Event } from 'utils/events';
interface AppTabsParams {

View File

@@ -1,121 +0,0 @@
import React from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
import CardActionArea from '@mui/material/CardActionArea';
import Stack from '@mui/material/Stack';
import { Link, To } from 'react-router-dom';
interface BaseCardProps {
title?: string;
text?: string;
image?: string | null;
icon?: React.ReactNode;
to?: To;
onClick?: () => void;
action?: boolean;
actionRef?: React.MutableRefObject<HTMLButtonElement | null>;
onActionClick?: () => void;
height?: number;
width?: number;
};
const BaseCard = ({
title,
text,
image,
icon,
to,
onClick,
action,
actionRef,
onActionClick,
height,
width
}: BaseCardProps) => {
return (
<Card
sx={{
display: 'flex',
flexDirection: 'column',
height: height || 240,
width: width
}}
>
<CardActionArea
{...(to && {
component: Link,
to: to
})}
onClick={onClick}
sx={{
display: 'flex',
flexGrow: 1,
alignItems: 'stretch'
}}
>
{image ? (
<CardMedia
sx={{ flexGrow: 1 }}
image={image}
title={title}
/>
) : (
<Box className={getDefaultBackgroundClass(title)} sx={{
flexGrow: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{icon}
</Box>
)}
</CardActionArea>
<CardContent
sx={{
minHeight: 50,
'&:last-child': {
paddingBottom: 2,
paddingRight: 1
}
}}>
<Stack flexGrow={1} direction='row'>
<Stack flexGrow={1}>
<Typography gutterBottom sx={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
}}>
{title}
</Typography>
{text && (
<Typography
variant='body2'
color='text.secondary'
sx={{
lineBreak: 'anywhere'
}}
>
{text}
</Typography>
)}
</Stack>
<Box>
{action ? (
<IconButton ref={actionRef} onClick={onActionClick}>
<MoreVertIcon />
</IconButton>
) : null}
</Box>
</Stack>
</CardContent>
</Card>
);
};
export default BaseCard;

View File

@@ -1,70 +0,0 @@
import Search from '@mui/icons-material/Search';
import InputBase, { type InputBaseProps } from '@mui/material/InputBase';
import { alpha, styled } from '@mui/material/styles';
import React, { type FC } from 'react';
const SearchContainer = styled('div')(({ theme }) => ({
display: 'flex',
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25)
},
width: '100%',
[theme.breakpoints.up('sm')]: {
width: 'auto'
}
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
flexGrow: 1,
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: '20ch'
}
}
}));
interface SearchInputProps extends InputBaseProps {
label?: string
}
const SearchInput: FC<SearchInputProps> = ({
label,
...props
}) => {
return (
<SearchContainer>
<SearchIconWrapper>
<Search />
</SearchIconWrapper>
<StyledInputBase
placeholder={label}
inputProps={{
'aria-label': label,
...props.inputProps
}}
{...props}
/>
</SearchContainer>
);
};
export default SearchInput;

View File

@@ -1,30 +0,0 @@
import React, { useCallback } from 'react';
import Snackbar, { SnackbarProps } from '@mui/material/Snackbar';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
const Toast = (props: SnackbarProps) => {
const onCloseClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
props.onClose?.(e, 'clickaway');
}, [ props ]);
const action = (
<IconButton
size='small'
color='inherit'
onClick={onCloseClick}
>
<CloseIcon fontSize='small' />
</IconButton>
);
return (
<Snackbar
autoHideDuration={3300}
action={action}
{ ...props }
/>
);
};
export default Toast;

View File

@@ -1,5 +1,4 @@
import Article from '@mui/icons-material/Article';
import Backup from '@mui/icons-material/Backup';
import Lan from '@mui/icons-material/Lan';
import Schedule from '@mui/icons-material/Schedule';
import VpnKey from '@mui/icons-material/VpnKey';
@@ -39,14 +38,6 @@ const AdvancedDrawerSection = () => {
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/backups'>
<ListItemIcon>
<Backup />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderBackups')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/logs'>
<ListItemIcon>

View File

@@ -1,5 +1,4 @@
import Analytics from '@mui/icons-material/Analytics';
import Devices from '@mui/icons-material/Devices';
import { Devices, Analytics } from '@mui/icons-material';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';

View File

@@ -1,5 +1,4 @@
import Dvr from '@mui/icons-material/Dvr';
import LiveTv from '@mui/icons-material/LiveTv';
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';
@@ -29,7 +28,7 @@ const LiveTvDrawerSection = () => {
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/dashboard/livetv/recordings'>
<ListItemLink to='/dashboard/recordings'>
<ListItemIcon>
<Dvr />
</ListItemIcon>

View File

@@ -1,5 +1,6 @@
import Extension from '@mui/icons-material/Extension';
import Folder from '@mui/icons-material/Folder';
import Public from '@mui/icons-material/Public';
import List from '@mui/material/List';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
@@ -32,16 +33,23 @@ const PluginDrawerSection = () => {
>
<ListItemLink
to='/dashboard/plugins'
includePaths={[
'/configurationpage',
'/dashboard/plugins/repositories'
]}
includePaths={[ '/configurationpage' ]}
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
>
<ListItemIcon>
<Extension />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabPlugins')} />
<ListItemText primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink
to='/dashboard/plugins/catalog'
includePaths={[ '/dashboard/plugins/repositories' ]}
>
<ListItemIcon>
<Public />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabCatalog')} />
</ListItemLink>
{pagesInfo?.map(pageInfo => (

View File

@@ -1,11 +1,4 @@
import Dashboard from '@mui/icons-material/Dashboard';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import LibraryAdd from '@mui/icons-material/LibraryAdd';
import Palette from '@mui/icons-material/Palette';
import People from '@mui/icons-material/People';
import PlayCircle from '@mui/icons-material/PlayCircle';
import Settings from '@mui/icons-material/Settings';
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';
@@ -76,12 +69,6 @@ const ServerDrawerSection = () => {
<ListItemText primary={globalize.translate('General')} />
</ListItemLink>
</ListItem>
<ListItemLink to='/dashboard/branding'>
<ListItemIcon>
<Palette />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderBranding')} />
</ListItemLink>
<ListItem disablePadding>
<ListItemLink to='/dashboard/users'>
<ListItemIcon>

View File

@@ -1,17 +0,0 @@
import format from 'date-fns/format';
import type { MRT_Cell, MRT_RowData } from 'material-react-table';
import { FC } from 'react';
import { useLocale } from 'hooks/useLocale';
interface CellProps {
cell: MRT_Cell<MRT_RowData>
}
const DateTimeCell: FC<CellProps> = ({ cell }) => {
const { dateFnsLocale } = useLocale();
return format(cell.getValue<Date>(), 'Pp', { locale: dateFnsLocale });
};
export default DateTimeCell;

View File

@@ -1,73 +0,0 @@
import Box from '@mui/material/Box/Box';
import Stack from '@mui/material/Stack/Stack';
import type {} from '@mui/material/themeCssVarsAugmentation';
import Typography from '@mui/material/Typography/Typography';
import { type MRT_RowData, type MRT_TableInstance, type MRT_TableOptions, MaterialReactTable } from 'material-react-table';
import React from 'react';
import Page, { type PageProps } from 'components/Page';
interface TablePageProps<T extends MRT_RowData> extends PageProps {
title: string
subtitle?: string
table: MRT_TableInstance<T>
}
export const DEFAULT_TABLE_OPTIONS: Partial<MRT_TableOptions<MRT_RowData>> = {
// Enable custom features
enableColumnPinning: true,
enableColumnResizing: true,
// Sticky header/footer
enableStickyFooter: true,
enableStickyHeader: true,
muiTableContainerProps: {
sx: {
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
}
}
};
const TablePage = <T extends MRT_RowData>({
title,
subtitle,
table,
children,
...pageProps
}: TablePageProps<T>) => {
return (
<Page
title={title}
{...pageProps}
>
<Box
className='content-primary'
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%'
}}
>
<Stack
spacing={2}
sx={{
marginBottom: 1
}}
>
<Typography variant='h1'>
{title}
</Typography>
{subtitle && (
<Typography>
{subtitle}
</Typography>
)}
</Stack>
<MaterialReactTable table={table} />
</Box>
{children}
</Page>
);
};
export default TablePage;

View File

@@ -1,36 +0,0 @@
import HelpOutline from '@mui/icons-material/HelpOutline';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip/Tooltip';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { HelpLinks } from 'apps/dashboard/constants/helpLinks';
import globalize from 'lib/globalize';
const HelpButton = () => (
<Routes>
{
HelpLinks.map(({ paths, url }) => paths.map(path => (
<Route
key={[url, path].join('-')}
path={path}
element={
<Tooltip title={globalize.translate('Help')}>
<IconButton
href={url}
rel='noopener noreferrer'
target='_blank'
size='large'
color='inherit'
>
<HelpOutline />
</IconButton>
</Tooltip>
}
/>
))).flat()
}
</Routes>
);
export default HelpButton;

View File

@@ -1,51 +0,0 @@
import React, { useMemo } from 'react';
import globalize from 'lib/globalize';
import Widget from './Widget';
import List from '@mui/material/List';
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
import subSeconds from 'date-fns/subSeconds';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
const ActivityLogWidget = () => {
const dayBefore = useMemo(() => (
subSeconds(new Date(), 24 * 60 * 60).toISOString()
), []);
const { data: logs, isPending } = useLogEntries({
startIndex: 0,
limit: 7,
minDate: dayBefore,
hasUserId: true
});
return (
<Widget
title={globalize.translate('HeaderActivity')}
href='/dashboard/activity?useractivity=true'
>
{isPending ? (
<Stack spacing={2}>
<Skeleton variant='rounded' height={60} />
<Skeleton variant='rounded' height={60} />
<Skeleton variant='rounded' height={60} />
<Skeleton variant='rounded' height={60} />
</Stack>
) : (
<List sx={{ bgcolor: 'background.paper' }}>
{logs?.Items?.map(entry => (
<ActivityListItem
key={entry.Id}
item={entry}
displayShortOverview={true}
to='/dashboard/activity?useractivity=true'
/>
))}
</List>
)}
</Widget>
);
};
export default ActivityLogWidget;

View File

@@ -1,42 +0,0 @@
import React, { useMemo } from 'react';
import globalize from 'lib/globalize';
import Widget from './Widget';
import List from '@mui/material/List';
import ActivityListItem from 'apps/dashboard/features/activity/components/ActivityListItem';
import subSeconds from 'date-fns/subSeconds';
import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries';
const AlertsLogWidget = () => {
const weekBefore = useMemo(() => (
subSeconds(new Date(), 7 * 24 * 60 * 60).toISOString()
), []);
const { data: alerts, isPending } = useLogEntries({
startIndex: 0,
limit: 4,
minDate: weekBefore,
hasUserId: false
});
if (isPending || alerts?.Items?.length === 0) return null;
return (
<Widget
title={globalize.translate('Alerts')}
href='/dashboard/activity?useractivity=false'
>
<List sx={{ bgcolor: 'background.paper' }}>
{alerts?.Items?.map(entry => (
<ActivityListItem
key={entry.Id}
item={entry}
displayShortOverview={false}
to='/dashboard/activity?useractivity=false'
/>
))}
</List>
</Widget>
);
};
export default AlertsLogWidget;

View File

@@ -1,28 +0,0 @@
import React from 'react';
import globalize from 'lib/globalize';
import Widget from './Widget';
import DeviceCard from 'apps/dashboard/features/devices/components/DeviceCard';
import Stack from '@mui/material/Stack';
import useLiveSessions from 'apps/dashboard/features/sessions/hooks/useLiveSessions';
const DevicesWidget = () => {
const { data: devices } = useLiveSessions();
return (
<Widget
title={globalize.translate('HeaderDevices')}
href='/dashboard/devices'
>
<Stack direction='row' flexWrap='wrap' gap={2}>
{devices?.map(device => (
<DeviceCard
key={device.Id}
device={device}
/>
))}
</Stack>
</Widget>
);
};
export default DevicesWidget;

View File

@@ -1,103 +0,0 @@
import type { ItemCounts } from '@jellyfin/sdk/lib/generated-client/models/item-counts';
import Book from '@mui/icons-material/Book';
import Movie from '@mui/icons-material/Movie';
import MusicNote from '@mui/icons-material/MusicNote';
import MusicVideo from '@mui/icons-material/MusicVideo';
import Tv from '@mui/icons-material/Tv';
import VideoLibrary from '@mui/icons-material/VideoLibrary';
import Grid from '@mui/material/Grid';
import SvgIcon from '@mui/material/SvgIcon';
import React, { useMemo } from 'react';
import { useItemCounts } from 'apps/dashboard/features/metrics/api/useItemCounts';
import MetricCard, { type MetricCardProps } from 'apps/dashboard/features/metrics/components/MetricCard';
import globalize from 'lib/globalize';
import Box from '@mui/material/Box';
interface MetricDefinition {
key: keyof ItemCounts
i18n: string
}
interface CardDefinition {
Icon: typeof SvgIcon
metrics: MetricDefinition[]
}
const CARD_DEFINITIONS: CardDefinition[] = [
{
Icon: Movie,
metrics: [{ key: 'MovieCount', i18n: 'Movies' }]
}, {
Icon: Tv,
metrics: [
{ key: 'SeriesCount', i18n: 'Series' },
{ key: 'EpisodeCount', i18n: 'Episodes' }
]
}, {
Icon: MusicNote,
metrics: [
{ key: 'AlbumCount', i18n: 'Albums' },
{ key: 'SongCount', i18n: 'Songs' }
]
}, {
Icon: MusicVideo,
metrics: [{ key: 'MusicVideoCount', i18n: 'MusicVideos' }]
}, {
Icon: Book,
metrics: [{ key: 'BookCount', i18n: 'Books' }]
}, {
Icon: VideoLibrary,
metrics: [{ key: 'BoxSetCount', i18n: 'Collections' }]
}
];
const ItemCountsWidget = () => {
const {
data: counts,
isPending
} = useItemCounts();
const cards: MetricCardProps[] = useMemo(() => {
return CARD_DEFINITIONS
.filter(def => (
// Include all cards while the request is pending
isPending
// Check if the metrics are present in counts
|| def.metrics.some(({ key }) => counts?.[key])
))
.map(({ Icon, metrics }) => ({
Icon,
metrics: metrics.map(({ i18n, key }) => ({
label: globalize.translate(i18n),
value: counts?.[key]
}))
}));
}, [ counts, isPending ]);
return (
<Box>
<Grid
container
spacing={2}
sx={{
alignItems: 'stretch'
}}
>
{cards.map(card => (
<Grid
key={card.metrics.map(metric => metric.label).join('-')}
item
xs={12}
sm={6}
lg={4}
>
<MetricCard {...card} />
</Grid>
))}
</Grid>
</Box>
);
};
export default ItemCountsWidget;

View File

@@ -1,42 +0,0 @@
import React, { useMemo } from 'react';
import globalize from 'lib/globalize';
import Widget from './Widget';
import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
import Paper from '@mui/material/Paper';
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
import Typography from '@mui/material/Typography';
import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
type RunningTasksWidgetProps = {
tasks?: TaskInfo[];
};
const RunningTasksWidget = ({ tasks }: RunningTasksWidgetProps) => {
const runningTasks = useMemo(() => {
return tasks?.filter(v => v.State == TaskState.Running) || [];
}, [ tasks ]);
if (runningTasks.length == 0) return null;
return (
<Widget
title={globalize.translate('HeaderRunningTasks')}
href='/dashboard/tasks'
>
<Paper sx={{ padding: 2 }}>
<Stack spacing={2} maxWidth={'330px'}>
{runningTasks.map((task => (
<Box key={task.Id}>
<Typography>{task.Name}</Typography>
<TaskProgress task={task} />
</Box>
)))}
</Stack>
</Paper>
</Widget>
);
};
export default RunningTasksWidget;

View File

@@ -1,104 +0,0 @@
import React from 'react';
import globalize from 'lib/globalize';
import Widget from './Widget';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Skeleton from '@mui/material/Skeleton';
import RefreshIcon from '@mui/icons-material/Refresh';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import { useSystemInfo } from 'hooks/useSystemInfo';
type ServerInfoWidgetProps = {
onScanLibrariesClick?: () => void;
onRestartClick?: () => void;
onShutdownClick?: () => void;
isScanning?: boolean;
};
const ServerInfoWidget = ({
onScanLibrariesClick,
onRestartClick,
onShutdownClick,
isScanning
}: ServerInfoWidgetProps) => {
const { data: systemInfo, isPending } = useSystemInfo();
return (
<Widget
title={globalize.translate('TabServer')}
href='/dashboard/settings'
>
<Stack spacing={2}>
<Paper sx={{
padding: 2
}}>
<Stack direction='row'>
<Stack flexGrow={1} spacing={1}>
<Typography fontWeight='bold'>{globalize.translate('LabelServerName')}</Typography>
<Typography fontWeight='bold'>{globalize.translate('LabelServerVersion')}</Typography>
<Typography fontWeight='bold'>{globalize.translate('LabelWebVersion')}</Typography>
<Typography fontWeight='bold'>{globalize.translate('LabelBuildVersion')}</Typography>
</Stack>
<Stack flexGrow={5} spacing={1}>
{isPending ? (
<>
<Skeleton />
<Skeleton />
<Skeleton />
<Skeleton />
</>
) : (
<>
<Typography>{systemInfo?.ServerName}</Typography>
<Typography>{systemInfo?.Version}</Typography>
<Typography>{__PACKAGE_JSON_VERSION__}</Typography>
<Typography>{__JF_BUILD_VERSION__}</Typography>
</>
)}
</Stack>
</Stack>
</Paper>
<Stack direction='row' spacing={1.5}>
<Button
onClick={onScanLibrariesClick}
startIcon={<RefreshIcon />}
sx={{
fontWeight: 'bold'
}}
disabled={isScanning}
>
{globalize.translate('ButtonScanAllLibraries')}
</Button>
<Button
onClick={onRestartClick}
startIcon={<RestartAltIcon />}
color='error'
sx={{
fontWeight: 'bold'
}}
>
{globalize.translate('Restart')}
</Button>
<Button
onClick={onShutdownClick}
startIcon={<PowerSettingsNewIcon />}
color='error'
sx={{
fontWeight: 'bold'
}}
>
{globalize.translate('ButtonShutdown')}
</Button>
</Stack>
</Stack>
</Widget>
);
};
export default ServerInfoWidget;

View File

@@ -1,50 +0,0 @@
import List from '@mui/material/List';
import React from 'react';
import StorageListItem from 'apps/dashboard/features/storage/components/StorageListItem';
import globalize from 'lib/globalize';
import Widget from './Widget';
import { useSystemStorage } from 'apps/dashboard/features/storage/api/useSystemStorage';
const ServerPathWidget = () => {
const { data: systemStorage } = useSystemStorage();
return (
<Widget
title={globalize.translate('HeaderPaths')}
href='/dashboard/settings'
>
<List sx={{ bgcolor: 'background.paper' }}>
<StorageListItem
label={globalize.translate('LabelCache')}
folder={systemStorage?.CacheFolder}
/>
<StorageListItem
label={globalize.translate('LabelImageCache')}
folder={systemStorage?.ImageCacheFolder}
/>
<StorageListItem
label={globalize.translate('LabelProgramData')}
folder={systemStorage?.ProgramDataFolder}
/>
<StorageListItem
label={globalize.translate('LabelLogs')}
folder={systemStorage?.LogFolder}
/>
<StorageListItem
label={globalize.translate('LabelMetadata')}
folder={systemStorage?.InternalMetadataFolder}
/>
<StorageListItem
label={globalize.translate('LabelTranscodes')}
folder={systemStorage?.TranscodingTempFolder}
/>
<StorageListItem
label={globalize.translate('LabelWeb')}
folder={systemStorage?.WebFolder}
/>
</List>
</Widget>
);
};
export default ServerPathWidget;

View File

@@ -1,38 +0,0 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import ChevronRight from '@mui/icons-material/ChevronRight';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
type WidgetProps = {
title: string;
href: string;
children: React.ReactNode;
};
const Widget = ({ title, href, children }: WidgetProps) => {
return (
<Box>
<Button
component={RouterLink}
to={href}
variant='text'
color='inherit'
endIcon={<ChevronRight />}
sx={{
marginTop: 1,
marginBottom: 1
}}
>
<Typography variant='h3' component='span'>
{title}
</Typography>
</Button>
{children}
</Box>
);
};
export default Widget;

View File

@@ -1,54 +0,0 @@
export const HelpLinks = [
{
paths: ['/dashboard/devices'],
url: 'https://jellyfin.org/docs/general/server/devices'
}, {
paths: ['/dashboard/libraries'],
url: 'https://jellyfin.org/docs/general/server/libraries'
}, {
paths: [
'/dashboard/livetv',
'/dashboard/livetv/tuner',
'/dashboard/recordings'
],
url: 'https://jellyfin.org/docs/general/server/live-tv/'
}, {
paths: ['/dashboard/livetv/guide'],
url: 'https://jellyfin.org/docs/general/server/live-tv/setup-guide#adding-guide-data'
}, {
paths: ['/dashboard/networking'],
url: 'https://jellyfin.org/docs/general/networking/'
}, {
paths: ['/dashboard/playback/transcoding'],
url: 'https://jellyfin.org/docs/general/server/transcoding'
}, {
paths: ['/dashboard/plugins'],
url: 'https://jellyfin.org/docs/general/server/plugins/'
}, {
paths: ['/dashboard/plugins/repositories'],
url: 'https://jellyfin.org/docs/general/server/plugins/#repositories'
}, {
paths: [
'/dashboard/branding',
'/dashboard/settings'
],
url: 'https://jellyfin.org/docs/general/server/settings'
}, {
paths: ['/dashboard/tasks'],
url: 'https://jellyfin.org/docs/general/server/tasks'
}, {
paths: ['/dashboard/users'],
url: 'https://jellyfin.org/docs/general/server/users/adding-managing-users'
}, {
paths: [
'/dashboard/users/access',
'/dashboard/users/parentalcontrol',
'/dashboard/users/password',
'/dashboard/users/profile'
],
url: 'https://jellyfin.org/docs/general/server/users/'
}, {
paths: ['/dashboard/backups'],
url: 'https://jellyfin.org/docs/general/administration/backup-and-restore/'
}
];

View File

@@ -7,10 +7,15 @@ import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
const fetchLogEntries = async (
api: Api,
api?: Api,
requestParams?: ActivityLogApiGetLogEntriesRequest,
options?: AxiosRequestConfig
) => {
if (!api) {
console.warn('[fetchLogEntries] No API instance available');
return;
}
const response = await getActivityLogApi(api).getLogEntries(requestParams, {
signal: options?.signal
});
@@ -18,15 +23,14 @@ const fetchLogEntries = async (
return response.data;
};
export const useLogEntries = (
export const useLogEntires = (
requestParams: ActivityLogApiGetLogEntriesRequest
) => {
const { api } = useApi();
return useQuery({
queryKey: ['ActivityLogEntries', requestParams],
queryKey: ['LogEntries', requestParams],
queryFn: ({ signal }) =>
fetchLogEntries(api!, requestParams, { signal }),
enabled: !!api,
refetchOnMount: false
fetchLogEntries(api, requestParams, { signal }),
enabled: !!api
});
};

View File

@@ -1,4 +1,4 @@
import IconButton from '@mui/material/IconButton';
import IconButton from '@mui/material/IconButton/IconButton';
import PermMedia from '@mui/icons-material/PermMedia';
import React, { type FC } from 'react';
import { Link } from 'react-router-dom';

View File

@@ -1,75 +0,0 @@
import React, { useMemo } from 'react';
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
import Notifications from '@mui/icons-material/Notifications';
import Avatar from '@mui/material/Avatar';
import ListItem from '@mui/material/ListItem';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import formatRelative from 'date-fns/formatRelative';
import { getLocale } from 'utils/dateFnsLocale';
import Stack from '@mui/material/Stack';
import getLogLevelColor from '../utils/getLogLevelColor';
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import ListItemLink from 'components/ListItemLink';
type ActivityListItemProps = {
item: ActivityLogEntry;
displayShortOverview: boolean;
to: string;
};
const ActivityListItem = ({ item, displayShortOverview, to }: ActivityListItemProps) => {
const relativeDate = useMemo(() => {
if (item.Date) {
return formatRelative(Date.parse(item.Date), Date.now(), { locale: getLocale() });
} else {
return 'N/A';
}
}, [ item ]);
return (
<ListItem disablePadding>
<ListItemLink to={to}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: getLogLevelColor(item.Severity || LogLevel.Information) + '.main' }}>
<Notifications sx={{ color: '#fff' }} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={<Typography sx={{ whiteSpace: 'pre-wrap' }}>{item.Name}</Typography>}
secondary={(
<Stack>
<Typography
sx={{
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
variant='body1'
color='text.secondary'
>
{relativeDate}
</Typography>
{displayShortOverview && (
<Typography
sx={{
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
variant='body1'
color='text.secondary'
>
{item.ShortOverview}
</Typography>
)}
</Stack>
)}
disableTypography
/>
</ListItemLink>
</ListItem>
);
};
export default ActivityListItem;

View File

@@ -1,17 +1,30 @@
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import Chip from '@mui/material/Chip';
import React, { useMemo } from 'react';
import React from 'react';
import globalize from 'lib/globalize';
import getLogLevelColor from '../utils/getLogLevelColor';
const LogLevelChip = ({ level }: { level: LogLevel }) => {
const levelText = useMemo(() => globalize.translate(`LogLevel.${level}`), [level]);
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={getLogLevelColor(level)}
color={color}
label={levelText}
title={levelText}
/>

View File

@@ -1,6 +1,5 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
import type { SxProps, Theme } from '@mui/material';
import IconButton from '@mui/material/IconButton';
import IconButton from '@mui/material/IconButton/IconButton';
import React, { type FC } from 'react';
import { Link } from 'react-router-dom';
@@ -8,21 +7,14 @@ import UserAvatar from 'components/UserAvatar';
interface UserAvatarButtonProps {
user?: UserDto
sx?: SxProps<Theme>
}
const UserAvatarButton: FC<UserAvatarButtonProps> = ({
user,
sx
}) => (
const UserAvatarButton: FC<UserAvatarButtonProps> = ({ user }) => (
user?.Id ? (
<IconButton
size='large'
color='inherit'
sx={{
padding: 0,
...sx
}}
sx={{ padding: 0 }}
title={user.Name || undefined}
component={Link}
to={`/dashboard/users/profile?userId=${user.Id}`}

View File

@@ -1,15 +0,0 @@
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
const getLogLevelColor = (level: LogLevel) => {
switch (level) {
case LogLevel.Information:
return 'info';
case LogLevel.Warning:
return 'warning';
case LogLevel.Error:
case LogLevel.Critical:
return 'error';
}
};
export default getLogLevelColor;

View File

@@ -1,37 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
import { useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'Backups';
const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
// FIXME: Replace with getBackupApi when available in SDK
const backupApi = new BackupApi(api.configuration, undefined, api.axiosInstance);
const response = await backupApi.listBackups(options);
const backups = response.data;
backups.sort((a, b) => {
if (a.DateCreated && b.DateCreated) {
return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
} else {
return 0;
}
});
return backups;
};
export const useBackups = () => {
const { api } = useApi();
return useQuery({
queryKey: [ QUERY_KEY ],
queryFn: ({ signal }) =>
fetchBackups(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,25 +0,0 @@
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { QUERY_KEY } from './useBackups';
import { queryClient } from 'utils/query/queryClient';
export const useCreateBackup = () => {
const { api } = useApi();
// FIXME: Replace with getBackupApi when available in SDK
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
return useMutation({
mutationFn: (backupOptions: BackupOptionsDto) => (
backupApi.createBackup({
backupOptionsDto: backupOptions
})
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -1,19 +0,0 @@
import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
export const useRestoreBackup = () => {
const { api } = useApi();
// FIXME: Replace with getBackupApi when available in SDK
const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
return useMutation({
mutationFn: (fileName: string) => (
backupApi.startRestoreBackup({
backupRestoreRequestDto: {
ArchiveFileName: fileName
}
})
)
});
};

View File

@@ -1,68 +0,0 @@
import React, { FunctionComponent, useCallback, useState } from 'react';
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import Restore from '@mui/icons-material/Restore';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Tooltip from '@mui/material/Tooltip';
import globalize from 'lib/globalize';
import BackupInfoDialog from './BackupInfoDialog';
type BackupProps = {
backup: BackupManifestDto;
onRestore: (backup: BackupManifestDto) => void;
};
const Backup: FunctionComponent<BackupProps> = ({ backup, onRestore }) => {
const [ isInfoDialogOpen, setIsInfoDialogOpen ] = useState(false);
const onDialogClose = useCallback(() => {
setIsInfoDialogOpen(false);
}, []);
const openDialog = useCallback(() => {
setIsInfoDialogOpen(true);
}, []);
const restore = useCallback(() => {
onRestore(backup);
}, [ backup, onRestore ]);
return (
<>
<BackupInfoDialog
backup={backup}
onClose={onDialogClose}
open={isInfoDialogOpen}
/>
<ListItem
disablePadding
secondaryAction={
<Tooltip disableInteractive title={globalize.translate('LabelRestore')}>
<IconButton onClick={restore}>
<Restore />
</IconButton>
</Tooltip>
}
>
<ListItemButton onClick={openDialog}>
<ListItemText
primary={backup.DateCreated}
secondary={backup.Path}
slotProps={{
primary: {
variant: 'h3'
},
secondary: {
variant: 'body1'
}
}}
/>
</ListItemButton>
</ListItem>
</>
);
};
export default Backup;

View File

@@ -1,146 +0,0 @@
import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import globalize from 'lib/globalize';
import React, { FunctionComponent, useCallback, useState } from 'react';
import Stack from '@mui/material/Stack';
import FormGroup from '@mui/material/FormGroup';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import ContentCopy from '@mui/icons-material/ContentCopy';
import IconButton from '@mui/material/IconButton';
import { copy } from 'scripts/clipboard';
import Toast from 'apps/dashboard/components/Toast';
type IProps = {
backup: BackupManifestDto;
open: boolean;
onClose: () => void;
};
const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }: IProps) => {
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
const handleToastClose = useCallback(() => {
setIsCopiedToastOpen(false);
}, []);
const copyPath = useCallback(async () => {
if (backup.Path) {
await copy(backup.Path);
setIsCopiedToastOpen(true);
}
}, [ backup.Path ]);
return (
<Dialog
onClose={onClose}
open={open}
maxWidth={'sm'}
fullWidth
>
<Toast
open={isCopiedToastOpen}
onClose={handleToastClose}
message={globalize.translate('Copied')}
/>
<DialogTitle>
{backup.DateCreated}
</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<Box>
<Stack
direction='row'
spacing={2}
>
<Typography fontWeight='bold'>{globalize.translate('LabelPath')}</Typography>
<Stack direction='row'>
<Typography color='text.secondary'>{backup.Path}</Typography>
<IconButton size='small' onClick={copyPath}>
<ContentCopy fontSize='small' />
</IconButton>
</Stack>
</Stack>
<Stack
direction='row'
spacing={2}
>
<Typography fontWeight='bold'>{globalize.translate('LabelVersion')}</Typography>
<Typography color='text.secondary'>{backup.ServerVersion}</Typography>
</Stack>
</Box>
<FormGroup>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Database'
defaultChecked={true}
disabled
/>
}
label={globalize.translate('LabelDatabase')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Metadata'
defaultChecked={backup.Options?.Metadata}
disabled
/>
}
label={globalize.translate('LabelMetadata')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Subtitles'
defaultChecked={backup.Options?.Subtitles}
disabled
/>
}
label={globalize.translate('Subtitles')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Trickplay'
defaultChecked={backup.Options?.Trickplay}
disabled
/>
}
label={globalize.translate('Trickplay')}
/>
</FormControl>
</FormGroup>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{globalize.translate('ButtonOk')}
</Button>
</DialogActions>
</Dialog>
);
};
export default BackupInfoDialog;

View File

@@ -1,27 +0,0 @@
import React, { FunctionComponent } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import LinearProgress from '@mui/material/LinearProgress';
import globalize from 'lib/globalize';
type IProps = {
open: boolean
};
const BackupProgressDialog: FunctionComponent<IProps> = ({ open }) => {
return (
<Dialog
open={open}
maxWidth={'xs'}
fullWidth
>
<DialogTitle>{globalize.translate('MessageBackupInProgress')}</DialogTitle>
<DialogContent>
<LinearProgress />
</DialogContent>
</Dialog>
);
};
export default BackupProgressDialog;

View File

@@ -1,123 +0,0 @@
import React, { FunctionComponent, useCallback } from 'react';
import globalize from 'lib/globalize';
import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import Stack from '@mui/material/Stack';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import FormGroup from '@mui/material/FormGroup';
import DialogContentText from '@mui/material/DialogContentText';
type IProps = {
open: boolean,
onClose?: () => void,
onCreate: (backupOptions: BackupOptionsDto) => void
};
const CreateBackupForm: FunctionComponent<IProps> = ({ open, onClose, onCreate }) => {
const onSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries());
const backupOptions: BackupOptionsDto = {
'Metadata': data.Metadata?.toString() === 'on',
'Trickplay': data.Trickplay?.toString() === 'on',
'Subtitles': data.Subtitles?.toString() === 'on'
};
onCreate(backupOptions);
}, [ onCreate ]);
return (
<Dialog
open={open}
maxWidth={'xs'}
fullWidth
onClose={onClose}
slotProps={{
paper: {
component: 'form',
onSubmit
}
}}
>
<DialogTitle>{globalize.translate('ButtonCreateBackup')}</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<DialogContentText>
{globalize.translate('MessageBackupDisclaimer')}
</DialogContentText>
<FormGroup>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Database'
defaultChecked={true}
disabled
/>
}
label={globalize.translate('LabelDatabase')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Metadata'
defaultChecked={false}
/>
}
label={globalize.translate('LabelMetadata')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Subtitles'
defaultChecked={false}
/>
}
label={globalize.translate('Subtitles')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='Trickplay'
defaultChecked={false}
/>
}
label={globalize.translate('Trickplay')}
/>
</FormControl>
</FormGroup>
</Stack>
</DialogContent>
<DialogActions>
<Button
onClick={onClose}
variant='text'
>{globalize.translate('ButtonCancel')}</Button>
<Button type='submit'>{globalize.translate('Create')}</Button>
</DialogActions>
</Dialog>
);
};
export default CreateBackupForm;

View File

@@ -1,46 +0,0 @@
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import globalize from 'lib/globalize';
import React, { FunctionComponent } from 'react';
type IProps = {
open: boolean;
onClose: () => void;
onConfirm: () => void;
};
const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, onConfirm }: IProps) => {
return (
<Dialog
open={open}
onClose={onClose}
maxWidth={'xs'}
fullWidth
>
<DialogTitle>
{globalize.translate('LabelRestore')}
</DialogTitle>
<DialogContent>
<DialogContentText>
{globalize.translate('MessageRestoreDisclaimer')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant='text'>
{globalize.translate('ButtonCancel')}
</Button>
<Button onClick={onConfirm}>
{globalize.translate('LabelRestore')}
</Button>
</DialogActions>
</Dialog>
);
};
export default RestoreConfirmationDialog;

View File

@@ -1,32 +0,0 @@
import React, { FunctionComponent } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import LinearProgress from '@mui/material/LinearProgress';
import DialogContentText from '@mui/material/DialogContentText';
import Stack from '@mui/material/Stack';
import globalize from 'lib/globalize';
type IProps = {
open: boolean
};
const RestoreProgressDialog: FunctionComponent<IProps> = ({ open }) => {
return (
<Dialog
open={open}
maxWidth={'xs'}
fullWidth
>
<DialogTitle>{globalize.translate('MessageRestoreInProgress')}</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<DialogContentText>{globalize.translate('MessageWaitingForServer')}</DialogContentText>
<LinearProgress />
</Stack>
</DialogContent>
</Dialog>
);
};
export default RestoreProgressDialog;

View File

@@ -1,30 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getBrandingApi } from '@jellyfin/sdk/lib/utils/api/branding-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'BrandingOptions';
const fetchBrandingOptions = async (
api: Api,
options?: AxiosRequestConfig
) => {
return getBrandingApi(api)
.getBrandingOptions(options)
.then(({ data }) => data);
};
export const getBrandingOptionsQuery = (
api?: Api
) => queryOptions({
queryKey: [ QUERY_KEY ],
queryFn: ({ signal }) => fetchBrandingOptions(api!, { signal }),
enabled: !!api
});
export const useBrandingOptions = () => {
const { api } = useApi();
return useQuery(getBrandingOptionsQuery(api));
};

View File

@@ -1,23 +0,0 @@
import type { DevicesApiDeleteDeviceRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useDevices';
export const useDeleteDevice = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: DevicesApiDeleteDeviceRequest) => (
getDevicesApi(api!)
.deleteDevice(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -1,33 +0,0 @@
import type { DevicesApiGetDevicesRequest } from '@jellyfin/sdk/lib/generated-client';
import type { AxiosRequestConfig } from 'axios';
import type { Api } from '@jellyfin/sdk';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'Devices';
const fetchDevices = async (
api: Api,
requestParams?: DevicesApiGetDevicesRequest,
options?: AxiosRequestConfig
) => {
const response = await getDevicesApi(api).getDevices(requestParams, {
signal: options?.signal
});
return response.data;
};
export const useDevices = (
requestParams: DevicesApiGetDevicesRequest
) => {
const { api } = useApi();
return useQuery({
queryKey: [QUERY_KEY, requestParams],
queryFn: ({ signal }) =>
fetchDevices(api!, requestParams, { signal }),
enabled: !!api
});
};

View File

@@ -1,23 +0,0 @@
import type { DevicesApiUpdateDeviceOptionsRequest } from '@jellyfin/sdk/lib/generated-client/api/devices-api';
import { getDevicesApi } from '@jellyfin/sdk/lib/utils/api/devices-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useDevices';
export const useUpdateDevice = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: DevicesApiUpdateDeviceOptionsRequest) => (
getDevicesApi(api!)
.updateDeviceOptions(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -1,259 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info';
import Typography from '@mui/material/Typography';
import Card from '@mui/material/Card';
import CardMedia from '@mui/material/CardMedia';
import { getDeviceIcon } from 'utils/image';
import Stack from '@mui/material/Stack';
import getNowPlayingName from '../../sessions/utils/getNowPlayingName';
import getSessionNowPlayingTime from '../../sessions/utils/getSessionNowPlayingTime';
import getNowPlayingImageUrl from '../../sessions/utils/getNowPlayingImageUrl';
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
import Comment from '@mui/icons-material/Comment';
import PlayArrow from '@mui/icons-material/PlayArrow';
import Pause from '@mui/icons-material/Pause';
import Stop from '@mui/icons-material/Stop';
import Info from '@mui/icons-material/Info';
import LinearProgress from '@mui/material/LinearProgress';
import CardActions from '@mui/material/CardActions';
import IconButton from '@mui/material/IconButton';
import SimpleAlert from 'components/SimpleAlert';
import playmethodhelper from 'components/playback/playmethodhelper';
import globalize from 'lib/globalize';
import getSessionNowPlayingStreamInfo from '../../sessions/utils/getSessionNowPlayingStreamInfo';
import { useSendPlayStateCommand } from '../../sessions/api/usePlayPauseSession';
import { PlaystateCommand } from '@jellyfin/sdk/lib/generated-client/models/playstate-command';
import InputDialog from 'components/InputDialog';
import { useSendMessage } from '../../sessions/api/useSendMessage';
type DeviceCardProps = {
device: SessionInfo;
};
const DeviceCard = ({ device }: DeviceCardProps) => {
const [ playbackInfoTitle, setPlaybackInfoTitle ] = useState('');
const [ playbackInfoDesc, setPlaybackInfoDesc ] = useState('');
const [ isPlaybackInfoOpen, setIsPlaybackInfoOpen ] = useState(false);
const [ isMessageDialogOpen, setIsMessageDialogOpen ] = useState(false);
const sendMessage = useSendMessage();
const playStateCommand = useSendPlayStateCommand();
const onPlayPauseSession = useCallback(() => {
if (device.Id) {
playStateCommand.mutate({
sessionId: device.Id,
command: PlaystateCommand.PlayPause
});
}
}, [ device, playStateCommand ]);
const onStopSession = useCallback(() => {
if (device.Id) {
playStateCommand.mutate({
sessionId: device.Id,
command: PlaystateCommand.Stop
});
}
}, [ device, playStateCommand ]);
const onMessageSend = useCallback((message: string) => {
if (device.Id) {
sendMessage.mutate({
sessionId: device.Id,
messageCommand: {
Text: message,
TimeoutMs: 5000
}
});
setIsMessageDialogOpen(false);
}
}, [ sendMessage, device ]);
const showMessageDialog = useCallback(() => {
setIsMessageDialogOpen(true);
}, []);
const onMessageDialogClose = useCallback(() => {
setIsMessageDialogOpen(false);
}, []);
const closePlaybackInfo = useCallback(() => {
setIsPlaybackInfoOpen(false);
}, []);
const showPlaybackInfo = useCallback(() => {
const displayPlayMethod = playmethodhelper.getDisplayPlayMethod(device);
switch (displayPlayMethod) {
case 'Remux':
setPlaybackInfoTitle(globalize.translate('Remuxing'));
setPlaybackInfoDesc(globalize.translate('RemuxHelp1') + '\n' + globalize.translate('RemuxHelp2'));
break;
case 'DirectStream':
setPlaybackInfoTitle(globalize.translate('DirectStreaming'));
setPlaybackInfoDesc(globalize.translate('DirectStreamHelp1') + '\n' + globalize.translate('DirectStreamHelp2'));
break;
case 'DirectPlay':
setPlaybackInfoTitle(globalize.translate('DirectPlaying'));
setPlaybackInfoDesc(globalize.translate('DirectPlayHelp'));
break;
case 'Transcode': {
const transcodeReasons = device.TranscodingInfo?.TranscodeReasons as string[] | undefined;
const localizedTranscodeReasons = transcodeReasons?.map(transcodeReason => globalize.translate(transcodeReason)) || [];
setPlaybackInfoTitle(globalize.translate('Transcoding'));
setPlaybackInfoDesc(
globalize.translate('MediaIsBeingConverted')
+ '\n\n' + getSessionNowPlayingStreamInfo(device)
+ '\n\n' + globalize.translate('LabelReasonForTranscoding')
+ '\n' + localizedTranscodeReasons.join('\n')
);
break;
}
}
setIsPlaybackInfoOpen(true);
}, [ device ]);
const nowPlayingName = useMemo(() => (
getNowPlayingName(device)
), [ device ]);
const nowPlayingImage = useMemo(() => (
device.NowPlayingItem && getNowPlayingImageUrl(device.NowPlayingItem)
), [device]);
const runningTime = useMemo(() => (
getSessionNowPlayingTime(device)
), [ device ]);
const deviceIcon = useMemo(() => (
getDeviceIcon(device)
), [ device ]);
const canControl = device.ServerId && device.SupportsRemoteControl;
const isPlayingMedia = !!device.NowPlayingItem;
return (
<Card sx={{ width: { xs: '100%', sm: '360px' } }}>
<InputDialog
open={isMessageDialogOpen}
onClose={onMessageDialogClose}
title={globalize.translate('HeaderSendMessage')}
label={globalize.translate('LabelMessageText')}
confirmButtonText={globalize.translate('ButtonSend')}
onConfirm={onMessageSend}
/>
<SimpleAlert
open={isPlaybackInfoOpen}
title={playbackInfoTitle}
text={playbackInfoDesc}
onClose={closePlaybackInfo}
/>
<CardMedia
sx={{
height: 200,
display: 'flex'
}}
className={getDefaultBackgroundClass(device.Id)}
image={nowPlayingImage || undefined}
>
<Stack
justifyContent={'space-between'}
flexGrow={1}
sx={{
backgroundColor: nowPlayingImage ? 'rgba(0, 0, 0, 0.7)' : null,
padding: 2
}}>
<Stack direction='row' alignItems='center' spacing={1}>
<img
src={deviceIcon}
style={{
maxWidth: '2.5em',
maxHeight: '2.5em'
}}
alt={device.DeviceName || ''}
/>
<Stack>
<Typography>{device.DeviceName}</Typography>
<Typography>{device.Client + ' ' + device.ApplicationVersion}</Typography>
</Stack>
</Stack>
<Stack direction='row' alignItems={'end'}>
<Stack flexGrow={1}>
{nowPlayingName.image ? (
<img
src={nowPlayingName.image}
style={{
maxHeight: '24px',
maxWidth: '130px',
alignSelf: 'flex-start'
}}
alt='Media Icon'
/>
) : (
<Typography>{nowPlayingName.topText}</Typography>
)}
<Typography>{nowPlayingName.bottomText}</Typography>
</Stack>
{device.NowPlayingItem && (
<Typography>{runningTime.start} / {runningTime.end}</Typography>
)}
</Stack>
</Stack>
</CardMedia>
{(device.PlayState?.PositionTicks != null && device.NowPlayingItem?.RunTimeTicks != null) && (
<LinearProgress
variant='buffer'
value={(device.PlayState.PositionTicks / device.NowPlayingItem.RunTimeTicks) * 100}
valueBuffer={device.TranscodingInfo?.CompletionPercentage || 0}
sx={{
'& .MuiLinearProgress-dashed': {
animation: 'none',
backgroundImage: 'none',
backgroundColor: 'background.paper'
},
'& .MuiLinearProgress-bar2': {
backgroundColor: '#dd4919'
}
}}
/>
)}
<CardActions disableSpacing>
<Stack direction='row' flexGrow={1} justifyContent='center'>
{canControl && isPlayingMedia && (
<>
<IconButton onClick={onPlayPauseSession}>
{device.PlayState?.IsPaused ? <PlayArrow /> : <Pause />}
</IconButton>
<IconButton onClick={onStopSession}>
<Stop />
</IconButton>
</>
)}
{isPlayingMedia && (
<IconButton onClick={showPlaybackInfo}>
<Info />
</IconButton>
)}
{canControl && (
<IconButton onClick={showMessageDialog}>
<Comment />
</IconButton>
)}
</Stack>
</CardActions>
{device.UserName && (
<Stack
direction='row'
flexGrow={1}
justifyContent='center'
sx={{ paddingBottom: 2 }}
>
<Typography>{device.UserName}</Typography>
</Stack>
)}
</Card>
);
};
export default DeviceCard;

View File

@@ -1,22 +0,0 @@
import React, { FC } from 'react';
import { DeviceInfoCell } from 'apps/dashboard/features/devices/types/deviceInfoCell';
import { getDeviceIcon } from 'utils/image';
const DeviceNameCell: FC<DeviceInfoCell> = ({ row, renderedCellValue }) => (
<>
<img
alt={row.original.AppName || undefined}
src={getDeviceIcon(row.original)}
style={{
display: 'inline-block',
maxWidth: '1.5em',
maxHeight: '1.5em',
marginRight: '1rem'
}}
/>
{renderedCellValue}
</>
);
export default DeviceNameCell;

View File

@@ -1,7 +0,0 @@
import type { DeviceInfoDto } from '@jellyfin/sdk/lib/generated-client/models/device-info-dto';
import type { MRT_Row } from 'material-react-table';
export interface DeviceInfoCell {
renderedCellValue: React.ReactNode
row: MRT_Row<DeviceInfoDto>
}

View File

@@ -1,23 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
import { useQuery } from '@tanstack/react-query';
import { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
export const QUERY_KEY = 'ApiKeys';
const fetchApiKeys = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getApiKeyApi(api).getKeys(options);
return response.data;
};
export const useApiKeys = () => {
const { api } = useApi();
return useQuery({
queryKey: [ QUERY_KEY ],
queryFn: ({ signal }) => fetchApiKeys(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,22 +0,0 @@
import { ApiKeyApiCreateKeyRequest } from '@jellyfin/sdk/lib/generated-client/api/api-key-api';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useApiKeys';
export const useCreateKey = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: ApiKeyApiCreateKeyRequest) => (
getApiKeyApi(api!)
.createKey(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -1,22 +0,0 @@
import type { ApiKeyApiRevokeKeyRequest } from '@jellyfin/sdk/lib/generated-client';
import { getApiKeyApi } from '@jellyfin/sdk/lib/utils/api/api-key-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useApiKeys';
export const useRevokeKey = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: ApiKeyApiRevokeKeyRequest) => (
getApiKeyApi(api!)
.revokeKey(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -1,21 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
const fetchCountries = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getLocalizationApi(api).getCountries(options);
return response.data;
};
export const useCountries = () => {
const { api } = useApi();
return useQuery({
queryKey: [ 'Countries' ],
queryFn: ({ signal }) => fetchCountries(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,21 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
const fetchCultures = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getLocalizationApi(api).getCultures(options);
return response.data;
};
export const useCultures = () => {
const { api } = useApi();
return useQuery({
queryKey: [ 'Cultures' ],
queryFn: ({ signal }) => fetchCultures(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,21 +0,0 @@
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
import { LibraryStructureApiRemoveVirtualFolderRequest } from '@jellyfin/sdk/lib/generated-client/api/library-structure-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
export const useRemoveVirtualFolder = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: LibraryStructureApiRemoveVirtualFolderRequest) => (
getLibraryStructureApi(api!)
.removeVirtualFolder(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ 'VirtualFolders' ]
});
}
});
};

View File

@@ -1,21 +0,0 @@
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
import { LibraryStructureApiRenameVirtualFolderRequest } from '@jellyfin/sdk/lib/generated-client/api/library-structure-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
export const useRenameVirtualFolder = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: LibraryStructureApiRenameVirtualFolderRequest) => (
getLibraryStructureApi(api!)
.renameVirtualFolder(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ 'VirtualFolders' ]
});
}
});
};

View File

@@ -1,21 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
import { getLibraryStructureApi } from '@jellyfin/sdk/lib/utils/api/library-structure-api';
const fetchVirtualFolders = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getLibraryStructureApi(api).getVirtualFolders(options);
return response.data;
};
export const useVirtualFolders = () => {
const { api } = useApi();
return useQuery({
queryKey: [ 'VirtualFolders' ],
queryFn: ({ signal }) => fetchVirtualFolders(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,237 +0,0 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import type { VirtualFolderInfo } from '@jellyfin/sdk/lib/generated-client/models/virtual-folder-info';
import BaseCard from 'apps/dashboard/components/BaseCard';
import getCollectionTypeOptions from '../utils/collectionTypeOptions';
import globalize from 'lib/globalize';
import Icon from '@mui/material/Icon';
import { getLibraryIcon } from 'utils/image';
import MediaLibraryEditor from 'components/mediaLibraryEditor/mediaLibraryEditor';
import { queryClient } from 'utils/query/queryClient';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import Folder from '@mui/icons-material/Folder';
import ImageIcon from '@mui/icons-material/Image';
import EditIcon from '@mui/icons-material/Edit';
import RefreshIcon from '@mui/icons-material/Refresh';
import DeleteIcon from '@mui/icons-material/Delete';
import ListItemText from '@mui/material/ListItemText';
import imageeditor from 'components/imageeditor/imageeditor';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import InputDialog from 'components/InputDialog';
import { useRenameVirtualFolder } from '../api/useRenameVirtualFolder';
import RefreshDialog from 'components/refreshdialog/refreshdialog';
import ConfirmDialog from 'components/ConfirmDialog';
import { useRemoveVirtualFolder } from '../api/useRemoveVirtualFolder';
import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
import { useApi } from 'hooks/useApi';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import dom from 'utils/dom';
type LibraryCardProps = {
virtualFolder: VirtualFolderInfo;
};
const LibraryCard = ({ virtualFolder }: LibraryCardProps) => {
const { api } = useApi();
const actionRef = useRef<HTMLButtonElement | null>(null);
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const [ isRenameLibraryDialogOpen, setIsRenameLibraryDialogOpen ] = useState(false);
const [ isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen ] = useState(false);
const renameVirtualFolder = useRenameVirtualFolder();
const removeVirtualFolder = useRemoveVirtualFolder();
const imageUrl = useMemo(() => {
if (virtualFolder.PrimaryImageItemId && virtualFolder.ItemId && api) {
return getImageApi(api)
.getItemImageUrlById(virtualFolder.ItemId, ImageType.Primary, {
maxWidth: Math.round(dom.getScreenWidth() * 0.40)
});
}
}, [ api, virtualFolder ]);
const typeName = getCollectionTypeOptions().filter(function (t) {
return t.value == virtualFolder.CollectionType;
})[0]?.name || globalize.translate('Other');
const openRenameDialog = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
setIsRenameLibraryDialogOpen(true);
}, []);
const hideRenameLibraryDialog = useCallback(() => {
setIsRenameLibraryDialogOpen(false);
}, []);
const onMenuClose = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
const onActionClick = useCallback(() => {
setAnchorEl(actionRef.current);
setIsMenuOpen(true);
}, []);
const renameLibrary = useCallback((newName: string) => {
if (virtualFolder.Name) {
renameVirtualFolder.mutate({
refreshLibrary: true,
newName: newName,
name: virtualFolder.Name
}, {
onSettled: () => {
hideRenameLibraryDialog();
}
});
}
}, [ renameVirtualFolder, virtualFolder, hideRenameLibraryDialog ]);
const showRefreshDialog = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
void new RefreshDialog({
itemIds: [ virtualFolder.ItemId ],
serverId: ServerConnections.currentApiClient()?.serverId(),
mode: 'scan'
}).show();
}, [ virtualFolder ]);
const showMediaLibraryEditor = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
const mediaLibraryEditor = new MediaLibraryEditor({
library: virtualFolder
}) as Promise<boolean>;
void mediaLibraryEditor.then((hasChanges: boolean) => {
if (hasChanges) {
void queryClient.invalidateQueries({
queryKey: ['VirtualFolders']
});
}
});
}, [ virtualFolder ]);
const showImageEditor = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
void imageeditor.show({
itemId: virtualFolder.ItemId,
serverId: ServerConnections.currentApiClient()?.serverId()
}).then(() => {
void queryClient.invalidateQueries({
queryKey: ['VirtualFolders']
});
}).catch(() => {
/* pop up closed */
});
}, [ virtualFolder ]);
const showDeleteLibraryDialog = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
setIsConfirmDeleteDialogOpen(true);
}, []);
const onCancelDeleteLibrary = useCallback(() => {
setIsConfirmDeleteDialogOpen(false);
}, []);
const onConfirmDeleteLibrary = useCallback(() => {
if (virtualFolder.Name) {
removeVirtualFolder.mutate({
name: virtualFolder.Name,
refreshLibrary: true
}, {
onSettled: () => {
setIsConfirmDeleteDialogOpen(false);
}
});
}
}, [ virtualFolder, removeVirtualFolder ]);
return (
<>
<InputDialog
title={globalize.translate('ButtonRename')}
open={isRenameLibraryDialogOpen}
onClose={hideRenameLibraryDialog}
label={globalize.translate('LabelNewName')}
helperText={globalize.translate('MessageRenameMediaFolder')}
initialText={virtualFolder.Name || ''}
confirmButtonText={globalize.translate('ButtonRename')}
onConfirm={renameLibrary}
/>
<ConfirmDialog
open={isConfirmDeleteDialogOpen}
title={globalize.translate('HeaderRemoveMediaFolder')}
text={
globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder') + '\n\n'
+ globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '\n\n'
+ virtualFolder.Locations?.join('\n')
}
confirmButtonText={globalize.translate('Delete')}
confirmButtonColor='error'
onConfirm={onConfirmDeleteLibrary}
onCancel={onCancelDeleteLibrary}
/>
<BaseCard
title={virtualFolder.Name || ''}
text={typeName}
image={imageUrl}
icon={<Icon sx={{ fontSize: 70 }}>{getLibraryIcon(virtualFolder.CollectionType)}</Icon>}
action={true}
actionRef={actionRef}
onActionClick={onActionClick}
onClick={showMediaLibraryEditor}
height={260}
/>
<Menu
anchorEl={anchorEl}
open={isMenuOpen}
onClose={onMenuClose}
>
<MenuItem onClick={showImageEditor}>
<ListItemIcon>
<ImageIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('EditImages')}</ListItemText>
</MenuItem>
<MenuItem onClick={showMediaLibraryEditor}>
<ListItemIcon>
<Folder />
</ListItemIcon>
<ListItemText>{globalize.translate('ManageLibrary')}</ListItemText>
</MenuItem>
<MenuItem onClick={openRenameDialog}>
<ListItemIcon>
<EditIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('ButtonRename')}</ListItemText>
</MenuItem>
<MenuItem onClick={showRefreshDialog}>
<ListItemIcon>
<RefreshIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('ScanLibrary')}</ListItemText>
</MenuItem>
<MenuItem onClick={showDeleteLibraryDialog}>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('ButtonRemove')}</ListItemText>
</MenuItem>
</Menu>
</>
);
};
export default LibraryCard;

View File

@@ -1,31 +0,0 @@
import globalize from 'lib/globalize';
const getCollectionTypeOptions = () => {
return [{
name: '',
value: ''
}, {
name: globalize.translate('Movies'),
value: 'movies'
}, {
name: globalize.translate('TabMusic'),
value: 'music'
}, {
name: globalize.translate('Shows'),
value: 'tvshows'
}, {
name: globalize.translate('Books'),
value: 'books'
}, {
name: globalize.translate('HomeVideosPhotos'),
value: 'homevideos'
}, {
name: globalize.translate('MusicVideos'),
value: 'musicvideos'
}, {
name: globalize.translate('MixedMoviesShows'),
value: 'mixed'
}];
};
export default getCollectionTypeOptions;

View File

@@ -1,19 +0,0 @@
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
import globalize from 'lib/globalize';
export function getImageResolutionOptions() {
return [
{
name: globalize.translate('ResolutionMatchSource'),
value: ImageResolution.MatchSource
},
{ name: '2160p', value: ImageResolution.P2160 },
{ name: '1440p', value: ImageResolution.P1440 },
{ name: '1080p', value: ImageResolution.P1080 },
{ name: '720p', value: ImageResolution.P720 },
{ name: '480p', value: ImageResolution.P480 },
{ name: '360p', value: ImageResolution.P360 },
{ name: '240p', value: ImageResolution.P240 },
{ name: '144p', value: ImageResolution.P144 }
];
};

View File

@@ -1,22 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { LiveTvApiDeleteListingProviderRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api';
export const useDeleteProvider = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: LiveTvApiDeleteListingProviderRequest) => (
getLiveTvApi(api!)
.deleteListingProvider(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ 'NamedConfiguration', 'livetv' ]
});
}
});
};

View File

@@ -1,22 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { LiveTvApiDeleteTunerHostRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api';
export const useDeleteTuner = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: LiveTvApiDeleteTunerHostRequest) => (
getLiveTvApi(api!)
.deleteTunerHost(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ 'NamedConfiguration', 'livetv' ]
});
}
});
};

View File

@@ -1,138 +0,0 @@
import React, { useCallback, useRef, useState } from 'react';
import type { ListingsProviderInfo } from '@jellyfin/sdk/lib/generated-client/models/listings-provider-info';
import Avatar from '@mui/material/Avatar';
import ListItem from '@mui/material/ListItem';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemLink from 'components/ListItemLink';
import DvrIcon from '@mui/icons-material/Dvr';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import getProviderConfigurationUrl from '../utils/getProviderConfigurationUrl';
import ListItemText from '@mui/material/ListItemText';
import getProviderName from '../utils/getProviderName';
import IconButton from '@mui/material/IconButton';
import ConfirmDialog from 'components/ConfirmDialog';
import globalize from 'lib/globalize';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
import DeleteIcon from '@mui/icons-material/Delete';
import ChannelMapper from 'components/channelMapper/channelMapper';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import { useDeleteProvider } from '../api/useDeleteProvider';
interface ProviderProps {
provider: ListingsProviderInfo
}
const Provider = ({ provider }: ProviderProps) => {
const [ isDeleteProviderDialogOpen, setIsDeleteProviderDialogOpen ] = useState(false);
const actionsRef = useRef<HTMLButtonElement | null>(null);
const [ anchorEl, setAnchorEl ] = useState<HTMLButtonElement | null>(null);
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const deleteProvider = useDeleteProvider();
const showChannelMapper = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
void new ChannelMapper({
serverId: ServerConnections.currentApiClient()?.serverId(),
providerId: provider.Id
}).show();
}, [ provider ]);
const showContextMenu = useCallback(() => {
setAnchorEl(actionsRef.current);
setIsMenuOpen(true);
}, []);
const showDeleteDialog = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
setIsDeleteProviderDialogOpen(true);
}, []);
const onDeleteProviderDialogCancel = useCallback(() => {
setIsDeleteProviderDialogOpen(false);
}, []);
const onMenuClose = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
const onConfirmDelete = useCallback(() => {
if (provider.Id) {
deleteProvider.mutate({
id: provider.Id
}, {
onSettled: () => {
setIsDeleteProviderDialogOpen(false);
}
});
}
}, [ deleteProvider, provider ]);
return (
<>
<ConfirmDialog
open={isDeleteProviderDialogOpen}
title={globalize.translate('HeaderDeleteProvider')}
text={globalize.translate('MessageConfirmDeleteGuideProvider')}
onCancel={onDeleteProviderDialogCancel}
onConfirm={onConfirmDelete}
confirmButtonText={globalize.translate('Delete')}
confirmButtonColor='error'
/>
<ListItem
disablePadding key={provider.Id}
secondaryAction={
<IconButton ref={actionsRef} onClick={showContextMenu}>
<MoreVertIcon />
</IconButton>
}
>
<ListItemLink to={getProviderConfigurationUrl(provider.Type || '') + '&id=' + provider.Id}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: 'primary.main' }}>
<DvrIcon sx={{ color: '#fff' }} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={getProviderName(provider.Type)}
secondary={provider.Path || provider.ListingsId}
slotProps={{
primary: {
variant: 'h3'
},
secondary: {
variant: 'body1'
}
}}
/>
</ListItemLink>
</ListItem>
<Menu
anchorEl={anchorEl}
open={isMenuOpen}
onClose={onMenuClose}
>
<MenuItem onClick={showChannelMapper}>
<ListItemIcon>
<LocationSearchingIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('MapChannels')}</ListItemText>
</MenuItem>
<MenuItem onClick={showDeleteDialog}>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('Delete')}</ListItemText>
</MenuItem>
</Menu>
</>
);
};
export default Provider;

View File

@@ -1,109 +0,0 @@
import React, { useCallback, useRef, useState } from 'react';
import type { TunerHostInfo } from '@jellyfin/sdk/lib/generated-client/models/tuner-host-info';
import BaseCard from 'apps/dashboard/components/BaseCard';
import DvrIcon from '@mui/icons-material/Dvr';
import getTunerName from '../utils/getTunerName';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import ListItemText from '@mui/material/ListItemText';
import globalize from 'lib/globalize';
import { useNavigate } from 'react-router-dom';
import ConfirmDialog from 'components/ConfirmDialog';
import { useDeleteTuner } from '../api/useDeleteTuner';
interface TunerDeviceCardProps {
tunerHost: TunerHostInfo;
}
const TunerDeviceCard = ({ tunerHost }: TunerDeviceCardProps) => {
const navigate = useNavigate();
const actionRef = useRef<HTMLButtonElement | null>(null);
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const [ isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen ] = useState(false);
const deleteTuner = useDeleteTuner();
const navigateToEditPage = useCallback(() => {
navigate(`/dashboard/livetv/tuner?id=${tunerHost.Id}`);
}, [ navigate, tunerHost ]);
const onDelete = useCallback(() => {
if (tunerHost.Id) {
deleteTuner.mutate({
id: tunerHost.Id
}, {
onSettled: () => {
setIsConfirmDeleteDialogOpen(false);
}
});
}
}, [ deleteTuner, tunerHost ]);
const showDeleteDialog = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
setIsConfirmDeleteDialogOpen(true);
}, []);
const onDeleteDialogClose = useCallback(() => {
setIsConfirmDeleteDialogOpen(false);
}, []);
const onActionClick = useCallback(() => {
setAnchorEl(actionRef.current);
setIsMenuOpen(true);
}, []);
const onMenuClose = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
return (
<>
<ConfirmDialog
open={isConfirmDeleteDialogOpen}
title={globalize.translate('HeaderDeleteDevice')}
text={globalize.translate('MessageConfirmDeleteTunerDevice')}
onCancel={onDeleteDialogClose}
onConfirm={onDelete}
confirmButtonColor='error'
confirmButtonText={globalize.translate('Delete')}
/>
<BaseCard
title={tunerHost.FriendlyName || getTunerName(tunerHost.Type) || ''}
text={tunerHost.Url || ''}
icon={<DvrIcon sx={{ fontSize: 70 }} />}
action={true}
actionRef={actionRef}
onActionClick={onActionClick}
onClick={navigateToEditPage}
/>
<Menu
anchorEl={anchorEl}
open={isMenuOpen}
onClose={onMenuClose}
>
<MenuItem onClick={navigateToEditPage}>
<ListItemIcon>
<EditIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('Edit')}</ListItemText>
</MenuItem>
<MenuItem onClick={showDeleteDialog}>
<ListItemIcon>
<DeleteIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('Delete')}</ListItemText>
</MenuItem>
</Menu>
</>
);
};
export default TunerDeviceCard;

View File

@@ -1,10 +0,0 @@
const getProviderConfigurationUrl = (providerId: string) => {
switch (providerId?.toLowerCase()) {
case 'xmltv':
return '/dashboard/livetv/guide?type=xmltv';
case 'schedulesdirect':
return '/dashboard/livetv/guide?type=schedulesdirect';
}
};
export default getProviderConfigurationUrl;

View File

@@ -1,12 +0,0 @@
const getProviderName = (providerId: string | null | undefined) => {
switch (providerId?.toLowerCase()) {
case 'schedulesdirect':
return 'Schedules Direct';
case 'xmltv':
return 'XMLTV';
default:
return 'Unknown';
}
};
export default getProviderName;

View File

@@ -1,16 +0,0 @@
const getTunerName = (providerId: string | null | undefined) => {
switch (providerId?.toLowerCase()) {
case 'm3u':
return 'M3U';
case 'hdhomerun':
return 'HDHomeRun';
case 'hauppauge':
return 'Hauppauge';
case 'satip':
return 'DVB';
default:
return 'Unknown';
}
};
export default getTunerName;

View File

@@ -1,31 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
const fetchServerLog = async (
api: Api,
name: string,
options?: AxiosRequestConfig
) => {
const response = await getSystemApi(api).getLogFile({ name }, options);
// FIXME: TypeScript SDK thinks it is returning a File but in reality it is a string
const data = response.data as never as string | object;
if (typeof data === 'object') {
return JSON.stringify(data, null, 2);
} else {
return data;
}
};
export const useServerLog = (name: string) => {
const { api } = useApi();
return useQuery({
queryKey: ['ServerLog', name],
queryFn: ({ signal }) => fetchServerLog(api!, name, { signal }),
enabled: !!api
});
};

View File

@@ -1,21 +0,0 @@
import { Api } from '@jellyfin/sdk';
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
const fetchServerLogs = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getSystemApi(api!).getServerLogs(options);
return response.data;
};
export const useServerLogs = () => {
const { api } = useApi();
return useQuery({
queryKey: [ 'ServerLogs' ],
queryFn: ({ signal }) => fetchServerLogs(api!, { signal }),
enabled: !!api
});
};

View File

@@ -1,45 +0,0 @@
import React, { FunctionComponent } from 'react';
import type { LogFile } from '@jellyfin/sdk/lib/generated-client/models/log-file';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import datetime from 'scripts/datetime';
import ListItemLink from 'components/ListItemLink';
type LogItemProps = {
logs: LogFile[];
};
const LogItemList: FunctionComponent<LogItemProps> = ({ logs }: LogItemProps) => {
const getDate = (logFile: LogFile) => {
const date = datetime.parseISO8601Date(logFile.DateModified, true);
return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
};
return (
<List sx={{ bgcolor: 'background.paper' }}>
{logs.map(log => {
return (
<ListItem key={log.Name} disablePadding>
<ListItemLink to={`/dashboard/logs/${log.Name}`}>
<ListItemText
primary={log.Name}
secondary={getDate(log)}
slotProps={{
primary: {
variant: 'h3'
},
secondary: {
variant: 'body1'
}
}}
/>
</ListItemLink>
</ListItem>
);
})}
</List>
);
};
export default LogItemList;

View File

@@ -1,34 +0,0 @@
import type { Api } from '@jellyfin/sdk';
import type { LibraryApiGetItemCountsRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';
import { useApi } from 'hooks/useApi';
const fetchItemCounts = async (
api: Api,
params?: LibraryApiGetItemCountsRequest,
options?: AxiosRequestConfig
) => {
const response = await getLibraryApi(api)
.getItemCounts(params, options);
return response.data;
};
const getItemCountsQuery = (
api?: Api,
params?: LibraryApiGetItemCountsRequest
) => queryOptions({
queryKey: [ 'ItemCounts', params ],
queryFn: ({ signal }) => fetchItemCounts(api!, params, { signal }),
enabled: !!api,
refetchOnWindowFocus: false
});
export const useItemCounts = (
params?: LibraryApiGetItemCountsRequest
) => {
const { api } = useApi();
return useQuery(getItemCountsQuery(api, params));
};

View File

@@ -1,71 +0,0 @@
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import SvgIcon from '@mui/material/SvgIcon';
import Typography from '@mui/material/Typography';
import React, { type FC } from 'react';
import { useLocale } from 'hooks/useLocale';
import { toDecimalString } from 'utils/number';
interface Metric {
label: string
value?: number
}
export interface MetricCardProps {
metrics: Metric[]
Icon: typeof SvgIcon
}
const MetricCard: FC<MetricCardProps> = ({
metrics,
Icon
}) => {
const { dateTimeLocale } = useLocale();
return (
<Card
sx={{
display: 'flex',
alignItems: 'center',
height: '100%'
}}
>
<Stack
direction='row'
sx={{
width: '100%',
padding: 2,
justifyContent: 'space-between',
alignItems: 'center'
}}
>
{metrics.map(({ label, value }) => (
<Box key={label}>
<Typography
variant='body2'
color='text.secondary'
>
{label}
</Typography>
<Typography
variant='h5'
component='div'
>
{typeof value !== 'undefined' ? (
toDecimalString(value, dateTimeLocale)
) : (
<Skeleton />
)}
</Typography>
</Box>
))}
<Icon fontSize='large' />
</Stack>
</Card>
);
};
export default MetricCard;

View File

@@ -1,113 +0,0 @@
/** List of codecs and their supported hardware acceleration types */
export const CODECS = [
{
name: 'H264',
codec: 'h264',
types: [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp',
'videotoolbox',
'v4l2m2m'
]
},
{
name: 'HEVC',
codec: 'hevc',
types: [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp',
'videotoolbox'
]
},
{
name: 'MPEG1',
codec: 'mpeg1video',
types: [ 'rkmpp' ]
},
{
name: 'MPEG2',
codec: 'mpeg2video',
types: [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp'
]
},
{
name: 'MPEG4',
codec: 'mpeg4',
types: [
'nvenc',
'rkmpp'
]
},
{
name: 'VC1',
codec: 'vc1',
types: [
'amf',
'nvenc',
'qsv',
'vaapi'
]
},
{
name: 'VP8',
codec: 'vp8',
types: [
'nvenc',
'qsv',
'vaapi',
'rkmpp',
'videotoolbox'
]
},
{
name: 'VP9',
codec: 'vp9',
types: [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp',
'videotoolbox'
]
},
{
name: 'AV1',
codec: 'av1',
types: [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp',
'videotoolbox'
]
}
];
/** Hardware decoders which support 10-bit HEVC & VP9 */
export const HEVC_VP9_HW_DECODING_TYPES = [
'amf',
'nvenc',
'qsv',
'vaapi',
'rkmpp'
];
/** Hardware decoders which support HEVC RExt */
export const HEVC_REXT_DECODING_TYPES = [
'nvenc',
'qsv',
'vaapi'
];

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