Compare commits

...

767 Commits

Author SHA1 Message Date
oyvhov
a8ac0578c3 Translated using Weblate (Norwegian Nynorsk)
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
Scheduled tasks 🕑 / Check stale issues and PRs (push) Has been cancelled
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nn/
2026-01-06 00:58:16 +00:00
mihlau
d181111e31 Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ro/
2026-01-05 18:05:52 +00:00
Dzmitry Zubialevich
9073ad587d Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2026-01-05 12:05:53 +00:00
Dzmitry Zubialevich
2bf602f8b3 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2026-01-05 10:05:53 +00:00
Translation expert
b9a21c45b5 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2026-01-05 07:05:53 +00:00
Fjuro
ae62467478 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2026-01-04 20:50:17 +00:00
ljaksys
5cfbfd946f Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2026-01-04 16:05:52 +00:00
aky
92b96971cb Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2026-01-02 14:06:27 +00:00
aky
1995eccb4b Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2026-01-02 13:50:20 +00:00
Dzmitry Zubialevich
e4e4f550c8 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2026-01-02 13:33:45 +00:00
MrPlow
37e5b9abea Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2026-01-02 12:19:01 +00:00
AfmanS
38b199ad3f Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2026-01-02 11:39:29 +00:00
Dzmitry Zubialevich
04f02010d5 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2026-01-02 11:39:28 +00:00
Dzmitry Zubialevich
3003d964df Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2026-01-02 09:05:52 +00:00
stunzzz
e11d8cba4e Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2026-01-02 06:05:52 +00:00
rimasx
64bf4e70ee Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2026-01-01 20:33:18 +00:00
Warper
201c98829b Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2026-01-01 01:05:52 +00:00
Warper
73c1274011 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2025-12-31 04:05:52 +00:00
Warper
6a70ed2ba7 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2025-12-31 02:05:52 +00:00
MrPlow
0ecdde9f26 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-12-29 17:05:52 +00:00
SamuWhale
2b18fc3636 Translated using Weblate (Thai)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/th/
2025-12-29 15:05:52 +00:00
stunzzz
e7f6930ca3 Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2025-12-29 15:05:52 +00:00
ilker
42d1a0563f Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-12-29 11:04:35 +00:00
Joker
e0b27bd1a6 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-12-29 11:04:35 +00:00
Bambus3000
40ad1fc595 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-12-28 23:05:52 +00:00
stunzzz
f4544a676f Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2025-12-28 18:47:13 +00:00
Spillutvikler
e9d0832cb2 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-12-28 14:05:52 +00:00
Jonne Saloranta
b80b971231 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-12-27 18:05:52 +00:00
Chris Stormrider
977bfbfe73 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-12-27 18:05:52 +00:00
Tales Costa
97bfdddacc Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-12-27 13:05:52 +00:00
Tristan
32cee8ac28 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-12-27 02:05:53 +00:00
Tristan
2b2672be70 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-12-27 02:05:52 +00:00
Eva Soler
ed4417b7de Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-12-25 21:05:52 +00:00
Eva Soler
6be1cb58a5 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2025-12-25 21:05:52 +00:00
Translation expert
a326654542 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2025-12-25 19:05:52 +00:00
Gallyam Biktashev
948d792677 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-12-22 19:05:52 +00:00
Warper
b7f1a46841 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2025-12-20 02:55:08 +00:00
Warper
ff42033d77 Translated using Weblate (Spanish (Latin America))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/
2025-12-20 00:55:08 +00:00
Bill Thornton
a1bc62d158 Merge pull request #7321 from kinke/tizen_anamorphic
Tizen: Enable direct-play of anamorphic videos
2025-12-19 11:51:53 -05:00
Stefano Mazzoleni
742918f39f Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-12-19 10:55:08 +00:00
Martin Kinkelin
08f8b2d2f7 Tizen: Enable direct-play of anamorphic videos
Anamorphic videos currently require a remux, which apparently breaks
the aspect ratio: https://github.com/jellyfin/jellyfin-tizen/issues/376

On my Tizen v9 TV, the web player handles the aspect ratio just fine
directly, correctly stretching the 720x472 pixels of some DVD source
to a 873x472 view. Tested with HEVC and H264 media.

Also got successful test reports on Tizen 6.0 and 6.5 TVs.
2025-12-19 11:38:48 +01:00
hoanghuy309
4781f5e99f Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-12-19 04:45:31 +00:00
Fabrizio Mansilla
d4599dc02c Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-12-19 04:45:31 +00:00
Tyler Mills
522b7455f4 Add support for scanning tartgeted paths with lint npm script (#7436) 2025-12-18 16:42:26 -05:00
SmartLink Solutions Pvt. Ltd.
9766d77fd4 Translated using Weblate (Gujarati)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gu/
2025-12-18 07:55:09 +00:00
Kityn
f39c9a3f57 Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-12-18 07:55:09 +00:00
Michael
1376d435b8 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-12-17 22:15:04 +00:00
sabretou
66013fe785 Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mr/
2025-12-17 01:55:12 +00:00
João Moura
890d0a6d41 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-12-16 16:55:08 +00:00
Jahidul Islam
63d9b03106 Translated using Weblate (Bengali (Bangladesh))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bn_BD/
2025-12-16 11:55:08 +00:00
Chris
dd20b323eb Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-12-16 09:55:08 +00:00
HaloCelsius
20dcfb4398 Backport pull request #7135 from jellyfin-web/release-10.11.z
Fix incorrect quality text label in video player

Original-merge: 9a207e9ba9

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-15 17:50:21 -05:00
Anton Antonov
5bf657c57a Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-12-15 20:55:08 +00:00
sabretou
5cf6b3e902 Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mr/
2025-12-15 16:55:09 +00:00
rimasx
10625a0360 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-12-15 11:55:09 +00:00
sabretou
b2703d0c59 Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mr/
2025-12-15 11:55:08 +00:00
Khalsior
d3a2115db5 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-12-15 11:55:08 +00:00
sabretou
01f1e97f3b Translated using Weblate (Marathi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mr/
2025-12-15 09:53:54 +00:00
alxhu
3909785249 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-12-15 09:53:53 +00:00
Translation expert
3ca6229023 Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2025-12-14 22:25:10 +00:00
yoga sree jagadam
c7ec17bf09 Translated using Weblate (Telugu)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/te/
2025-12-14 14:55:09 +00:00
Dino Horvat
f583c72e13 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-12-13 19:55:08 +00:00
Fabrizio Mansilla
d0e37f6373 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-12-13 06:55:09 +00:00
queeup
4b8f6d14a0 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-12-12 23:55:08 +00:00
dmitrylyzo
7ccc98ec7a Backport pull request #7415 from jellyfin-web/release-10.11.z
Fix the return value for 'behavior' property

Original-merge: e2ae48d8e5

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-12 13:32:38 -05:00
shempignon
e41ea5a293 Backport pull request #7409 from jellyfin-web/release-10.11.z
Fix subtitles need to be uploaded twice

Original-merge: bc39ee10ba

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-12 13:32:37 -05:00
Björn Tenje Persson
193d610d0d Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-12-12 13:55:07 +00:00
Björn Tenje Persson
1985c7deb7 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-12-12 12:04:12 +00:00
Arnau Galofré
3b1509afc0 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-12-11 17:20:30 +00:00
Arnau Galofré
0e97d3a7f8 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-12-11 14:55:07 +00:00
Arnau Galofré
42e6180700 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-12-11 12:55:07 +00:00
Bill Thornton
bf72cd699a Merge pull request #7420 from thornbill/ded-refresher 2025-12-11 01:32:23 -05:00
Bill Thornton
6ba590b59d Remove dead itemsrefresher class 2025-12-10 17:36:14 -05:00
thornbill
dc7226eeea Backport pull request #7411 from jellyfin-web/release-10.11.z
Fix browser detection overwrites

Original-merge: 6bfff061ce

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-10 09:56:14 -05:00
Bill Thornton
46ddff668b Merge pull request #7412 from thornbill/download-sdk 2025-12-10 01:51:56 -05:00
Veldermon-rbg
ca3cf922f9 Translated using Weblate (Maori)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mi/
2025-12-10 01:55:08 +00:00
dodog
122e6aadeb Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-12-10 01:55:07 +00:00
Veldermon-rbg
2abce18d1a Added translation using Weblate (Maori) 2025-12-10 00:29:33 +00:00
Milo Ivir
0769eec314 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-12-09 23:55:07 +00:00
Bill Thornton
3e93be0292 Use sdk for item download url 2025-12-09 18:22:13 -05:00
TidosDK
364841c24c Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-12-09 21:55:07 +00:00
Bill Thornton
15644cb097 Merge pull request #7406 from jellyfin/renovate/jellyfin-sdk-0.x
Update dependency @jellyfin/sdk to v0.0.0-unstable.202512091852
2025-12-09 14:27:14 -05:00
renovate[bot]
19ba529a70 Update dependency @jellyfin/sdk to v0.0.0-unstable.202512091852 2025-12-09 19:17:39 +00:00
psaintmalo
6d8dce739b Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-12-09 15:55:08 +00:00
Stefán Örvar Sigmundsson
da336b75be Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-12-09 15:55:07 +00:00
millallo
c9e04a33f5 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-12-09 12:55:09 +00:00
Andi Chandler
e2c38ae3f1 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-12-09 12:55:09 +00:00
MrPlow
32de578f50 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-12-09 12:55:08 +00:00
Gargotaire
768dba60bd Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-12-09 07:33:17 +00:00
Thadah D. Denyse
0a6309de44 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-12-08 22:55:08 +00:00
無情天
225ca1fd41 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-12-08 22:55:08 +00:00
Bill Thornton
46223f4af3 Merge pull request #7382 from thornbill/theme-structure
Refactor themes structure
2025-12-08 11:51:55 -05:00
Bill Thornton
d3793f02eb Refactor themes structure 2025-12-08 11:40:03 -05:00
nenadsuperzmaj
7bbfa02c4a Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-12-08 14:55:07 +00:00
Thunderstrike116
3b5b1345fa Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-12-08 10:55:07 +00:00
st7105
eafc65c508 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-12-08 08:55:08 +00:00
stelle
23b4ed4fee Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ms/
2025-12-08 08:55:07 +00:00
Fjuro
6cb53c7c13 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-12-07 23:55:07 +00:00
Bas
21f708d3f4 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-12-07 21:52:43 +00:00
Kityn
56275a3a7b Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-12-07 14:55:07 +00:00
Lukáš Kucharczyk
2a10d11253 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-12-07 08:09:43 +00:00
Bill Thornton
5df49ca297 Merge pull request #7279 from thornbill/item-details-padding 2025-12-06 18:16:53 -05:00
Bill Thornton
b527141177 Merge pull request #7342 from thornbill/directory-browser-cleanup 2025-12-06 18:16:31 -05:00
Bill Thornton
269590adb5 Merge pull request #7389 from thornbill/refactor-item-actions 2025-12-06 18:15:14 -05:00
Bill Thornton
2f16a16375 Merge pull request #7393 from thornbill/experimental-default 2025-12-06 18:14:40 -05:00
Bill Thornton
762f95cb72 Merge pull request #7394 from thornbill/experimental-links 2025-12-06 18:13:58 -05:00
Bill Thornton
ecb7a35425 Merge pull request #7395 from thornbill/reference-framerate 2025-12-06 18:13:03 -05:00
Dan Tsivinsky
f7cebe7381 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-12-06 19:25:12 +00:00
Bill Thornton
024ea6b1f7 Remove usages of deprecated framerate properties 2025-12-05 17:13:22 -05:00
Bill Thornton
ae1f025557 Add support for custom links in the experimental layout 2025-12-05 16:32:53 -05:00
Gargotaire
068f2e691b Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-12-05 20:55:07 +00:00
Bill Thornton
86db4bd0e1 Update layout settings 2025-12-05 11:57:46 -05:00
Bill Thornton
4008ec04b9 Use experimental layout by default 2025-12-05 11:19:03 -05:00
Kityn
c32a3c8386 Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-12-05 10:55:07 +00:00
Lukáš Kucharczyk
a8090af035 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-12-05 10:55:07 +00:00
Bill Thornton
7e4eb2f43f Merge pull request #6376 from imtsuki/add-hevc-186-check 2025-12-04 22:29:46 -05:00
Bill Thornton
3d721d9658 Merge pull request #5488 from kevgrig/issue5486
Pass the new player to nextTrack to allow mixed playlist playback
2025-12-04 18:04:01 -05:00
Bas
6c38e30e31 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-12-04 22:55:07 +00:00
MaîtreGEEK
3a59ab4c32 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-12-04 22:55:07 +00:00
Bill Thornton
c24b7376f8 Merge pull request #7038 from nesnass/rn/slideshow-configurable-delay 2025-12-04 15:55:51 -05:00
Bill Thornton
a12dc57099 Update slideshow interval labels 2025-12-04 14:53:11 -05:00
Richard Nesnass
bef60736b0 Add translation keys to en-us.json 2025-12-04 13:43:08 -05:00
Richard Nesnass
a2fef21af2 New display setting to configure slideshow delay 2025-12-04 13:43:08 -05:00
Furqaan Dawood
c2df080ad8 Added translation using Weblate (Swahili) 2025-12-04 14:41:48 +00:00
Bill Thornton
56d23e13eb Fix vite path support 2025-12-03 17:53:19 -05:00
Bill Thornton
cb8b2836c2 Use enum for item actions 2025-12-03 17:31:12 -05:00
thornbill
7e663d57f1 Backport pull request #7387 from jellyfin-web/release-10.11.z
Fix card actions in experimental layout

Original-merge: 9f1370f242

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-03 16:21:39 -05:00
Bill Thornton
cd8f3c4831 Merge pull request #7386 from thornbill/base-theme-icon-color
Fix base theme icon color
2025-12-03 10:37:18 -05:00
Bill Thornton
7dc51349de Fix base theme icon color 2025-12-03 10:31:25 -05:00
theguymadmax
2ec16d73dc Backport pull request #7380 from jellyfin-web/release-10.11.z
Add help link to backups page

Original-merge: 69d169e45f

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-02 16:02:45 -05:00
Collin-Swish
6c372f61f1 Backport pull request #7379 from jellyfin-web/release-10.11.z
Fix duplicate years due to type mismatch

Original-merge: 6fba30a0a9

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-02 16:02:44 -05:00
thornbill
d9e4818c70 Backport pull request #7378 from jellyfin-web/release-10.11.z
Fix card and logo position for rtl languages

Original-merge: 06f5442fc9

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-02 16:02:42 -05:00
sildur
6c12efe202 Backport pull request #7376 from jellyfin-web/release-10.11.z
Bind PlayerChange before handlers to report correct isLocalPlayer

Original-merge: 4e9c2e71a9

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-02 16:02:41 -05:00
dkanada
3894236c46 Backport pull request #7344 from jellyfin-web/release-10.11.z
modify TOC and button layout in book player

Original-merge: 4bdc0fd974

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-12-02 16:02:40 -05:00
KSAm3lm
f11ca9584a Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2025-12-02 15:55:07 +00:00
Hasan Abdulaal
e1c761031c Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2025-12-02 11:55:08 +00:00
Prasanth Baskar
46616a1d25 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ta/
2025-12-02 02:07:34 +00:00
Bill Thornton
e7b7938841 Merge pull request #7347 from thornbill/base-theme-light 2025-12-01 15:54:57 -05:00
dodog
498efbe493 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-12-01 10:55:07 +00:00
rimasx
1b54ae6cac Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-11-29 20:32:05 +00:00
Jacky He
0efb74e0b7 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-11-29 19:55:07 +00:00
Martín
468f3085d2 Added translation using Weblate (Occitan) 2025-11-28 20:01:22 +00:00
kallum
b575915f24 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-27 18:28:40 +00:00
John Doe
5bbcf2dd96 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-11-27 12:55:07 +00:00
John Doe
1f7ee737c3 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-11-27 11:19:19 +00:00
rimasx
96467b7c51 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-11-27 08:23:48 +00:00
kallum
e1d9abc3f9 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-27 04:55:08 +00:00
Bill Thornton
c272eba4f5 Fix base theme styles 2025-11-25 11:42:00 -05:00
dkanada
41bde8ecd8 Backport pull request #7345 from jellyfin-web/release-10.11.z
fix book playback in continue reading home section

Original-merge: 1686788be5

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-25 11:00:07 -05:00
Bill Thornton
a9ea556b15 Merge pull request #7089 from viown/ts-sdk-user
Migrate dashboard user pages to use TS SDK
2025-11-25 10:57:41 -05:00
binglebongledingledanglee
20fbb8f24e Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-11-24 20:55:07 +00:00
Hestadgard
d9adbd6bc8 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-11-23 22:55:06 +00:00
Malik
c23b722426 Added translation using Weblate (Abkhazian) 2025-11-23 06:49:32 +00:00
Centauri
fa122fc15a Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-11-22 14:55:06 +00:00
Bill Thornton
d9d44c49c1 Refactor light theme to use base theme 2025-11-21 17:57:27 -05:00
Oleksandr Yurov
4e4d211117 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/
2025-11-21 22:55:06 +00:00
Oatavandi
11860af730 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ta/
2025-11-21 17:55:06 +00:00
drip-droop
761bd31519 Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hi/
2025-11-20 13:55:08 +00:00
Bill Thornton
652682bbe1 Merge pull request #6151 from viown/fix-empty-message-box 2025-11-19 22:45:32 -05:00
Bill Thornton
3ec4b58ff8 Merge pull request #7343 from thornbill/base-theme
Refactor dark theme to base theme
2025-11-19 17:32:45 -05:00
Bill Thornton
0a9e08cdab Refactor dark theme to base theme 2025-11-19 16:55:12 -05:00
Bill Thornton
74240bd265 Merge pull request #7277 from thornbill/dark-theme-vars
Add css variable support for dark theme
2025-11-19 16:06:58 -05:00
viown
abcc625b60 Remove redundant check 2025-11-19 20:57:57 +03:00
viown
13c49d5144 Remove systemInfo from directory browser 2025-11-19 20:57:57 +03:00
viown
9673982c79 Add instruction 2025-11-19 20:57:57 +03:00
viown
a2516723d4 Hide empty message box if there isn't text 2025-11-19 20:57:57 +03:00
Bill Thornton
1a7fe7ece0 Merge pull request #7204 from jellyfin/renovate/ci-deps 2025-11-19 09:17:01 -05:00
Bill Thornton
699e749a49 Remove unused network share references 2025-11-19 09:09:31 -05:00
renovate[bot]
571e699b7e Update CI dependencies 2025-11-19 11:14:03 +00:00
Roi Gabay
6e06787a0a Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-11-18 21:55:06 +00:00
theguymadmax
2d7a38c9cc Backport pull request #7331 from jellyfin-web/release-10.11.z
Fix trickplay not displaying when content is played from a playlist

Original-merge: b807ebfa4a

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 12:13:17 -05:00
gnattu
a2e9231983 Backport pull request #7335 from jellyfin-web/release-10.11.z
Blacklist Firefox native mkv playback

Original-merge: f2d2c5b26e

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:19 -05:00
viown
573c31032d Backport pull request #7325 from jellyfin-web/release-10.11.z
Disable scan button when scanning

Original-merge: 3c6a5160a6

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:17 -05:00
theguymadmax
2a9be36c7b Backport pull request #7313 from jellyfin-web/release-10.11.z
Fix missing Live TV sections in experimental layout

Original-merge: 3d55ce3724

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:16 -05:00
viown
c796529544 Backport pull request #7312 from jellyfin-web/release-10.11.z
Enhance log viewer

Original-merge: e6141968d7

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:15 -05:00
viown
73d66d5612 Backport pull request #7306 from jellyfin-web/release-10.11.z
Add line break to card's secondary text

Original-merge: 1c8f221006

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:14 -05:00
thornbill
3dd636d520 Backport pull request #7301 from jellyfin-web/release-10.11.z
Fix first episode when playing a series

Original-merge: f077e294a9

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:12 -05:00
gnattu
0c9e61fdc2 Backport pull request #7298 from jellyfin-web/release-10.11.z
Relax dynamic HDR device Profile

Original-merge: f445e53f7e

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:11 -05:00
thornbill
463b3559b1 Backport pull request #7296 from jellyfin-web/release-10.11.z
Revert "Enable use of hls.js when LG WebOS 4 or newer is used."

Original-merge: 03c2cebbd3

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:10 -05:00
thornbill
d744639c22 Backport pull request #7295 from jellyfin-web/release-10.11.z
Fix default values in display settings

Original-merge: d1379dce8a

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:09 -05:00
theguymadmax
bf8b002142 Backport pull request #7290 from jellyfin-web/release-10.11.z
Fix library not refreshing after adding new media library

Original-merge: 21d7dd86ea

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:07 -05:00
kinke
e64e1f6535 Backport pull request #7283 from jellyfin-web/release-10.11.z
Restrict recently added max-32-streams limit to Tizen < v6.5

Original-merge: 39f971ffa4

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-11-17 11:14:06 -05:00
Gargotaire
23184b3e18 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-11-17 08:55:06 +00:00
Justin
611f9eb962 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-11-16 17:10:05 +00:00
hoanghuy309
0bec3f2577 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-11-15 06:48:17 +00:00
hoanghuy309
f1e6d8726c Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-11-15 03:25:52 +00:00
Gopinath Muruti
1a8e6d4add Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ms/
2025-11-15 02:16:51 +00:00
rimasx
026349babb Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-11-13 10:55:05 +00:00
Grant Alexander
e1724f5ec1 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-12 23:22:20 +00:00
gregcarmo
7e89b5f564 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-11-12 17:55:05 +00:00
Gargotaire
77df7d5d85 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-11-11 22:55:05 +00:00
Grant Alexander
afb4c4c830 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-11 18:32:09 +00:00
Hunam (Elie TD)
1295d5592e Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-09 10:56:44 +00:00
Jacky He
c70fd69315 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-11-08 14:08:05 +00:00
rimasx
d4ff5a27de Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-11-08 11:28:10 +00:00
rimasx
acf7b6889d Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-11-06 15:55:05 +00:00
Gargotaire
1d1542c446 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-11-05 23:55:05 +00:00
Jacky He
c5c5fc6743 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-11-05 19:55:06 +00:00
Jacky He
20ce3da458 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-11-05 19:55:06 +00:00
st7105
d1df6ae42b Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-11-05 10:51:53 +00:00
Andrew
220ff70d30 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-11-04 23:51:54 +00:00
Jacky He
0427ae6bb1 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-11-04 21:51:53 +00:00
Gargotaire
702412a425 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-11-04 19:51:53 +00:00
Gargotaire
f4883e19a8 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-11-03 22:51:53 +00:00
Dariottolo
2fae712ea9 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-11-03 19:51:53 +00:00
AfmanS
0a545e1c7c Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-11-03 14:51:53 +00:00
nyanmisaka
530df76316 Backport pull request #7274 from jellyfin-web/release-10.11.z
Use hls.js instead of native HLS in Chromium

Original-merge: 2c45c5ba4a

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:42 -05:00
viown
5b85e0e0ac Backport pull request #7272 from jellyfin-web/release-10.11.z
Use legacy grid for item counts widget

Original-merge: 7491722364

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:41 -05:00
viown
b517c27bf6 Backport pull request #7271 from jellyfin-web/release-10.11.z
Fix tuner devices list

Original-merge: cdde002ca6

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:40 -05:00
viown
7be11bc9d9 Backport pull request #7269 from jellyfin-web/release-10.11.z
Fix JSON in log viewer

Original-merge: 6e2c62525a

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:39 -05:00
nielsvanvelzen
af10633a7d Backport pull request #7259 from jellyfin-web/release-10.11.z
Avoid native browser alerts

Original-merge: 907947c523

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:38 -05:00
nielsvanvelzen
91cfc15e1c Backport pull request #7258 from jellyfin-web/release-10.11.z
Add Titan OS detection

Original-merge: e102334812

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-11-02 21:59:37 -05:00
皇甫朝云
2687b3daf1 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-11-02 12:51:53 +00:00
Jacky He
de2fe3f52d Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-31 21:51:54 +00:00
Jacky He
9b2ce2886e Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-31 21:51:53 +00:00
Bill Thornton
0a9ddb490d Use consistent padding on item details screen 2025-10-31 14:22:06 -04:00
Bill Thornton
99fea57a95 Add css var support for dark theme 2025-10-31 09:41:04 -04:00
無情天
ac2d059219 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-31 11:29:12 +00:00
Jacky He
134848d082 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 22:24:50 +00:00
Jacky He
95da2b3868 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-30 20:52:01 +00:00
Jacky He
d1c1f74763 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 20:52:00 +00:00
Jacky He
ca6204518a Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-30 20:51:53 +00:00
Jacky He
4c88d6b28b Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 19:34:57 +00:00
Jacky He
7edcf9501e Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-30 19:34:52 +00:00
Jacky He
741babbbc3 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 19:34:51 +00:00
Jacky He
3616f5b81e Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 18:51:54 +00:00
Jacky He
5857c02921 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-30 16:51:56 +00:00
Jacky He
1aa990f1bf Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 16:51:55 +00:00
Jacky He
bd8d0e786d Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-30 16:51:53 +00:00
Jacky He
d46c34a901 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-30 15:48:36 +00:00
Jacky He
c02c9690a6 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-30 15:48:35 +00:00
Jacky He
04df0b9106 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-30 15:48:33 +00:00
Jacky He
90744a57ba Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-29 23:51:57 +00:00
Jacky He
78926c2bea Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-29 23:51:57 +00:00
Jacky He
76b704d897 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-29 23:51:53 +00:00
Jacky He
5648423c12 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-29 22:47:06 +00:00
Jacky He
f1cb49ec38 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-29 22:47:05 +00:00
Jacky He
2454034d3f Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-29 22:46:54 +00:00
Andi Chandler
8a6a97a437 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-10-29 22:46:54 +00:00
Battseren Badral
81663eec15 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-29 20:51:57 +00:00
Jacky He
d74a148db4 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-29 20:51:54 +00:00
Rogal
a75f1124a6 Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-10-29 20:51:53 +00:00
Grant Alexander
4da2be5038 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-10-29 15:19:29 +00:00
Grant Alexander
cbac146558 Translated using Weblate (English (United States))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_US/
2025-10-28 19:51:55 +00:00
Battseren Badral
837f4e7479 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-28 19:51:53 +00:00
Grant Alexander
625ab3fede Added translation using Weblate (English (United States)) 2025-10-28 18:28:04 +00:00
Battseren Badral
290f8285c7 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-28 18:28:03 +00:00
Jacky He
13f35d0e4b Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-10-27 22:51:57 +00:00
Pascal Wiesmann
918af8fa65 Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-10-27 22:51:52 +00:00
bernarden
d168815ba4 Backport pull request #7252 from jellyfin-web/release-10.11.z
Wraps registration of all mediaSession action handlers in try catch.

Original-merge: 7643885c6b

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:14 -04:00
thornbill
8026ae3137 Backport pull request #7248 from jellyfin-web/release-10.11.z
Fix multiple album artists in card footer

Original-merge: 7c0c2e088f

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:13 -04:00
PeachesMLG
bef489cba4 Backport pull request #7245 from jellyfin-web/release-10.11.z
Fixed issue where waiting event is not being called correctly

Original-merge: 0989a3034f

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:11 -04:00
thornbill
1d02d2e9d2 Backport pull request #7241 from jellyfin-web/release-10.11.z
Revert scroller overflow change for tv layout

Original-merge: 4b072633fb

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:10 -04:00
thornbill
8c1958db46 Backport pull request #7240 from jellyfin-web/release-10.11.z
Handle browsers lacking stop media session action support

Original-merge: f7583a842b

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:09 -04:00
viown
c778f9cc2c Backport pull request #7233 from jellyfin-web/release-10.11.z
Increase restore check interval to 45s

Original-merge: 4f83e97592

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:08 -04:00
PeachesMLG
ebf2e85af8 Backport pull request #7227 from jellyfin-web/release-10.11.z
Fix unpause and pause references in syncplay video player

Original-merge: 17a1e2e94c

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:07 -04:00
theguymadmax
e30d2a324e Backport pull request #7224 from jellyfin-web/release-10.11.z
Enable backdrop image rotation in Firefox

Original-merge: 0bb8f7cb47

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:05 -04:00
SohamGanmote
f69e509898 Backport pull request #7221 from jellyfin-web/release-10.11.z
Fix: Add minimum value 0 for SyncPlay Settings SpeedToSync input

Original-merge: 12079b9462

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: Joshua M. Boniface <joshua@boniface.me>
2025-10-27 15:47:04 -04:00
Tiago Filipe
a9955fcb35 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-10-27 18:51:53 +00:00
czlevi7
430e8fb98c Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ro/
2025-10-27 14:51:53 +00:00
Joshua M. Boniface
1e77ecffcc Merge pull request #7263 from joshuaboniface/codeowners
Update CODEOWNERS to capture bump_version
2025-10-27 09:24:10 -04:00
kreaxv
788475b7b8 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-27 09:41:37 +00:00
Gargotaire
c72f093f88 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-10-27 07:51:53 +00:00
Joshua M. Boniface
b5fb51bfa9 Improve handling of .github dir 2025-10-26 22:17:42 -04:00
Joshua M. Boniface
ce2958351b Update CODEOWNERS to capture bump_version 2025-10-26 22:12:03 -04:00
Starbuck
54a828c123 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-10-26 21:48:14 +00:00
j9x49
0cd8dcf946 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hu/
2025-10-26 13:11:37 +00:00
MrPlow
c466497733 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-10-26 13:11:35 +00:00
rimasx
813f0a6399 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-26 11:51:52 +00:00
皇甫朝云
9d17ef0dce Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-26 06:51:53 +00:00
Battseren Badral
b94f7021dc Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-25 17:51:54 +00:00
rimasx
29bd1a2dc8 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-25 06:51:52 +00:00
HanHwanHo
95f910cc6a Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ko/
2025-10-24 21:51:54 +00:00
RickThaD
531ceedcbb Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-10-24 15:54:17 +00:00
Vilhelm Prytz
d2afde2e01 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-10-23 15:18:17 +00:00
rimasx
860fbbf371 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-23 12:51:26 +00:00
Lukáš Kucharczyk
37432ff513 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-10-23 12:51:26 +00:00
Dan Johansen
0a14b8212d Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-10-23 09:04:23 +00:00
Anrijs Vitolins
068a42c5bf Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/
2025-10-22 09:51:26 +00:00
Rostyslav
7f2bd12e98 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/
2025-10-22 06:07:25 +00:00
hoanghuy309
6ae937eab4 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-10-22 02:51:26 +00:00
ZJC-GH
535104ac80 Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-21 19:51:27 +00:00
ZJC-GH
1915ad08e2 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-21 19:51:26 +00:00
thornbill
01e1345a89 Backport pull request #7219 from jellyfin-web/release-10.11.z
Fix skip button not displaying correctly with OSD (#6583)

Original-merge: 81698d5da7

Merged-by: thornbill <thornbill@users.noreply.github.com>

Backported-by: thornbill <thornbill@users.noreply.github.com>
2025-10-21 15:51:05 -04:00
Jonas Engen Larsen
258ee7bacf Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-10-21 15:20:50 +00:00
Grant Alexander
48eee02ead Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_MX/
2025-10-21 15:20:49 +00:00
maayan cohen
64870f9247 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-10-21 10:31:17 +00:00
nextlooper42
7ff6490028 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-10-21 09:51:26 +00:00
hoanghuy309
a7d5b09bbd Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-10-21 06:24:41 +00:00
hoanghuy309
cc933c6678 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-10-21 05:33:07 +00:00
Thunderstrike116
751985cab3 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-10-20 23:19:34 +00:00
Bill Thornton
e956a0a635 Merge pull request #7203 from jellyfin/renovate/jellyfin-sdk-0.x
Update dependency @jellyfin/sdk to v0.0.0-unstable.202510201847
2025-10-20 15:00:21 -04:00
renovate[bot]
57a139d80c Update dependency @jellyfin/sdk to v0.0.0-unstable.202510201847 2025-10-20 18:50:48 +00:00
MrPlow
f701a1fcbd Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-10-20 18:45:46 +00:00
Thadah D. Denyse
d0341fb3d8 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-10-20 16:20:17 +00:00
Thunderstrike116
c8fd928167 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-10-20 16:20:17 +00:00
Thadah D. Denyse
a8c93b3394 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-10-20 14:30:04 +00:00
Thadah D. Denyse
b5db940fc3 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-10-20 14:29:50 +00:00
Romulo Alves
505cc64ef6 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-10-20 13:51:26 +00:00
Aindriú Mac Giolla Eoin
24477e8025 Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2025-10-20 12:10:13 +00:00
faquino
23097a4502 Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gl/
2025-10-20 10:51:27 +00:00
faquino
54bfe07a01 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-10-20 10:51:27 +00:00
hoanghuy309
5464ee5ba4 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-10-20 08:39:58 +00:00
nenadsuperzmaj
9e284cc93e Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-10-20 07:51:27 +00:00
st7105
9741d0c603 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-10-20 07:51:26 +00:00
Yohann Nizon
6441aa0269 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-10-20 07:51:26 +00:00
Gargotaire
4373c8b058 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-10-20 07:51:26 +00:00
Jellyfin Release Bot
4c14a8b529 Bump version to 10.12.0 2025-10-19 20:45:17 -04:00
queeup
c09237f4ce Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-10-19 21:29:50 +00:00
Kityn
ad342a0b1e Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-10-19 17:51:26 +00:00
Helak
f1a77af8d3 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-10-19 17:51:26 +00:00
Bas
c68dd09ebe Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-10-19 15:13:19 +00:00
Bill Thornton
daee19c4ac Merge pull request #7082 from viown/react-livetv 2025-10-19 10:37:52 -04:00
Bill Thornton
edb196c6b0 Merge pull request #7202 from viown/fix-clipped-delete-button 2025-10-19 10:34:24 -04:00
rimasx
d0eabd3116 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-19 12:51:26 +00:00
Fjuro
1189b6b84b Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-10-19 09:38:51 +00:00
Bill Thornton
e31e646b7b Merge pull request #7121 from jellyfin/renovate/hls.js-1.x 2025-10-18 16:44:58 -04:00
renovate[bot]
9b837ff89e Update dependency hls.js to v1.6.13 2025-10-18 20:37:23 +00:00
Bill Thornton
5b0c88bd6b Merge pull request #7210 from thornbill/set-npm-version 2025-10-18 16:18:35 -04:00
Bill Thornton
921d13517f Set maximum npm version 2025-10-18 16:06:46 -04:00
myrad2267
22f0706789 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-10-18 17:51:26 +00:00
myrad2267
b2951f0282 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-10-18 17:51:26 +00:00
viown
dfba17fdbc Fix clipped delete button in devices page 2025-10-18 15:51:52 +03:00
viown
39777707b0 Use loading state for refresh guide 2025-10-18 15:39:52 +03:00
rimasx
7606dfaf4b Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-18 09:51:26 +00:00
皇甫朝云
dae70c60e4 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-18 09:51:26 +00:00
JohnCaveson
4f9a105921 Show all album artists on cards (#6929)
* Remove indexing and accept a full array of albumartists into the function

* removing console.debug

* Use Array.isArray for array type check

* Fix missing paren

---------

Co-authored-by: greergoodman6@gmail.com <ggoodman@DESKTOP-R652T9J.localdomain>
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2025-10-18 02:56:42 -04:00
Kityn
1dc435986c Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-10-18 06:51:26 +00:00
John Garland
fbbf879006 Fix ends at not always accounting for playback position (#6965)
* fix: "Ends At" not always accounting for playback position

fixes #6964

* Update contributors

* Remove redundant `?? 0`

* Remove redundant assignments
2025-10-18 02:47:52 -04:00
Bas
6dab926437 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-10-18 00:45:20 +00:00
Renato Uštar
b183690db6 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-10-18 00:45:20 +00:00
Renato Uštar
c2d94327d0 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-10-17 22:55:33 +00:00
Bill Thornton
bf32030b23 Merge pull request #7200 from thornbill/page-size-warning 2025-10-17 18:55:29 -04:00
Renato Uštar
91f210f378 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-10-17 22:24:09 +00:00
viown
8a7148164b Update src/apps/dashboard/routes/users/index.tsx
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-18 00:36:44 +03:00
viown
780905e670 Replace Dashboard.navigate with react router navigate 2025-10-18 00:32:40 +03:00
viown
b0c9e11404 Add abort signal to hooks 2025-10-18 00:29:05 +03:00
viown
fee9b12f1b Add libraryMenu as a dependency 2025-10-18 00:28:45 +03:00
viown
f510ad5874 Move user pages to TS SDK 2025-10-18 00:28:44 +03:00
Bill Thornton
7a9b8fe7ae Add warning for page size setting 2025-10-17 17:12:38 -04:00
viown
e58063f457 Add actions context menu 2025-10-18 00:04:53 +03:00
viown
62f61fa167 Add provider list 2025-10-18 00:04:53 +03:00
viown
96024d3025 Migrate Live TV page to React 2025-10-18 00:04:52 +03:00
viown
7281ce480d Migrate libraries page to React (#7078)
* Migrate libraries page to React

* Fix aspect ratio for library card images

* Fix loading

* Use image url utility from TS SDK

* Add width prop to BaseCard

* Apply review feedback
2025-10-17 16:52:29 -04:00
Bill Thornton
3bcaf84ecb Merge pull request #7177 from thornbill/scroller-overflow
Show scroller content that overflows
2025-10-17 16:43:17 -04:00
Bill Thornton
4fa5176982 Use float for item details poster (#7195)
* Use float for item details poster

* Add list view children to primary content

* Move additional sections to primary container

* Add series to list view children

* Fix order of primary content sections
2025-10-17 16:42:40 -04:00
JA
1ed047df3d Prevent navigation during horizontal scroll (#6915)
* Prevent unwanted navigation history changes during horizontal scroll on trackpads by evaluating scroll start/end.

Prevent unwanted navigation history changes during horizontal scroll on trackpads by evaluating scroll start/end.

* Fix code reviews and change event listener opts in all implementacion of scroll navegation wheelEvent, scrollHandler

Fix code reviews and change event listener opts in all implementacion of scroll navegation wheelEvent, scrollHandler

* remove line space

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-17 16:41:42 -04:00
Bas
d48e2c4cd7 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-10-17 14:51:26 +00:00
Roi Gabay
7dc276ab51 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-10-17 14:51:26 +00:00
rimasx
a95599b60f Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-17 11:51:26 +00:00
rimasx
dfd461cf4c Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-17 06:43:48 +00:00
Bill Thornton
3215be4cd8 Merge pull request #7196 from thornbill/role-xss
Fix xss for person roles

Credit for discovery to Carlos García-Olalla Olivera
2025-10-16 09:36:03 -04:00
Romulo Alves
e91a7556cf Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-10-16 09:51:26 +00:00
Bill Thornton
e6d57d8e89 Merge pull request #7197 from thornbill/require-person-type 2025-10-15 22:06:27 -04:00
Bill Thornton
2c2311415f Remove blank person type option 2025-10-15 16:43:49 -04:00
Bill Thornton
16fd2a01aa Fix xss for person roles 2025-10-15 14:31:58 -04:00
Tiago Filipe
0682ca3b99 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-10-15 01:51:25 +00:00
Ziga Zajc
d39c58675d Translated using Weblate (Slovenian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/
2025-10-14 15:51:25 +00:00
rimasx
5292162fdd Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-14 11:51:25 +00:00
Bill Thornton
f44b642514 Merge pull request #7183 from thornbill/theme-videos
Improve theme video support
2025-10-13 16:25:02 -04:00
Bill Thornton
d37b6304fa Merge pull request #7190 from theguymadmax/fix-more-from
Fix "More From" section to show all albums by album artist
2025-10-13 15:36:20 -04:00
theguymadmax
40fb2ddc93 Apply suggestion from @thornbill
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2025-10-13 15:13:35 -04:00
theguymadmax
184cc7e9d1 Fix "More From" section to show all albums by album artist 2025-10-13 11:42:29 -04:00
kreaxv
7d20728ae3 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-13 11:46:00 +00:00
GolanGitHub
ac94190e0c Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-10-13 11:46:00 +00:00
kreaxv
cdf9613e08 Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-13 08:51:25 +00:00
rimasx
d188880e7e Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-13 08:51:25 +00:00
kreaxv
0f6fcd8daf Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-10-13 06:51:33 +00:00
spicy-weasel
0e3384e7a4 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ta/
2025-10-13 06:51:26 +00:00
faquino
b9769d9547 Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gl/
2025-10-13 06:51:26 +00:00
faquino
24860e373a Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-10-13 06:51:26 +00:00
MFride1
b89a90ebf5 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-10-12 23:00:45 +00:00
Gargotaire
a98f740ad8 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-10-12 16:51:25 +00:00
rimasx
c6c951a377 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-12 11:51:25 +00:00
rimasx
a8af5c31cd Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-12 08:51:25 +00:00
rimasx
b847506c1b Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-12 05:51:25 +00:00
rimasx
776755a81c Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-11 10:51:25 +00:00
rimasx
f936c9366f Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-11 08:51:25 +00:00
rimasx
9b7d921845 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-11 06:55:15 +00:00
rimasx
b08df1ed80 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-11 05:51:25 +00:00
Bill Thornton
9ebe4b7f57 Merge pull request #7175 from thornbill/app-bar-visibility 2025-10-10 17:57:11 -04:00
rimasx
9f6964fb51 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-10 20:51:25 +00:00
nextlooper42
3787889b41 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-10-10 16:51:25 +00:00
rimasx
91da2edae5 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-10 12:14:26 +00:00
Muhannad Alnemer
d5e54157ed Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ar/
2025-10-10 10:51:25 +00:00
Ärik
ed8dbf1bd9 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-10-10 06:51:25 +00:00
Bill Thornton
a19ccf5439 Merge pull request #7105 from viown/use-mui-dialogs-for-api-keys
Use mui dialogs for api keys page
2025-10-10 01:42:53 -04:00
theguymadmax
ae99ac8b03 Add Play All and Shuffle buttons to Music Videos Libraries (#6866)
* Add play all & shuffle to Music Videos

* Update comments

---------

Co-authored-by: Max <no@example.com>
2025-10-10 01:10:04 -04:00
皇甫朝云
e83279b69f Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-10 02:51:25 +00:00
Jagadam Dinesh Reddy
5bf0b0314c Merge pull request #7127 from jagadam97/enable-screenSaver-in-player
Enable screen saver in player if the video is paused
2025-10-09 16:47:14 -04:00
Bas
5d1a19a65d Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-10-09 19:51:25 +00:00
Bill Thornton
ce24556dad Merge pull request #7178 from thornbill/movie-collections-alpha
Enable alphapicker in movie collections tab
2025-10-09 15:38:03 -04:00
Bill Thornton
2b92a87006 Merge pull request #7028 from theguymadmax/add-play-button-movies
Add Play All button to movies
2025-10-09 15:33:46 -04:00
Bill Thornton
2a58eb8194 Merge pull request #7181 from theguymadmax/movie-tab-col-alpha
Enable alphapicker in movie collections tab - standard layout
2025-10-09 14:56:40 -04:00
Bill Thornton
2b96e9d6c7 Fix loading indicator showing for theme videos 2025-10-09 14:02:26 -04:00
Bill Thornton
fd4c897642 Use cover aspect ratio for theme videos 2025-10-09 13:49:57 -04:00
faquino
76fbfbbe84 Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gl/
2025-10-09 12:39:37 +00:00
rimasx
d26cc473a9 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-09 11:51:25 +00:00
faquino
715b026b0f Translated using Weblate (Galician)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gl/
2025-10-09 11:51:25 +00:00
rimasx
a3baf9a257 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-09 07:51:25 +00:00
theguymadmax
fbd480cd55 Enable alphapicker in movie collections tab (standard layout) 2025-10-08 20:21:43 -05:00
oddib
1435ea1560 Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-10-08 19:31:50 +00:00
Bill Thornton
4523b9f790 Enable alphapicker in movie collections tab 2025-10-08 12:52:51 -04:00
Bill Thornton
b691f62fc7 Show scroller content that overflows 2025-10-08 12:39:37 -04:00
Bill Thornton
1d07721de8 Merge pull request #7174 from YouKnowBlom/fix-resetpassword-dialog
Prevent dialog backdrop from displaying above dialog content
2025-10-08 10:27:26 -04:00
jeremydobber
5a1ca91bab Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hu/
2025-10-08 14:22:48 +00:00
jeremydobber
70530a562c Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-10-08 14:22:48 +00:00
Bill Thornton
5b622a547d Fix app bar visibility on item details page 2025-10-08 10:06:22 -04:00
viown
e2e9a5523d Use mui dialogs for api keys page 2025-10-08 17:00:54 +03:00
Azurite
41e5b7b6bc Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ja/
2025-10-08 05:51:25 +00:00
Milo Ivir
1194cff68d Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-10-07 19:45:36 +00:00
Milo Ivir
35507a8303 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-10-07 18:51:25 +00:00
rimasx
4d59c20550 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-07 11:51:25 +00:00
Andreas B
f974a39938 FIX: prevent dialog backdrop from appearing above dialog content 2025-10-07 11:45:06 +02:00
rimasx
e9e56af092 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-07 05:37:03 +00:00
rimasx
3efd339d91 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-06 20:51:25 +00:00
Adrián HM
403d116338 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_MX/
2025-10-06 18:32:53 +00:00
Nicolas N
0a9db2bda9 Translated using Weblate (Haitian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ht/
2025-10-06 14:51:25 +00:00
rimasx
2ee0caab6a Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-06 14:51:25 +00:00
rimasx
39ab3a52d8 Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-06 12:51:25 +00:00
Thadah D. Denyse
49988dbd35 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-10-06 10:40:50 +00:00
rimasx
39278b1e4e Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-06 10:40:50 +00:00
Aindriú Mac Giolla Eoin
d061871955 Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2025-10-06 08:38:23 +00:00
Francesco Lo Faro
ec73f0e0fc Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-10-06 07:51:25 +00:00
Khalsior
58c43e72c0 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-10-06 07:51:25 +00:00
rimasx
3d75ba4a7e Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-06 06:40:24 +00:00
nenadsuperzmaj
ec80e82625 Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-10-06 06:40:24 +00:00
Gallyam Biktashev
f09ada7f87 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-10-06 06:40:24 +00:00
Francesco Lo Faro
b6be3c3866 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-10-06 06:40:23 +00:00
Blackspirits
527c25388e Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-10-05 17:51:25 +00:00
Blackspirits
2d041661ce Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-10-05 17:51:25 +00:00
khanthaphot
68d69351ea Translated using Weblate (Thai)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/th/
2025-10-05 12:22:45 +00:00
rimasx
72e20c95ae Translated using Weblate (Estonian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/et/
2025-10-05 11:21:53 +00:00
khanthaphot
8dab9a6f12 Translated using Weblate (Thai)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/th/
2025-10-05 11:21:53 +00:00
AfmanS
fc3ac97e75 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-10-05 07:51:25 +00:00
MrPlow
73b23092ed Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-10-05 07:51:25 +00:00
Pavel Miniutka
3cf3a345db Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-10-05 07:51:25 +00:00
Pavel Miniutka
72392ec2ed Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-10-04 22:51:24 +00:00
Bill Thornton
e602b50e5b Merge pull request #7150 from thornbill/download-music-album 2025-10-04 16:14:06 -04:00
Bill Thornton
eaa0ca4b79 Merge pull request #7170 from thornbill/fix-css-theming 2025-10-04 14:08:09 -04:00
nenadsuperzmaj
c8ca4f3bb4 Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-10-04 15:51:25 +00:00
felix920506
36fa0fb9be Translated using Weblate (Chinese (Traditional Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant/
2025-10-04 15:51:25 +00:00
hoanghuy309
3e30c04941 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-10-04 09:51:25 +00:00
Bill Thornton
874a3cc727 Merge pull request #7128 from viown/redirect-to-devices-on-activity-click 2025-10-03 22:30:28 -04:00
S1NJED
8a0176eba2 Fix pdf extension check (#7124)
* fix: lowercase item path for pdf extension

Signed-off-by: sinjed <oooguuh@gmail.com>

* lint: single quote

Signed-off-by: sinjed <oooguuh@gmail.com>

---------

Signed-off-by: sinjed <oooguuh@gmail.com>
2025-10-03 16:33:55 -04:00
Bill Thornton
f20aaa3195 Merge pull request #7167 from nyanmisaka/fix-tonemap-box-in-sw
Fix the tonemap box not hidden in software transcoding
2025-10-03 14:29:54 -04:00
Bill Thornton
abce5b1bea Move event types to constants 2025-10-03 14:12:05 -04:00
Bill Thornton
ad00b16069 Fix text in activity list overflowing (#7112) 2025-10-03 12:18:08 -04:00
renovate[bot]
cc16d73fac Update dependency @jellyfin/sdk to v0.0.0-unstable.202510030502 (#7153)
* Update dependency @jellyfin/sdk to v0.0.0-unstable.202510030502

* Fix lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bill Thornton <thornbill@users.noreply.github.com>
2025-10-03 12:15:59 -04:00
Klomer
9c4bb658f6 Translated using Weblate (Breton)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/br/
2025-10-03 14:51:24 +00:00
Bill Thornton
77c2366dbe Apply unsaved changes 2025-10-03 10:34:15 -04:00
Bill Thornton
a32b2613ac Fix material react table theming 2025-10-03 09:46:25 -04:00
Klomer
429170bb65 Added translation using Weblate (Breton) 2025-10-03 13:21:01 +00:00
nyanmisaka
b1e083f9c7 Drop redundant AllowTonemappingSoftwareHelp string
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-10-03 00:23:07 +08:00
Bill Thornton
c93c25481d Fix components using default theme 2025-10-02 11:56:41 -04:00
nyanmisaka
f10573ff46 Fix the tonemap box not hidden in software transcoding
a3872ff is not included in b3de4af

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2025-10-02 17:30:06 +08:00
Arham
c3d1f78e15 Translated using Weblate (Urdu (Pakistan))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ur_PK/
2025-10-02 08:51:24 +00:00
Arham
3667493bc2 Translated using Weblate (Urdu (Pakistan))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ur_PK/
2025-10-02 06:51:25 +00:00
無情天
5660931dd1 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-10-02 01:14:39 +00:00
myrad2267
ab62a00574 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-10-01 11:50:02 +00:00
myrad2267
af6b205781 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-10-01 11:50:01 +00:00
Kityn
a55eea3e62 Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-10-01 06:11:06 +00:00
Bas
9b2f036296 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-09-30 19:51:24 +00:00
Helak
26c065c52d Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-09-30 17:51:26 +00:00
Bas
46a683a56b Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-09-30 17:51:26 +00:00
queeup
1586880776 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-09-30 17:51:26 +00:00
Bill Thornton
3a747addbf Merge pull request #7088 from viown/replace-toast-with-snackbar-on-dashboard
Replace toast with snackbar on dashboard
2025-09-30 13:13:03 -04:00
viown
1702604e32 Replace toast with snackbar on dashboard 2025-09-30 13:09:21 -04:00
Bas
ae56c9ee64 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-09-30 16:48:43 +00:00
linkandzelda91
f87421bde8 Update LanNetworksHelp description for clarity (#7118)
* Update LanNetworksHelp description for clarity

previously, it indicated that 
"If left blank, only the server's subnet is considered to be on the local network."
I tested this and afaik it's wrong, leaving it blank sets all RFC1918 addresses to local.

* Update src/strings/en-us.json

Co-authored-by: viown <48097677+viown@users.noreply.github.com>

---------

Co-authored-by: viown <48097677+viown@users.noreply.github.com>
2025-09-30 12:43:36 -04:00
Emmanuel Eytan
5411a0a0e7 Changed the erroneous spelling of the OK button in localizations. (#7125)
* Changed the erroneous spelling of the OK button in localizations.

* Only keeping the en-us modification.

* Extra comma removed.
2025-09-30 12:40:02 -04:00
Bill Thornton
47889a5789 Only download supported items 2025-09-30 12:25:04 -04:00
Bill Thornton
6c03684db5 Add support for download all for collections 2025-09-30 12:08:47 -04:00
Bill Thornton
98c1dfa597 Add support for downloading all songs in albums 2025-09-30 12:08:47 -04:00
Bill Thornton
ff42b28520 Merge pull request #6862 from qm3jp/fix-person-playback
Fix play all & shuffle for Person view
2025-09-30 09:06:46 -04:00
Bill Thornton
a516de5fc7 Use enum values in getPlaybackPromise 2025-09-30 08:24:00 -04:00
Aubrey Benedetti
d5423d2d56 Add qm3jp to contributors list 2025-09-30 01:19:30 -04:00
Aubrey Benedetti
4b36146b34 Fix play all & shuffle not working on Person 2025-09-30 01:19:30 -04:00
Bill Thornton
37aa7b8b08 Merge pull request #7156 from thornbill/fav-seasons
Add favorite seasons
2025-09-30 01:18:46 -04:00
Bill Thornton
5346444689 Merge pull request #7155 from thornbill/edit-artist-album
Allow editing artists for music albums
2025-09-30 01:17:52 -04:00
Bill Thornton
473b8cb428 Add favorite seasons 2025-09-30 01:15:02 -04:00
Bill Thornton
929c8b3cc7 Allow editing artists for music albums 2025-09-30 01:14:06 -04:00
hoanghuy309
b39360bf61 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-09-30 01:02:18 +00:00
Bill Thornton
13f3f61b39 Merge pull request #7157 from thornbill/fix-browser-type 2025-09-29 18:02:28 -04:00
Bill Thornton
e225dce119 Fix browser type definition 2025-09-29 17:00:52 -04:00
Nicolas N
a238b5ef8a Translated using Weblate (Haitian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ht/
2025-09-28 06:51:24 +00:00
Bill Thornton
713bb551cf Merge pull request #7145 from Shadowghost/set-parental-subrating 2025-09-27 17:59:28 -04:00
Bill Thornton
35082b8712 Merge pull request #7114 from dmitrylyzo/fix-xss-wizard-dashboard 2025-09-27 17:58:07 -04:00
Joshua M. Boniface
68eb5b9e36 Merge pull request #7141 from thornbill/subtitle-styling
Extract native/custom subtitle element logic to separate typescript file
2025-09-27 17:52:56 -04:00
daswesen123
966e69354a Translated using Weblate (English (Pirate))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en@pirate/
2025-09-26 23:51:24 +00:00
Chris Stormrider
89c5119aed Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-09-26 09:58:49 +00:00
Shadowghost
3a95e751d0 Fixup 2025-09-24 01:17:58 +02:00
Bill Thornton
cc7799cf49 Use custom subtitle element in safari 2025-09-23 11:38:46 -04:00
Bill Thornton
139ecd8146 Add typing for browser.js 2025-09-23 11:38:46 -04:00
Bill Thornton
eff386ffd8 Remove unnecessary dynamic imports 2025-09-23 11:38:46 -04:00
Bill Thornton
b58ee4c1ba Refactor native subtitle styling check 2025-09-23 11:38:46 -04:00
Blackspirits
38fc5db9c2 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-09-23 14:52:32 +00:00
Blackspirits
2a59c296da Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-09-23 14:52:31 +00:00
Shadowghost
fb58a759ac Fix grouping again 2025-09-23 15:10:05 +02:00
Shadowghost
2e4dde35f4 Fix grouping 2025-09-23 14:30:07 +02:00
Shadowghost
952a83d282 Set MaxParentalRating and MaxParentalSubRating when setting parental controls 2025-09-23 14:24:19 +02:00
Looooke
a8f06c4fa8 Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-09-22 22:16:08 +00:00
Blackspirits
2729f77aa8 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-09-22 19:07:43 +00:00
Blackspirits
ee717bab07 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-09-22 19:07:42 +00:00
Jan Zachar
0606493bd9 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-09-22 12:04:49 +00:00
Jan Zachar
4f0f1635be Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-09-22 12:04:49 +00:00
Thadah D. Denyse
b78b5fc4f0 Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-09-22 11:51:24 +00:00
Jan Zachar
f97cbe0fc5 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-09-22 11:51:23 +00:00
itoudium
646773b30a Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ja/
2025-09-21 10:45:38 +00:00
queeup
6f615b7cd9 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-09-21 10:45:38 +00:00
yoga sree jagadam
6c3a3a7205 Translated using Weblate (Telugu)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/te/
2025-09-16 14:51:23 +00:00
Aindriú Mac Giolla Eoin
df1626e95b Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2025-09-15 15:51:23 +00:00
Hit360D
e9cc027340 Translated using Weblate (Hindi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hi/
2025-09-15 06:30:34 +00:00
Robbie Jones
e8846f71a1 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-09-14 05:09:03 +00:00
Gargotaire
8c099c87fe Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-09-13 20:33:19 +00:00
Gargotaire
79d2c178e9 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-09-13 19:55:51 +00:00
nenadsuperzmaj
a25295194f Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-09-12 20:38:01 +00:00
nenadsuperzmaj
9562a188c4 Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-09-12 18:09:53 +00:00
nenadsuperzmaj
d581dd9c68 Translated using Weblate (Serbian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sr/
2025-09-12 17:51:23 +00:00
Plexiglass Ageless
19a28b441e Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-09-11 14:51:23 +00:00
Bill Thornton
ddae83d2ed Merge pull request #7133 from thornbill/fix-lodash-import
Fix lodash import for tree-shaking
2025-09-10 16:35:11 -04:00
Bill Thornton
b13942fbd5 Fix lodash import for tree-shaking 2025-09-10 16:27:18 -04:00
Ärik
e04c867424 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-09-10 20:14:33 +00:00
Looooke
eaf4b16abb Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-09-09 21:51:23 +00:00
Lucas
82d9e465a3 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-09-09 05:34:22 +00:00
Adrián HM
b2db3370b4 Translated using Weblate (Spanish (Mexico))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_MX/
2025-09-08 19:51:23 +00:00
Fjuro
2d35d763b9 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-09-08 15:02:02 +00:00
Gallyam Biktashev
5f6b7138e0 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-09-08 10:51:22 +00:00
hoanghuy309
ac6b24b3eb Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-09-07 10:51:23 +00:00
Gargotaire
db94421f5f Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-09-07 10:51:22 +00:00
Arty
edeb5d6f0c Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-09-06 06:12:41 +00:00
Arty
94b007544a Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-09-06 05:51:05 +00:00
Arty
5969f7b600 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-09-06 04:51:31 +00:00
Tomas Camaj
60d810b3a1 Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sq/
2025-09-05 20:39:05 +00:00
viown
a2222e4272 Go to activities on activity item click 2025-09-05 16:44:14 +03:00
Tomas Camaj
2e90aa6c54 Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sq/
2025-09-05 10:51:22 +00:00
Md Ashikur Rahman
44f4d9c537 Translated using Weblate (Bengali)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bn/
2025-09-02 21:51:22 +00:00
Milo Ivir
808ece5db4 Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-09-01 14:51:22 +00:00
Aindriú Mac Giolla Eoin
69b7c5216e Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2025-09-01 12:48:59 +00:00
Aindriú Mac Giolla Eoin
a5fce23949 Translated using Weblate (Irish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ga/
2025-09-01 10:51:22 +00:00
Fjuro
5a17f34fe4 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-09-01 10:51:22 +00:00
Battseren Badral
77272cb35c Translated using Weblate (Mongolian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/mn/
2025-09-01 07:51:23 +00:00
AfmanS
87db7e61e4 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-09-01 07:51:22 +00:00
Bahri Rizaldi
89a59608c9 Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2025-08-31 07:04:33 +00:00
Bill Thornton
107bdd276c Merge pull request #7120 from jellyfin/renovate/jellyfin-sdk-0.x 2025-08-30 11:28:50 -04:00
renovate[bot]
f0c3c98b6f Update dependency @jellyfin/sdk to v0.0.0-unstable.202508300501 2025-08-30 15:25:12 +00:00
Bill Thornton
b620fcaf96 Merge pull request #7119 from theguymadmax/comic-extension-fix 2025-08-30 11:24:23 -04:00
Bill Thornton
122379306c Merge pull request #7122 from jellyfin/renovate/ci-deps 2025-08-29 23:37:56 -04:00
renovate[bot]
2fa8079bc6 Update CI dependencies 2025-08-29 21:45:19 +00:00
theguymadmax
24954abee7 Fix comic player not loading images with uppercase extensions 2025-08-29 16:20:33 -04:00
Dmitry Lyzo
ca2d669924 Fix XSS at wizard and dashboard library 2025-08-27 12:01:06 +03:00
Andras Milassin
18f3083e69 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hu/
2025-08-27 07:10:47 +00:00
viown
db3ce49e9e Fix text in activity list overflowing 2025-08-26 12:19:38 +03:00
Ajoro
b006b48772 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-08-26 09:07:22 +00:00
Filip Radić
018bfa2c8c Translated using Weblate (Croatian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hr/
2025-08-25 09:29:13 +00:00
hoanghuy309
a2f8d43970 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-08-24 14:26:14 +00:00
Gargotaire
d8363144e3 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-24 09:38:14 +00:00
Lucas
b2e634bc95 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-08-23 20:14:58 +00:00
RadvinM
f8c12e7c17 Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 20:00:12 +00:00
Lucas
bc7bfcfb8a Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-08-23 20:00:11 +00:00
RadvinM
cf234ccb98 Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 16:48:55 +00:00
Lucas
392cbff73b Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-08-23 16:48:55 +00:00
Lucas
6bab9cd7b9 Translated using Weblate (Spanish (Argentina))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/
2025-08-23 15:15:52 +00:00
RadvinM
8644db92b0 Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 13:42:00 +00:00
RadvinM
66a3a6ffb7 Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 13:20:13 +00:00
RadvinM
c6ce01eaa6 Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 13:13:58 +00:00
RadvinM
d7b9ea641a Translated using Weblate (Persian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fa/
2025-08-23 13:06:09 +00:00
Yohann Nizon
89879edf92 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-08-23 09:57:18 +00:00
Gjelbrim Haskaj
992289c8cd Translated using Weblate (Albanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sq/
2025-08-23 00:15:34 +00:00
spurdl
796301ca9b Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-08-22 20:57:15 +00:00
Joshua
ca2e4523f9 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-08-22 11:51:21 +00:00
Zefanya
b49de1b9e0 Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2025-08-22 08:01:42 +00:00
Nipsu
4c5a025e95 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-08-21 07:00:11 +00:00
Bill Thornton
221d678899 Merge pull request #7106 from viown/fix-cancel-color-on-backups 2025-08-20 13:27:16 -04:00
ReDFiRe
a7d041ae96 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-20 15:51:21 +00:00
viown
118a0c375f Fix cancel button color on restore dialog 2025-08-20 14:55:14 +03:00
Bill Thornton
6d11d4ab40 Merge pull request #7104 from thornbill/fix-duped-music-videos
Refactor music video section handling for artists
2025-08-19 15:43:21 -04:00
Bill Thornton
c965b6169d Refactor music video section for artists 2025-08-19 14:41:37 -04:00
intelligentdonut
25481cae7e Translated using Weblate (English (Pirate))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en@pirate/
2025-08-19 17:42:49 +00:00
Zhelyan Radoev
be68305bd7 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-19 03:40:19 +00:00
gnc.07
09ca934c0d Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-08-18 22:23:00 +00:00
Francesco Lo Faro
c4c009b795 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-08-18 20:28:33 +00:00
TheLostDims
c4f8cfc589 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-18 17:22:57 +00:00
Bill Thornton
af8c65a4c2 Merge pull request #7098 from brad1111/fix-tv-focus-issues
Fix TV dpad focus issues if the body of the page is the focused element.
2025-08-18 13:14:14 -04:00
Bill Thornton
fb622b15f9 Merge pull request #7102 from thornbill/fix-disco-theme
Use theme manager to update mui theme
2025-08-18 12:42:58 -04:00
Acrotos
a988a97d75 Translated using Weblate (Romanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ro/
2025-08-18 16:40:54 +00:00
ReDFiRe
95e0e43417 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 16:40:54 +00:00
ReDFiRe
efd1609049 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 15:18:08 +00:00
Bill Thornton
723369acec Use theme manager to update mui theme 2025-08-18 11:13:53 -04:00
Zhelyan Radoev
ebca102eaa Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 15:12:13 +00:00
ReDFiRe
7694ee695b Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 14:36:30 +00:00
ReDFiRe
6ea2a7b220 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 14:14:38 +00:00
ReDFiRe
481ba04d4e Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 14:08:35 +00:00
ReDFiRe
d75d84484d Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 13:55:38 +00:00
ReDFiRe
48a5a6eef7 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 13:09:45 +00:00
ReDFiRe
bbae91088f Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 13:08:34 +00:00
ReDFiRe
8a25ff534b Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 11:16:46 +00:00
Fjuro
065a97cf9f Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-08-18 10:46:49 +00:00
ReDFiRe
c6518a8e2f Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-18 10:46:49 +00:00
Romulo Alves
bba17cc2fa Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-08-18 10:02:59 +00:00
Ella Veter
872d7fde93 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-08-18 09:06:42 +00:00
Rudolfs Caune
9b9ee3c258 Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/
2025-08-18 08:48:12 +00:00
ArvidTales
693b4c1383 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-08-18 08:48:11 +00:00
Thunderstrike116
c64fa5a612 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-08-18 08:48:11 +00:00
Anrijs Vitolins
df99a37356 Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/
2025-08-18 07:17:50 +00:00
whaliin
5d33900607 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-08-18 07:17:50 +00:00
sharanchius
49ae7017e5 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lt/
2025-08-18 07:17:49 +00:00
Tal Sarid
b55899587f Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-08-18 07:17:49 +00:00
Jacob Rasmussen
5e26465fe1 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-18 07:17:49 +00:00
st7105
8b39997166 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-08-17 13:51:21 +00:00
nextlooper42
6d24d7ccf5 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-08-17 09:51:21 +00:00
Gargotaire
9cbcd891ef Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-16 08:51:21 +00:00
myrad2267
ea760e6eb0 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-08-15 18:56:32 +00:00
myrad2267
838d83d214 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-08-15 18:56:32 +00:00
Bradley Eaton
75f18a2853 Fix TV dpad focus issues if the body of the page is the focused element. 2025-08-15 15:55:27 +01:00
Centauri
261536a671 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-15 13:51:21 +00:00
stanol
a7f676aa3a Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/
2025-08-15 11:51:21 +00:00
MrPlow
8205df5fc5 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-08-15 11:51:21 +00:00
Oatavandi
185223d2fc Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ta/
2025-08-15 09:51:21 +00:00
Nyanmisaka
ac7b5e6231 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-08-15 09:51:21 +00:00
Fjuro
2be0186750 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-08-15 09:51:21 +00:00
Bill Thornton
9a6b54aa9a Merge pull request #7095 from jellyfin/renovate/hls.js-1.x 2025-08-14 23:56:59 -04:00
Bill Thornton
d2485b5426 Merge pull request #7096 from jellyfin/renovate/ci-deps 2025-08-14 23:56:43 -04:00
renovate[bot]
343f73bf0c Update dependency hls.js to v1.6.9 2025-08-15 03:53:08 +00:00
Bill Thornton
761df06ef3 Merge pull request #7094 from jellyfin/renovate/jellyfin-sdk-0.x 2025-08-14 23:49:56 -04:00
renovate[bot]
07726e2311 Update CI dependencies 2025-08-15 03:40:20 +00:00
renovate[bot]
9a82ba5cef Update dependency @jellyfin/sdk to v0.0.0-unstable.202508141352 2025-08-15 03:39:03 +00:00
Kityn
e483dfcc89 Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-08-14 23:51:21 +00:00
Bas
b0e42be494 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-08-14 23:51:21 +00:00
Bill Thornton
4ef907f89e Merge pull request #7083 from viown/add-icons-to-dashboard-buttons
Add start icons to dashboard buttons
2025-08-14 17:00:19 -04:00
Bill Thornton
110c2052d3 Merge pull request #7084 from viown/fix-incorrect-profile-location
Fix incorrect user menu location
2025-08-14 16:07:23 -04:00
Bill Thornton
0fbaa49a1b Merge pull request #7081 from enter-a-random-username/patch-3
Fix bug in JMP flex alignment
2025-08-14 16:06:45 -04:00
Bill Thornton
97420e5213 Merge pull request #7076 from viown/fix-locale-parenthesis-issue
Fix aborted by server shutdown message wrapped in parenthesis
2025-08-14 16:05:44 -04:00
Lyall
68e338c113 Merge pull request #7075 from Lyall-A/spellcheck-custom-css
Disable spellcheck on custom CSS textbox
2025-08-14 16:04:30 -04:00
Bill Thornton
7740c5d61a Merge pull request #7092 from brad1111/fix-edge-uwp-detection
Fix Edge WebView 2 being detected as old Edge.
2025-08-14 14:18:50 -04:00
Zhelyan Radoev
13412e3eea Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-13 09:51:21 +00:00
Bradley Eaton
cc81d133c8 Fix Edge Webview 2 being detected as old Edge. 2025-08-13 09:30:30 +01:00
sharanchius
f7e580196c Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lt/
2025-08-12 08:51:04 +00:00
Gargotaire
a4084a0610 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-11 20:51:20 +00:00
Alessandro Mettifogo
ccd9448c49 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-08-11 12:51:20 +00:00
Alessandro Mettifogo
8d09be6664 Translated using Weblate (Italian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/
2025-08-11 11:09:52 +00:00
Zhelyan Radoev
7e26935a41 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-08-11 06:51:20 +00:00
Thunderstrike116
e9f943cc99 Translated using Weblate (Greek)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/el/
2025-08-10 13:57:28 +00:00
Arnau Galofré
fd876f4def Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-10 00:12:56 +00:00
viown
e782fbd7b0 Fix incorrect user menu location 2025-08-09 23:45:45 +03:00
viown
c62bcfdb87 Add start icons to dashboard buttons 2025-08-09 22:23:15 +03:00
Julio García
45495caa55 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-08-09 08:52:06 +00:00
enter-a-random-username
b386d349a4 Fix bug in JMP flex alignment
JMP seems to not support `end` and was avoided/ replaced so far with `flex-end`
2025-08-09 07:28:33 +02:00
TheLostDims
3aea9bc7ae Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-08 16:37:40 +00:00
Blackspirits
c92e06a4c2 Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-08-07 19:51:21 +00:00
Blackspirits
f62081c277 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-08-07 19:51:20 +00:00
viown
b6a8159238 Fix aborted by server shutdown message wrapped in parenthesis 2025-08-07 11:11:13 +03:00
Yusuke, Hirota
9f947ceac8 Translated using Weblate (Japanese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ja/
2025-08-06 16:35:14 +00:00
Hebők András
15f5c5df64 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/hu/
2025-08-05 18:51:20 +00:00
Gargotaire
9082f53c89 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-05 18:51:20 +00:00
hoanghuy309
b0cfdab7c3 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-08-05 11:51:20 +00:00
Troj@
187f1368c2 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-08-05 07:51:20 +00:00
TheGoose
50bfabff70 Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/
2025-08-05 03:17:21 +00:00
Signar Kamparås
dae7cfc8a8 Translated using Weblate (Swedish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sv/
2025-08-04 21:51:20 +00:00
Roi Gabay
761a367637 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-08-04 21:51:20 +00:00
Oatavandi
cbb6dd2466 Translated using Weblate (Tamil)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ta/
2025-08-04 18:51:20 +00:00
刘汉源
4cbcc47f57 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-08-04 18:51:20 +00:00
TheLostDims
e2e592a722 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-04 15:51:20 +00:00
Bill Thornton
71102e3d55 Merge pull request #7029 from viown/use-legacy-grid 2025-08-04 10:58:32 -04:00
TheLostDims
279ecdd62d Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-08-04 14:22:02 +00:00
gnc.07
452479b7b2 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_BR/
2025-08-04 13:57:13 +00:00
Erick Marx
acada92e7d Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt/
2025-08-04 13:27:27 +00:00
queeup
e0409ced8f Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-08-04 12:21:22 +00:00
queeup
4debb10c49 Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/tr/
2025-08-04 10:55:47 +00:00
hoanghuy309
2e026b5790 Translated using Weblate (Vietnamese)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/vi/
2025-08-04 09:51:21 +00:00
Tal Sarid
837bbe98e1 Translated using Weblate (Hebrew)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/he/
2025-08-04 09:51:20 +00:00
viown
8d0517b00a Move plugin details page to legacy grid 2025-08-04 12:25:45 +03:00
viown
6dbbe0e78b Use legacy grid on dashboard page 2025-08-04 12:19:36 +03:00
Thadah D. Denyse
7a8e934c7e Translated using Weblate (Basque)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/eu/
2025-08-04 07:29:23 +00:00
Rudolfs Caune
9fc58b1d5c Translated using Weblate (Latvian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/
2025-08-04 07:29:22 +00:00
dikson804
47a3ecbea5 Translated using Weblate (Chinese (Traditional Han script, Hong Kong))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hant_HK/
2025-08-04 06:57:56 +00:00
Gargotaire
efae432f9a Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-08-04 06:57:55 +00:00
無情天
e607c45162 Translated using Weblate (Chinese (Simplified Han script))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/zh_Hans/
2025-08-03 21:51:20 +00:00
Looooke
7142d41a74 Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-08-03 21:51:20 +00:00
Looooke
274228a095 Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-08-03 19:42:28 +00:00
Looooke
d39bbc69e2 Translated using Weblate (Alemannic)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/gsw/
2025-08-03 18:51:20 +00:00
Jesse Suksia
faf6b14cf4 Translated using Weblate (Finnish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fi/
2025-08-03 09:51:20 +00:00
Bill Thornton
5fc9fb3084 Merge pull request #7031 from brad1111/fix-tv-focus-issues 2025-08-02 23:44:31 -04:00
Oleksandr Yurov
e9cb48d7ef Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/
2025-08-02 09:01:59 +00:00
Bradley Eaton
0c6f582f28 Focus pin code text field on password reset. 2025-08-01 16:40:13 +01:00
AfmanS
b66d846324 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-08-01 07:28:21 +00:00
AfmanS
6724c72ce5 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pt_PT/
2025-08-01 07:15:58 +00:00
Thiago David
b4efb6f5de Merge pull request #7050 from Thiteago/align-os-media-status
Fix vertical alignment during fetch of media aditional content
2025-07-31 14:18:36 -04:00
Bill Thornton
3d2f060373 Merge pull request #6974 from brad1111/checkbox-controller-fix
Fix checkbox toggle on controller
2025-07-31 14:17:17 -04:00
nextlooper42
b1c69890d8 Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sk/
2025-07-31 08:56:44 +00:00
nomener
29fa59d8b3 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-07-31 07:52:58 +00:00
Troj@
aa3f1e8969 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-31 07:52:57 +00:00
nomener
bf1b7cae80 Translated using Weblate (German)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/de/
2025-07-31 07:25:36 +00:00
Lukáš Kucharczyk
7d87f8c5b2 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/cs/
2025-07-31 06:23:22 +00:00
Bas
a75de89d43 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nl/
2025-07-31 05:54:48 +00:00
myrad2267
6bc6831c13 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr/
2025-07-31 01:38:27 +00:00
myrad2267
287e14a83c Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-07-31 01:38:26 +00:00
st7105
1b281ef299 Translated using Weblate (Russian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ru/
2025-07-31 01:22:47 +00:00
ArturoCarrilloSolorzano
0a532a96f3 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-07-31 01:08:42 +00:00
Kityn
9903b9052c Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/pl/
2025-07-30 23:13:48 +00:00
Bill Thornton
c23f1ba231 Merge pull request #7042 from viown/add-plugin-loading
Add loading indicator when installing plugin
2025-07-30 16:41:09 -04:00
Hestadgard
6c4cb665bf Translated using Weblate (Norwegian Bokmål)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/nb_NO/
2025-07-30 20:27:28 +00:00
Bill Thornton
88d44bfe8f Merge pull request #7017 from thornbill/plugin-unity
Add unified plugin page
2025-07-30 16:27:25 -04:00
Bill Thornton
1098ca4447 Use query parameters for plugin filters 2025-07-30 14:15:18 -04:00
Bill Thornton
357ce7c9b8 Extract search param handling to common hook 2025-07-30 12:59:50 -04:00
Bill Thornton
38ac335544 Merge pull request #6979 from thornbill/webos-infinite-screen 2025-07-29 23:16:35 -04:00
Troj@
a7eb42c439 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-29 08:13:35 +00:00
Troj@
1245d89224 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-29 07:45:23 +00:00
Troj@
f8f71a8b1f Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-29 07:01:59 +00:00
Troj@
4c828845d4 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-29 06:41:02 +00:00
Hossain Rizbi
c3df42c6cc Translated using Weblate (Bengali)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bn/
2025-07-28 20:57:07 +00:00
Nero
fb0f4ee284 Translated using Weblate (Spanish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es/
2025-07-28 16:45:43 +00:00
Troj@
b7f330e01c Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-28 15:51:19 +00:00
Bill Thornton
2912bf50c5 Fix eslint config 2025-07-28 10:52:45 -04:00
Troj@
c9d7e20b44 Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-28 13:51:19 +00:00
Troj@
d1460f2d3c Translated using Weblate (Belarusian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/be/
2025-07-28 07:51:20 +00:00
Gargotaire
de7176af1b Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-07-28 00:51:19 +00:00
Martynas
4b658123c1 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lt/
2025-07-27 12:04:14 +00:00
Martynas
fa59e0c2b9 Translated using Weblate (Lithuanian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lt/
2025-07-27 11:21:10 +00:00
Bradley Eaton
0e447a6eb4 Apply suggestions from code review
Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com>
2025-07-27 11:19:16 +01:00
Fayaz (Quill)
88a065a80d Translated using Weblate (Dhivehi)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/dv/
2025-07-27 09:34:51 +00:00
myrad2267
5e3aa28d57 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/
2025-07-27 02:13:59 +00:00
Djwarf
4ffbaed5ac Added translation using Weblate (Kurdish (Central)) 2025-07-26 19:19:30 +00:00
TheLostDims
5170125228 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-07-26 15:28:29 +00:00
TheLostDims
b193e454a4 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-07-26 13:17:25 +00:00
TheLostDims
9afc6e6bf3 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-07-26 11:34:14 +00:00
TheLostDims
9089f3d450 Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-07-26 11:12:07 +00:00
TheLostDims
a643738e2f Translated using Weblate (Danish)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/da/
2025-07-26 10:41:15 +00:00
Gargotaire
5fec647f11 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-07-26 09:46:20 +00:00
viown
86f18bfa08 Add loading indicator when installing plugin 2025-07-26 11:10:49 +03:00
Bill Thornton
4fd2a4041f Update manage repositories button and use legacy grid 2025-07-25 17:30:13 -04:00
Zhelyan Radoev
2ce3f72c0a Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-25 18:51:14 +00:00
Zhelyan Radoev
949e8684c1 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-25 17:25:29 +00:00
Zhelyan Radoev
998e991264 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-25 17:14:21 +00:00
Zhelyan Radoev
069d9f62bd Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-25 05:20:32 +00:00
Zhelyan Radoev
5e1af7e40c Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-25 04:21:45 +00:00
Gargotaire
ba8d4f9c2b Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-07-24 20:12:53 +00:00
Gargotaire
9424d8d79c Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-07-24 17:27:33 +00:00
Zhelyan Radoev
f38afeb06f Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-24 15:51:19 +00:00
Zhelyan Radoev
cb97eb834d Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-24 11:51:19 +00:00
Khoirul Umam
bfa516664d Translated using Weblate (Indonesian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/id/
2025-07-24 09:51:19 +00:00
Zhelyan Radoev
a8577f363e Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-24 05:51:19 +00:00
Gargotaire
671c0aa7a0 Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ca/
2025-07-23 23:51:19 +00:00
Zhelyan Radoev
801b96413b Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-23 16:51:19 +00:00
Zhelyan Radoev
c9412241d8 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-23 14:51:19 +00:00
Zhelyan Radoev
2b0499c341 Translated using Weblate (Bulgarian)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/bg/
2025-07-23 11:51:19 +00:00
zag
bc7f5547e1 Translated using Weblate (Malay)
Translation: Jellyfin/Jellyfin Web
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/ms/
2025-07-22 08:21:50 +00:00
Bill Thornton
93821aed8c Update view all plugins behavior 2025-07-21 17:44:37 -04:00
Bradley Eaton
fcd1a65522 Make changes based on code review. 2025-07-19 15:23:00 +01:00
Bradley Eaton
6bea19f54a Fix a couple focus issues in TV layout with keyboard navigation. 2025-07-19 14:22:36 +01:00
theguymadmax
e0e9853d49 Add play all button to movies 2025-07-18 17:15:11 -04:00
Bill Thornton
325ff3b105 Add plugin status filter independent of categories 2025-07-18 17:02:06 -04:00
Bill Thornton
89e07f2f2b Add no plugins messaging 2025-07-16 13:26:35 -04:00
Bill Thornton
9fd0fcc175 Remove plugin catalog code 2025-07-16 13:26:34 -04:00
Bill Thornton
0eeed43d85 Add categories for built-in plugins 2025-07-16 13:26:34 -04:00
Bill Thornton
a9106642bd Add unified plugin page 2025-07-16 13:26:34 -04:00
Bradley Eaton
d5bdd3cd5a Prevent clickOnKeyUp if keyup/down behaviour is prevented in gamepadtokey 2025-07-15 18:59:19 +01:00
Bill Thornton
d96bb5a61e Fix dom import 2025-07-11 18:42:41 -04:00
Bill Thornton
4fd3d72c08 Fix reversed width/height assignment 2025-07-11 18:29:29 -04:00
Bill Thornton
5b953440a3 Fix webOS destructuring incompatibility 2025-07-11 18:28:55 -04:00
Bill Thornton
6b7ac54d06 Add fallback to 4K if window reports infinite size 2025-07-11 18:28:55 -04:00
Bill Thornton
a36908b4c4 Cleanup white space and default export 2025-07-11 18:28:55 -04:00
Bill Thornton
2346943348 Move dom.js to utils 2025-07-11 18:28:55 -04:00
kevgrig
2cdbbd3f2d Merge branch 'master' into issue5486 2025-07-10 21:40:51 -05:00
Kevin G
5c8c86b766 Update based on feedback
Signed-off-by: Kevin G <kevin@myplaceonline.com>
2025-07-10 21:40:30 -05:00
Kevin G
4cccd63831 Issue #5486: When changing player types, destroy old player
Signed-off-by: Kevin G <kevin@myplaceonline.com>
2025-04-14 12:28:11 -04:00
imtsuki
c1161c7c5a Add hevc level 6.2 check
Signed-off-by: imtsuki <me@qjx.app>
2024-12-13 20:37:00 +08:00
375 changed files with 11924 additions and 6703 deletions

4
.github/CODEOWNERS vendored
View File

@@ -1 +1,5 @@
* @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

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ inputs.commit || github.sha }}

View File

@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ inputs.commit }}
show-progress: false
- name: Scan
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
with:
## Workaround from https://github.com/actions/dependency-review-action/issues/456
## TODO: Remove when necessary
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout ⬇️
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ inputs.commit }}
show-progress: false

View File

@@ -80,7 +80,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -95,6 +95,6 @@ jobs:
run: npm ci --no-audit
- name: Run eslint
uses: CatChen/eslint-suggestion-action@4dda35decf912ab18ea3e071acec2c6c2eda00b6 # v4.1.18
uses: CatChen/eslint-suggestion-action@4ee415529307a8ca0260b4a3775484802523e5af # v4.1.19
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -101,6 +101,8 @@
- [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

812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jellyfin-web",
"version": "10.11.0",
"version": "10.12.0",
"description": "Web interface for Jellyfin",
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
@@ -66,6 +66,7 @@
"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",
"webpack-bundle-analyzer": "4.10.2",
@@ -84,7 +85,7 @@
"@fontsource/noto-sans-sc": "5.2.6",
"@fontsource/noto-sans-tc": "5.2.6",
"@jellyfin/libass-wasm": "4.2.3",
"@jellyfin/sdk": "0.0.0-unstable.202507090504",
"@jellyfin/sdk": "0.0.0-unstable.202512091852",
"@jellyfin/ux-web": "1.0.0",
"@mui/icons-material": "6.4.12",
"@mui/material": "6.4.12",
@@ -106,7 +107,7 @@
"flv.js": "1.6.2",
"headroom.js": "0.12.0",
"history": "5.3.0",
"hls.js": "1.6.5",
"hls.js": "1.6.13",
"intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.11.0",
"jquery": "3.7.1",
@@ -161,14 +162,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",
"npm": ">=9.6.4 <11.0.0",
"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,
@@ -13,12 +13,16 @@ 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 UserThemeProvider from 'themes/UserThemeProvider';
import { LayoutMode } from 'constants/layoutMode';
import browser from 'scripts/browser';
import appTheme from 'themes';
import { ThemeStorageManager } from 'themes/themeStorageManager';
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';
const layoutMode = browser.tv ? LayoutMode.Tv : localStorage.getItem(LAYOUT_SETTING_KEY);
const isExperimentalLayout = !layoutMode || layoutMode === LayoutMode.Experimental;
const router = createHashRouter([
{
@@ -51,11 +55,15 @@ function RootAppLayout() {
.some(path => location.pathname.startsWith(`/${path}`));
return (
<UserThemeProvider>
<ThemeProvider
theme={appTheme}
defaultMode='dark'
storageManager={ThemeStorageManager}
>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
<Outlet />
</UserThemeProvider>
</ThemeProvider>
);
}

1
src/apiclient.d.ts vendored
View File

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

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 'types/eventType';
import { EventType } from 'constants/eventType';
import Events, { type Event } from 'utils/events';
interface AppTabsParams {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
@@ -11,9 +11,8 @@ import CardActionArea from '@mui/material/CardActionArea';
import Stack from '@mui/material/Stack';
import { Link, To } from 'react-router-dom';
interface IProps {
interface BaseCardProps {
title?: string;
secondaryTitle?: string;
text?: string;
image?: string | null;
icon?: React.ReactNode;
@@ -22,15 +21,30 @@ interface IProps {
action?: boolean;
actionRef?: React.MutableRefObject<HTMLButtonElement | null>;
onActionClick?: () => void;
height?: number;
width?: number;
};
const BaseCard = ({ title, secondaryTitle, text, image, icon, to, onClick, action, actionRef, onActionClick }: IProps) => {
const BaseCard = ({
title,
text,
image,
icon,
to,
onClick,
action,
actionRef,
onActionClick,
height,
width
}: BaseCardProps) => {
return (
<Card
sx={{
display: 'flex',
flexDirection: 'column',
height: 240
height: height || 240,
width: width
}}
>
<CardActionArea
@@ -62,30 +76,44 @@ const BaseCard = ({ title, secondaryTitle, text, image, icon, to, onClick, actio
</Box>
)}
</CardActionArea>
<CardHeader
title={
<Stack direction='row' gap={1} alignItems='center'>
<Typography sx={{
<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>
{secondaryTitle && (
<Typography variant='body2' color='text.secondary'>{secondaryTitle}</Typography>
{text && (
<Typography
variant='body2'
color='text.secondary'
sx={{
lineBreak: 'anywhere'
}}
>
{text}
</Typography>
)}
</Stack>
}
subheader={text}
action={
action ? (
<IconButton ref={actionRef} onClick={onActionClick}>
<MoreVertIcon />
</IconButton>
) : null
}
/>
<Box>
{action ? (
<IconButton ref={actionRef} onClick={onActionClick}>
<MoreVertIcon />
</IconButton>
) : null}
</Box>
</Stack>
</CardContent>
</Card>
);
};

View File

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

@@ -0,0 +1,30 @@
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,6 +1,5 @@
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';
@@ -33,23 +32,16 @@ const PluginDrawerSection = () => {
>
<ListItemLink
to='/dashboard/plugins'
includePaths={[ '/configurationpage' ]}
includePaths={[
'/configurationpage',
'/dashboard/plugins/repositories'
]}
excludePaths={pagesInfo?.map(p => `/${Dashboard.getPluginUrl(p.Name)}`)}
>
<ListItemIcon>
<Extension />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink
to='/dashboard/plugins/catalog'
includePaths={[ '/dashboard/plugins/repositories' ]}
>
<ListItemIcon>
<Public />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabCatalog')} />
<ListItemText primary={globalize.translate('TabPlugins')} />
</ListItemLink>
{pagesInfo?.map(pageInfo => (

View File

@@ -1,7 +1,8 @@
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, MaterialReactTable } from 'material-react-table';
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';
@@ -12,7 +13,7 @@ interface TablePageProps<T extends MRT_RowData> extends PageProps {
table: MRT_TableInstance<T>
}
export const DEFAULT_TABLE_OPTIONS = {
export const DEFAULT_TABLE_OPTIONS: Partial<MRT_TableOptions<MRT_RowData>> = {
// Enable custom features
enableColumnPinning: true,
enableColumnResizing: true,

View File

@@ -39,6 +39,7 @@ const ActivityLogWidget = () => {
key={entry.Id}
item={entry}
displayShortOverview={true}
to='/dashboard/activity?useractivity=true'
/>
))}
</List>

View File

@@ -31,6 +31,7 @@ const AlertsLogWidget = () => {
key={entry.Id}
item={entry}
displayShortOverview={false}
to='/dashboard/activity?useractivity=false'
/>
))}
</List>

View File

@@ -5,13 +5,14 @@ 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/Grid2';
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
@@ -75,23 +76,27 @@ const ItemCountsWidget = () => {
}, [ counts, isPending ]);
return (
<Grid
container
spacing={2}
sx={{
alignItems: 'stretch',
marginTop: 2
}}
>
{cards.map(card => (
<Grid
key={card.metrics.map(metric => metric.label).join('-')}
size={{ xs: 12, sm: 6, lg: 4 }}
>
<MetricCard {...card} />
</Grid>
))}
</Grid>
<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>
);
};

View File

@@ -6,15 +6,24 @@ 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 }: ServerInfoWidgetProps) => {
const ServerInfoWidget = ({
onScanLibrariesClick,
onRestartClick,
onShutdownClick,
isScanning
}: ServerInfoWidgetProps) => {
const { data: systemInfo, isPending } = useSystemInfo();
return (
@@ -27,13 +36,13 @@ const ServerInfoWidget = ({ onScanLibrariesClick, onRestartClick, onShutdownClic
padding: 2
}}>
<Stack direction='row'>
<Stack flexGrow={1} gap={1}>
<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} gap={1}>
<Stack flexGrow={5} spacing={1}>
{isPending ? (
<>
<Skeleton />
@@ -53,18 +62,21 @@ const ServerInfoWidget = ({ onScanLibrariesClick, onRestartClick, onShutdownClic
</Stack>
</Paper>
<Stack direction='row' gap={1.5} flexWrap={'wrap'}>
<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'
@@ -75,6 +87,7 @@ const ServerInfoWidget = ({ onScanLibrariesClick, onRestartClick, onShutdownClic
<Button
onClick={onShutdownClick}
startIcon={<PowerSettingsNewIcon />}
color='error'
sx={{
fontWeight: 'bold'

View File

@@ -22,10 +22,7 @@ export const HelpLinks = [
paths: ['/dashboard/playback/transcoding'],
url: 'https://jellyfin.org/docs/general/server/transcoding'
}, {
paths: [
'/dashboard/plugins',
'/dashboard/plugins/catalog'
],
paths: ['/dashboard/plugins'],
url: 'https://jellyfin.org/docs/general/server/plugins/'
}, {
paths: ['/dashboard/plugins/repositories'],
@@ -50,5 +47,8 @@ export const HelpLinks = [
'/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

@@ -1,14 +0,0 @@
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent" data-title="${HeaderLibraries}">
<div>
<div class="content-primary">
<div class="padded-top padded-bottom">
<button is="emby-button" type="button" class="raised btnRefresh">
<span>${ButtonScanAllLibraries}</span>
</button>
<progress max="100" min="0" style="display: inline-block; vertical-align: middle;" class="refreshProgress"></progress>
</div>
<div id="divVirtualFolders"></div>
</div>
</div>
</div>

View File

@@ -1,383 +0,0 @@
import escapeHtml from 'escape-html';
import taskButton from 'scripts/taskbutton';
import loading from 'components/loading/loading';
import globalize from 'lib/globalize';
import dom from 'scripts/dom';
import imageHelper from 'utils/image';
import 'components/cardbuilder/card.scss';
import 'elements/emby-itemrefreshindicator/emby-itemrefreshindicator';
import { pageClassOn, pageIdOn } from 'utils/dashboard';
import confirm from 'components/confirm/confirm';
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
function addVirtualFolder(page) {
import('components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => {
new MediaLibraryCreator({
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
return !f.hidden;
}),
refresh: shouldRefreshLibraryAfterChanges(page)
}).then(function (hasChanges) {
if (hasChanges) {
reloadLibrary(page);
}
});
});
}
function editVirtualFolder(page, virtualFolder) {
import('components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: MediaLibraryEditor }) => {
new MediaLibraryEditor({
refresh: shouldRefreshLibraryAfterChanges(page),
library: virtualFolder
}).then(function (hasChanges) {
if (hasChanges) {
reloadLibrary(page);
}
});
});
}
function deleteVirtualFolder(page, virtualFolder) {
let msg = globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder');
if (virtualFolder.Locations.length) {
msg += '<br/><br/>' + globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '<br/><br/>';
msg += virtualFolder.Locations.join('<br/>');
}
confirm({
text: msg,
title: globalize.translate('HeaderRemoveMediaFolder'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
ApiClient.removeVirtualFolder(virtualFolder.Name, refreshAfterChange).then(function () {
reloadLibrary(page);
});
});
}
function refreshVirtualFolder(page, virtualFolder) {
import('components/refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
new RefreshDialog({
itemIds: [virtualFolder.ItemId],
serverId: ApiClient.serverId(),
mode: 'scan'
}).show();
});
}
function renameVirtualFolder(page, virtualFolder) {
import('components/prompt/prompt').then(({ default: prompt }) => {
prompt({
label: globalize.translate('LabelNewName'),
description: globalize.translate('MessageRenameMediaFolder'),
confirmText: globalize.translate('ButtonRename')
}).then(function (newName) {
if (newName && newName != virtualFolder.Name) {
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
ApiClient.renameVirtualFolder(virtualFolder.Name, newName, refreshAfterChange).then(function () {
reloadLibrary(page);
});
}
});
});
}
function showCardMenu(page, elem, virtualFolders) {
const card = dom.parentWithClass(elem, 'card');
const index = parseInt(card.getAttribute('data-index'), 10);
const virtualFolder = virtualFolders[index];
const menuItems = [];
menuItems.push({
name: globalize.translate('EditImages'),
id: 'editimages',
icon: 'photo'
});
menuItems.push({
name: globalize.translate('ManageLibrary'),
id: 'edit',
icon: 'folder'
});
menuItems.push({
name: globalize.translate('ButtonRename'),
id: 'rename',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ScanLibrary'),
id: 'refresh',
icon: 'refresh'
});
menuItems.push({
name: globalize.translate('ButtonRemove'),
id: 'delete',
icon: 'delete'
});
import('components/actionSheet/actionSheet').then((actionsheet) => {
actionsheet.show({
items: menuItems,
positionTo: elem,
callback: function (resultId) {
switch (resultId) {
case 'edit':
editVirtualFolder(page, virtualFolder);
break;
case 'editimages':
editImages(page, virtualFolder);
break;
case 'rename':
renameVirtualFolder(page, virtualFolder);
break;
case 'delete':
deleteVirtualFolder(page, virtualFolder);
break;
case 'refresh':
refreshVirtualFolder(page, virtualFolder);
}
}
});
});
}
function reloadLibrary(page) {
loading.show();
ApiClient.getVirtualFolders().then(function (result) {
reloadVirtualFolders(page, result);
});
}
function shouldRefreshLibraryAfterChanges(page) {
return page.id === 'mediaLibraryPage';
}
function reloadVirtualFolders(page, virtualFolders) {
let html = '';
virtualFolders.push({
Name: globalize.translate('ButtonAddMediaLibrary'),
icon: 'add_circle',
Locations: [],
showType: false,
showLocations: false,
showMenu: false,
showNameWithIcon: false,
elementId: 'addLibrary'
});
for (let i = 0; i < virtualFolders.length; i++) {
const virtualFolder = virtualFolders[i];
html += getVirtualFolderHtml(page, virtualFolder, i);
}
const divVirtualFolders = page.querySelector('#divVirtualFolders');
divVirtualFolders.innerHTML = html;
divVirtualFolders.classList.add('itemsContainer');
divVirtualFolders.classList.add('vertical-wrap');
const btnCardMenuElements = divVirtualFolders.querySelectorAll('.btnCardMenu');
btnCardMenuElements.forEach(function (btn) {
btn.addEventListener('click', function () {
showCardMenu(page, btn, virtualFolders);
});
});
divVirtualFolders.querySelector('#addLibrary').addEventListener('click', function () {
addVirtualFolder(page);
});
const libraryEditElements = divVirtualFolders.querySelectorAll('.editLibrary');
libraryEditElements.forEach(function (btn) {
btn.addEventListener('click', function () {
const card = dom.parentWithClass(btn, 'card');
const index = parseInt(card.getAttribute('data-index'), 10);
const virtualFolder = virtualFolders[index];
if (virtualFolder.ItemId) {
editVirtualFolder(page, virtualFolder);
}
});
});
loading.hide();
}
function editImages(page, virtualFolder) {
import('components/imageeditor/imageeditor').then((imageEditor) => {
imageEditor.show({
itemId: virtualFolder.ItemId,
serverId: ApiClient.serverId()
}).then(function () {
reloadLibrary(page);
});
});
}
function getLink(text, url) {
return globalize.translate(text, '<a is="emby-linkbutton" class="button-link" href="' + url + '" target="_blank" data-autohide="true">', '</a>');
}
function getCollectionTypeOptions() {
return [{
name: '',
value: ''
}, {
name: globalize.translate('Movies'),
value: 'movies',
message: getLink('MovieLibraryHelp', 'https://jellyfin.org/docs/general/server/media/movies')
}, {
name: globalize.translate('TabMusic'),
value: 'music',
message: getLink('MusicLibraryHelp', 'https://jellyfin.org/docs/general/server/media/music')
}, {
name: globalize.translate('Shows'),
value: 'tvshows',
message: getLink('TvLibraryHelp', 'https://jellyfin.org/docs/general/server/media/shows')
}, {
name: globalize.translate('Books'),
value: 'books',
message: getLink('BookLibraryHelp', 'https://jellyfin.org/docs/general/server/media/books')
}, {
name: globalize.translate('HomeVideosPhotos'),
value: 'homevideos'
}, {
name: globalize.translate('MusicVideos'),
value: 'musicvideos'
}, {
name: globalize.translate('MixedMoviesShows'),
value: 'mixed',
message: globalize.translate('MessageUnsetContentHelp')
}];
}
function getVirtualFolderHtml(page, virtualFolder, index) {
let html = '';
const elementId = virtualFolder.elementId ? `id="${virtualFolder.elementId}" ` : '';
html += '<div ' + elementId + 'class="card backdropCard scalableCard backdropCard-scalable" data-index="' + index + '" data-id="' + virtualFolder.ItemId + '">';
html += '<div class="cardBox visualCardBox">';
html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="cardPadder cardPadder-backdrop"></div>';
html += '<div class="cardContent">';
let imgUrl = '';
if (virtualFolder.PrimaryImageItemId) {
imgUrl = ApiClient.getScaledImageUrl(virtualFolder.PrimaryImageItemId, {
maxWidth: Math.round(dom.getScreenWidth() * 0.40),
type: 'Primary'
});
}
let hasCardImageContainer;
if (imgUrl) {
html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : getDefaultBackgroundClass()}" style="cursor:pointer">`;
html += `<img src="${imgUrl}" style="width:100%" />`;
hasCardImageContainer = true;
} else if (!virtualFolder.showNameWithIcon) {
html += `<div class="cardImageContainer editLibrary ${getDefaultBackgroundClass()}" style="cursor:pointer;">`;
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
hasCardImageContainer = true;
}
if (hasCardImageContainer) {
html += '<div class="cardIndicators backdropCardIndicators">';
html += '<div is="emby-itemrefreshindicator"' + (virtualFolder.RefreshProgress || virtualFolder.RefreshStatus && virtualFolder.RefreshStatus !== 'Idle' ? '' : ' class="hide"') + ' data-progress="' + (virtualFolder.RefreshProgress || 0) + '" data-status="' + virtualFolder.RefreshStatus + '"></div>';
html += '</div>';
html += '</div>';
}
if (!imgUrl && virtualFolder.showNameWithIcon) {
html += '<h3 class="cardImageContainer addLibrary" style="position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer;flex-direction:column;">';
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
if (virtualFolder.showNameWithIcon) {
html += '<div style="margin:1em 0;position:width:100%;">';
html += escapeHtml(virtualFolder.Name);
html += '</div>';
}
html += '</h3>';
}
html += '</div>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
if (virtualFolder.showMenu !== false) {
const dirTextAlign = globalize.getIsRTL() ? 'left' : 'right';
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
html += '</div>';
}
html += "<div class='cardText'>";
if (virtualFolder.showNameWithIcon) {
html += '&nbsp;';
} else {
html += escapeHtml(virtualFolder.Name);
}
html += '</div>';
let typeName = getCollectionTypeOptions().filter(function (t) {
return t.value == virtualFolder.CollectionType;
})[0];
typeName = typeName ? typeName.name : globalize.translate('Other');
html += "<div class='cardText cardText-secondary'>";
if (virtualFolder.showType === false) {
html += '&nbsp;';
} else {
html += typeName;
}
html += '</div>';
if (virtualFolder.showLocations === false) {
html += "<div class='cardText cardText-secondary'>";
html += '&nbsp;';
html += '</div>';
} else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) {
html += "<div class='cardText cardText-secondary' dir='ltr' style='text-align:left;'>";
html += virtualFolder.Locations[0];
html += '</div>';
} else {
html += "<div class='cardText cardText-secondary'>";
html += globalize.translate('NumLocationsValue', virtualFolder.Locations.length);
html += '</div>';
}
html += '</div>';
html += '</div>';
html += '</div>';
return html;
}
pageClassOn('pageshow', 'mediaLibraryPage', function () {
reloadLibrary(this);
});
pageIdOn('pageshow', 'mediaLibraryPage', function () {
const page = this;
taskButton({
mode: 'on',
progressElem: page.querySelector('.refreshProgress'),
taskKey: 'RefreshLibrary',
button: page.querySelector('.btnRefresh')
});
});
pageIdOn('pagebeforehide', 'mediaLibraryPage', function () {
const page = this;
taskButton({
mode: 'off',
progressElem: page.querySelector('.refreshProgress'),
taskKey: 'RefreshLibrary',
button: page.querySelector('.btnRefresh')
});
});

View File

@@ -1,40 +0,0 @@
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
<div>
<div class="content-primary">
<div class="verticalSection verticalSection-extrabottompadding">
<div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards">
<span>${HeaderTunerDevices}</span>
</h2>
<button is="emby-button" type="button" class="fab btnAddDevice submit sectionTitleButton" style="margin-left:1em;" title="${Add}">
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="devicesList itemsContainer vertical-wrap" data-hovermenu="false" data-multiselect="false" style="margin-top: .5em;"></div>
</div>
</div>
<div class="readOnlyContent">
<div class="verticalSection">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">${HeaderGuideProviders}</h2>
<button is="emby-button" type="button" class="fab btnAddProvider submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="providerList">
</div>
<div>
<button is="emby-button" type="button" class="raised btnRefresh block button-cancel">
<span>${ButtonRefreshGuideData}</span>
</button>
<progress max="100" min="0" style="width: 100%;" class="refreshGuideProgress"></progress>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,338 +0,0 @@
import 'jquery';
import globalize from 'lib/globalize';
import taskButton from 'scripts/taskbutton';
import dom from 'scripts/dom';
import layoutManager from 'components/layoutManager';
import loading from 'components/loading/loading';
import browser from 'scripts/browser';
import 'components/listview/listview.scss';
import 'styles/flexstyles.scss';
import 'elements/emby-itemscontainer/emby-itemscontainer';
import 'components/cardbuilder/card.scss';
import 'material-design-icons-iconfont';
import 'elements/emby-button/emby-button';
import Dashboard from 'utils/dashboard';
import confirm from 'components/confirm/confirm';
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
const enableFocusTransform = !browser.slow && !browser.edge;
function getDeviceHtml(device) {
const padderClass = 'cardPadder-backdrop';
let cssClass = 'card scalableCard backdropCard backdropCard-scalable';
const cardBoxCssClass = 'cardBox visualCardBox';
let html = '';
// TODO move card creation code to Card component
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
}
html += '<div type="button" class="' + cssClass + '" data-id="' + device.Id + '">';
html += '<div class="' + cardBoxCssClass + '">';
html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="' + padderClass + '"></div>';
html += '<div class="cardContent searchImage">';
html += `<div class="cardImageContainer coveredImage ${getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr" aria-hidden="true"></span></div>`;
html += '</div>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions autoSize" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
html += '<div class="cardText">' + (device.FriendlyName || getTunerName(device.Type)) + '</div>';
html += '<div class="cardText cardText-secondary">';
html += device.Url || '&nbsp;';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
return html;
}
function renderDevices(page, devices) {
page.querySelector('.devicesList').innerHTML = devices.map(getDeviceHtml).join('');
}
function deleteDevice(page, id) {
const message = globalize.translate('MessageConfirmDeleteTunerDevice');
confirm(message, globalize.translate('HeaderDeleteDevice')).then(function () {
loading.show();
ApiClient.ajax({
type: 'DELETE',
url: ApiClient.getUrl('LiveTv/TunerHosts', {
Id: id
})
}).then(function () {
reload(page);
});
});
}
function reload(page) {
loading.show();
ApiClient.getNamedConfiguration('livetv').then(function (config) {
renderDevices(page, config.TunerHosts);
renderProviders(page, config.ListingProviders);
});
loading.hide();
}
function submitAddDeviceForm(page) {
page.querySelector('.dlgAddDevice').close();
loading.show();
ApiClient.ajax({
type: 'POST',
url: ApiClient.getUrl('LiveTv/TunerHosts'),
data: JSON.stringify({
Type: page.querySelector('#selectTunerDeviceType').value,
Url: page.querySelector('#txtDevicePath').value
}),
contentType: 'application/json'
}).then(function () {
reload(page);
}, function () {
Dashboard.alert({
message: globalize.translate('ErrorAddingTunerDevice')
});
});
}
function renderProviders(page, providers) {
let html = '';
if (providers.length) {
html += '<div class="paperList">';
for (let i = 0, length = providers.length; i < length; i++) {
const provider = providers[i];
html += '<div class="listItem">';
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true"></span>';
html += '<div class="listItemBody two-line">';
html += '<a is="emby-linkbutton" style="display:block;padding:0;margin:0;text-align:left;" class="clearLink" href="' + getProviderConfigurationUrl(provider.Type) + '&id=' + provider.Id + '">';
html += '<h3 class="listItemBodyText">';
html += getProviderName(provider.Type);
html += '</h3>';
html += '<div class="listItemBodyText secondary">';
html += provider.Path || provider.ListingsId || '';
html += '</div>';
html += '</a>';
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" class="btnOptions" data-id="' + provider.Id + '"><span class="material-icons listItemAside more_vert" aria-hidden="true"></span></button>';
html += '</div>';
}
html += '</div>';
}
const elem = page.querySelector('.providerList');
elem.innerHTML = html;
if (elem.querySelector('.btnOptions')) {
const btnOptionElements = elem.querySelectorAll('.btnOptions');
btnOptionElements.forEach(function (btn) {
btn.addEventListener('click', function () {
const id = this.getAttribute('data-id');
showProviderOptions(page, id, btn);
});
});
}
}
function showProviderOptions(page, providerId, button) {
const items = [];
items.push({
name: globalize.translate('Delete'),
id: 'delete'
});
items.push({
name: globalize.translate('MapChannels'),
id: 'map'
});
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: items,
positionTo: button
}).then(function (id) {
switch (id) {
case 'delete':
deleteProvider(page, providerId);
break;
case 'map':
mapChannels(page, providerId);
}
});
});
}
function mapChannels(page, providerId) {
import('components/channelMapper/channelMapper').then(({ default: ChannelMapper }) => {
new ChannelMapper({
serverId: ApiClient.serverInfo().Id,
providerId: providerId
}).show();
});
}
function deleteProvider(page, id) {
const message = globalize.translate('MessageConfirmDeleteGuideProvider');
confirm(message, globalize.translate('HeaderDeleteProvider')).then(function () {
loading.show();
ApiClient.ajax({
type: 'DELETE',
url: ApiClient.getUrl('LiveTv/ListingProviders', {
Id: id
})
}).then(function () {
reload(page);
}, function () {
reload(page);
});
});
}
function getTunerName(providerId) {
switch (providerId.toLowerCase()) {
case 'm3u':
return 'M3U';
case 'hdhomerun':
return 'HDHomeRun';
case 'hauppauge':
return 'Hauppauge';
case 'satip':
return 'DVB';
default:
return 'Unknown';
}
}
function getProviderName(providerId) {
switch (providerId.toLowerCase()) {
case 'schedulesdirect':
return 'Schedules Direct';
case 'xmltv':
return 'XMLTV';
default:
return 'Unknown';
}
}
function getProviderConfigurationUrl(providerId) {
switch (providerId.toLowerCase()) {
case 'xmltv':
return '#/dashboard/livetv/guide?type=xmltv';
case 'schedulesdirect':
return '#/dashboard/livetv/guide?type=schedulesdirect';
}
}
function addProvider(button) {
const menuItems = [];
menuItems.push({
name: 'Schedules Direct',
id: 'SchedulesDirect'
});
menuItems.push({
name: 'XMLTV',
id: 'xmltv'
});
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: button,
callback: function (id) {
Dashboard.navigate(getProviderConfigurationUrl(id));
}
});
});
}
function addDevice() {
Dashboard.navigate('dashboard/livetv/tuner');
}
function showDeviceMenu(button, tunerDeviceId) {
const items = [];
items.push({
name: globalize.translate('Delete'),
id: 'delete'
});
items.push({
name: globalize.translate('Edit'),
id: 'edit'
});
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: items,
positionTo: button
}).then(function (id) {
switch (id) {
case 'delete':
deleteDevice(dom.parentWithClass(button, 'page'), tunerDeviceId);
break;
case 'edit':
Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId);
}
});
});
}
function onDevicesListClick(e) {
const card = dom.parentWithClass(e.target, 'card');
if (card) {
const id = card.getAttribute('data-id');
const btnCardOptions = dom.parentWithClass(e.target, 'btnCardOptions');
if (btnCardOptions) {
showDeviceMenu(btnCardOptions, id);
} else {
Dashboard.navigate('dashboard/livetv/tuner?id=' + id);
}
}
}
$(document).on('pageinit', '#liveTvStatusPage', function () {
const page = this;
page.querySelector('.btnAddDevice').addEventListener('click', function () {
addDevice();
});
if (page.querySelector('.formAddDevice')) {
// NOTE: unused?
page.querySelector('.formAddDevice').addEventListener('submit', function (e) {
e.preventDefault();
submitAddDeviceForm(page);
});
}
page.querySelector('.btnAddProvider').addEventListener('click', function () {
addProvider(this);
});
page.querySelector('.devicesList').addEventListener('click', onDevicesListClick);
}).on('pageshow', '#liveTvStatusPage', function () {
const page = this;
reload(page);
taskButton({
mode: 'on',
progressElem: page.querySelector('.refreshGuideProgress'),
taskKey: 'RefreshGuide',
button: page.querySelector('.btnRefresh')
});
}).on('pagehide', '#liveTvStatusPage', function () {
const page = this;
taskButton({
mode: 'off',
progressElem: page.querySelector('.refreshGuideProgress'),
taskKey: 'RefreshGuide',
button: page.querySelector('.btnRefresh')
});
});

View File

@@ -1,6 +1,6 @@
import globalize from 'lib/globalize';
import loading from 'components/loading/loading';
import dom from 'scripts/dom';
import dom from 'utils/dom';
import 'elements/emby-input/emby-input';
import 'elements/emby-button/emby-button';
import 'elements/emby-checkbox/emby-checkbox';

View File

@@ -4,7 +4,6 @@ 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 ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import formatRelative from 'date-fns/formatRelative';
@@ -12,13 +11,15 @@ 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 }: ActivityListItemProps) => {
const ActivityListItem = ({ item, displayShortOverview, to }: ActivityListItemProps) => {
const relativeDate = useMemo(() => {
if (item.Date) {
return formatRelative(Date.parse(item.Date), Date.now(), { locale: getLocale() });
@@ -29,7 +30,7 @@ const ActivityListItem = ({ item, displayShortOverview }: ActivityListItemProps)
return (
<ListItem disablePadding>
<ListItemButton>
<ListItemLink to={to}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: getLogLevelColor(item.Severity || LogLevel.Information) + '.main' }}>
<Notifications sx={{ color: '#fff' }} />
@@ -37,14 +38,28 @@ const ActivityListItem = ({ item, displayShortOverview }: ActivityListItemProps)
</ListItemAvatar>
<ListItemText
primary={<Typography>{item.Name}</Typography>}
primary={<Typography sx={{ whiteSpace: 'pre-wrap' }}>{item.Name}</Typography>}
secondary={(
<Stack>
<Typography variant='body1' color='text.secondary'>
<Typography
sx={{
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
variant='body1'
color='text.secondary'
>
{relativeDate}
</Typography>
{displayShortOverview && (
<Typography variant='body1' color='text.secondary'>
<Typography
sx={{
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
variant='body1'
color='text.secondary'
>
{item.ShortOverview}
</Typography>
)}
@@ -52,7 +67,7 @@ const ActivityListItem = ({ item, displayShortOverview }: ActivityListItemProps)
)}
disableTypography
/>
</ListItemButton>
</ListItemLink>
</ListItem>
);
};

View File

@@ -7,7 +7,7 @@ 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 } from 'react';
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';
@@ -16,7 +16,7 @@ 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 'components/toast/toast';
import Toast from 'apps/dashboard/components/Toast';
type IProps = {
backup: BackupManifestDto;
@@ -25,10 +25,16 @@ type IProps = {
};
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);
toast({ text: globalize.translate('Copied') });
setIsCopiedToastOpen(true);
}
}, [ backup.Path ]);
@@ -39,16 +45,21 @@ const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }:
maxWidth={'sm'}
fullWidth
>
<Toast
open={isCopiedToastOpen}
onClose={handleToastClose}
message={globalize.translate('Copied')}
/>
<DialogTitle>
{backup.DateCreated}
</DialogTitle>
<DialogContent>
<Stack gap={2}>
<Stack spacing={2}>
<Box>
<Stack
direction='row'
gap={2}
spacing={2}
>
<Typography fontWeight='bold'>{globalize.translate('LabelPath')}</Typography>
<Stack direction='row'>
@@ -60,7 +71,7 @@ const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }:
</Stack>
<Stack
direction='row'
gap={2}
spacing={2}
>
<Typography fontWeight='bold'>{globalize.translate('LabelVersion')}</Typography>
<Typography color='text.secondary'>{backup.ServerVersion}</Typography>

View File

@@ -32,7 +32,7 @@ const RestoreConfirmationDialog: FunctionComponent<IProps> = ({ open, onClose, o
</DialogContent>
<DialogActions>
<Button onClick={onClose} color='error'>
<Button onClick={onClose} variant='text'>
{globalize.translate('ButtonCancel')}
</Button>
<Button onClick={onConfirm}>

View File

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

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

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

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

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

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

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

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

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

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

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

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

@@ -12,7 +12,13 @@ const fetchServerLog = async (
const response = await getSystemApi(api).getLogFile({ name }, options);
// FIXME: TypeScript SDK thinks it is returning a File but in reality it is a string
return response.data as never as 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();

View File

@@ -0,0 +1,119 @@
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
import { useMemo } from 'react';
import { useApi } from 'hooks/useApi';
import { PluginCategory } from '../constants/pluginCategory';
import type { PluginDetails } from '../types/PluginDetails';
import { findBestConfigurationPage } from './configurationPage';
import { findBestPluginInfo } from './pluginInfo';
import { useConfigurationPages } from './useConfigurationPages';
import { usePackages } from './usePackages';
import { usePlugins } from './usePlugins';
export const usePluginDetails = () => {
const { api } = useApi();
const {
data: configurationPages,
isError: isConfigurationPagesError,
isPending: isConfigurationPagesPending
} = useConfigurationPages();
const {
data: packages,
isError: isPackagesError,
isPending: isPackagesPending
} = usePackages();
const {
data: plugins,
isError: isPluginsError,
isPending: isPluginsPending
} = usePlugins();
const pluginDetails = useMemo<PluginDetails[]>(() => {
if (!isPackagesPending && !isPluginsPending) {
const pluginIds = new Set<string>();
packages?.forEach(({ guid }) => {
if (guid) pluginIds.add(guid);
});
plugins?.forEach(({ Id }) => {
if (Id) pluginIds.add(Id);
});
return Array.from(pluginIds)
.map(id => {
const packageInfo = packages?.find(pkg => pkg.guid === id);
const pluginInfo = findBestPluginInfo(id, plugins);
let version;
if (pluginInfo) {
// Find the installed version
const repoVersion = packageInfo?.versions?.find(v => v.version === pluginInfo.Version);
version = repoVersion || {
version: pluginInfo.Version,
VersionNumber: pluginInfo.Version
};
} else {
// Use the latest version
version = packageInfo?.versions?.[0];
}
let imageUrl;
if (pluginInfo?.HasImage) {
imageUrl = api?.getUri(`/Plugins/${pluginInfo.Id}/${pluginInfo.Version}/Image`);
}
let category = packageInfo?.category;
if (!packageInfo) {
switch (id) {
case 'a629c0dafac54c7e931a7174223f14c8': // AudioDB
case '8c95c4d2e50c4fb0a4f36c06ff0f9a1a': // MusicBrainz
category = PluginCategory.Music;
break;
case 'a628c0dafac54c7e9d1a7134223f14c8': // OMDb
case 'b8715ed16c4745289ad3f72deb539cd4': // TMDb
category = PluginCategory.MoviesAndShows;
break;
case '872a78491171458da6fb3de3d442ad30': // Studio Images
category = PluginCategory.General;
}
}
return {
canUninstall: !!pluginInfo?.CanUninstall,
category,
description: pluginInfo?.Description || packageInfo?.description || packageInfo?.overview,
id,
imageUrl: imageUrl || packageInfo?.imageUrl || undefined,
isEnabled: pluginInfo?.Status !== PluginStatus.Disabled,
name: pluginInfo?.Name || packageInfo?.name,
owner: packageInfo?.owner,
status: pluginInfo?.Status,
configurationPage: findBestConfigurationPage(configurationPages || [], id),
version,
versions: packageInfo?.versions || []
};
})
.sort(({ name: nameA }, { name: nameB }) => (
(nameA || '').localeCompare(nameB || '')
));
}
return [];
}, [
api,
configurationPages,
isPluginsPending,
packages,
plugins
]);
return {
data: pluginDetails,
isError: isConfigurationPagesError || isPackagesError || isPluginsError,
isPending: isConfigurationPagesPending || isPackagesPending || isPluginsPending
};
};

View File

@@ -0,0 +1,51 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import React, { type FC } from 'react';
import globalize from 'lib/globalize';
interface NoPluginResultsProps {
isFiltered: boolean
onViewAll: () => void
query: string
}
const NoPluginResults: FC<NoPluginResultsProps> = ({
isFiltered,
onViewAll,
query
}) => {
return (
<Box
sx={{
textAlign: 'center'
}}
>
<Typography
component='div'
sx={{
marginTop: 2,
marginBottom: 1
}}
>
{
query ?
globalize.translate('SearchResultsEmpty', query) :
globalize.translate('NoSubtitleSearchResultsFound')
}
</Typography>
{isFiltered && (
<Button
variant='text'
onClick={onViewAll}
>
{globalize.translate('ViewAllPlugins')}
</Button>
)}
</Box>
);
};
export default NoPluginResults;

View File

@@ -1,28 +0,0 @@
import React from 'react';
import type { PackageInfo } from '@jellyfin/sdk/lib/generated-client/models/package-info';
import ExtensionIcon from '@mui/icons-material/Extension';
import BaseCard from 'apps/dashboard/components/BaseCard';
import { useLocation } from 'react-router-dom';
type IProps = {
pkg: PackageInfo;
};
const PackageCard = ({ pkg }: IProps) => {
const location = useLocation();
return (
<BaseCard
title={pkg.name}
image={pkg.imageUrl}
icon={<ExtensionIcon sx={{ width: 80, height: 80 }} />}
to={{
pathname: `/dashboard/plugins/${pkg.guid}`,
search: `?name=${encodeURIComponent(pkg.name || '')}`,
hash: location.hash
}}
/>
);
};
export default PackageCard;

View File

@@ -1,171 +1,34 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useApi } from 'hooks/useApi';
import type { PluginInfo } from '@jellyfin/sdk/lib/generated-client/models/plugin-info';
import globalize from 'lib/globalize';
import BaseCard from 'apps/dashboard/components/BaseCard';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import Settings from '@mui/icons-material/Settings';
import Delete from '@mui/icons-material/Delete';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import BlockIcon from '@mui/icons-material/Block';
import ExtensionIcon from '@mui/icons-material/Extension';
import ListItemText from '@mui/material/ListItemText';
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
import type { ConfigurationPageInfo } from '@jellyfin/sdk/lib/generated-client/models/configuration-page-info';
import { useEnablePlugin } from '../api/useEnablePlugin';
import { useDisablePlugin } from '../api/useDisablePlugin';
import { useUninstallPlugin } from '../api/useUninstallPlugin';
import ConfirmDialog from 'components/ConfirmDialog';
import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
interface IProps {
plugin: PluginInfo;
configurationPage?: ConfigurationPageInfo;
import BaseCard from 'apps/dashboard/components/BaseCard';
import { PluginDetails } from '../types/PluginDetails';
interface PluginCardProps {
plugin: PluginDetails;
};
const PluginCard = ({ plugin, configurationPage }: IProps) => {
const PluginCard = ({ plugin }: PluginCardProps) => {
const location = useLocation();
const navigate = useNavigate();
const actionRef = useRef<HTMLButtonElement | null>(null);
const enablePlugin = useEnablePlugin();
const disablePlugin = useDisablePlugin();
const uninstallPlugin = useUninstallPlugin();
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const [ isUninstallConfirmOpen, setIsUninstallConfirmOpen ] = useState(false);
const { api } = useApi();
const pluginPage = useMemo(() => (
{
pathname: '/configurationpage',
search: `?name=${encodeURIComponent(configurationPage?.Name || '')}`,
pathname: `/dashboard/plugins/${plugin.id}`,
search: `?name=${encodeURIComponent(plugin.name || '')}`,
hash: location.hash
}
), [ location, configurationPage ]);
const navigateToPluginSettings = useCallback(() => {
navigate(pluginPage);
}, [ navigate, pluginPage ]);
const onEnablePlugin = useCallback(() => {
if (plugin.Id && plugin.Version) {
enablePlugin.mutate({
pluginId: plugin.Id,
version: plugin.Version
});
setAnchorEl(null);
setIsMenuOpen(false);
}
}, [ plugin, enablePlugin ]);
const onDisablePlugin = useCallback(() => {
if (plugin.Id && plugin.Version) {
disablePlugin.mutate({
pluginId: plugin.Id,
version: plugin.Version
});
setAnchorEl(null);
setIsMenuOpen(false);
}
}, [ plugin, disablePlugin ]);
const onCloseUninstallConfirmDialog = useCallback(() => {
setIsUninstallConfirmOpen(false);
}, []);
const showUninstallConfirmDialog = useCallback(() => {
setIsUninstallConfirmOpen(true);
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
const onUninstall = useCallback(() => {
if (plugin.Id && plugin.Version) {
uninstallPlugin.mutate({
pluginId: plugin.Id,
version: plugin.Version
});
setAnchorEl(null);
setIsMenuOpen(false);
}
}, [ plugin, uninstallPlugin ]);
const onMenuClose = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
const onActionClick = useCallback(() => {
setAnchorEl(actionRef.current);
setIsMenuOpen(true);
}, []);
), [ location, plugin ]);
return (
<>
<BaseCard
title={plugin.Name}
secondaryTitle={plugin.Version}
to={pluginPage}
text={`${globalize.translate('LabelStatus')} ${plugin.Status}`}
image={plugin.HasImage ? api?.getUri(`/Plugins/${plugin.Id}/${plugin.Version}/Image`) : null}
icon={<ExtensionIcon sx={{ width: 80, height: 80 }} />}
action={true}
actionRef={actionRef}
onActionClick={onActionClick}
/>
<Menu
anchorEl={anchorEl}
open={isMenuOpen}
onClose={onMenuClose}
>
{configurationPage && (
<MenuItem onClick={navigateToPluginSettings}>
<ListItemIcon>
<Settings />
</ListItemIcon>
<ListItemText>{globalize.translate('Settings')}</ListItemText>
</MenuItem>
)}
{(plugin.CanUninstall && plugin.Status === PluginStatus.Active) && (
<MenuItem onClick={onDisablePlugin}>
<ListItemIcon>
<BlockIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('DisablePlugin')}</ListItemText>
</MenuItem>
)}
{(plugin.CanUninstall && plugin.Status === PluginStatus.Disabled) && (
<MenuItem onClick={onEnablePlugin}>
<ListItemIcon>
<CheckCircleOutlineIcon />
</ListItemIcon>
<ListItemText>{globalize.translate('EnablePlugin')}</ListItemText>
</MenuItem>
)}
{plugin.CanUninstall && (
<MenuItem onClick={showUninstallConfirmDialog}>
<ListItemIcon>
<Delete />
</ListItemIcon>
<ListItemText>{globalize.translate('ButtonUninstall')}</ListItemText>
</MenuItem>
)}
</Menu>
<ConfirmDialog
open={isUninstallConfirmOpen}
title={globalize.translate('HeaderUninstallPlugin')}
text={globalize.translate('UninstallPluginConfirmation', plugin.Name || '')}
onCancel={onCloseUninstallConfirmDialog}
onConfirm={onUninstall}
confirmButtonColor='error'
confirmButtonText={globalize.translate('ButtonUninstall')}
/>
</>
<BaseCard
title={plugin.name}
to={pluginPage}
text={[plugin.version?.VersionNumber, plugin.status].filter(t => t).join(' ')}
image={plugin.imageUrl}
icon={<ExtensionIcon sx={{ width: 80, height: 80 }} />}
/>
);
};

View File

@@ -72,6 +72,9 @@ const PluginDetailsTable: FC<PluginDetailsTableProps> = ({
<TableCell>
{
(isRepositoryLoading && <Skeleton />)
|| (pluginDetails?.status && pluginDetails?.canUninstall === false
&& globalize.translate('LabelBundled')
)
|| (pluginDetails?.version?.repositoryUrl && (
<Link
component={RouterLink}

View File

@@ -1,15 +1,14 @@
import { PluginCategory } from './pluginCategory';
/** A mapping of category names used by the plugin repository to translation keys. */
export const CATEGORY_LABELS: Record<string, string> = {
Administration: 'HeaderAdmin',
Anime: 'Anime',
Authentication: 'LabelAuthProvider', // Legacy
Books: 'Books',
Channel: 'Channels', // Unused?
General: 'General',
LiveTV: 'LiveTV',
Metadata: 'LabelMetadata', // Legacy
MoviesAndShows: 'MoviesAndShows',
Music: 'TabMusic',
Subtitles: 'Subtitles',
Other: 'Other'
export const CATEGORY_LABELS: Record<PluginCategory, string> = {
[PluginCategory.Administration]: 'HeaderAdmin',
[PluginCategory.General]: 'General',
[PluginCategory.Anime]: 'Anime',
[PluginCategory.Books]: 'Books',
[PluginCategory.LiveTV]: 'LiveTV',
[PluginCategory.MoviesAndShows]: 'MoviesAndShows',
[PluginCategory.Music]: 'TabMusic',
[PluginCategory.Subtitles]: 'Subtitles',
[PluginCategory.Other]: 'Other'
};

View File

@@ -0,0 +1,12 @@
/** Supported plugin category values. */
export enum PluginCategory {
Administration = 'Administration',
General = 'General',
Anime = 'Anime',
Books = 'Books',
LiveTV = 'LiveTV',
MoviesAndShows = 'MoviesAndShows',
Music = 'Music',
Subtitles = 'Subtitles',
Other = 'Other'
}

View File

@@ -0,0 +1,6 @@
/** Options for filtering plugins based on the installation status. */
export enum PluginStatusOption {
All = 'All',
Available = 'Available',
Installed = 'Installed'
}

View File

@@ -2,6 +2,7 @@ import type { ConfigurationPageInfo, PluginStatus, VersionInfo } from '@jellyfin
export interface PluginDetails {
canUninstall: boolean
category?: string
description?: string
id: string
imageUrl?: string

View File

@@ -1,17 +0,0 @@
import type { PackageInfo } from '@jellyfin/sdk/lib/generated-client/models/package-info';
const getPackageCategories = (packages?: PackageInfo[]) => {
if (!packages) return [];
const categories: string[] = [];
for (const pkg of packages) {
if (pkg.category && !categories.includes(pkg.category)) {
categories.push(pkg.category);
}
}
return categories.sort((a, b) => a.localeCompare(b));
};
export default getPackageCategories;

View File

@@ -1,17 +0,0 @@
import type { PackageInfo } from '@jellyfin/sdk/lib/generated-client/models/package-info';
const getPackagesByCategory = (packages: PackageInfo[] | undefined, category: string) => {
if (!packages) return [];
return packages
.filter(pkg => pkg.category === category)
.sort((a, b) => {
if (a.name && b.name) {
return a.name.localeCompare(b.name);
} else {
return 0;
}
});
};
export default getPackagesByCategory;

View File

@@ -1,7 +1,7 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import dom from 'scripts/dom';
import dom from 'utils/dom';
const getNowPlayingImageUrl = (item: BaseItemDto) => {
if (!item.ServerId) return null;

View File

@@ -164,7 +164,7 @@ const NewTriggerForm: FunctionComponent<IProps> = ({ open, title, onClose, onAdd
<DialogActions>
<Button
onClick={onClose}
color='error'
variant='text'
>{globalize.translate('ButtonCancel')}</Button>
<Button type='submit'>{globalize.translate('Add')}</Button>
</DialogActions>

View File

@@ -8,7 +8,15 @@ const TaskProgress: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
const progress = task.CurrentProgressPercentage;
return (
<Box sx={{ display: 'flex', alignItems: 'center', height: '1.2rem', mr: 2 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
height: '1.2rem',
mr: 2,
minWidth: '170px'
}}
>
{progress != null ? (
<>
<Box sx={{ width: '100%', mr: 1 }}>

View File

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

View File

@@ -0,0 +1,22 @@
import { Api } from '@jellyfin/sdk';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { getChannelsApi } from '@jellyfin/sdk/lib/utils/api/channels-api';
import { ChannelsApiGetChannelsRequest } from '@jellyfin/sdk/lib/generated-client/api/channels-api';
import type { AxiosRequestConfig } from 'axios';
const fetchChannels = async (api: Api, params?: ChannelsApiGetChannelsRequest, options?: AxiosRequestConfig) => {
const response = await getChannelsApi(api).getChannels(params, options);
return response.data;
};
export const useChannels = (params?: ChannelsApiGetChannelsRequest) => {
const { api } = useApi();
return useQuery({
queryKey: [ 'Channels' ],
queryFn: ({ signal }) => fetchChannels(api!, params, { signal }),
enabled: !!api
});
};

View File

@@ -0,0 +1,15 @@
import { UserApiCreateUserByNameRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
export const useCreateUser = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: UserApiCreateUserByNameRequest) => (
getUserApi(api!)
.createUserByName(params)
)
});
};

View File

@@ -0,0 +1,22 @@
import { UserApiDeleteUserRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { QUERY_KEY } from 'hooks/useUsers';
import { queryClient } from 'utils/query/queryClient';
export const useDeleteUser = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: UserApiDeleteUserRequest) => (
getUserApi(api!)
.deleteUser(params)
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
}
});
};

View File

@@ -0,0 +1,22 @@
import { Api } from '@jellyfin/sdk';
import { LibraryApiGetMediaFoldersRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api';
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
const fetchLibraryMediaFolders = async (api: Api, params?: LibraryApiGetMediaFoldersRequest, options?: AxiosRequestConfig) => {
const response = await getLibraryApi(api).getMediaFolders(params, options);
return response.data;
};
export const useLibraryMediaFolders = (params?: LibraryApiGetMediaFoldersRequest) => {
const { api } = useApi();
return useQuery({
queryKey: ['LibraryMediaFolders'],
queryFn: ({ signal }) => fetchLibraryMediaFolders(api!, params, { signal }),
enabled: !!api
});
};

View File

@@ -0,0 +1,22 @@
import { Api } from '@jellyfin/sdk';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
import type { AxiosRequestConfig } from 'axios';
import type { NetworkConfiguration } from '@jellyfin/sdk/lib/generated-client/models/network-configuration';
const fetchNetworkConfig = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getConfigurationApi(api).getNamedConfiguration({ key: 'network' }, options);
return response.data as NetworkConfiguration;
};
export const useNetworkConfig = () => {
const { api } = useApi();
return useQuery({
queryKey: [ 'NetConfig' ],
queryFn: ({ signal }) => fetchNetworkConfig(api!, { signal }),
enabled: !!api
});
};

View File

@@ -0,0 +1,21 @@
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 fetchParentalRatings = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getLocalizationApi(api).getParentalRatings(options);
return response.data;
};
export const useParentalRatings = () => {
const { api } = useApi();
return useQuery({
queryKey: ['ParentalRatings'],
queryFn: ({ signal }) => fetchParentalRatings(api!, { signal }),
enabled: !!api
});
};

View File

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

View File

@@ -0,0 +1,22 @@
import { UserApiUpdateUserRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useUser';
export const useUpdateUser = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: UserApiUpdateUserRequest) => (
getUserApi(api!)
.updateUser(params)
),
onSuccess: (_, params) => {
void queryClient.invalidateQueries({
queryKey: [QUERY_KEY, params.userId]
});
}
});
};

View File

@@ -0,0 +1,23 @@
import { UserApiUpdateUserPolicyRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import { useMutation } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { queryClient } from 'utils/query/queryClient';
import { QUERY_KEY } from './useUser';
export const useUpdateUserPolicy = () => {
const { api } = useApi();
return useMutation({
mutationFn: (params: UserApiUpdateUserPolicyRequest) => (
getUserApi(api!)
.updateUserPolicy(params)
),
onSuccess: (_, params) => {
void queryClient.invalidateQueries({
queryKey: [QUERY_KEY, params.userId]
});
}
});
};

View File

@@ -0,0 +1,24 @@
import { Api } from '@jellyfin/sdk';
import { UserApiGetUserByIdRequest } from '@jellyfin/sdk/lib/generated-client/api/user-api';
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
import { useQuery } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import type { AxiosRequestConfig } from 'axios';
export const QUERY_KEY = 'User';
const fetchUser = async (api: Api, params: UserApiGetUserByIdRequest, options?: AxiosRequestConfig) => {
const response = await getUserApi(api).getUserById(params, options);
return response.data;
};
export const useUser = (params?: UserApiGetUserByIdRequest) => {
const { api } = useApi();
return useQuery({
queryKey: [ QUERY_KEY, params?.userId ],
queryFn: ({ signal }) => fetchUser(api!, params!, { signal }),
enabled: !!api && !!params
});
};

View File

@@ -9,9 +9,11 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'devices', type: AppType.Dashboard },
{ path: 'settings', type: AppType.Dashboard },
{ path: 'keys', type: AppType.Dashboard },
{ path: 'libraries', type: AppType.Dashboard },
{ path: 'libraries/display', type: AppType.Dashboard },
{ path: 'libraries/metadata', type: AppType.Dashboard },
{ path: 'libraries/nfo', type: AppType.Dashboard },
{ path: 'livetv', type: AppType.Dashboard },
{ path: 'livetv/recordings', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard },
{ path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard },
@@ -21,7 +23,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'playback/trickplay', type: AppType.Dashboard },
{ path: 'plugins', type: AppType.Dashboard },
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
{ path: 'plugins/catalog', type: AppType.Dashboard },
{ path: 'plugins/repositories', type: AppType.Dashboard },
{ path: 'tasks', type: AppType.Dashboard },
{ path: 'tasks/:id', page: 'tasks/task', type: AppType.Dashboard },

View File

@@ -9,13 +9,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'networking',
view: 'networking.html'
}
}, {
path: 'libraries',
pageProps: {
appType: AppType.Dashboard,
controller: 'library',
view: 'library.html'
}
}, {
path: 'livetv/guide',
pageProps: {
@@ -23,13 +16,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'livetvguideprovider',
view: 'livetvguideprovider.html'
}
}, {
path: 'livetv',
pageProps: {
appType: AppType.Dashboard,
controller: 'livetvstatus',
view: 'livetvstatus.html'
}
}, {
path: 'livetv/tuner',
pageProps: {

View File

@@ -2,9 +2,10 @@ import parseISO from 'date-fns/parseISO';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
import { useTheme } from '@mui/material/styles';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table';
import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
import { useSearchParams } from 'react-router-dom';
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
@@ -53,6 +54,8 @@ export const Component = () => {
const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails();
const theme = useTheme();
const UserCell = getUserCell(users);
const activityParams = useMemo(() => ({
@@ -156,8 +159,15 @@ export const Component = () => {
}
}, [ activityView, searchParams, setSearchParams ]);
// NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used
// https://github.com/KevinVandy/material-react-table/issues/1429
const mrtTheme = useMemo<Partial<MRT_Theme>>(() => ({
baseBackgroundColor: theme.palette.background.paper
}), [ theme ]);
const table = useMaterialReactTable({
...DEFAULT_TABLE_OPTIONS,
mrtTheme,
columns,
data: logEntries,

View File

@@ -102,7 +102,7 @@ export const Component = () => {
}).catch(() => {
// Server is still down
});
}, 5000);
}, 45000);
return () => {
clearInterval(serverCheckInterval);

View File

@@ -292,6 +292,7 @@ export const Component = () => {
name={BrandingOption.CustomCss}
label={globalize.translate('LabelCustomCss')}
helperText={globalize.translate('LabelCustomCssHelp')}
spellCheck={false}
value={brandingOptions?.CustomCss}
onChange={setBrandingOption}
slotProps={{

View File

@@ -4,9 +4,10 @@ import Edit from '@mui/icons-material/Edit';
import Box from '@mui/material/Box/Box';
import Button from '@mui/material/Button/Button';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip/Tooltip';
import parseISO from 'date-fns/parseISO';
import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table';
import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
import React, { useCallback, useMemo, useState } from 'react';
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
@@ -41,6 +42,7 @@ export const Component = () => {
data?.Items || []
), [ data ]);
const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails();
const theme = useTheme();
const [ isDeleteConfirmOpen, setIsDeleteConfirmOpen ] = useState(false);
const [ isDeleteAllConfirmOpen, setIsDeleteAllConfirmOpen ] = useState(false);
@@ -137,8 +139,15 @@ export const Component = () => {
}
], [ UserCell, userNames ]);
// NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used
// https://github.com/KevinVandy/material-react-table/issues/1429
const mrtTheme = useMemo<Partial<MRT_Theme>>(() => ({
baseBackgroundColor: theme.palette.background.paper
}), [ theme ]);
const mrTable = useMaterialReactTable({
...DEFAULT_TABLE_OPTIONS,
mrtTheme,
columns,
data: devices,
@@ -184,16 +193,25 @@ export const Component = () => {
positionActionsColumn: 'last',
displayColumnDefOptions: {
'mrt-row-actions': {
header: ''
header: '',
size: 100
}
},
renderRowActions: ({ row, table }) => {
const isDeletable = api && row.original.Id && api.deviceInfo.id === row.original.Id;
return (
<Box sx={{ display: 'flex', gap: 1 }}>
<Box
sx={{
display: 'flex',
gap: 1,
'&&': {
backgroundColor: 'transparent !important'
}
}}
>
<Tooltip title={globalize.translate('Edit')}>
<IconButton
// eslint-disable-next-line react/jsx-no-bind
// eslint-disable-next-line react/jsx-no-bind
onClick={() => table.setEditingRow(row)}
>
<Edit />

View File

@@ -1,14 +1,12 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid2';
import Grid from '@mui/material/Grid';
import ServerPathWidget from '../components/widgets/ServerPathWidget';
import ServerInfoWidget from '../components/widgets/ServerInfoWidget';
import ActivityLogWidget from '../components/widgets/ActivityLogWidget';
import AlertsLogWidget from '../components/widgets/AlertsLogWidget';
import useTheme from '@mui/material/styles/useTheme';
import useMediaQuery from '@mui/material/useMediaQuery';
import Stack from '@mui/material/Stack';
import useShutdownServer from '../features/system/api/useShutdownServer';
import useRestartServer from '../features/system/api/useRestartServer';
@@ -18,11 +16,9 @@ import RunningTasksWidget from '../components/widgets/RunningTasksWidget';
import DevicesWidget from '../components/widgets/DevicesWidget';
import { useStartTask } from '../features/tasks/api/useStartTask';
import ItemCountsWidget from '../components/widgets/ItemCountsWidget';
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
export const Component = () => {
const theme = useTheme();
const isMedium = useMediaQuery(theme.breakpoints.only('md'));
const isExtraLarge = useMediaQuery(theme.breakpoints.only('xl'));
const [ isRestartConfirmDialogOpen, setIsRestartConfirmDialogOpen ] = useState(false);
const [ isShutdownConfirmDialogOpen, setIsShutdownConfirmDialogOpen ] = useState(false);
const startTask = useStartTask();
@@ -31,6 +27,10 @@ export const Component = () => {
const { data: tasks } = useLiveTasks({ isHidden: false });
const librariesTask = useMemo(() => (
tasks?.find((value) => value.Key === 'RefreshLibrary')
), [ tasks ]);
const promptRestart = useCallback(() => {
setIsRestartConfirmDialogOpen(true);
}, []);
@@ -93,36 +93,28 @@ export const Component = () => {
/>
<Box className='content-primary'>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 12, lg: 8, xl: 6 }}>
<Grid item xs={12} md={7} lg={7} xl={6}>
<Stack spacing={3}>
<ServerInfoWidget
onScanLibrariesClick={onScanLibraries}
onRestartClick={promptRestart}
onShutdownClick={promptShutdown}
isScanning={librariesTask?.State !== TaskState.Idle}
/>
<ItemCountsWidget />
<RunningTasksWidget tasks={tasks} />
<DevicesWidget />
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 4, xl: 3 }}>
<Grid item xs={12} md={5} lg={5} xl={3}>
<ActivityLogWidget />
</Grid>
{isMedium || isExtraLarge ? (
<Grid size={{ md: 6, xl: 3 }}>
<Stack spacing={3}>
<AlertsLogWidget />
<ServerPathWidget />
</Stack>
</Grid>
) : (
<Grid size={12}>
<Stack spacing={3}>
<AlertsLogWidget />
<ServerPathWidget />
</Stack>
</Grid>
)}
<Grid item xs={12} md={6} lg={12} xl={3}>
<Stack spacing={3}>
<AlertsLogWidget />
<ServerPathWidget />
</Stack>
</Grid>
</Grid>
</Box>
</Page>

View File

@@ -2,31 +2,34 @@ import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/mode
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import parseISO from 'date-fns/parseISO';
import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table';
import React, { useCallback, useMemo } from 'react';
import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
import React, { useCallback, useMemo, useState } from 'react';
import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell';
import TablePage, { DEFAULT_TABLE_OPTIONS } from 'apps/dashboard/components/table/TablePage';
import { useApiKeys } from 'apps/dashboard/features/keys/api/useApiKeys';
import { useRevokeKey } from 'apps/dashboard/features/keys/api/useRevokeKey';
import { useCreateKey } from 'apps/dashboard/features/keys/api/useCreateKey';
import confirm from 'components/confirm/confirm';
import prompt from 'components/prompt/prompt';
import { useApi } from 'hooks/useApi';
import globalize from 'lib/globalize';
import InputDialog from 'components/InputDialog';
import ConfirmDialog from 'components/ConfirmDialog';
export const Component = () => {
const { api } = useApi();
const [ isCreateApiKeyPromptOpen, setIsCreateApiKeyPromptOpen ] = useState(false);
const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState(false);
const [ apiKeyToDelete, setApiKeyToDelete ] = useState('');
const { data, isLoading } = useApiKeys();
const keys = useMemo(() => (
data?.Items || []
), [ data ]);
const revokeKey = useRevokeKey();
const createKey = useCreateKey();
const theme = useTheme();
const columns = useMemo<MRT_ColumnDef<AuthenticationInfo>[]>(() => [
{
@@ -49,8 +52,15 @@ export const Component = () => {
}
], []);
// NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used
// https://github.com/KevinVandy/material-react-table/issues/1429
const mrtTheme = useMemo<Partial<MRT_Theme>>(() => ({
baseBackgroundColor: theme.palette.background.paper
}), [ theme ]);
const table = useMaterialReactTable({
...DEFAULT_TABLE_OPTIONS,
mrtTheme,
columns,
data: keys,
@@ -96,41 +106,72 @@ export const Component = () => {
});
const onRevokeKey = useCallback((accessToken: string) => {
if (!api) return;
confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () {
revokeKey.mutate({
key: accessToken
});
}).catch(err => {
console.error('[apikeys] failed to show confirmation dialog', err);
});
}, [api, revokeKey]);
setApiKeyToDelete(accessToken);
setIsConfirmDeleteOpen(true);
}, []);
const showNewKeyPopup = useCallback(() => {
if (!api) return;
setIsCreateApiKeyPromptOpen(true);
}, []);
prompt({
title: globalize.translate('HeaderNewApiKey'),
label: globalize.translate('LabelAppName'),
description: globalize.translate('LabelAppNameExample')
}).then((value) => {
createKey.mutate({
app: value
});
}).catch(() => {
// popup closed
const onCreateApiKeyPromptClose = useCallback(() => {
setIsCreateApiKeyPromptOpen(false);
}, []);
const onConfirmDelete = useCallback(() => {
revokeKey.mutate({
key: apiKeyToDelete
}, {
onSettled: () => {
setApiKeyToDelete('');
setIsConfirmDeleteOpen(false);
}
});
}, [api, createKey]);
}, [ revokeKey, apiKeyToDelete ]);
const onConfirmDeleteCancel = useCallback(() => {
setApiKeyToDelete('');
setIsConfirmDeleteOpen(false);
}, []);
const onConfirmCreate = useCallback((name: string) => {
createKey.mutate({
app: name
}, {
onSettled: () => {
setIsCreateApiKeyPromptOpen(false);
}
});
}, [ createKey ]);
return (
<TablePage
id='apiKeysPage'
title={globalize.translate('HeaderApiKeys')}
subtitle={globalize.translate('HeaderApiKeysHelp')}
className='mainAnimatedPage type-interior'
table={table}
/>
<>
<ConfirmDialog
open={isConfirmDeleteOpen}
title={globalize.translate('HeaderConfirmRevokeApiKey')}
text={globalize.translate('MessageConfirmRevokeApiKey')}
confirmButtonColor='error'
confirmButtonText={globalize.translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onConfirmDeleteCancel}
/>
<InputDialog
open={isCreateApiKeyPromptOpen}
title={globalize.translate('HeaderNewApiKey')}
label={globalize.translate('LabelAppName')}
helperText={globalize.translate('LabelAppNameExample')}
confirmButtonText={globalize.translate('Create')}
onConfirm={onConfirmCreate}
onClose={onCreateApiKeyPromptClose}
/>
<TablePage
id='apiKeysPage'
title={globalize.translate('HeaderApiKeys')}
subtitle={globalize.translate('HeaderApiKeysHelp')}
className='mainAnimatedPage type-interior'
table={table}
/>
</>
);
};

View File

@@ -0,0 +1,108 @@
import React, { useCallback, useMemo } from 'react';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import { useVirtualFolders } from 'apps/dashboard/features/libraries/api/useVirtualFolders';
import useLiveTasks from 'apps/dashboard/features/tasks/hooks/useLiveTasks';
import { useStartTask } from 'apps/dashboard/features/tasks/api/useStartTask';
import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress';
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
import Grid from '@mui/material/Grid';
import LibraryCard from 'apps/dashboard/features/libraries/components/LibraryCard';
import Loading from 'components/loading/LoadingComponent';
import MediaLibraryCreator from 'components/mediaLibraryCreator/mediaLibraryCreator';
import getCollectionTypeOptions from 'apps/dashboard/features/libraries/utils/collectionTypeOptions';
import { queryClient } from 'utils/query/queryClient';
import RefreshIcon from '@mui/icons-material/Refresh';
import Add from '@mui/icons-material/Add';
export const Component = () => {
const { data: virtualFolders, isPending: isVirtualFoldersPending } = useVirtualFolders();
const startTask = useStartTask();
const { data: tasks, isPending: isLiveTasksPending } = useLiveTasks({ isHidden: false });
const librariesTask = useMemo(() => (
tasks?.find((value) => value.Key === 'RefreshLibrary')
), [ tasks ]);
const showMediaLibraryCreator = useCallback(() => {
const mediaLibraryCreator = new MediaLibraryCreator({
collectionTypeOptions: getCollectionTypeOptions(),
refresh: true
}) as Promise<boolean>;
void mediaLibraryCreator.then((hasChanges: boolean) => {
if (hasChanges) {
void queryClient.invalidateQueries({
queryKey: ['VirtualFolders']
});
}
});
}, []);
const onScanLibraries = useCallback(() => {
if (librariesTask?.Id) {
startTask.mutate({
taskId: librariesTask.Id
});
}
}, [ startTask, librariesTask ]);
if (isVirtualFoldersPending || isLiveTasksPending) return <Loading />;
return (
<Page
id='mediaLibraryPage'
title={globalize.translate('HeaderLibraries')}
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Stack spacing={3} mt={2}>
<Stack direction='row' alignItems={'center'} spacing={1.5}>
<Button
startIcon={<Add />}
onClick={showMediaLibraryCreator}
>
{globalize.translate('ButtonAddMediaLibrary')}
</Button>
<Button
onClick={onScanLibraries}
startIcon={<RefreshIcon />}
loading={librariesTask && librariesTask.State !== TaskState.Idle}
loadingPosition='start'
variant='outlined'
>
{globalize.translate('ButtonScanAllLibraries')}
</Button>
{(librariesTask && librariesTask.State == TaskState.Running) && (
<TaskProgress task={librariesTask} />
)}
</Stack>
<Box>
<Grid container spacing={2}>
{virtualFolders?.map(virtualFolder => (
<Grid
key={virtualFolder?.ItemId}
item
xs={12}
sm={6}
md={3}
lg={2.4}
>
<LibraryCard
virtualFolder={virtualFolder}
/>
</Grid>
))}
</Grid>
</Box>
</Stack>
</Box>
</Page>
);
};
Component.displayName = 'LibrariesPage';

View File

@@ -0,0 +1,179 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Page from 'components/Page';
import { useNamedConfiguration } from 'hooks/useNamedConfiguration';
import type { LiveTvOptions } from '@jellyfin/sdk/lib/generated-client/models/live-tv-options';
import globalize from 'lib/globalize';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Loading from 'components/loading/LoadingComponent';
import TunerDeviceCard from 'apps/dashboard/features/livetv/components/TunerDeviceCard';
import useLiveTasks from 'apps/dashboard/features/tasks/hooks/useLiveTasks';
import Button from '@mui/material/Button';
import RefreshIcon from '@mui/icons-material/Refresh';
import AddIcon from '@mui/icons-material/Add';
import { Link, useNavigate } from 'react-router-dom';
import { useStartTask } from 'apps/dashboard/features/tasks/api/useStartTask';
import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state';
import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemText from '@mui/material/ListItemText';
import Alert from '@mui/material/Alert';
import List from '@mui/material/List';
import Provider from 'apps/dashboard/features/livetv/components/Provider';
import Grid from '@mui/material/Grid';
const CONFIG_KEY = 'livetv';
export const Component = () => {
const navigate = useNavigate();
const {
data: config,
isPending: isConfigPending,
isError: isConfigError
} = useNamedConfiguration<LiveTvOptions>(CONFIG_KEY);
const {
data: tasks,
isPending: isTasksPending,
isError: isTasksError
} = useLiveTasks({ isHidden: false });
const providerButtonRef = useRef<HTMLButtonElement | null>(null);
const [ anchorEl, setAnchorEl ] = useState<HTMLButtonElement | null>(null);
const [ isMenuOpen, setIsMenuOpen ] = useState(false);
const startTask = useStartTask();
const navigateToSchedulesDirect = useCallback(() => {
navigate('/dashboard/livetv/guide?type=schedulesdirect');
}, [ navigate ]);
const navigateToXMLTV = useCallback(() => {
navigate('/dashboard/livetv/guide?type=xmltv');
}, [ navigate ]);
const showProviderMenu = useCallback(() => {
setAnchorEl(providerButtonRef.current);
setIsMenuOpen(true);
}, []);
const onMenuClose = useCallback(() => {
setAnchorEl(null);
setIsMenuOpen(false);
}, []);
const refreshGuideTask = useMemo(() => (
tasks?.find((value) => value.Key === 'RefreshGuide')
), [ tasks ]);
const refreshGuideData = useCallback(() => {
if (refreshGuideTask?.Id) {
startTask.mutate({
taskId: refreshGuideTask.Id
});
}
}, [ startTask, refreshGuideTask ]);
if (isConfigPending || isTasksPending) return <Loading />;
return (
<Page
id='liveTvStatusPage'
title={globalize.translate('LiveTV')}
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
{(isConfigError || isTasksError) ? (
<Alert severity='error'>{globalize.translate('HeaderError')}</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h2'>{globalize.translate('HeaderTunerDevices')}</Typography>
<Button
sx={{ alignSelf: 'flex-start' }}
startIcon={<AddIcon />}
component={Link}
to='/dashboard/livetv/tuner'
>
{globalize.translate('ButtonAddTunerDevice')}
</Button>
<Box>
<Grid container spacing={2}>
{config.TunerHosts?.map(tunerHost => (
<Grid
key={tunerHost.Id}
item
xs={12}
sm={6}
md={3}
lg={2.4}
>
<TunerDeviceCard
key={tunerHost.Id}
tunerHost={tunerHost}
/>
</Grid>
))}
</Grid>
</Box>
<Typography variant='h2'>{globalize.translate('HeaderGuideProviders')}</Typography>
<Stack sx={{ alignSelf: 'flex-start' }} spacing={2}>
<Stack direction='row' spacing={1.5}>
<Button
sx={{ alignSelf: 'flex-start' }}
startIcon={<AddIcon />}
onClick={showProviderMenu}
ref={providerButtonRef}
>
{globalize.translate('ButtonAddProvider')}
</Button>
<Button
sx={{ alignSelf: 'flex-start' }}
startIcon={<RefreshIcon />}
variant='outlined'
onClick={refreshGuideData}
loading={refreshGuideTask && refreshGuideTask.State === TaskState.Running}
loadingPosition='start'
>
{globalize.translate('ButtonRefreshGuideData')}
</Button>
</Stack>
{(refreshGuideTask && refreshGuideTask.State === TaskState.Running) && (
<TaskProgress task={refreshGuideTask} />
)}
</Stack>
<Menu
anchorEl={anchorEl}
open={isMenuOpen}
onClose={onMenuClose}
>
<MenuItem onClick={navigateToSchedulesDirect}>
<ListItemText>Schedules Direct</ListItemText>
</MenuItem>
<MenuItem onClick={navigateToXMLTV}>
<ListItemText>XMLTV</ListItemText>
</MenuItem>
</Menu>
{(config.ListingProviders && config.ListingProviders?.length > 0) && (
<List sx={{ backgroundColor: 'background.paper' }}>
{config.ListingProviders?.map(provider => (
<Provider
key={provider.Id}
provider={provider}
/>
))}
</List>
)}
</Stack>
)}
</Box>
</Page>
);
};
Component.displayName = 'LiveTvPage';

View File

@@ -1,6 +1,6 @@
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useServerLog } from 'apps/dashboard/features/logs/api/useServerLog';
import Alert from '@mui/material/Alert';
@@ -13,8 +13,8 @@ import Typography from '@mui/material/Typography';
import ContentCopy from '@mui/icons-material/ContentCopy';
import FileDownload from '@mui/icons-material/FileDownload';
import globalize from 'lib/globalize';
import toast from 'components/toast/toast';
import { copy } from 'scripts/clipboard';
import Toast from 'apps/dashboard/components/Toast';
export const Component = () => {
const { file: fileName } = useParams();
@@ -24,13 +24,18 @@ export const Component = () => {
data: log,
refetch
} = useServerLog(fileName ?? '');
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
const retry = useCallback(() => refetch(), [refetch]);
const handleToastClose = useCallback(() => {
setIsCopiedToastOpen(false);
}, []);
const copyToClipboard = useCallback(async () => {
if (log) {
await copy(log);
toast({ text: globalize.translate('CopyLogSuccess') });
setIsCopiedToastOpen(true);
}
}, [log]);
@@ -52,7 +57,12 @@ export const Component = () => {
title={fileName}
className='mainAnimatedPage type-interior'
>
<Container className='content-primary'>
<Toast
open={isCopiedToastOpen}
onClose={handleToastClose}
message={globalize.translate('CopyLogSuccess')}
/>
<Container className='content-primary' maxWidth={false}>
<Box>
<Typography variant='h1'>{fileName}</Typography>
@@ -96,7 +106,14 @@ export const Component = () => {
<Paper sx={{ mt: 2 }}>
<code>
<pre style={{ overflow:'auto', margin: 0, padding: '16px' }}>{log}</pre>
<pre style={{
overflow:'auto',
margin: 0,
padding: '16px',
whiteSpace: 'pre-wrap'
}}>
{log}
</pre>
</code>
</Paper>
</>

View File

@@ -471,19 +471,21 @@ export const Component = () => {
{(hardwareAccelType === 'none' || isHwaSelected) && (
<>
<FormControl>
<FormControlLabel
label={globalize.translate('EnableTonemapping')}
control={
<Checkbox
name='EnableTonemapping'
checked={config.EnableTonemapping}
onChange={onCheckboxChange}
/>
}
/>
<FormHelperText>{globalize.translate(isHwaSelected ? 'AllowTonemappingHelp' : 'AllowTonemappingSoftwareHelp')}</FormHelperText>
</FormControl>
{isHwaSelected && (
<FormControl>
<FormControlLabel
label={globalize.translate('EnableTonemapping')}
control={
<Checkbox
name='EnableTonemapping'
checked={config.EnableTonemapping}
onChange={onCheckboxChange}
/>
}
/>
<FormHelperText>{globalize.translate('AllowTonemappingHelp')}</FormHelperText>
</FormControl>
)}
<TextField
name='TonemappingAlgorithm'

View File

@@ -1,96 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { usePackages } from 'apps/dashboard/features/plugins/api/usePackages';
import Loading from 'components/loading/LoadingComponent';
import getPackageCategories from 'apps/dashboard/features/plugins/utils/getPackageCategories';
import Stack from '@mui/material/Stack';
import getPackagesByCategory from 'apps/dashboard/features/plugins/utils/getPackagesByCategory';
import PackageCard from 'apps/dashboard/features/plugins/components/PackageCard';
import Grid from '@mui/material/Grid2';
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import Settings from '@mui/icons-material/Settings';
import { Link } from 'react-router-dom';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
export const Component = () => {
const { data: packages, isPending: isPackagesPending } = usePackages();
const [ searchQuery, setSearchQuery ] = useState('');
const filteredPackages = useMemo(() => {
return packages?.filter(i => i.name?.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()));
}, [ packages, searchQuery ]);
const packageCategories = getPackageCategories(filteredPackages);
const updateSearchQuery = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
}, []);
const getCategoryLabel = (category: string) => {
const categoryKey = category.replace(/\s/g, '');
if (CATEGORY_LABELS[categoryKey]) {
return globalize.translate(CATEGORY_LABELS[categoryKey]);
}
console.warn('[AvailablePlugins] unmapped category label', category);
return category;
};
if (isPackagesPending) {
return <Loading />;
}
return (
<Page
id='pluginCatalogPage'
className='mainAnimatedPage type-interior'
title={globalize.translate('TabCatalog')}
>
<Box className='content-primary'>
<Stack spacing={3}>
<Stack direction='row' gap={1}>
<Typography variant='h1'>{globalize.translate('TabCatalog')}</Typography>
<IconButton
component={Link}
to='/dashboard/plugins/repositories'
sx={{
backgroundColor: 'background.paper'
}}
>
<Settings />
</IconButton>
</Stack>
<TextField
label={globalize.translate('Search')}
value={searchQuery}
onChange={updateSearchQuery}
/>
{packageCategories.map(category => (
<Stack key={category} spacing={2}>
<Typography variant='h2'>{getCategoryLabel(category)}</Typography>
<Grid container spacing={2} columns={{ xs: 1, sm: 4, md: 9, lg: 8, xl: 10 }}>
{getPackagesByCategory(filteredPackages, category).map(pkg => (
<Grid key={pkg.guid} size={{ xs: 1, sm: 2, md: 3, lg: 2 }}>
<PackageCard
pkg={pkg}
/>
</Grid>
))}
</Grid>
</Stack>
))}
</Stack>
</Box>
</Page>
);
};
Component.displayName = 'PluginsCatalogPage';

View File

@@ -1,44 +1,93 @@
import React, { useCallback, useMemo, useState } from 'react';
import Box from '@mui/material/Box';
import Page from 'components/Page';
import globalize from 'lib/globalize';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import { usePlugins } from 'apps/dashboard/features/plugins/api/usePlugins';
import Loading from 'components/loading/LoadingComponent';
import Alert from '@mui/material/Alert';
import Grid from '@mui/material/Grid2';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import React, { useCallback, useMemo } from 'react';
import { Link } from 'react-router-dom';
import SearchInput from 'apps/dashboard/components/SearchInput';
import { usePluginDetails } from 'apps/dashboard/features/plugins/api/usePluginDetails';
import NoPluginResults from 'apps/dashboard/features/plugins/components/NoPluginResults';
import PluginCard from 'apps/dashboard/features/plugins/components/PluginCard';
import { useConfigurationPages } from 'apps/dashboard/features/plugins/api/useConfigurationPages';
import { findBestConfigurationPage } from 'apps/dashboard/features/plugins/api/configurationPage';
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
import { PluginCategory } from 'apps/dashboard/features/plugins/constants/pluginCategory';
import { PluginStatusOption } from 'apps/dashboard/features/plugins/constants/pluginStatusOption';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import useSearchParam from 'hooks/useSearchParam';
import globalize from 'lib/globalize';
/**
* The list of primary/main categories.
* Any category not in this list will be added to the "other" category.
*/
const MAIN_CATEGORIES = [
PluginCategory.Administration.toLowerCase(),
PluginCategory.General.toLowerCase(),
PluginCategory.Anime.toLowerCase(),
PluginCategory.Books.toLowerCase(),
PluginCategory.LiveTV.toLowerCase(),
PluginCategory.MoviesAndShows.toLowerCase(),
PluginCategory.Music.toLowerCase(),
PluginCategory.Subtitles.toLowerCase()
];
const CATEGORY_PARAM = 'category';
const QUERY_PARAM = 'query';
const STATUS_PARAM = 'status';
export const Component = () => {
const {
data: plugins,
isPending,
isError
} = usePlugins();
const {
data: configurationPages,
isError: isConfigurationPagesError,
isPending: isConfigurationPagesPending
} = useConfigurationPages();
const [ searchQuery, setSearchQuery ] = useState('');
data: pluginDetails,
isError,
isPending
} = usePluginDetails();
const [ category, setCategory ] = useSearchParam(CATEGORY_PARAM);
const [ searchQuery, setSearchQuery ] = useSearchParam(QUERY_PARAM);
const [ status, setStatus ] = useSearchParam(STATUS_PARAM, PluginStatusOption.Installed);
const onSearchChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setSearchQuery(event.target.value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onViewAll = useCallback(() => {
if (category) setCategory('');
else setStatus(PluginStatusOption.All);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ category ]);
const filteredPlugins = useMemo(() => {
if (plugins) {
return plugins.filter(i => i.Name?.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()));
if (pluginDetails) {
let filtered = pluginDetails;
if (status === PluginStatusOption.Installed) {
filtered = filtered.filter(p => p.status);
} else if (status === PluginStatusOption.Available) {
filtered = filtered.filter(p => !p.status);
}
if (category) {
if (category === PluginCategory.Other.toLowerCase()) {
filtered = filtered.filter(p => (
p.category && !MAIN_CATEGORIES.includes(p.category.toLowerCase())
));
} else {
filtered = filtered.filter(p => p.category?.toLowerCase() === category);
}
}
return filtered
.filter(i => i.name?.toLowerCase().includes(searchQuery.toLowerCase()));
} else {
return [];
}
}, [ plugins, searchQuery ]);
}, [ category, pluginDetails, searchQuery, status ]);
if (isPending || isConfigurationPagesPending) {
if (isPending) {
return <Loading />;
}
@@ -49,31 +98,161 @@ export const Component = () => {
className='type-interior mainAnimatedPage'
>
<Box className='content-primary'>
{isError || isConfigurationPagesError ? (
<Alert severity='error'>{globalize.translate('PluginsLoadError')}</Alert>
{isError ? (
<Alert
severity='error'
sx={{ marginBottom: 2 }}
>
{globalize.translate('PluginsLoadError')}
</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('TabMyPlugins')}
</Typography>
<Stack spacing={2}>
<Stack
direction='row'
sx={{
flexWrap: {
xs: 'wrap',
sm: 'nowrap'
}
}}
>
<Typography
variant='h1'
component='span'
sx={{
flexGrow: 1,
verticalAlign: 'middle'
}}
>
{globalize.translate('TabPlugins')}
</Typography>
<TextField
label={globalize.translate('Search')}
value={searchQuery}
onChange={onSearchChange}
/>
<Button
component={Link}
to='/dashboard/plugins/repositories'
variant='outlined'
sx={{
marginLeft: 2
}}
>
{globalize.translate('ManageRepositories')}
</Button>
<Box
sx={{
display: 'flex',
justifyContent: 'end',
marginTop: {
xs: 2,
sm: 0
},
marginLeft: {
xs: 0,
sm: 2
},
width: {
xs: '100%',
sm: 'auto'
}
}}
>
<SearchInput
label={globalize.translate('Search')}
value={searchQuery}
onChange={onSearchChange}
/>
</Box>
</Stack>
<Box>
<Grid container spacing={2} columns={{ xs: 1, sm: 4, md: 9, lg: 8, xl: 10 }}>
{filteredPlugins.map(plugin => (
<Grid key={plugin.Id} size={{ xs: 1, sm: 2, md: 3, lg: 2 }}>
<PluginCard
plugin={plugin}
configurationPage={findBestConfigurationPage(configurationPages, plugin.Id || '')}
/>
</Grid>
<Stack
direction='row'
spacing={1}
sx={{
marginLeft: '-1rem',
marginRight: '-1rem',
paddingLeft: '1rem',
paddingRight: '1rem',
paddingBottom: {
xs: 1,
md: 0.5
},
overflowX: 'auto'
}}
>
<Chip
color={status === PluginStatusOption.All ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setStatus(PluginStatusOption.All)}
label={globalize.translate('All')}
/>
<Chip
color={status === PluginStatusOption.Available ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setStatus(PluginStatusOption.Available)}
label={globalize.translate('LabelAvailable')}
/>
<Chip
color={status === PluginStatusOption.Installed ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setStatus(PluginStatusOption.Installed)}
label={globalize.translate('LabelInstalled')}
/>
<Divider orientation='vertical' flexItem />
<Chip
color={!category ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setCategory('')}
label={globalize.translate('All')}
/>
{Object.values(PluginCategory).map(c => (
<Chip
key={c}
color={category === c.toLowerCase() ? 'primary' : undefined}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => setCategory(c.toLowerCase())}
label={globalize.translate(CATEGORY_LABELS[c as PluginCategory])}
/>
))}
</Grid>
</Stack>
<Divider />
</Box>
<Box>
{filteredPlugins.length > 0 ? (
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
// eslint-disable-next-line @typescript-eslint/no-deprecated
<Grid container spacing={2}>
{filteredPlugins.map(plugin => (
// NOTE: Legacy Grid is required due to lack of gap support in JMP on some OSs
// eslint-disable-next-line @typescript-eslint/no-deprecated
<Grid
key={plugin.id}
item
xs={12}
sm={6}
md={4}
lg={3}
xl={2}
>
<PluginCard
plugin={plugin}
/>
</Grid>
))}
</Grid>
) : (
<NoPluginResults
isFiltered={!!category || status !== PluginStatusOption.All}
onViewAll={onViewAll}
query={searchQuery}
/>
)}
</Box>
</Stack>
)}

View File

@@ -1,15 +1,15 @@
import { PluginStatus } from '@jellyfin/sdk/lib/generated-client/models/plugin-status';
import type { VersionInfo } from '@jellyfin/sdk/lib/generated-client/models/version-info';
import Alert from '@mui/material/Alert/Alert';
import Button from '@mui/material/Button/Button';
import Container from '@mui/material/Container/Container';
import FormControlLabel from '@mui/material/FormControlLabel/FormControlLabel';
import FormGroup from '@mui/material/FormGroup/FormGroup';
import Grid from '@mui/material/Grid2/Grid2';
import Skeleton from '@mui/material/Skeleton/Skeleton';
import Stack from '@mui/material/Stack/Stack';
import Switch from '@mui/material/Switch/Switch';
import Typography from '@mui/material/Typography/Typography';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import Grid from '@mui/material/Grid';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import Delete from '@mui/icons-material/Delete';
import Download from '@mui/icons-material/Download';
import Extension from '@mui/icons-material/Extension';
@@ -56,6 +56,7 @@ const PluginPage: FC = () => {
const [ isEnabledOverride, setIsEnabledOverride ] = useState<boolean>();
const [ isInstallConfirmOpen, setIsInstallConfirmOpen ] = useState(false);
const [ isInstalling, setIsInstalling ] = useState(false);
const [ isUninstallConfirmOpen, setIsUninstallConfirmOpen ] = useState(false);
const [ pendingInstallVersion, setPendingInstallVersion ] = useState<VersionInfo>();
@@ -115,7 +116,7 @@ const PluginPage: FC = () => {
isEnabled: (isEnabledOverride && pluginInfo?.Status === PluginStatus.Restart)
?? pluginInfo?.Status !== PluginStatus.Disabled,
name: pluginName || pluginInfo?.Name || packageInfo?.name,
owner: packageInfo?.owner,
owner: pluginInfo?.CanUninstall === false ? 'jellyfin' : packageInfo?.owner,
status: pluginInfo?.Status,
configurationPage: findBestConfigurationPage(configurationPages || [], pluginId),
version,
@@ -168,7 +169,8 @@ const PluginPage: FC = () => {
alerts.push({ messageKey: 'PluginLoadConfigError' });
}
if (isPackageInfoError) {
// Don't show package load error for built-in plugins
if (!isPluginsLoading && pluginDetails?.canUninstall && isPackageInfoError) {
alerts.push({
severity: 'warning',
messageKey: 'PluginLoadRepoError'
@@ -188,6 +190,8 @@ const PluginPage: FC = () => {
isConfigurationPagesError,
isPackageInfoError,
isPluginsError,
isPluginsLoading,
pluginDetails?.canUninstall,
uninstallPlugin.isError
]);
@@ -243,6 +247,7 @@ const PluginPage: FC = () => {
console.debug('[PluginPage] installing plugin', installVersion);
setIsInstalling(true);
installPlugin.mutate({
name: pluginDetails.name,
assemblyGuid: pluginDetails.id,
@@ -250,6 +255,7 @@ const PluginPage: FC = () => {
repositoryUrl: installVersion.repositoryUrl
}, {
onSettled: () => {
setIsInstalling(false);
setPendingInstallVersion(undefined);
disablePlugin.reset();
enablePlugin.reset();
@@ -310,13 +316,17 @@ const PluginPage: FC = () => {
<Container className='content-primary'>
{alertMessages.map(({ severity = 'error', messageKey }) => (
<Alert key={messageKey} severity={severity}>
<Alert
key={messageKey}
severity={severity}
sx={{ marginBottom: 2 }}
>
{globalize.translate(messageKey)}
</Alert>
))}
<Grid container spacing={2} sx={{ marginTop: 0 }}>
<Grid size={{ xs: 12, lg: 8 }}>
<Grid item xs={12} lg={8}>
<Stack spacing={2}>
<Typography variant='h1'>
{pluginDetails?.name || pluginName}
@@ -332,7 +342,7 @@ const PluginPage: FC = () => {
</Stack>
</Grid>
<Grid size={{ lg: 4 }} sx={{ display: { xs: 'none', lg: 'initial' } }}>
<Grid item lg={4} sx={{ display: { xs: 'none', lg: 'initial' } }}>
<Image
isLoading={isLoading}
alt={pluginDetails?.name}
@@ -341,7 +351,7 @@ const PluginPage: FC = () => {
/>
</Grid>
<Grid size={{ xs: 12, lg: 8 }} sx={{ order: { xs: 1, lg: 'initial' } }}>
<Grid item xs={12} lg={8} sx={{ order: { xs: 1, lg: 'initial' } }}>
{!!pluginDetails?.versions.length && (
<>
<Typography variant='h3' sx={{ marginBottom: 2 }}>
@@ -355,7 +365,7 @@ const PluginPage: FC = () => {
)}
</Grid>
<Grid size={{ xs: 12, lg: 4 }}>
<Grid item xs={12} lg={4}>
<Stack spacing={2} direction={{ xs: 'column', sm: 'row-reverse', lg: 'column' }}>
<Stack spacing={1} sx={{ flexBasis: '50%' }}>
{!isLoading && !pluginDetails?.status && (
@@ -367,6 +377,7 @@ const PluginPage: FC = () => {
<Button
startIcon={<Download />}
onClick={onInstall()}
loading={isInstalling}
>
{globalize.translate('HeaderInstall')}
</Button>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { RouteObject } from 'react-router-dom';
import { Navigate, RouteObject } from 'react-router-dom';
import ConnectionRequired from 'components/ConnectionRequired';
import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
@@ -26,7 +26,11 @@ export const DASHBOARD_APP_ROUTES: RouteObject[] = [
path: DASHBOARD_APP_PATHS.Dashboard,
children: [
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute),
{
path: 'plugins/catalog',
element: <Navigate replace to='/dashboard/plugins' />
}
],
errorElement: <ErrorBoundary pageClasses={[ 'type-interior' ]} />
},

View File

@@ -7,10 +7,11 @@ import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
import Loading from 'components/loading/LoadingComponent';
import { MRT_ColumnDef, MRT_Table, useMaterialReactTable } from 'material-react-table';
import { type MRT_ColumnDef, MRT_Table, type MRT_Theme, useMaterialReactTable } from 'material-react-table';
import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info';
import globalize from '../../../../lib/globalize';
import { useTask } from 'apps/dashboard/features/tasks/api/useTask';
@@ -26,6 +27,7 @@ export const Component = () => {
const [ isAddTriggerDialogOpen, setIsAddTriggerDialogOpen ] = useState(false);
const [ isRemoveConfirmOpen, setIsRemoveConfirmOpen ] = useState(false);
const [ pendingDeleteTrigger, setPendingDeleteTrigger ] = useState<TaskTriggerInfo | null>(null);
const theme = useTheme();
const onCloseRemoveConfirmDialog = useCallback(() => {
setPendingDeleteTrigger(null);
@@ -80,7 +82,15 @@ export const Component = () => {
}
], []);
// NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used
// https://github.com/KevinVandy/material-react-table/issues/1429
const mrtTheme = useMemo<Partial<MRT_Theme>>(() => ({
baseBackgroundColor: theme.palette.background.paper
}), [ theme ]);
const table = useMaterialReactTable({
mrtTheme,
columns,
data: task?.Triggers || [],

View File

@@ -4,13 +4,13 @@ import { useSearchParams } from 'react-router-dom';
import loading from '../../../../components/loading/loading';
import globalize from '../../../../lib/globalize';
import toast from '../../../../components/toast/toast';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import Button from '../../../../elements/emby-button/Button';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../../../components/Page';
import Toast from 'apps/dashboard/components/Toast';
type ItemsArr = {
Name?: string | null;
@@ -23,6 +23,7 @@ type ItemsArr = {
const UserLibraryAccess = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@@ -31,6 +32,10 @@ const UserLibraryAccess = () => {
const element = useRef<HTMLDivElement>(null);
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
const triggerChange = (select: HTMLInputElement) => {
const evt = new Event('change', { bubbles: false, cancelable: true });
select.dispatchEvent(evt);
@@ -220,7 +225,7 @@ const UserLibraryAccess = () => {
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
setIsSettingsSavedToastOpen(true);
};
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
@@ -243,6 +248,11 @@ const UserLibraryAccess = () => {
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -1,21 +1,21 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import type { BaseItemDto, CreateUserByName } from '@jellyfin/sdk/lib/generated-client';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Input from '../../../../elements/emby-input/Input';
import Button from '../../../../elements/emby-button/Button';
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../../../components/Page';
import Toast from 'apps/dashboard/components/Toast';
type UserInput = {
Name?: string;
Password?: string;
};
import { useLibraryMediaFolders } from 'apps/dashboard/features/users/api/useLibraryMediaFolders';
import { useChannels } from 'apps/dashboard/features/users/api/useChannels';
import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy';
import { useCreateUser } from 'apps/dashboard/features/users/api/useCreateUser';
import { useNavigate } from 'react-router-dom';
type ItemsArr = {
Name?: string | null;
@@ -23,10 +23,21 @@ type ItemsArr = {
};
const UserNew = () => {
const navigate = useNavigate();
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const [ isErrorToastOpen, setIsErrorToastOpen ] = useState(false);
const element = useRef<HTMLDivElement>(null);
const handleToastClose = useCallback(() => {
setIsErrorToastOpen(false);
}, []);
const { data: mediaFolders, isSuccess: isMediaFoldersSuccess } = useLibraryMediaFolders();
const { data: channels, isSuccess: isChannelsSuccess } = useChannels();
const createUser = useCreateUser();
const updateUserPolicy = useUpdateUserPolicy();
const getItemsResult = (items: BaseItemDto[]) => {
return items.map(item =>
({
@@ -44,9 +55,7 @@ const UserNew = () => {
return;
}
const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders);
setMediaFoldersItems(getItemsResult(result));
const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
folderAccess.dispatchEvent(new CustomEvent('create'));
@@ -62,15 +71,15 @@ const UserNew = () => {
return;
}
const channels = getItemsResult(result);
const channelItems = getItemsResult(result);
setChannelsItems(channels);
setChannelsItems(channelItems);
const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
channelAccess.dispatchEvent(new CustomEvent('create'));
const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement;
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
channelItems.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
}, []);
@@ -82,22 +91,26 @@ const UserNew = () => {
console.error('Unexpected null reference');
return;
}
if (!mediaFolders?.Items) {
console.error('[add] mediaFolders not available');
return;
}
if (!channels?.Items) {
console.error('[add] channels not available');
return;
}
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
loadMediaFolders(mediaFolders?.Items);
loadChannels(channels?.Items);
loading.hide();
}, [loadChannels, loadMediaFolders, mediaFolders, channels]);
useEffect(() => {
loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(responses[0].Items);
loadChannels(responses[1].Items);
loading.hide();
}).catch(err => {
console.error('[usernew] failed to load data', err);
});
}, [loadChannels, loadMediaFolders]);
if (isMediaFoldersSuccess && isChannelsSuccess) {
loadUser();
}
}, [loadUser, isMediaFoldersSuccess, isChannelsSuccess]);
useEffect(() => {
const page = element.current;
@@ -107,51 +120,54 @@ const UserNew = () => {
return;
}
loadUser();
const saveUser = () => {
const userInput: UserInput = {};
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value.trim();
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
const userInput: CreateUserByName = {
Name: (page.querySelector('#txtUsername') as HTMLInputElement).value,
Password: (page.querySelector('#txtPassword') as HTMLInputElement).value
};
createUser.mutate({ createUserByName: userInput }, {
onSuccess: (response) => {
const user = response.data;
window.ApiClient.createUser(userInput).then(function (user) {
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = [];
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id)
.catch(err => {
console.error('[usernew] failed to navigate to edit user page', err);
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}).catch(err => {
console.error('[usernew] failed to update user policy', err);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
loading.hide();
}
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
updateUserPolicy.mutate({
userId: user.Id,
userPolicy: user.Policy
}, {
onSuccess: () => {
navigate(`/dashboard/users/profile?userId=${user.Id}`);
},
onError: () => {
console.error('[usernew] failed to update user policy');
setIsErrorToastOpen(true);
}
});
}
});
};
@@ -163,28 +179,43 @@ const UserNew = () => {
return false;
};
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const enableAllChannelsChange = function (this: HTMLInputElement) {
const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
});
};
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const enableAllFoldersChange = function (this: HTMLInputElement) {
const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
});
};
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
const onCancelClick = () => {
window.history.back();
});
}, [loadUser]);
};
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', enableAllChannelsChange);
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', enableAllFoldersChange);
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', onCancelClick);
return () => {
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).removeEventListener('change', enableAllChannelsChange);
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).removeEventListener('change', enableAllFoldersChange);
(page.querySelector('.newUserProfileForm') as HTMLFormElement).removeEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).removeEventListener('click', onCancelClick);
};
}, [loadUser, createUser, updateUserPolicy, navigate]);
return (
<Page
id='newUserPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isErrorToastOpen}
onClose={handleToastClose}
message={globalize.translate('ErrorDefault')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -1,10 +1,5 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import globalize from '../../../../lib/globalize';
import loading from '../../../../components/loading/loading';
import dom from '../../../../scripts/dom';
import confirm from '../../../../components/confirm/confirm';
import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
@@ -14,6 +9,12 @@ import '../../../../components/cardbuilder/card.scss';
import '../../../../components/indicators/indicators.scss';
import '../../../../styles/flexstyles.scss';
import Page from '../../../../components/Page';
import { useLocation, useNavigate } from 'react-router-dom';
import Toast from 'apps/dashboard/components/Toast';
import { useUsers } from 'hooks/useUsers';
import Loading from 'components/loading/LoadingComponent';
import { useDeleteUser } from 'apps/dashboard/features/users/api/useDeleteUser';
import dom from 'utils/dom';
type MenuEntry = {
name?: string;
@@ -22,30 +23,30 @@ type MenuEntry = {
};
const UserProfiles = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const location = useLocation();
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
const element = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const { data: users, isPending } = useUsers();
const deleteUser = useDeleteUser();
const loadData = () => {
loading.show();
window.ApiClient.getUsers().then(function (result) {
setUsers(result);
loading.hide();
}).catch(err => {
console.error('[userprofiles] failed to fetch users', err);
});
};
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
useEffect(() => {
const page = element.current;
if (location.state?.openSavedToast) {
setIsSettingsSavedToastOpen(true);
window.history.replaceState({}, '');
}
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card?.getAttribute('data-userid');
@@ -86,28 +87,19 @@ const UserProfiles = () => {
callback: function (id: string) {
switch (id) {
case 'open':
Dashboard.navigate('/dashboard/users/profile?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to user edit page', err);
});
navigate(`/dashboard/users/profile?userId=${userId}`);
break;
case 'access':
Dashboard.navigate('/dashboard/users/access?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to user library page', err);
});
navigate(`/dashboard/users/access?userId=${userId}`);
break;
case 'parentalcontrol':
Dashboard.navigate('/dashboard/users/parentalcontrol?userId=' + userId)
.catch(err => {
console.error('[userprofiles] failed to navigate to parental control page', err);
});
navigate(`/dashboard/users/parentalcontrol?userId=${userId}`);
break;
case 'delete':
deleteUser(userId, username);
confirmDeleteUser(userId, username);
}
}
}).catch(() => {
@@ -118,7 +110,7 @@ const UserProfiles = () => {
});
};
const deleteUser = (id: string, username?: string | null) => {
const confirmDeleteUser = (id: string, username?: string | null) => {
const title = username ? globalize.translate('DeleteName', username) : globalize.translate('DeleteUser');
const text = globalize.translate('DeleteUserConfirmation');
@@ -128,32 +120,38 @@ const UserProfiles = () => {
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
window.ApiClient.deleteUser(id).then(function () {
loadData();
}).catch(err => {
console.error('[userprofiles] failed to delete user', err);
deleteUser.mutate({
userId: id
});
}).catch(() => {
// confirm dialog closed
});
};
page.addEventListener('click', function (e) {
const onPageClick = function (e: MouseEvent) {
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
};
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('/dashboard/users/add')
.catch(err => {
console.error('[userprofiles] failed to navigate to new user page', err);
});
});
}, []);
const onAddUserClick = function() {
navigate('/dashboard/users/add');
};
page.addEventListener('click', onPageClick);
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', onAddUserClick);
return () => {
page.removeEventListener('click', onPageClick);
(page.querySelector('#btnAddUser') as HTMLButtonElement).removeEventListener('click', onAddUserClick);
};
}, [navigate, deleteUser, location.state?.openSavedToast]);
if (isPending) {
return <Loading />;
}
return (
<Page
@@ -161,6 +159,11 @@ const UserProfiles = () => {
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
title={globalize.translate('HeaderUsers')}
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
@@ -174,7 +177,7 @@ const UserProfiles = () => {
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
{users?.map(user => {
return <UserCardBox key={user.Id} user={user} />;
})}
</div>

View File

@@ -12,12 +12,12 @@ import Button from '../../../../elements/emby-button/Button';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
import prompt from '../../../../components/prompt/prompt';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import Toast from 'apps/dashboard/components/Toast';
type NamedItem = {
name: string;
@@ -30,6 +30,7 @@ type UnratedNamedItem = NamedItem & {
function handleSaveUser(
page: HTMLDivElement,
parentalRatingsRef: React.MutableRefObject<ParentalRating[]>,
getSchedulesFromPage: () => AccessSchedule[],
getAllowedTagsFromPage: () => string[],
getBlockedTagsFromPage: () => string[],
@@ -42,8 +43,12 @@ function handleSaveUser(
throw new Error('Unexpected null user id or policy');
}
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
userPolicy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
const parentalRatingIndex = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
const parentalRating = parentalRatingsRef.current[parentalRatingIndex] as ParentalRating;
const score = parentalRating?.RatingScore?.score;
const subScore = parentalRating?.RatingScore?.subScore;
userPolicy.MaxParentalRating = Number.isNaN(score) ? null : score;
userPolicy.MaxParentalSubRating = Number.isNaN(subScore) ? null : subScore;
userPolicy.BlockUnratedItems = Array.prototype.filter
.call(page.querySelectorAll('.chkUnratedItem'), i => i.checked)
.map(i => i.getAttribute('data-itemtype'));
@@ -69,33 +74,14 @@ const UserParentalControl = () => {
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []);
const element = useRef<HTMLDivElement>(null);
const parentalRatingsRef = useRef<ParentalRating[]>([]);
const populateRatings = useCallback((allParentalRatings: ParentalRating[]) => {
let rating;
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
setParentalRatings(ratings);
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
const loadUnratedItems = useCallback((user: UserDto) => {
@@ -161,16 +147,52 @@ const UserParentalControl = () => {
setAllowedTags(user.Policy?.AllowedTags || []);
setBlockedTags(user.Policy?.BlockedTags || []);
populateRatings(allParentalRatings);
let ratingValue = '';
allParentalRatings.forEach(rating => {
if (rating.Value != null && user.Policy?.MaxParentalRating != null && user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = `${rating.Value}`;
// Build the grouped ratings array
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.RatingScore?.score === rating.RatingScore?.score && lastRating.RatingScore?.subScore == rating.RatingScore?.subScore) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
});
setMaxParentalRating(ratingValue);
ratings.push(rating);
}
setParentalRatings(ratings);
parentalRatingsRef.current = ratings;
// Find matching rating - first try exact match with score and subscore
let ratingIndex = '';
const userMaxRating = user.Policy?.MaxParentalRating;
const userMaxSubRating = user.Policy?.MaxParentalSubRating;
if (userMaxRating != null) {
// First try to find exact match with both score and subscore
ratings.forEach((rating, index) => {
if (rating.RatingScore?.score === userMaxRating
&& rating.RatingScore?.subScore === userMaxSubRating) {
ratingIndex = `${index}`;
}
});
// If no exact match found, fallback to score-only match
if (!ratingIndex) {
ratings.forEach((rating, index) => {
if (rating.RatingScore?.score != null
&& rating.RatingScore.score <= userMaxRating) {
ratingIndex = `${index}`;
}
});
}
}
setMaxParentalRating(ratingIndex);
if (user.Policy?.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
@@ -179,7 +201,7 @@ const UserParentalControl = () => {
}
setAccessSchedules(user.Policy?.AccessSchedules || []);
loading.hide();
}, [libraryMenu, setAllowedTags, setBlockedTags, loadUnratedItems, populateRatings]);
}, [libraryMenu, setAllowedTags, setBlockedTags, loadUnratedItems]);
const loadData = useCallback(() => {
if (!userId) {
@@ -283,10 +305,10 @@ const UserParentalControl = () => {
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
setIsSettingsSavedToastOpen(true);
};
const saveUser = handleSaveUser(page, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
const saveUser = handleSaveUser(page, parentalRatingsRef, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
const onSubmit = (e: Event) => {
if (!userId) {
@@ -342,11 +364,11 @@ const UserParentalControl = () => {
const optionMaxParentalRating = () => {
let content = '';
content += '<option value=\'\'></option>';
for (const rating of parentalRatings) {
if (rating.Value != null) {
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
parentalRatings.forEach((rating, index) => {
if (rating.RatingScore != null) {
content += `<option value='${index}'>${escapeHTML(rating.Name)}</option>`;
}
}
});
return content;
};
@@ -370,6 +392,11 @@ const UserParentalControl = () => {
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -1,37 +1,21 @@
import React, { useCallback, useEffect, useState } from 'react';
import React from 'react';
import { useSearchParams } from 'react-router-dom';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Page from '../../../../components/Page';
import loading from '../../../../components/loading/loading';
import { useUser } from 'apps/dashboard/features/users/api/useUser';
import Loading from 'components/loading/LoadingComponent';
const UserPassword = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState('');
const { data: user, isPending } = useUser(userId ? { userId: userId } : undefined);
const loadUser = useCallback(() => {
if (!userId) {
console.error('[userpassword] missing user id');
return;
}
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
setUserName(user.Name);
loading.hide();
}).catch(err => {
console.error('[userpassword] failed to fetch user', err);
});
}, [userId]);
useEffect(() => {
loadUser();
}, [loadUser]);
if (isPending || !user) {
return <Loading />;
}
return (
<Page
@@ -41,13 +25,13 @@ const UserPassword = () => {
<div className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
title={user?.Name || undefined}
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
userId={userId}
user={user}
/>
</div>
</div>

View File

@@ -1,9 +1,8 @@
import type { BaseItemDto, NameIdPair, SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import escapeHTML from 'escape-html';
import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import Button from '../../../../elements/emby-button/Button';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
@@ -12,9 +11,16 @@ import Input from '../../../../elements/emby-input/Input';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
import { useUser } from 'apps/dashboard/features/users/api/useUser';
import { useAuthProviders } from 'apps/dashboard/features/users/api/useAuthProviders';
import { usePasswordResetProviders } from 'apps/dashboard/features/users/api/usePasswordResetProviders';
import { useLibraryMediaFolders } from 'apps/dashboard/features/users/api/useLibraryMediaFolders';
import { useChannels } from 'apps/dashboard/features/users/api/useChannels';
import { useUpdateUser } from 'apps/dashboard/features/users/api/useUpdateUser';
import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy';
import { useNetworkConfig } from 'apps/dashboard/features/users/api/useNetworkConfig';
type ResetProvider = BaseItemDto & {
checkedAttribute: string
@@ -25,27 +31,26 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
.map(e => e.getAttribute('data-id'))
);
function onSaveComplete() {
Dashboard.navigate('/dashboard/users')
.catch(err => {
console.error('[useredit] failed to navigate to user profile', err);
});
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const UserEdit = () => {
const navigate = useNavigate();
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userDto, setUserDto ] = useState<UserDto>();
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<NameIdPair[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<NameIdPair[]>([]);
const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
const { data: userDto, isSuccess: isUserSuccess } = useUser(userId ? { userId: userId } : undefined);
const { data: authProviders, isSuccess: isAuthProvidersSuccess } = useAuthProviders();
const { data: passwordResetProviders, isSuccess: isPasswordResetProvidersSuccess } = usePasswordResetProviders();
const { data: mediaFolders, isSuccess: isMediaFoldersSuccess } = useLibraryMediaFolders({ isHidden: false });
const { data: channels, isSuccess: isChannelsSuccess } = useChannels({ supportsMediaDeletion: true });
const { data: netConfig, isSuccess: isNetConfigSuccess } = useNetworkConfig();
const updateUser = useUpdateUser();
const updateUserPolicy = useUpdateUserPolicy();
const element = useRef<HTMLDivElement>(null);
const triggerChange = (select: HTMLInputElement) => {
@@ -53,17 +58,10 @@ const UserEdit = () => {
select.dispatchEvent(evt);
};
const getUser = () => {
if (!userId) throw new Error('missing user id');
return window.ApiClient.getUser(userId);
};
const loadAuthProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => {
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
setAuthProviders(providers);
const currentProviderId = user.Policy?.AuthenticationProviderId || '';
setAuthenticationProviderId(currentProviderId);
}, []);
@@ -72,30 +70,26 @@ const UserEdit = () => {
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
setPasswordResetProviders(providers);
const currentProviderId = user.Policy?.PasswordResetProviderId || '';
setPasswordResetProviderId(currentProviderId);
}, []);
const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, mediaFolders: BaseItemDto[]) => {
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
const itemsArr: ResetProvider[] = [];
const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, folders: BaseItemDto[]) => {
let isChecked;
let checkedAttribute;
const itemsArr: ResetProvider[] = [];
for (const mediaFolder of mediaFolders) {
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
...mediaFolder,
checkedAttribute: checkedAttribute
});
}
for (const mediaFolder of folders) {
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
...mediaFolder,
checkedAttribute: checkedAttribute
});
}
for (const channel of channelsResult.Items) {
if (channels?.Items) {
for (const channel of channels.Items) {
isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(channel.Id || '') != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
@@ -103,16 +97,66 @@ const UserEdit = () => {
checkedAttribute: checkedAttribute
});
}
}
setDeleteFoldersAccess(itemsArr);
setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false;
triggerChange(chkEnableDeleteAllFolders);
}).catch(err => {
console.error('[useredit] failed to fetch channels', err);
});
}, []);
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false;
triggerChange(chkEnableDeleteAllFolders);
}, [channels]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('[useredit] Unexpected null page reference');
return;
}
if (userDto && isAuthProvidersSuccess && authProviders != null) {
loadAuthProviders(page, userDto, authProviders);
}
}, [authProviders, isAuthProvidersSuccess, userDto, loadAuthProviders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('[useredit] Unexpected null page reference');
return;
}
if (userDto && isPasswordResetProvidersSuccess && passwordResetProviders != null) {
loadPasswordResetProviders(page, userDto, passwordResetProviders);
}
}, [passwordResetProviders, isPasswordResetProvidersSuccess, userDto, loadPasswordResetProviders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('[useredit] Unexpected null page reference');
return;
}
if (userDto && isMediaFoldersSuccess && isChannelsSuccess && mediaFolders?.Items != null) {
loadDeleteFolders(page, userDto, mediaFolders.Items);
}
}, [userDto, mediaFolders, isMediaFoldersSuccess, isChannelsSuccess, channels, loadDeleteFolders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('[useredit] Unexpected null page reference');
return;
}
if (netConfig && isNetConfigSuccess) {
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !netConfig.EnableRemoteAccess);
}
}, [netConfig, isNetConfigSuccess]);
const loadUser = useCallback((user: UserDto) => {
const page = element.current;
@@ -122,24 +166,6 @@ const UserEdit = () => {
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(page, user, providers);
}).catch(err => {
console.error('[useredit] failed to fetch auth providers', err);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(page, user, providers);
}).catch(err => {
console.error('[useredit] failed to fetch password reset providers', err);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
})).then(function (folders) {
loadDeleteFolders(page, user, folders.Items);
}).catch(err => {
console.error('[useredit] failed to fetch media folders', err);
});
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled);
@@ -149,7 +175,6 @@ const UserEdit = () => {
void libraryMenu.then(menu => menu.setTitle(user.Name));
setUserDto(user);
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || '';
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled;
@@ -173,16 +198,22 @@ const UserEdit = () => {
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0';
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess);
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
}, [ libraryMenu ]);
const loadData = useCallback(() => {
if (!userDto) {
console.error('[profile] No user available');
return;
}
loading.show();
getUser().then(function (user) {
loadUser(user);
}).catch(err => {
console.error('[useredit] failed to load data', err);
});
}, [loadUser]);
loadUser(userDto);
}, [userDto, loadUser]);
useEffect(() => {
if (isUserSuccess) {
loadData();
}
}, [loadData, isUserSuccess]);
useEffect(() => {
const page = element.current;
@@ -192,8 +223,6 @@ const UserEdit = () => {
return;
}
loadData();
const saveUser = (user: UserDto) => {
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
@@ -225,50 +254,58 @@ const UserEdit = () => {
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
window.ApiClient.updateUser(user).then(() => (
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' })
)).then(() => {
onSaveComplete();
}).catch(err => {
console.error('[useredit] failed to update user', err);
updateUser.mutate({ userId: user.Id, userDto: user }, {
onSuccess: () => {
if (user.Id) {
updateUserPolicy.mutate({
userId: user.Id,
userPolicy: user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' }
}, {
onSuccess: () => {
loading.hide();
navigate('/dashboard/users', {
state: { openSavedToast: true }
});
}
});
}
}
});
};
const onSubmit = (e: Event) => {
loading.show();
getUser().then(function (result) {
saveUser(result);
}).catch(err => {
console.error('[useredit] failed to fetch user', err);
});
if (userDto) {
saveUser(userDto);
}
e.preventDefault();
e.stopPropagation();
return false;
};
const onBtnCancelClick = () => {
window.history.back();
};
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
});
window.ApiClient.getNamedConfiguration('network').then(function (config) {
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
}).catch(err => {
console.error('[useredit] failed to load network config', err);
});
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', onBtnCancelClick);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadData]);
return () => {
(page.querySelector('.editUserProfileForm') as HTMLFormElement).removeEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).removeEventListener('click', onBtnCancelClick);
};
}, [loadData, updateUser, userDto, updateUserPolicy, navigate]);
const optionLoginProvider = authProviders.map((provider) => {
const optionLoginProvider = authProviders?.map((provider) => {
const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionPasswordResetProvider = passwordResetProviders.map((provider) => {
const optionPasswordResetProvider = passwordResetProviders?.map((provider) => {
const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});

View File

@@ -27,7 +27,18 @@ $mui-bp-xl: 1536px;
padding-top: 3.25rem !important;
}
// Fix backdrop position on mobile item details page
.layout-mobile .itemBackdrop {
margin-top: 0 !important;
.layout-mobile {
.itemBackdrop {
// Fix backdrop position on mobile item details page
margin-top: 0 !important;
// Add a subtle gradient over the backdrop to ensure the app bar buttons are visible
&::before {
display: block;
content: "";
height: 100%;
width: 100%;
background: linear-gradient(180deg, rgba(32, 32, 32, 0.6) 0%, rgba(32, 32, 32, 0.2) 4rem, rgba(0, 0, 0, 0) 50%);
}
}
}

View File

@@ -4,7 +4,7 @@ import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Cast from '@mui/icons-material/Cast';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import type {} from '@mui/material/themeCssVarsAugmentation';
import Tooltip from '@mui/material/Tooltip';
import { playbackManager } from 'components/playback/playbackmanager';
@@ -15,7 +15,6 @@ import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu';
const RemotePlayButton = () => {
const theme = useTheme();
const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo());
const updatePlayerInfo = useCallback(() => {
@@ -70,9 +69,10 @@ const RemotePlayButton = () => {
aria-haspopup='true'
onClick={onRemotePlayActiveButtonClick}
color='inherit'
sx={{
color: theme.palette.primary.main
}}
// eslint-disable-next-line react/jsx-no-bind
sx={(theme) => ({
color: theme.vars.palette.primary.main
})}
>
{playerInfo.deviceName || playerInfo.name}
</Button>

View File

@@ -3,6 +3,7 @@ import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collec
import ArrowDropDown from '@mui/icons-material/ArrowDropDown';
import Favorite from '@mui/icons-material/Favorite';
import Button from '@mui/material/Button/Button';
import Icon from '@mui/material/Icon';
import { Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { useCallback, useMemo, useState } from 'react';
@@ -15,6 +16,7 @@ import { appRouter } from 'components/router/appRouter';
import { useApi } from 'hooks/useApi';
import useCurrentTab from 'hooks/useCurrentTab';
import { useUserViews } from 'hooks/useUserViews';
import { useWebConfig } from 'hooks/useWebConfig';
import globalize from 'lib/globalize';
import UserViewsMenu from './UserViewsMenu';
@@ -56,14 +58,19 @@ const UserViewNav = () => {
const libraryId = searchParams.get('topParentId') || searchParams.get('parentId');
const collectionType = searchParams.get('collectionType');
const { activeTab } = useCurrentTab();
const webConfig = useWebConfig();
const isExtraLargeScreen = useMediaQuery((t: Theme) => t.breakpoints.up('xl'));
const isLargeScreen = useMediaQuery((t: Theme) => t.breakpoints.up('lg'));
const maxViews = useMemo(() => {
if (isExtraLargeScreen) return MAX_USER_VIEWS_XL;
if (isLargeScreen) return MAX_USER_VIEWS_LG;
return MAX_USER_VIEWS_MD;
}, [ isExtraLargeScreen, isLargeScreen ]);
let _maxViews = MAX_USER_VIEWS_MD;
if (isExtraLargeScreen) _maxViews = MAX_USER_VIEWS_XL;
else if (isLargeScreen) _maxViews = MAX_USER_VIEWS_LG;
const customLinks = (webConfig.menuLinks || []).length;
return _maxViews - customLinks;
}, [ isExtraLargeScreen, isLargeScreen, webConfig.menuLinks ]);
const { user } = useApi();
const {
@@ -108,6 +115,21 @@ const UserViewNav = () => {
{globalize.translate(MetaView.Favorites.Name)}
</Button>
{webConfig.menuLinks?.map(link => (
<Button
key={link.name}
variant='text'
color='inherit'
startIcon={<Icon>{link.icon || 'link'}</Icon>}
component='a'
href={link.url}
target='_blank'
rel='noopener noreferrer'
>
{link.name}
</Button>
))}
{primaryViews?.map(view => (
<Button
key={view.Id}

View File

@@ -9,6 +9,7 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import classNames from 'classnames';
import React, { type FC, useCallback } from 'react';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';
import { useGetItemsViewByType } from 'hooks/useFetchItems';
@@ -99,7 +100,7 @@ const ItemsView: FC<ItemsViewProps> = ({
if (viewType === LibraryTab.Songs) {
listOptions.showParentTitle = true;
listOptions.action = 'playallfromhere';
listOptions.action = ItemAction.PlayAllFromHere;
listOptions.smallIcon = true;
listOptions.showArtist = true;
listOptions.addToListButton = true;
@@ -300,7 +301,7 @@ const ItemsView: FC<ItemsViewProps> = ({
xs: 1,
sm: 0
},
justifyContent: 'end'
justifyContent: 'flex-end'
}}
>
{!isPending && (

View File

@@ -1,14 +1,16 @@
import React, { type FC } from 'react';
import { useApi } from 'hooks/useApi';
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
import { appRouter } from 'components/router/appRouter';
import globalize from 'lib/globalize';
import Loading from 'components/loading/LoadingComponent';
import NoItemsMessage from 'components/common/NoItemsMessage';
import SectionContainer from 'components/common/SectionContainer';
import { CardShape } from 'utils/card';
import Loading from 'components/loading/LoadingComponent';
import { appRouter } from 'components/router/appRouter';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi';
import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems';
import globalize from 'lib/globalize';
import type { ParentId } from 'types/library';
import type { Section, SectionType } from 'types/sections';
import { CardShape } from 'utils/card';
interface ProgramsSectionViewProps {
parentId: ParentId;
@@ -92,7 +94,7 @@ const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
showChannelName: false,
cardLayout: true,
centerText: false,
action: 'edit',
action: ItemAction.Edit,
cardFooterAside: 'none',
preferThumb: true,
coverImage: true,

View File

@@ -4,6 +4,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import ReplayIcon from '@mui/icons-material/Replay';
import { useQueryClient } from '@tanstack/react-query';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi';
import { getChannelQuery } from 'hooks/api/liveTvHooks/useGetChannel';
import globalize from 'lib/globalize';
@@ -76,7 +77,7 @@ const PlayOrResumeButton: FC<PlayOrResumeButtonProps> = ({
return (
<IconButton
className='button-flat btnPlayOrResume'
data-action={isResumable ? 'resume' : 'play'}
data-action={isResumable ? ItemAction.Resume : ItemAction.Play}
title={
isResumable ?
globalize.translate('ButtonResume') :

View File

@@ -12,6 +12,7 @@ import React, { Fragment } from 'react';
import { appHost } from 'components/apphost';
import { AppFeature } from 'constants/appFeature';
import { LayoutMode } from 'constants/layoutMode';
import { useApi } from 'hooks/useApi';
import { useThemes } from 'hooks/useThemes';
import globalize from 'lib/globalize';
@@ -45,11 +46,10 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere
onChange={onChange}
value={values.layout}
>
<MenuItem value='auto'>{globalize.translate('Auto')}</MenuItem>
<MenuItem value='desktop'>{globalize.translate('Desktop')}</MenuItem>
<MenuItem value='mobile'>{globalize.translate('Mobile')}</MenuItem>
<MenuItem value='tv'>{globalize.translate('TV')}</MenuItem>
<MenuItem value='experimental'>{globalize.translate('Experimental')}</MenuItem>
<MenuItem value={LayoutMode.Auto}>{globalize.translate('Auto')}</MenuItem>
<MenuItem value={LayoutMode.Desktop}>{globalize.translate('Desktop')}</MenuItem>
<MenuItem value={LayoutMode.Mobile}>{globalize.translate('Mobile')}</MenuItem>
<MenuItem value={LayoutMode.Tv}>{globalize.translate('TV')}</MenuItem>
</Select>
<FormHelperText component={Stack} id='display-settings-layout-description'>
<span>{globalize.translate('DisplayModeHelp')}</span>
@@ -169,6 +169,30 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere
</Fragment>
) }
<FormControl fullWidth>
<TextField
aria-describedby='display-settings-slideshow-interval-description'
value={values.slideshowInterval}
label={globalize.translate('LabelSlideshowInterval')}
name='slideshowInterval'
onChange={onChange}
slotProps={{
htmlInput: {
inputMode: 'numeric',
max: '3600',
min: '1',
pattern: '[0-9]',
required: true,
step: '1',
type: 'number'
}
}}
/>
<FormHelperText id='display-settings-slideshow-interval-description'>
{globalize.translate('LabelSlideshowIntervalHelp')}
</FormHelperText>
</FormControl>
<FormControl fullWidth>
<FormControlLabel
aria-describedby='display-settings-faster-animations-description'

View File

@@ -10,6 +10,9 @@ import themeManager from 'scripts/themeManager';
import { currentSettings, UserSettings } from 'scripts/settings/userSettings';
import type { DisplaySettingsValues } from '../types/displaySettingsValues';
import { useThemes } from 'hooks/useThemes';
import { Theme } from 'types/webConfig';
import { FALLBACK_THEME_ID } from 'hooks/useUserTheme';
interface UseDisplaySettingsParams {
userId?: string | null;
@@ -20,6 +23,7 @@ export function useDisplaySettings({ userId }: UseDisplaySettingsParams) {
const [userSettings, setUserSettings] = useState<UserSettings>();
const [displaySettings, setDisplaySettings] = useState<DisplaySettingsValues>();
const { __legacyApiClient__, user: currentUser } = useApi();
const { defaultTheme } = useThemes();
useEffect(() => {
if (!userId || !currentUser || !__legacyApiClient__) {
@@ -29,7 +33,7 @@ export function useDisplaySettings({ userId }: UseDisplaySettingsParams) {
setLoading(true);
void (async () => {
const loadedSettings = await loadDisplaySettings({ api: __legacyApiClient__, currentUser, userId });
const loadedSettings = await loadDisplaySettings({ api: __legacyApiClient__, currentUser, userId, defaultTheme });
setDisplaySettings(loadedSettings.displaySettings);
setUserSettings(loadedSettings.userSettings);
@@ -62,15 +66,17 @@ export function useDisplaySettings({ userId }: UseDisplaySettingsParams) {
}
interface LoadDisplaySettingsParams {
currentUser: UserDto;
userId?: string;
api: ApiClient;
currentUser: UserDto
userId?: string
api: ApiClient
defaultTheme?: Theme
}
async function loadDisplaySettings({
currentUser,
userId,
api
api,
defaultTheme
}: LoadDisplaySettingsParams) {
const settings = (!userId || userId === currentUser?.Id) ? currentSettings : new UserSettings();
const user = (!userId || userId === currentUser?.Id) ? currentUser : await api.getUser(userId);
@@ -78,8 +84,8 @@ async function loadDisplaySettings({
await settings.setUserInfo(userId, api);
const displaySettings = {
customCss: settings.customCss(),
dashboardTheme: settings.dashboardTheme() || 'auto',
customCss: settings.customCss() || '',
dashboardTheme: settings.dashboardTheme() || defaultTheme?.id || FALLBACK_THEME_ID,
dateTimeLocale: settings.dateTimeLocale() || 'auto',
disableCustomCss: Boolean(settings.disableCustomCss()),
displayMissingEpisodes: user?.Configuration?.DisplayMissingEpisodes ?? false,
@@ -97,7 +103,8 @@ async function loadDisplaySettings({
maxDaysForNextUp: settings.maxDaysForNextUp(),
screensaver: settings.screensaver() || 'none',
screensaverInterval: settings.backdropScreensaverInterval(),
theme: settings.theme()
slideshowInterval: settings.slideshowInterval(),
theme: settings.theme() || defaultTheme?.id || FALLBACK_THEME_ID
};
return {
@@ -125,7 +132,7 @@ async function saveDisplaySettings({
userSettings.language(normalizeValue(newDisplaySettings.language));
}
userSettings.customCss(normalizeValue(newDisplaySettings.customCss));
userSettings.dashboardTheme(normalizeValue(newDisplaySettings.dashboardTheme));
userSettings.dashboardTheme(newDisplaySettings.dashboardTheme);
userSettings.dateTimeLocale(normalizeValue(newDisplaySettings.dateTimeLocale));
userSettings.disableCustomCss(newDisplaySettings.disableCustomCss);
userSettings.enableBlurhash(newDisplaySettings.enableBlurHash);

View File

@@ -18,5 +18,6 @@ export interface DisplaySettingsValues {
maxDaysForNextUp: number;
screensaver: string;
screensaverInterval: number;
slideshowInterval: number;
theme: string;
}

View File

@@ -5,7 +5,7 @@ import globalize from '../../../lib/globalize';
import { clearBackdrop } from '../../../components/backdrop/backdrop';
import layoutManager from '../../../components/layoutManager';
import Page from '../../../components/Page';
import { EventType } from 'types/eventType';
import { EventType } from 'constants/eventType';
import Events from 'utils/events';
import '../../../elements/emby-tabs/emby-tabs';

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