Compare commits
277 Commits
renovate/a
...
release-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e37078b60 | ||
|
|
a5184bb843 | ||
|
|
b9ee65e49d | ||
|
|
d5ebc64fcb | ||
|
|
f4c49427fd | ||
|
|
6c97b7a6d8 | ||
|
|
0859d4d881 | ||
|
|
4b6bbcfe26 | ||
|
|
1a1735340f | ||
|
|
5dad4b4486 | ||
|
|
201aec56c6 | ||
|
|
d274d7f741 | ||
|
|
b270d4051b | ||
|
|
aa09fa710e | ||
|
|
c33c2b1e26 | ||
|
|
eb7fb6b39d | ||
|
|
5fd5292f6d | ||
|
|
1250c76567 | ||
|
|
1ba6bde32a | ||
|
|
3c80bf7b19 | ||
|
|
1e1af1c67f | ||
|
|
8d1c34f80e | ||
|
|
d3e3bc7282 | ||
|
|
0ad87f3b87 | ||
|
|
9e871d43ec | ||
|
|
11ae2ff43f | ||
|
|
76c55116ce | ||
|
|
884ce171ea | ||
|
|
51bd2bef1a | ||
|
|
0bfe91b0fa | ||
|
|
a5feddb48b | ||
|
|
7d27596d6b | ||
|
|
e0cb79088b | ||
|
|
b2f3720282 | ||
|
|
b67f4eccfb | ||
|
|
331fa87216 | ||
|
|
59813ee0ea | ||
|
|
eaae0f3c55 | ||
|
|
6304e27940 | ||
|
|
7ada8796a7 | ||
|
|
eab36f9934 | ||
|
|
b044bc25de | ||
|
|
5cc91f2ee0 | ||
|
|
2ffb833daf | ||
|
|
93d63330fd | ||
|
|
f1b0b504dd | ||
|
|
919be18c84 | ||
|
|
cf530b30d5 | ||
|
|
dd004ec06b | ||
|
|
a11d74ae68 | ||
|
|
da9eece6c0 | ||
|
|
509cbabedb | ||
|
|
62246fe0a9 | ||
|
|
35a7dfbed6 | ||
|
|
b93221a9b2 | ||
|
|
1a858d9dda | ||
|
|
07ce5c44a1 | ||
|
|
2eda12ba8f | ||
|
|
9266e51aaf | ||
|
|
069ea049eb | ||
|
|
56af039fb9 | ||
|
|
1fb5c4d95d | ||
|
|
828fa340d5 | ||
|
|
a77a8c7aec | ||
|
|
55714d5341 | ||
|
|
b88a5951e1 | ||
|
|
bd480aa1db | ||
|
|
cf0cf93e47 | ||
|
|
f942072e53 | ||
|
|
f28db6699d | ||
|
|
a447786a24 | ||
|
|
c9e5b35e42 | ||
|
|
a2ffa9dfaf | ||
|
|
3c8c6ad469 | ||
|
|
62dbf0d106 | ||
|
|
22d1f40587 | ||
|
|
909f03460c | ||
|
|
7e99e3ec51 | ||
|
|
af27e084d5 | ||
|
|
8f75a42669 | ||
|
|
1040685c1e | ||
|
|
38aebf4e42 | ||
|
|
62d0354fe0 | ||
|
|
e580f0c869 | ||
|
|
551f12fdfb | ||
|
|
00d59c546d | ||
|
|
0864432105 | ||
|
|
0c3c47b8b4 | ||
|
|
5f7d2659dc | ||
|
|
72c66e91ed | ||
|
|
acb6519ef9 | ||
|
|
0ff86d9ea0 | ||
|
|
b94d14399f | ||
|
|
f4c8dd6b1f | ||
|
|
9139153d16 | ||
|
|
0ff3cf321c | ||
|
|
fe65e0c3b3 | ||
|
|
a7bd7e30c6 | ||
|
|
ea79d2651a | ||
|
|
21a3bae204 | ||
|
|
4bc0eebee0 | ||
|
|
21cf0f5f8e | ||
|
|
96234eafb7 | ||
|
|
ae907cd8a6 | ||
|
|
0ce839c4a8 | ||
|
|
a3e64088b1 | ||
|
|
255df81375 | ||
|
|
5bfffd6209 | ||
|
|
8877e2f758 | ||
|
|
b909369127 | ||
|
|
869cbef571 | ||
|
|
ff4e6a6778 | ||
|
|
fda6d3c969 | ||
|
|
8a549eb45b | ||
|
|
4f3ac34739 | ||
|
|
36fce00270 | ||
|
|
de8ee44b22 | ||
|
|
1b33cb6f9b | ||
|
|
eb47e5c374 | ||
|
|
aca8b0ed18 | ||
|
|
e516014dc0 | ||
|
|
4c99480c42 | ||
|
|
c200a7d2c6 | ||
|
|
aa009091d5 | ||
|
|
f81f0ef9d7 | ||
|
|
66baa9e069 | ||
|
|
375bf86a22 | ||
|
|
fb39a56700 | ||
|
|
0b9415a041 | ||
|
|
b14d76a3d4 | ||
|
|
1263468d49 | ||
|
|
fe4ee0c101 | ||
|
|
30df221bbb | ||
|
|
dfa9b33949 | ||
|
|
354157c003 | ||
|
|
72d538e902 | ||
|
|
f1574e0f42 | ||
|
|
ebfd28d396 | ||
|
|
1861605958 | ||
|
|
f85f7d2fe6 | ||
|
|
03ce4210af | ||
|
|
689a65cc92 | ||
|
|
44a5d7bb8d | ||
|
|
072e20b585 | ||
|
|
89ec4f4e8a | ||
|
|
89d92e738f | ||
|
|
c9f4e3c301 | ||
|
|
a80fa25a68 | ||
|
|
bb040b90d1 | ||
|
|
747f7beae7 | ||
|
|
eb4159788d | ||
|
|
2feaff3648 | ||
|
|
45fe89c26f | ||
|
|
b167bf2d37 | ||
|
|
6fe43e45e9 | ||
|
|
d23aa6ada4 | ||
|
|
7936502047 | ||
|
|
79c53c6458 | ||
|
|
5489f34b92 | ||
|
|
5f659b0ef6 | ||
|
|
db346e4c05 | ||
|
|
b05c7e3309 | ||
|
|
8e7a3045dd | ||
|
|
7c33579260 | ||
|
|
a38d0bbacc | ||
|
|
f4ee7076dd | ||
|
|
42bec6c11e | ||
|
|
b93c244e2d | ||
|
|
e1ed816a13 | ||
|
|
bc48691738 | ||
|
|
ae83d1d356 | ||
|
|
66b86044a9 | ||
|
|
d967ce860c | ||
|
|
d11b51d0f1 | ||
|
|
0e0dd46c1b | ||
|
|
5978d157e7 | ||
|
|
ba34384b71 | ||
|
|
a792737add | ||
|
|
22a77ce54e | ||
|
|
cab6e34390 | ||
|
|
c20243c8bf | ||
|
|
54e3276ba2 | ||
|
|
f1ff81884a | ||
|
|
2b0091eca2 | ||
|
|
37b4203967 | ||
|
|
08d77664a7 | ||
|
|
b0ab4d3e63 | ||
|
|
f06cd961d5 | ||
|
|
c8590d37ed | ||
|
|
8544bf08ac | ||
|
|
6142283e99 | ||
|
|
721bc54dbd | ||
|
|
f2a826bb5d | ||
|
|
8660f72915 | ||
|
|
8d531976a1 | ||
|
|
cbfa0acfb3 | ||
|
|
7195e8a15d | ||
|
|
26ec0e8e4a | ||
|
|
5d5be25008 | ||
|
|
390a0edf70 | ||
|
|
2896cfbdb2 | ||
|
|
2ff0a67e10 | ||
|
|
0bd774dd45 | ||
|
|
4fdfcde018 | ||
|
|
eaefc7bea1 | ||
|
|
427bdb2203 | ||
|
|
e8e531ffb2 | ||
|
|
a3f9f45c54 | ||
|
|
801d656d48 | ||
|
|
e727eed1d1 | ||
|
|
0701c4dff3 | ||
|
|
57312e5cd5 | ||
|
|
07d2537de6 | ||
|
|
46f31b3f15 | ||
|
|
abbc5963f2 | ||
|
|
f1274041ce | ||
|
|
f2c747ce19 | ||
|
|
8f4e87dd1f | ||
|
|
3df8bd8be8 | ||
|
|
46daea5238 | ||
|
|
1be3d30027 | ||
|
|
6b362fb591 | ||
|
|
cd31ae7afe | ||
|
|
a3fd4ba62f | ||
|
|
9b8507706c | ||
|
|
ea0161d132 | ||
|
|
a36f515b30 | ||
|
|
3ed1a9d098 | ||
|
|
a07d5b7bd1 | ||
|
|
88e129f793 | ||
|
|
4582e6185a | ||
|
|
3ef4622772 | ||
|
|
0895163344 | ||
|
|
44b990e331 | ||
|
|
ae32ece346 | ||
|
|
c929b6946d | ||
|
|
ff95eba35f | ||
|
|
38ef0e2bf2 | ||
|
|
a8c20ca35b | ||
|
|
790b2a0b14 | ||
|
|
cf7a93cd80 | ||
|
|
d606a2aad5 | ||
|
|
91c9a26e4d | ||
|
|
add924e35e | ||
|
|
3d901d3680 | ||
|
|
f2226ee745 | ||
|
|
f2c27dc1b5 | ||
|
|
d43418bf05 | ||
|
|
d0ee66c2ce | ||
|
|
bd25a4bdab | ||
|
|
a088f6afb3 | ||
|
|
bc5c7817a8 | ||
|
|
76ca94094b | ||
|
|
191c6dd678 | ||
|
|
a5163d0be4 | ||
|
|
4792631f06 | ||
|
|
f7e2f07c05 | ||
|
|
bc0288e57f | ||
|
|
ee3c4a2681 | ||
|
|
b00e93dd3d | ||
|
|
d03aed23c8 | ||
|
|
9b697ce832 | ||
|
|
f2b9dc3aaa | ||
|
|
9c9b2721c5 | ||
|
|
dace55907f | ||
|
|
5ede3c8e47 | ||
|
|
b69b9227c4 | ||
|
|
dc956eb48c | ||
|
|
5820416ede | ||
|
|
e7b80b7fa2 | ||
|
|
33b1f039ea | ||
|
|
a514d168bf | ||
|
|
847a81afd3 | ||
|
|
ef811e699c | ||
|
|
d13ea90c23 | ||
|
|
9338dd082b | ||
|
|
b4fce063b0 |
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -2,9 +2,9 @@ name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, release* ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ master, release* ]
|
||||
|
||||
jobs:
|
||||
run-eslint:
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [taku0](https://github.com/taku0)
|
||||
- [Peter Spenler](https://github.com/peterspenler)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -115,3 +117,5 @@
|
||||
- [Tim Hobbs](https://github.com/timhobbs)
|
||||
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
|
||||
- [jomp16](https://github.com/jomp16)
|
||||
- [Leon de Klerk](https://github.com/leondeklerk)
|
||||
- [CrispyBaguette](https://github.com/CrispyBaguette)
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin-web"
|
||||
version: "10.8.0"
|
||||
version: "10.8.13"
|
||||
packages:
|
||||
- debian.all
|
||||
- fedora.all
|
||||
|
||||
84
debian/changelog
vendored
84
debian/changelog
vendored
@@ -1,12 +1,90 @@
|
||||
jellyfin-web (10.8.13-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.13; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.13
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 28 Nov 2023 22:21:29 -0500
|
||||
|
||||
jellyfin-web (10.8.12-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.12; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.12
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 04 Nov 2023 14:42:41 -0400
|
||||
|
||||
jellyfin-web (10.8.11-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.11; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.11
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 23 Sep 2023 21:41:40 -0400
|
||||
|
||||
jellyfin-web (10.8.10-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.10; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.10
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 23 Apr 2023 11:01:33 -0400
|
||||
|
||||
jellyfin-web (10.8.9-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.9; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.9
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 22 Jan 2023 14:09:13 -0500
|
||||
|
||||
jellyfin-web (10.8.8-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.8; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.8
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 29 Nov 2022 13:42:54 -0500
|
||||
|
||||
jellyfin-web (10.8.7-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.7; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.7
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 31 Oct 2022 23:06:34 -0400
|
||||
|
||||
jellyfin-web (10.8.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.6; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.6
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 28 Oct 2022 22:44:15 -0400
|
||||
|
||||
jellyfin-web (10.8.5-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.5; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.5
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 24 Sep 2022 22:02:26 -0400
|
||||
|
||||
jellyfin-web (10.8.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 13 Aug 2022 21:52:04 -0400
|
||||
|
||||
jellyfin-web (10.8.3-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.3
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 01 Aug 2022 20:22:00 -0400
|
||||
|
||||
jellyfin-web (10.8.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.2
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 01 Aug 2022 14:27:56 -0400
|
||||
|
||||
jellyfin-web (10.8.1-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.1
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 26 Jun 2022 20:59:39 -0400
|
||||
|
||||
jellyfin-web (10.8.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
* New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:58:23 -0500
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 10 Jun 2022 22:16:40 -0400
|
||||
|
||||
jellyfin-web (10.7.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
* New upstream version 10.7.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.7.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:13:31 -0400
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM fedora:33
|
||||
FROM fedora:36
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
@@ -11,7 +11,7 @@ ENV IS_DOCKER=YES
|
||||
|
||||
# Prepare Fedora environment
|
||||
RUN dnf update -y \
|
||||
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel
|
||||
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core nodejs autoconf automake glibc-devel make
|
||||
|
||||
# Link to build script
|
||||
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora /build.sh
|
||||
|
||||
@@ -9,8 +9,12 @@ TARBALL :=$(NAME)-$(subst -,~,$(VERSION)).tar.gz
|
||||
|
||||
epel-7-x86_64_repos := https://rpm.nodesource.com/pub_16.x/el/\$$releasever/\$$basearch/
|
||||
|
||||
fed_ver := $(shell rpm -E %fedora)
|
||||
# fallback when not running on Fedora
|
||||
fed_ver ?= 36
|
||||
TARGET ?= fedora-$(fed_ver)-x86_64
|
||||
|
||||
outdir ?= $(PWD)/$(DIR)/
|
||||
TARGET ?= fedora-35-x86_64
|
||||
|
||||
srpm: $(DIR)/$(SRPM)
|
||||
tarball: $(DIR)/$(TARBALL)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
%global debug_package %{nil}
|
||||
|
||||
Name: jellyfin-web
|
||||
Version: 10.8.0
|
||||
Version: 10.8.13
|
||||
Release: 1%{?dist}
|
||||
Summary: The Free Software Media System web client
|
||||
License: GPLv3
|
||||
License: GPLv2
|
||||
URL: https://jellyfin.org
|
||||
# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
|
||||
Source0: jellyfin-web-%{version}.tar.gz
|
||||
@@ -17,9 +17,6 @@ BuildRequires: git
|
||||
BuildRequires: npm
|
||||
%endif
|
||||
|
||||
# Disable Automatic Dependency Processing
|
||||
AutoReqProv: no
|
||||
|
||||
%description
|
||||
Jellyfin is a free software media system that puts you in control of managing and streaming your media.
|
||||
|
||||
@@ -27,27 +24,53 @@ Jellyfin is a free software media system that puts you in control of managing an
|
||||
%prep
|
||||
%autosetup -n jellyfin-web-%{version} -b 0
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
%if 0%{?rhel} > 0 && 0%{?rhel} < 8
|
||||
# Required for CentOS build
|
||||
chown root:root -R .
|
||||
%endif
|
||||
|
||||
|
||||
%build
|
||||
npm ci --no-audit --unsafe-perm
|
||||
%{__mkdir} -p %{buildroot}%{_datadir}
|
||||
mv dist %{buildroot}%{_datadir}/jellyfin-web
|
||||
%{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
|
||||
|
||||
|
||||
%install
|
||||
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
||||
%{__cp} -r dist/* %{buildroot}%{_libdir}/jellyfin/jellyfin-web
|
||||
|
||||
|
||||
%files
|
||||
%defattr(644,root,root,755)
|
||||
%{_datadir}/jellyfin-web
|
||||
%{_datadir}/licenses/jellyfin/LICENSE
|
||||
%{_libdir}/jellyfin/jellyfin-web
|
||||
%license LICENSE
|
||||
|
||||
|
||||
%changelog
|
||||
* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- Forthcoming stable release
|
||||
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- Forthcoming stable release
|
||||
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- Forthcoming stable release
|
||||
* Tue Nov 28 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.13; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.13
|
||||
* Sat Nov 04 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.12; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.12
|
||||
* Sat Sep 23 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.11; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.11
|
||||
* Sun Apr 23 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.10; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.10
|
||||
* Sun Jan 22 2023 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.9; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.9
|
||||
* Tue Nov 29 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.8; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.8
|
||||
* Mon Oct 31 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.7; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.7
|
||||
* Fri Oct 28 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.6; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.6
|
||||
* Sat Sep 24 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.5; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.5
|
||||
* Sat Aug 13 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.4; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.4
|
||||
* Mon Aug 01 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.3; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.3
|
||||
* Mon Aug 01 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.2; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.2
|
||||
* Sun Jun 26 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.1; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.1
|
||||
* Fri Jun 10 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.8.0
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.8.0",
|
||||
"version": "10.8.13",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2450,6 +2450,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans-sc/-/noto-sans-sc-4.5.2.tgz",
|
||||
"integrity": "sha512-q9a61ND72RZdb4S1SG3w0c8POLS7s7QtfMMbfFG/jlsKxidsBnUS66jAE+h+OhaQCl8FeQrY8yMnHOff/4paFw=="
|
||||
},
|
||||
"@fontsource/noto-sans-tc": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans-tc/-/noto-sans-tc-4.5.2.tgz",
|
||||
"integrity": "sha512-K7c/1Uh7SlBdxRBNsYONtC/oPG/6wJ4xsbnYY4Q+XqpwggJkKYB9dmAgdC1XTDHcY9dZX8AO+oH+wyB4rvdI/w=="
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
||||
@@ -2467,6 +2472,11 @@
|
||||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jellyfin/libass-wasm": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jellyfin/libass-wasm/-/libass-wasm-4.1.1.tgz",
|
||||
"integrity": "sha512-xQVJw+lZUg4U1TmLS80reBECfPtpCgRF8hhUSvUUQM9g68OvINyUU3K2yqRH+8tomGpghiRaIcr/bUJ83e0veA=="
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
|
||||
@@ -5243,9 +5253,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"epubjs": {
|
||||
"version": "0.3.90",
|
||||
"resolved": "https://registry.npmjs.org/epubjs/-/epubjs-0.3.90.tgz",
|
||||
"integrity": "sha512-8S9Zi9aE3QHvkJbq1mJTfaE1++UysKxgeb2GEv3oR9PEsK+Sx3tzxs3QaRselAVPoTlP5gfLyEgp9BQIyAu8lA==",
|
||||
"version": "0.3.93",
|
||||
"resolved": "https://registry.npmjs.org/epubjs/-/epubjs-0.3.93.tgz",
|
||||
"integrity": "sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==",
|
||||
"requires": {
|
||||
"@types/localforage": "0.0.34",
|
||||
"@xmldom/xmldom": "^0.7.5",
|
||||
@@ -8041,10 +8051,6 @@
|
||||
"resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz",
|
||||
"integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg=="
|
||||
},
|
||||
"libass-wasm": {
|
||||
"version": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6",
|
||||
"from": "libass-wasm@git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4"
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "10.8.0",
|
||||
"version": "10.8.13",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
@@ -69,13 +69,15 @@
|
||||
"@fontsource/noto-sans-jp": "4.5.2",
|
||||
"@fontsource/noto-sans-kr": "4.5.2",
|
||||
"@fontsource/noto-sans-sc": "4.5.2",
|
||||
"@fontsource/noto-sans-tc": "4.5.2",
|
||||
"@jellyfin/libass-wasm": "4.1.1",
|
||||
"blurhash": "1.1.4",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.20.2",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.4",
|
||||
"epubjs": "0.3.90",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"fast-text-encoding": "1.0.3",
|
||||
"flv.js": "1.6.2",
|
||||
@@ -86,7 +88,6 @@
|
||||
"jquery": "3.6.0",
|
||||
"jstree": "3.3.12",
|
||||
"libarchive.js": "1.3.0",
|
||||
"libass-wasm": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4",
|
||||
"lodash-es": "4.17.21",
|
||||
"marked": "4.0.10",
|
||||
"material-design-icons-iconfont": "6.1.1",
|
||||
|
||||
2
src/apiclient.d.ts
vendored
2
src/apiclient.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
// TODO: Move to jellyfin-apiclient
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module 'jellyfin-apiclient' {
|
||||
import {
|
||||
import type {
|
||||
AllThemeMediaResult,
|
||||
AuthenticationResult,
|
||||
BaseItemDto,
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
@import "../../styles/noto-sans/index.scss";
|
||||
|
||||
@mixin font($weight: null, $size: null) {
|
||||
font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC", sans-serif;
|
||||
font-weight: $weight;
|
||||
font-size: $size;
|
||||
}
|
||||
|
||||
html {
|
||||
@include font($size: 93%);
|
||||
font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC", "Noto Sans TC", sans-serif;
|
||||
text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
html[lang|="ja"] {
|
||||
font-family: "Noto Sans", "Noto Sans JP", "Noto Sans HK", "Noto Sans KR", "Noto Sans SC", "Noto Sans TC", sans-serif;
|
||||
}
|
||||
|
||||
html[lang|="ko"] {
|
||||
font-family: "Noto Sans", "Noto Sans KR", "Noto Sans HK", "Noto Sans JP", "Noto Sans SC", "Noto Sans TC", sans-serif;
|
||||
}
|
||||
|
||||
html[lang|="zh-CN"] {
|
||||
font-family: "Noto Sans", "Noto Sans SC", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans TC", sans-serif;
|
||||
}
|
||||
|
||||
html[lang|="zh-TW"] {
|
||||
font-family: "Noto Sans", "Noto Sans TC", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
html[lang|="zh-HK"] {
|
||||
font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC", "Noto Sans TC", sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include font(400, 1.8em);
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
.itemBackdrop {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 0;
|
||||
background-position: center center;
|
||||
background-attachment: fixed;
|
||||
height: 40vh;
|
||||
position: relative;
|
||||
@@ -447,6 +447,7 @@
|
||||
|
||||
.layout-mobile & {
|
||||
background-attachment: initial;
|
||||
background-position: top center;
|
||||
margin-top: 3rem;
|
||||
|
||||
@media all and (orientation: portrait) and (max-width: 40em) {
|
||||
@@ -523,10 +524,6 @@
|
||||
margin: -0.25em 0 0.25em;
|
||||
}
|
||||
|
||||
.layout-mobile .itemExternalLinks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mainDetailButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -742,6 +739,8 @@
|
||||
left: 3.3%;
|
||||
top: -80%;
|
||||
width: 25vw;
|
||||
// FIXME: the fixed width + max height cause the card to be cropped this needs a proper fix
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.layout-tv & {
|
||||
|
||||
@@ -7,3 +7,8 @@
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: background sizing for cards really needs revisited, but these are particularly terrible
|
||||
#channelsTab .cardImageContainer {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
@@ -59,8 +59,6 @@ class AppRouter {
|
||||
this.baseRoute = this.baseRoute.substring(0, this.baseRoute.length - 1);
|
||||
}
|
||||
|
||||
this.setBaseRoute();
|
||||
|
||||
// paths that start with a hashbang (i.e. /#!/page.html) get transformed to starting with //
|
||||
// we need to strip one "/" for our routes to work
|
||||
page('//*', (ctx) => {
|
||||
@@ -68,18 +66,6 @@ class AppRouter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setBaseRoute() {
|
||||
let baseRoute = window.location.pathname.replace(this.getRequestFile(), '');
|
||||
if (baseRoute.lastIndexOf('/') === baseRoute.length - 1) {
|
||||
baseRoute = baseRoute.substring(0, baseRoute.length - 1);
|
||||
}
|
||||
console.debug('setting page base to ' + baseRoute);
|
||||
page.base(baseRoute);
|
||||
}
|
||||
|
||||
addRoute(path, newRoute) {
|
||||
page(path, this.getHandler(newRoute));
|
||||
this.allRoutes.push(newRoute);
|
||||
|
||||
@@ -39,7 +39,8 @@ function getDeviceProfile(item) {
|
||||
profile = profileBuilder(builderOpts);
|
||||
}
|
||||
|
||||
const maxTranscodingVideoWidth = appHost.screen()?.maxAllowedWidth;
|
||||
const maxVideoWidth = appSettings.maxVideoWidth();
|
||||
const maxTranscodingVideoWidth = maxVideoWidth < 0 ? appHost.screen()?.maxAllowedWidth : maxVideoWidth;
|
||||
|
||||
if (maxTranscodingVideoWidth) {
|
||||
profile.TranscodingProfiles.forEach((transcodingProfile) => {
|
||||
|
||||
@@ -820,7 +820,7 @@ import ServerConnections from '../ServerConnections';
|
||||
if (isUsingLiveTvNaming(item)) {
|
||||
lines.push(escapeHtml(item.Name));
|
||||
|
||||
if (!item.EpisodeTitle) {
|
||||
if (!item.EpisodeTitle && !item.IndexNumber) {
|
||||
titleAdded = true;
|
||||
}
|
||||
} else {
|
||||
@@ -949,7 +949,7 @@ import ServerConnections from '../ServerConnections';
|
||||
|
||||
}, item.ChannelName));
|
||||
} else {
|
||||
lines.push(escapeHtml(item.ChannelName) || ' ');
|
||||
lines.push(escapeHtml(item.ChannelName || '') || ' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -981,7 +981,7 @@ import ServerConnections from '../ServerConnections';
|
||||
if (item.RecordAnyChannel) {
|
||||
lines.push(globalize.translate('AllChannels'));
|
||||
} else {
|
||||
lines.push(escapeHtml(item.ChannelName) || globalize.translate('OneChannel'));
|
||||
lines.push(escapeHtml(item.ChannelName || '') || globalize.translate('OneChannel'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1347,7 +1347,7 @@ import ServerConnections from '../ServerConnections';
|
||||
|
||||
cardImageContainerClose = '</div>';
|
||||
} else {
|
||||
const cardImageContainerAriaLabelAttribute = ` aria-label="${item.Name}"`;
|
||||
const cardImageContainerAriaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
|
||||
|
||||
// Don't use the IMG tag with safari because it puts a white border around it
|
||||
cardImageContainerOpen = imgUrl ? ('<button data-action="' + action + '" class="' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + cardImageContainerAriaLabelAttribute + '>') : ('<button data-action="' + action + '" class="' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction"' + cardImageContainerAriaLabelAttribute + '>');
|
||||
@@ -1430,7 +1430,7 @@ import ServerConnections from '../ServerConnections';
|
||||
if (tagName === 'button') {
|
||||
className += ' itemAction';
|
||||
actionAttribute = ' data-action="' + action + '"';
|
||||
ariaLabelAttribute = ` aria-label="${item.Name}"`;
|
||||
ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
|
||||
} else {
|
||||
actionAttribute = '';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
type IProps = {
|
||||
@@ -17,7 +18,7 @@ const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checke
|
||||
class="${className}"
|
||||
${dataAttributes} ${checkedAttribute}
|
||||
/>
|
||||
<span>${Name} ${AppName}</span>
|
||||
<span>${escapeHtml(Name || '')} ${AppName}</span>
|
||||
</label>`
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { localeWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import { getLocaleWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import cardBuilder from '../../cardbuilder/cardBuilder';
|
||||
|
||||
@@ -31,7 +31,7 @@ type IProps = {
|
||||
|
||||
const getLastSeenText = (lastActivityDate?: string | null) => {
|
||||
if (lastActivityDate) {
|
||||
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
|
||||
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), getLocaleWithSuffix()));
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import Dashboard from '../../../scripts/clientUtils';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
@@ -43,7 +43,7 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
||||
Promise.all(promises).then(
|
||||
responses => {
|
||||
const folders = responses[0];
|
||||
const parentPath = responses[1] || '';
|
||||
const parentPath = (responses[1] ? JSON.parse(responses[1]) : '') || '';
|
||||
let html = '';
|
||||
|
||||
page.querySelector('.results').scrollTop = 0;
|
||||
@@ -267,7 +267,7 @@ class DirectoryBrowser {
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += escapeHtml(options.header) || globalize.translate('HeaderSelectPath');
|
||||
html += escapeHtml(options.header || '') || globalize.translate('HeaderSelectPath');
|
||||
html += '</h3>';
|
||||
html += '</div>';
|
||||
html += getEditorHtml(options, systemInfo);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<option value="es_DO">Español (Dominicana)</option>
|
||||
<option value="es-MX">Español (México)</option>
|
||||
<option value="et">Eesti</option>
|
||||
<option value="eu">Euskara</option>
|
||||
<option value="fa">فارسی</option>
|
||||
<option value="fi">Suomi</option>
|
||||
<option value="fil">Filipino</option>
|
||||
|
||||
@@ -87,7 +87,7 @@ import template from './filterdialog.template.html';
|
||||
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
|
||||
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
|
||||
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
|
||||
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
|
||||
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
|
||||
@@ -272,15 +272,25 @@ import template from './filterdialog.template.html';
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkHDFilter = context.querySelector('.chkHDFilter');
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkHDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsHD = chkHDFilter.checked ? true : null;
|
||||
if (chkHDFilter.checked) {
|
||||
chkSDFilter.checked = false;
|
||||
query.IsHD = true;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkSDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsHD = chkSDFilter.checked ? false : null;
|
||||
if (chkSDFilter.checked) {
|
||||
chkHDFilter.checked = false;
|
||||
query.IsHD = false;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
|
||||
@@ -72,11 +72,14 @@ import ServerConnections from '../ServerConnections';
|
||||
promises.push(loadSection(elem, apiClient, user, userSettings, userViews, sections, i));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(function () {
|
||||
return resume(elem, {
|
||||
refresh: true
|
||||
return Promise.all(promises)
|
||||
// Timeout for polyfilled CustomElements (webOS 1.2)
|
||||
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
||||
.then(() => {
|
||||
return resume(elem, {
|
||||
refresh: true
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let noLibDescription;
|
||||
if (user['Policy'] && user['Policy']['IsAdministrator']) {
|
||||
|
||||
@@ -131,7 +131,8 @@ import { Events } from 'jellyfin-apiclient';
|
||||
}
|
||||
|
||||
function setCurrentTimeIfNeeded(element, seconds) {
|
||||
if (Math.abs(element.currentTime || 0, seconds) <= 1) {
|
||||
// If it's worth skipping (1 sec or less of a difference)
|
||||
if (Math.abs((element.currentTime || 0) - seconds) >= 1) {
|
||||
element.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ import template from './imageDownloader.template.html';
|
||||
let selectedProvider;
|
||||
let browsableParentId;
|
||||
|
||||
function getBaseRemoteOptions(page) {
|
||||
function getBaseRemoteOptions(page, forceCurrentItemId = false) {
|
||||
const options = {};
|
||||
|
||||
if (page.querySelector('#chkShowParentImages').checked && browsableParentId) {
|
||||
if (!forceCurrentItemId && page.querySelector('#chkShowParentImages').checked && browsableParentId) {
|
||||
options.itemId = browsableParentId;
|
||||
} else {
|
||||
options.itemId = currentItemId;
|
||||
@@ -140,7 +140,7 @@ import template from './imageDownloader.template.html';
|
||||
}
|
||||
|
||||
function downloadRemoteImage(page, apiClient, url, type, provider) {
|
||||
const options = getBaseRemoteOptions(page);
|
||||
const options = getBaseRemoteOptions(page, true);
|
||||
|
||||
options.Type = type;
|
||||
options.ImageUrl = url;
|
||||
|
||||
@@ -83,7 +83,7 @@ worker.addEventListener(
|
||||
source = entry;
|
||||
}
|
||||
|
||||
if (entry.intersectionRatio > 0) {
|
||||
if (entry.isIntersecting) {
|
||||
if (source) {
|
||||
fillImageElement(target, source);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export function canEdit(user, item) {
|
||||
}
|
||||
|
||||
export function isLocalItem(item) {
|
||||
if (item && item.Id && item.Id.indexOf('local') === 0) {
|
||||
if (item && item.Id && typeof item.Id === 'string' && item.Id.indexOf('local') === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -331,6 +331,17 @@ export function supportsMediaSourceSelection (item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function sortTracks (trackA, trackB) {
|
||||
let cmp = trackA.IsExternal - trackB.IsExternal;
|
||||
if (cmp != 0) return cmp;
|
||||
cmp = trackB.IsForced - trackA.IsForced;
|
||||
if (cmp != 0) return cmp;
|
||||
cmp = trackB.IsDefault - trackA.IsDefault;
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
return trackA.Index - trackB.Index;
|
||||
}
|
||||
|
||||
export default {
|
||||
getDisplayName: getDisplayName,
|
||||
supportsAddingToCollection: supportsAddingToCollection,
|
||||
@@ -346,5 +357,6 @@ export default {
|
||||
canRate: canRate,
|
||||
canConvert: canConvert,
|
||||
canRefreshMetadata: canRefreshMetadata,
|
||||
supportsMediaSourceSelection: supportsMediaSourceSelection
|
||||
supportsMediaSourceSelection: supportsMediaSourceSelection,
|
||||
sortTracks: sortTracks
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import toast from '../toast/toast';
|
||||
import { copy } from '../../scripts/clipboard';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import itemHelper from '../../components/itemHelper';
|
||||
import loading from '../loading/loading';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../listview/listview.scss';
|
||||
@@ -71,8 +72,8 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </s
|
||||
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
|
||||
}
|
||||
for (let i = 0, length = version.MediaStreams.length; i < length; i++) {
|
||||
const stream = version.MediaStreams[i];
|
||||
version.MediaStreams.sort(itemHelper.sortTracks);
|
||||
for (const stream of version.MediaStreams) {
|
||||
if (stream.Type === 'Data') {
|
||||
continue;
|
||||
}
|
||||
@@ -112,7 +113,7 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </s
|
||||
if (stream.Profile) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoProfile'), stream.Profile));
|
||||
}
|
||||
if (stream.Level) {
|
||||
if (stream.Level > 0) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLevel'), stream.Level));
|
||||
}
|
||||
if (stream.Width || stream.Height) {
|
||||
@@ -127,7 +128,7 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </s
|
||||
}
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoInterlaced'), (stream.IsInterlaced ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.AverageFrameRate || stream.RealFrameRate) {
|
||||
if ((stream.AverageFrameRate || stream.RealFrameRate) && stream.Type === 'Video') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoFramerate'), (stream.AverageFrameRate || stream.RealFrameRate)));
|
||||
}
|
||||
if (stream.ChannelLayout) {
|
||||
@@ -136,7 +137,7 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </s
|
||||
if (stream.Channels) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoChannels'), `${stream.Channels} ch`));
|
||||
}
|
||||
if (stream.BitRate && stream.Codec !== 'mjpeg') {
|
||||
if (stream.BitRate) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitrate'), `${parseInt(stream.BitRate / 1000)} kbps`));
|
||||
}
|
||||
if (stream.SampleRate) {
|
||||
@@ -148,6 +149,36 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </s
|
||||
if (stream.VideoRange) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
|
||||
}
|
||||
if (stream.VideoRangeType) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
|
||||
}
|
||||
if (stream.VideoDoViTitle) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDoViTitle'), stream.VideoDoViTitle));
|
||||
if (stream.DvVersionMajor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMajor'), stream.DvVersionMajor));
|
||||
}
|
||||
if (stream.DvVersionMinor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMinor'), stream.DvVersionMinor));
|
||||
}
|
||||
if (stream.DvProfile != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvProfile'), stream.DvProfile));
|
||||
}
|
||||
if (stream.DvLevel != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvLevel'), stream.DvLevel));
|
||||
}
|
||||
if (stream.RpuPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoRpuPresentFlag'), stream.RpuPresentFlag));
|
||||
}
|
||||
if (stream.ElPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoElPresentFlag'), stream.ElPresentFlag));
|
||||
}
|
||||
if (stream.BlPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBlPresentFlag'), stream.BlPresentFlag));
|
||||
}
|
||||
if (stream.DvBlSignalCompatibilityId != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvBlSignalCompatibilityId'), stream.DvBlSignalCompatibilityId));
|
||||
}
|
||||
}
|
||||
if (stream.ColorSpace) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorSpace'), stream.ColorSpace));
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ import template from './itemidentifier.template.html';
|
||||
} else {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
}
|
||||
html += escapeHtml(lines[i]) || ' ';
|
||||
html += escapeHtml(lines[i] || '') || ' ';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription chkAutomaticallyAddToCollectionContainer hide advanced">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkAutomaticallyAddToCollection" checked />
|
||||
<input is="emby-checkbox" type="checkbox" id="chkAutomaticallyAddToCollection" />
|
||||
<span>${LabelAutomaticallyAddToCollection}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelAutomaticallyAddToCollectionHelp}</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="fldCommunityRating" class="hide inputContainer">
|
||||
<input is="emby-input" id="txtCommunityRating" type="number" step=".1" min="0" max="10" label="${LabelCommunityRating}" />
|
||||
<input is="emby-input" id="txtCommunityRating" type="number" step="any" min="0" max="10" label="${LabelCommunityRating}" />
|
||||
</div>
|
||||
<div id="fldCriticRating" class="hide inputContainer">
|
||||
<input is="emby-input" id="txtCriticRating" type="number" step=".1" label="${LabelCriticRating}" />
|
||||
|
||||
@@ -24,6 +24,7 @@ import { appRouter } from '../appRouter';
|
||||
|
||||
let currentTimeElement;
|
||||
let nowPlayingImageElement;
|
||||
let nowPlayingImageUrl;
|
||||
let nowPlayingTextElement;
|
||||
let nowPlayingUserData;
|
||||
let muteButton;
|
||||
@@ -488,7 +489,6 @@ import { appRouter } from '../appRouter';
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentImgUrl;
|
||||
function updateNowPlayingInfo(state) {
|
||||
const nowPlayingItem = state.NowPlayingItem;
|
||||
|
||||
@@ -502,14 +502,14 @@ import { appRouter } from '../appRouter';
|
||||
textLines[1].secondary = true;
|
||||
if (textLines[1].text) {
|
||||
const text = document.createElement('a');
|
||||
text.innerHTML = textLines[1].text;
|
||||
text.innerText = textLines[1].text;
|
||||
secondaryText.appendChild(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (textLines[0].text) {
|
||||
const text = document.createElement('a');
|
||||
text.innerHTML = textLines[0].text;
|
||||
text.innerText = textLines[0].text;
|
||||
itemText.appendChild(text);
|
||||
}
|
||||
nowPlayingTextElement.appendChild(itemText);
|
||||
@@ -524,17 +524,14 @@ import { appRouter } from '../appRouter';
|
||||
height: imgHeight
|
||||
})) : null;
|
||||
|
||||
let isRefreshing = false;
|
||||
|
||||
if (url !== currentImgUrl) {
|
||||
currentImgUrl = url;
|
||||
isRefreshing = true;
|
||||
|
||||
if (url !== nowPlayingImageUrl) {
|
||||
if (url) {
|
||||
imageLoader.lazyImage(nowPlayingImageElement, url);
|
||||
nowPlayingImageUrl = url;
|
||||
imageLoader.lazyImage(nowPlayingImageElement, nowPlayingImageUrl);
|
||||
nowPlayingImageElement.style.display = null;
|
||||
nowPlayingTextElement.style.marginLeft = null;
|
||||
} else {
|
||||
nowPlayingImageUrl = null;
|
||||
nowPlayingImageElement.style.backgroundImage = '';
|
||||
nowPlayingImageElement.style.display = 'none';
|
||||
nowPlayingTextElement.style.marginLeft = '1em';
|
||||
@@ -542,36 +539,34 @@ import { appRouter } from '../appRouter';
|
||||
}
|
||||
|
||||
if (nowPlayingItem.Id) {
|
||||
if (isRefreshing) {
|
||||
const apiClient = ServerConnections.getApiClient(nowPlayingItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), nowPlayingItem.Id).then(function (item) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
if (!layoutManager.mobile) {
|
||||
let contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
// We remove the previous event listener by replacing the item in each update event
|
||||
const contextButtonClone = contextButton.cloneNode(true);
|
||||
contextButton.parentNode.replaceChild(contextButtonClone, contextButton);
|
||||
contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
const options = {
|
||||
play: false,
|
||||
queue: false,
|
||||
stopPlayback: true,
|
||||
clearQueue: true,
|
||||
positionTo: contextButton
|
||||
};
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
contextButton.addEventListener('click', function () {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
user: user
|
||||
}, options));
|
||||
});
|
||||
const apiClient = ServerConnections.getApiClient(nowPlayingItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), nowPlayingItem.Id).then(function (item) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
if (!layoutManager.mobile) {
|
||||
let contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
// We remove the previous event listener by replacing the item in each update event
|
||||
const contextButtonClone = contextButton.cloneNode(true);
|
||||
contextButton.parentNode.replaceChild(contextButtonClone, contextButton);
|
||||
contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu');
|
||||
const options = {
|
||||
play: false,
|
||||
queue: false,
|
||||
stopPlayback: true,
|
||||
clearQueue: true,
|
||||
positionTo: contextButton
|
||||
};
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
contextButton.addEventListener('click', function () {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
user: user
|
||||
}, options));
|
||||
});
|
||||
}
|
||||
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
});
|
||||
} else {
|
||||
nowPlayingUserData.innerHTML = '';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../scripts/clientUtils';
|
||||
import globalize from '../../scripts/globalize';
|
||||
@@ -263,7 +263,7 @@ const UserEditPage: FunctionComponent = () => {
|
||||
}
|
||||
});
|
||||
|
||||
window.ApiClient.getServerConfiguration().then(function (config) {
|
||||
window.ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
|
||||
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { AccessSchedule, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import { DynamicDayOfWeek } from '@thornbill/jellyfin-sdk/dist/generated-client/models/dynamic-day-of-week';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ImageType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import { ImageType } from '@thornbill/jellyfin-sdk/dist/generated-client/models/image-type';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import Dashboard from '../../scripts/clientUtils';
|
||||
@@ -134,7 +135,9 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
});
|
||||
|
||||
(page.querySelector('.btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
|
||||
(page.querySelector('#uploadImage') as HTMLInputElement).click();
|
||||
const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement;
|
||||
uploadImage.value = '';
|
||||
uploadImage.click();
|
||||
});
|
||||
|
||||
(page.querySelector('#uploadImage') as HTMLInputElement).addEventListener('change', function (evt: Event) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
|
||||
import Dashboard from '../../scripts/clientUtils';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
export function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) {
|
||||
let topItem = nowPlayingItem;
|
||||
let bottomItem = null;
|
||||
@@ -61,13 +59,13 @@ export function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) {
|
||||
const list = [];
|
||||
|
||||
list.push({
|
||||
text: escapeHtml(topText),
|
||||
text: topText,
|
||||
item: topItem
|
||||
});
|
||||
|
||||
if (bottomText) {
|
||||
list.push({
|
||||
text: escapeHtml(bottomText),
|
||||
text: bottomText,
|
||||
item: bottomItem
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { appHost } from '../apphost';
|
||||
import Screenfull from 'screenfull';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import { includesAny } from '../../utils/container.ts';
|
||||
|
||||
const UNLIMITED_ITEMS = -1;
|
||||
|
||||
@@ -1306,6 +1307,7 @@ class PlaybackManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
const container = mediaSource.Container.toLowerCase();
|
||||
const codec = (mediaStream.Codec || '').toLowerCase();
|
||||
|
||||
if (!codec) {
|
||||
@@ -1314,22 +1316,11 @@ class PlaybackManager {
|
||||
|
||||
const profiles = deviceProfile.DirectPlayProfiles || [];
|
||||
|
||||
return profiles.filter(function (p) {
|
||||
if (p.Type === 'Video') {
|
||||
if (!p.AudioCodec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is an exclusion filter
|
||||
if (p.AudioCodec.indexOf('-') === 0) {
|
||||
return p.AudioCodec.toLowerCase().indexOf(codec) === -1;
|
||||
}
|
||||
|
||||
return p.AudioCodec.toLowerCase().indexOf(codec) !== -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).length > 0;
|
||||
return profiles.some(function (p) {
|
||||
return p.Type === 'Video'
|
||||
&& includesAny((p.Container || '').toLowerCase(), container)
|
||||
&& includesAny((p.AudioCodec || '').toLowerCase(), codec);
|
||||
});
|
||||
}
|
||||
|
||||
self.setAudioStreamIndex = function (index, player) {
|
||||
@@ -1686,6 +1677,7 @@ class PlaybackManager {
|
||||
const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks, player);
|
||||
streamInfo.fullscreen = currentPlayOptions.fullscreen;
|
||||
streamInfo.lastMediaInfoQuery = lastMediaInfoQuery;
|
||||
streamInfo.resetSubtitleOffset = false;
|
||||
|
||||
if (!streamInfo.url) {
|
||||
showPlaybackInfoErrorMessage(self, 'PlaybackErrorNoCompatibleStream');
|
||||
@@ -1742,6 +1734,8 @@ class PlaybackManager {
|
||||
}
|
||||
|
||||
function translateItemsForPlayback(items, options) {
|
||||
if (!items.length) return Promise.resolve([]);
|
||||
|
||||
if (items.length > 1 && options && options.ids) {
|
||||
// Use the original request id array for sorting the result in the proper order
|
||||
items.sort(function (a, b) {
|
||||
@@ -2275,7 +2269,7 @@ class PlaybackManager {
|
||||
score += 1;
|
||||
if (prevRelIndex == newRelIndex)
|
||||
score += 1;
|
||||
if (prevStream.Title && prevStream.Title == stream.Title)
|
||||
if (prevStream.DisplayTitle && prevStream.DisplayTitle == stream.DisplayTitle)
|
||||
score += 2;
|
||||
if (prevStream.Language && prevStream.Language != 'und' && prevStream.Language == stream.Language)
|
||||
score += 2;
|
||||
@@ -2300,7 +2294,7 @@ class PlaybackManager {
|
||||
}
|
||||
}
|
||||
|
||||
function autoSetNextTracks(prevSource, mediaSource) {
|
||||
function autoSetNextTracks(prevSource, mediaSource, audio, subtitle) {
|
||||
try {
|
||||
if (!prevSource) return;
|
||||
|
||||
@@ -2309,18 +2303,13 @@ class PlaybackManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof prevSource.DefaultAudioStreamIndex != 'number'
|
||||
|| typeof prevSource.DefaultSubtitleStreamIndex != 'number')
|
||||
return;
|
||||
|
||||
if (typeof mediaSource.DefaultAudioStreamIndex != 'number'
|
||||
|| typeof mediaSource.DefaultSubtitleStreamIndex != 'number') {
|
||||
console.warn('AutoSet - No stream indexes (but prevSource has them)');
|
||||
return;
|
||||
if (audio && typeof prevSource.DefaultAudioStreamIndex == 'number') {
|
||||
rankStreamType(prevSource.DefaultAudioStreamIndex, prevSource, mediaSource, 'Audio');
|
||||
}
|
||||
|
||||
rankStreamType(prevSource.DefaultAudioStreamIndex, prevSource, mediaSource, 'Audio');
|
||||
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
|
||||
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
|
||||
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`AutoSet - Caught unexpected error: ${e}`);
|
||||
}
|
||||
@@ -2384,9 +2373,9 @@ class PlaybackManager {
|
||||
// this reference was only needed by sendPlaybackListToPlayer
|
||||
playOptions.items = null;
|
||||
|
||||
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(function (mediaSource) {
|
||||
if (userSettings.enableSetUsingLastTracks())
|
||||
autoSetNextTracks(prevSource, mediaSource);
|
||||
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(async (mediaSource) => {
|
||||
const user = await apiClient.getCurrentUser();
|
||||
autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
|
||||
|
||||
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
|
||||
|
||||
@@ -3034,7 +3023,7 @@ class PlaybackManager {
|
||||
|
||||
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
|
||||
|
||||
if (streamInfo) {
|
||||
if (streamInfo?.url) {
|
||||
const currentlyPreventsVideoStreamCopy = streamInfo.url.toLowerCase().indexOf('allowvideostreamcopy=false') !== -1;
|
||||
const currentlyPreventsAudioStreamCopy = streamInfo.url.toLowerCase().indexOf('allowaudiostreamcopy=false') !== -1;
|
||||
|
||||
@@ -3657,7 +3646,7 @@ class PlaybackManager {
|
||||
if (player.audioTracks) {
|
||||
const result = player.audioTracks();
|
||||
if (result) {
|
||||
return result;
|
||||
return result.sort(itemHelper.sortTracks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3666,14 +3655,14 @@ class PlaybackManager {
|
||||
const mediaStreams = (mediaSource || {}).MediaStreams || [];
|
||||
return mediaStreams.filter(function (s) {
|
||||
return s.Type === 'Audio';
|
||||
});
|
||||
}).sort(itemHelper.sortTracks);
|
||||
}
|
||||
|
||||
subtitleTracks(player = this._currentPlayer) {
|
||||
if (player.subtitleTracks) {
|
||||
const result = player.subtitleTracks();
|
||||
if (result) {
|
||||
return result;
|
||||
return result.sort(itemHelper.sortTracks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3682,7 +3671,7 @@ class PlaybackManager {
|
||||
const mediaStreams = (mediaSource || {}).MediaStreams || [];
|
||||
return mediaStreams.filter(function (s) {
|
||||
return s.Type === 'Subtitle';
|
||||
});
|
||||
}).sort(itemHelper.sortTracks);
|
||||
}
|
||||
|
||||
getSupportedCommands(player) {
|
||||
|
||||
@@ -199,7 +199,8 @@ function showWithUser(options, player, user) {
|
||||
});
|
||||
}
|
||||
|
||||
if (user && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
if (options.quality && supportedCommands.includes('SetMaxStreamingBitrate')
|
||||
&& user?.Policy?.EnableVideoPlaybackTranscoding) {
|
||||
const secondaryQualityText = getQualitySecondaryText(player);
|
||||
|
||||
menuItems.push({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import browser from '../../scripts/browser';
|
||||
import appSettings from '../../scripts/settings/appSettings';
|
||||
import { appHost } from '../apphost';
|
||||
import focusManager from '../focusManager';
|
||||
@@ -41,7 +40,7 @@ import template from './playbackSettings.template.html';
|
||||
select.innerHTML = html;
|
||||
}
|
||||
|
||||
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
|
||||
function fillQuality(select, isInNetwork, mediatype, maxVideoWidth) {
|
||||
const options = mediatype === 'Audio' ? qualityoptions.getAudioQualityOptions({
|
||||
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
@@ -52,7 +51,8 @@ import template from './playbackSettings.template.html';
|
||||
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
|
||||
enableAuto: true
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
|
||||
});
|
||||
|
||||
@@ -60,6 +60,10 @@ import template from './playbackSettings.template.html';
|
||||
// render empty string instead of 0 for the auto option
|
||||
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
|
||||
fillQuality(select, isInNetwork, mediatype);
|
||||
|
||||
if (appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype)) {
|
||||
select.value = '';
|
||||
@@ -68,12 +72,13 @@ import template from './playbackSettings.template.html';
|
||||
}
|
||||
}
|
||||
|
||||
function fillChromecastQuality(select) {
|
||||
function fillChromecastQuality(select, maxVideoWidth) {
|
||||
const options = qualityoptions.getVideoQualityOptions({
|
||||
|
||||
currentMaxBitrate: appSettings.maxChromecastBitrate(),
|
||||
isAutomaticBitrateEnabled: !appSettings.maxChromecastBitrate(),
|
||||
enableAuto: true
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
});
|
||||
|
||||
select.innerHTML = options.map(i => {
|
||||
@@ -133,15 +138,6 @@ import template from './playbackSettings.template.html';
|
||||
});
|
||||
}
|
||||
|
||||
function showOrHideEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
const loggedInUserId = apiClient.getCurrentUserId();
|
||||
const userId = user.Id;
|
||||
@@ -180,7 +176,8 @@ import template from './playbackSettings.template.html';
|
||||
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
|
||||
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
|
||||
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
|
||||
context.querySelector('.chkSetUsingLastTracks').checked = userSettings.enableSetUsingLastTracks();
|
||||
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
|
||||
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
|
||||
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
|
||||
|
||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
@@ -192,6 +189,9 @@ import template from './playbackSettings.template.html';
|
||||
const selectChromecastVersion = context.querySelector('.selectChromecastVersion');
|
||||
selectChromecastVersion.value = userSettings.chromecastVersion();
|
||||
|
||||
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
|
||||
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
|
||||
|
||||
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
|
||||
fillSkipLengths(selectSkipForwardLength);
|
||||
selectSkipForwardLength.value = userSettings.skipForwardLength();
|
||||
@@ -200,8 +200,6 @@ import template from './playbackSettings.template.html';
|
||||
fillSkipLengths(selectSkipBackLength);
|
||||
selectSkipBackLength.value = userSettings.skipBackLength();
|
||||
|
||||
showOrHideEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
@@ -209,6 +207,7 @@ import template from './playbackSettings.template.html';
|
||||
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
|
||||
|
||||
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
|
||||
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
|
||||
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||
@@ -222,7 +221,8 @@ import template from './playbackSettings.template.html';
|
||||
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
|
||||
|
||||
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);
|
||||
userSettingsInstance.enableSetUsingLastTracks(context.querySelector('.chkSetUsingLastTracks').checked);
|
||||
user.Configuration.RememberAudioSelections = context.querySelector('.chkRememberAudioSelections').checked;
|
||||
user.Configuration.RememberSubtitleSelections = context.querySelector('.chkRememberSubtitleSelections').checked;
|
||||
userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value);
|
||||
userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value);
|
||||
userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value);
|
||||
@@ -247,6 +247,36 @@ import template from './playbackSettings.template.html';
|
||||
});
|
||||
}
|
||||
|
||||
function setSelectValue(select, value, defaultValue) {
|
||||
select.value = value;
|
||||
|
||||
if (select.selectedIndex < 0) {
|
||||
select.value = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function onMaxVideoWidthChange(e) {
|
||||
const context = this.options.element;
|
||||
|
||||
const selectVideoInNetworkQuality = context.querySelector('.selectVideoInNetworkQuality');
|
||||
const selectVideoInternetQuality = context.querySelector('.selectVideoInternetQuality');
|
||||
const selectChromecastVideoQuality = context.querySelector('.selectChromecastVideoQuality');
|
||||
|
||||
const selectVideoInNetworkQualityValue = selectVideoInNetworkQuality.value;
|
||||
const selectVideoInternetQualityValue = selectVideoInternetQuality.value;
|
||||
const selectChromecastVideoQualityValue = selectChromecastVideoQuality.value;
|
||||
|
||||
const maxVideoWidth = parseInt(e.target.value || '0', 10) || 0;
|
||||
|
||||
fillQuality(selectVideoInNetworkQuality, true, 'Video', maxVideoWidth);
|
||||
fillQuality(selectVideoInternetQuality, false, 'Video', maxVideoWidth);
|
||||
fillChromecastQuality(selectChromecastVideoQuality, maxVideoWidth);
|
||||
|
||||
setSelectValue(selectVideoInNetworkQuality, selectVideoInNetworkQualityValue, '');
|
||||
setSelectValue(selectVideoInternetQuality, selectVideoInternetQualityValue, '');
|
||||
setSelectValue(selectChromecastVideoQuality, selectChromecastVideoQualityValue, '');
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
@@ -274,6 +304,8 @@ import template from './playbackSettings.template.html';
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
|
||||
|
||||
self.loadData();
|
||||
|
||||
if (options.autoFocus) {
|
||||
|
||||
@@ -41,6 +41,19 @@
|
||||
<div class="selectContainer fldChromecastQuality hide">
|
||||
<select is="emby-select" class="selectChromecastVideoQuality" label="${LabelMaxChromecastBitrate}"></select>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" class="selectLabelMaxVideoWidth" label="${LabelMaxVideoResolution}">
|
||||
<option value="0">${Auto}</option>
|
||||
<option value="-1">${ScreenResolution}</option>
|
||||
<option value="640">360p</option>
|
||||
<option value="852">480p</option>
|
||||
<option value="1280">720p</option>
|
||||
<option value="1920">1080p</option>
|
||||
<option value="3840">4K</option>
|
||||
<option value="7680">8K</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection verticalSection-extrabottompadding musicQualitySection hide">
|
||||
@@ -75,7 +88,7 @@
|
||||
<div class="fieldDescription checkboxFieldDescription">${CinemaModeConfigurationHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer fldEpisodeAutoPlay hide">
|
||||
<div class="checkboxContainer fldEpisodeAutoPlay">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkEpisodeAutoPlay" />
|
||||
<span>${PlayNextEpisodeAutomatically}</span>
|
||||
@@ -84,10 +97,18 @@
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkSetUsingLastTracks" />
|
||||
<span>${SetUsingLastTracks}</span>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkRememberAudioSelections" />
|
||||
<span>${RememberAudioSelections}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${SetUsingLastTracksHelp}</div>
|
||||
<div class="fieldDescription checkboxFieldDescription">${RememberAudioSelectionsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkRememberSubtitleSelections" />
|
||||
<span>${RememberSubtitleSelections}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${RememberSubtitleSelectionsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldEnableNextVideoOverlay">
|
||||
|
||||
@@ -269,31 +269,10 @@ import ServerConnections from '../ServerConnections';
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.VideoRange) {
|
||||
if (videoStream.VideoRangeType) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoRange'),
|
||||
value: videoStream.VideoRange
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.ColorSpace) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelColorSpace'),
|
||||
value: videoStream.ColorSpace
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.ColorTransfer) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelColorTransfer'),
|
||||
value: videoStream.ColorTransfer
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.ColorPrimaries) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelColorPrimaries'),
|
||||
value: videoStream.ColorPrimaries
|
||||
label: globalize.translate('LabelVideoRangeType'),
|
||||
value: videoStream.VideoRangeType
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { appHost } from '../components/apphost';
|
||||
import globalize from '../scripts/globalize';
|
||||
import appSettings from '../scripts/settings/appSettings';
|
||||
|
||||
export function getVideoQualityOptions(options) {
|
||||
const maxStreamingBitrate = options.currentMaxBitrate;
|
||||
@@ -12,7 +13,9 @@ export function getVideoQualityOptions(options) {
|
||||
videoWidth = videoHeight * (16 / 9);
|
||||
}
|
||||
|
||||
const hostScreenWidth = appHost.screen()?.maxAllowedWidth || 4096;
|
||||
const maxVideoWidth = options.maxVideoWidth == null ? appSettings.maxVideoWidth() : options.maxVideoWidth;
|
||||
|
||||
const hostScreenWidth = (maxVideoWidth < 0 ? appHost.screen()?.maxAllowedWidth : maxVideoWidth) || 4096;
|
||||
const maxAllowedWidth = videoWidth || 4096;
|
||||
|
||||
const qualityOptions = [];
|
||||
|
||||
@@ -13,9 +13,12 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import itemContextMenu from '../itemContextMenu';
|
||||
import '../cardbuilder/card.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import './remotecontrol.scss';
|
||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-slider/emby-slider';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../appRouter';
|
||||
@@ -145,9 +148,7 @@ function updateNowPlayingInfo(context, state, serverId) {
|
||||
if (item.Artists != null) {
|
||||
if (item.ArtistItems != null) {
|
||||
for (const artist of item.ArtistItems) {
|
||||
const artistName = escapeHtml(artist.Name);
|
||||
const artistId = artist.Id;
|
||||
artistsSeries += `<a class="button-link emby-button" is="emby-linkbutton" href="#!/details?id=${artistId}&serverId=${nowPlayingServerId}">${escapeHtml(artistName)}</a>`;
|
||||
artistsSeries += `<a class="button-link emby-button" is="emby-linkbutton" href="#!/details?id=${artist.Id}&serverId=${nowPlayingServerId}">${escapeHtml(artist.Name)}</a>`;
|
||||
if (artist !== item.ArtistItems.slice(-1)[0]) {
|
||||
artistsSeries += ', ';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import classNames from 'classnames';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
@@ -54,7 +54,6 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||
IncludeArtists: false
|
||||
});
|
||||
|
||||
// FIXME: This query does not support Live TV filters
|
||||
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
||||
apiClient?.getCurrentUserId(),
|
||||
{
|
||||
@@ -79,11 +78,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||
// Movies row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: true,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
IsMovie: true
|
||||
}).then(result => setMovies(result.Items || []));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, {
|
||||
@@ -97,28 +92,16 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
||||
// Sports row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: true,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
IsSports: true
|
||||
}).then(result => setSports(result.Items || []));
|
||||
// Kids row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: true,
|
||||
IsNews: false
|
||||
IsKids: true
|
||||
}).then(result => setKids(result.Items || []));
|
||||
// News row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: false,
|
||||
IsSeries: false,
|
||||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: true
|
||||
}).then(result => setNews(result.Items || []));
|
||||
// Programs row
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import classNames from 'classnames';
|
||||
import { ApiClient } from 'jellyfin-apiclient';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import type { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
|
||||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ function init(instance) {
|
||||
playbackManager.setSubtitleOffset(inputOffset, player);
|
||||
// synchronize with slider value
|
||||
subtitleSyncSlider.updateOffset(
|
||||
getPercentageFromOffset(inputOffset));
|
||||
getSliderValueFromOffset(inputOffset));
|
||||
} else {
|
||||
this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's';
|
||||
}
|
||||
@@ -79,17 +79,17 @@ function init(instance) {
|
||||
}
|
||||
};
|
||||
|
||||
subtitleSyncSlider.updateOffset = function (percent) {
|
||||
// default value is 0s = 50%
|
||||
this.value = percent === undefined ? 50 : percent;
|
||||
subtitleSyncSlider.updateOffset = function (sliderValue) {
|
||||
// default value is 0s = 0ms
|
||||
this.value = sliderValue === undefined ? 0 : sliderValue;
|
||||
};
|
||||
|
||||
subtitleSyncSlider.addEventListener('change', function () {
|
||||
// set new offset
|
||||
playbackManager.setSubtitleOffset(getOffsetFromPercentage(this.value), player);
|
||||
playbackManager.setSubtitleOffset(getOffsetFromSliderValue(this.value), player);
|
||||
// synchronize with textField value
|
||||
subtitleSyncTextField.updateOffset(
|
||||
getOffsetFromPercentage(this.value));
|
||||
getOffsetFromSliderValue(this.value));
|
||||
});
|
||||
|
||||
subtitleSyncSlider.getBubbleHtml = function (value) {
|
||||
@@ -108,20 +108,22 @@ function init(instance) {
|
||||
}
|
||||
|
||||
function getOffsetFromPercentage(value) {
|
||||
// convert percent to fraction
|
||||
// convert percentage to fraction
|
||||
let offset = (value - 50) / 50;
|
||||
// multiply by offset min/max range value (-x to +x) :
|
||||
offset *= 30;
|
||||
return offset.toFixed(1);
|
||||
}
|
||||
|
||||
function getPercentageFromOffset(value) {
|
||||
// divide by offset min/max range value (-x to +x) :
|
||||
let percentValue = value / 30;
|
||||
// convert fraction to percent
|
||||
percentValue *= 50;
|
||||
percentValue += 50;
|
||||
return Math.min(100, Math.max(0, percentValue.toFixed(1)));
|
||||
function getOffsetFromSliderValue(value) {
|
||||
// convert slider value to offset
|
||||
const offset = value / 10;
|
||||
return offset.toFixed(1);
|
||||
}
|
||||
|
||||
function getSliderValueFromOffset(value) {
|
||||
const sliderValue = value * 10;
|
||||
return Math.min(300, Math.max(-300, sliderValue.toFixed(1)));
|
||||
}
|
||||
|
||||
class SubtitleSync {
|
||||
@@ -152,8 +154,8 @@ class SubtitleSync {
|
||||
if (playbackManager.isShowingSubtitleOffsetEnabled(player) && playbackManager.canHandleOffsetOnCurrentSubtitle(player)) {
|
||||
// if no subtitle offset is defined or element has focus (offset being defined)
|
||||
if (!(playbackManager.getPlayerSubtitleOffset(player) || subtitleSyncTextField.hasFocus)) {
|
||||
// set default offset to '0' = 50%
|
||||
subtitleSyncSlider.value = '50';
|
||||
// set default offset to '0' = 0ms
|
||||
subtitleSyncSlider.value = '0';
|
||||
subtitleSyncTextField.textContent = '0s';
|
||||
playbackManager.setSubtitleOffset(0, player);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<button type="button" is="paper-icon-button-light" class="subtitleSync-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>
|
||||
<div class="subtitleSyncTextField" contenteditable="true" spellcheck="false">0s</div>
|
||||
<div class="sliderContainer subtitleSyncSliderContainer">
|
||||
<input is="emby-slider" type="range" step=".1" min="0" max="100" value="50" class="subtitleSyncSlider" data-slider-keep-progress="true" />
|
||||
<input is="emby-slider" type="range" step="1" min="-300" max="300" value="0" class="subtitleSyncSlider" data-slider-keep-progress="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import dom from '../../scripts/dom';
|
||||
import loading from '../../components/loading/loading';
|
||||
import scrollHelper from '../../libraries/scroller';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import template from './subtitleuploader.template.html';
|
||||
@@ -61,7 +63,7 @@ function setFiles(page, files) {
|
||||
reader.onload = (function (theFile) {
|
||||
return function () {
|
||||
// Render file.
|
||||
const html = '<a><span class="material-icons subtitles" aria-hidden="true" style="transform: translateY(25%);"></span><span>' + escape(theFile.name) + '</span><a/>';
|
||||
const html = `<div><span class="material-icons subtitles" aria-hidden="true" style="transform: translateY(25%);"></span><span>${escapeHtml(theFile.name)}</span></div>`;
|
||||
|
||||
page.querySelector('#subtitleOutput').innerHTML = html;
|
||||
page.querySelector('#fldUpload').classList.remove('hide');
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="selectContainer flex-grow">
|
||||
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}" autofocus></select>
|
||||
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}"></select>
|
||||
</div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Upload}</span>
|
||||
|
||||
@@ -108,7 +108,7 @@ function renderDevices(view, devices) {
|
||||
function discoverDevices(view) {
|
||||
loading.show();
|
||||
view.querySelector('.loadingContent').classList.remove('hide');
|
||||
return ApiClient.getJSON(ApiClient.getUrl('LiveTv/Tuners/Discvover', {
|
||||
return ApiClient.getJSON(ApiClient.getUrl('LiveTv/Tuners/Discover', {
|
||||
NewDevicesOnly: true
|
||||
})).then(function (devices) {
|
||||
currentDevices = devices;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import datetime from '../../scripts/datetime';
|
||||
import loading from '../../components/loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
@@ -25,13 +27,13 @@ import { pageIdOn } from '../../scripts/clientUtils';
|
||||
let html = '';
|
||||
html += '<tr class="detailTableBodyRow detailTableBodyRow-shaded">';
|
||||
html += '<td class="detailTableBodyCell">';
|
||||
html += '<button type="button" is="emby-button" data-token="' + item.AccessToken + '" class="raised raised-mini btnRevoke" data-mini="true" title="' + globalize.translate('ButtonRevoke') + '" style="margin:0;">' + globalize.translate('ButtonRevoke') + '</button>';
|
||||
html += '<button type="button" is="emby-button" data-token="' + escapeHTML(item.AccessToken) + '" class="raised raised-mini btnRevoke" data-mini="true" title="' + globalize.translate('ButtonRevoke') + '" style="margin:0;">' + globalize.translate('ButtonRevoke') + '</button>';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AccessToken;
|
||||
html += escapeHTML(item.AccessToken);
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AppName || '';
|
||||
html += escapeHTML(item.AppName) || '';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
const date = datetime.parseISO8601Date(item.DateCreated, true);
|
||||
|
||||
@@ -6,7 +6,7 @@ import serverNotifications from '../../scripts/serverNotifications';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { localeWithSuffix } from '../../scripts/dfnshelper';
|
||||
import { getLocaleWithSuffix } from '../../scripts/dfnshelper';
|
||||
import loading from '../../components/loading/loading';
|
||||
import playMethodHelper from '../../components/playback/playmethodhelper';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
@@ -476,7 +476,7 @@ import confirm from '../../components/confirm/confirm';
|
||||
// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences
|
||||
if (!nowPlayingItem) {
|
||||
return {
|
||||
html: globalize.translate('LastSeen', formatDistanceToNow(Date.parse(session.LastActivityDate), localeWithSuffix)),
|
||||
html: globalize.translate('LastSeen', formatDistanceToNow(Date.parse(session.LastActivityDate), getLocaleWithSuffix())),
|
||||
image: imgUrl
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import dom from '../../../scripts/dom';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import imageHelper from '../../../scripts/imagehelper';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { localeWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import { getLocaleWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../../components/cardbuilder/card.scss';
|
||||
@@ -91,14 +91,17 @@ import confirm from '../../../components/confirm/confirm';
|
||||
}
|
||||
|
||||
function load(page, devices) {
|
||||
const localeWithSuffix = getLocaleWithSuffix();
|
||||
|
||||
let html = '';
|
||||
html += devices.map(function (device) {
|
||||
let deviceHtml = '';
|
||||
deviceHtml += "<div data-id='" + device.Id + "' class='card backdropCard'>";
|
||||
deviceHtml += "<div data-id='" + escapeHtml(device.Id) + "' class='card backdropCard'>";
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="${canEdit ? '#!/device.html?id=' + device.Id : '#'}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
deviceHtml += `<a is="emby-linkbutton" href="${canEdit ? '#!/device.html?id=' + escapeHtml(device.Id) : '#'}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
// audit note: getDeviceIcon returns static text
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
@@ -114,7 +117,7 @@ import confirm from '../../../components/confirm/confirm';
|
||||
|
||||
if (canEdit || canDelete(device.Id)) {
|
||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + device.Id + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<span>MPEG2</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="amf,nvenc,videotoolbox" />
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="nvenc,videotoolbox" />
|
||||
<span>MPEG4</span>
|
||||
</label>
|
||||
<label>
|
||||
@@ -110,7 +110,7 @@
|
||||
<span>${EnableIntelLowPowerHevcHwEncoder}</span>
|
||||
</label>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://01.org/linuxgraphics/downloads/firmware" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration/intel#configure-and-verify-lp-mode-on-linux" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,13 +125,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription fldVppTonemapping hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkVppTonemapping" />
|
||||
<span>${EnableVppTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowVppTonemappingHelp}</div>
|
||||
<div class="vppTonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkVppTonemapping" />
|
||||
<span>${EnableVppTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowVppTonemappingHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtVppTonemappingBrightness" pattern="[0-9]*" min="0" max="100" step=".00001" label="${LabelVppTonemappingBrightness}" />
|
||||
<div class="fieldDescription">${LabelVppTonemappingBrightnessHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtVppTonemappingContrast" pattern="[0-9]*" min="1" max="2" step=".00001" label="${LabelVppTonemappingContrast}" />
|
||||
<div class="fieldDescription">${LabelVppTonemappingContrastHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
@@ -155,6 +166,14 @@
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingMode" label="${LabelTonemappingMode}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="max">MAX</option>
|
||||
<option value="rgb">RGB</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${TonemappingModeHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingRange" label="${LabelTonemappingRange}">
|
||||
<option value="auto">${Auto}</option>
|
||||
@@ -167,10 +186,6 @@
|
||||
<input is="emby-input" type="number" id="txtTonemappingDesat" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingDesat}" />
|
||||
<div class="fieldDescription">${LabelTonemappingDesatHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingThreshold" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingThreshold}" />
|
||||
<div class="fieldDescription">${LabelTonemappingThresholdHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingPeak" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingPeak}" />
|
||||
<div class="fieldDescription">${LabelTonemappingPeakHelp}</div>
|
||||
@@ -208,9 +223,8 @@
|
||||
<div class="inputContainer fldEncoderPath">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" class="txtEncoderPath" label="${LabelffmpegPath}" autocomplete="off" />
|
||||
<input is="emby-input" class="txtEncoderPath" label="${LabelffmpegPath}" autocomplete="off" disabled />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectEncoderPath" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">
|
||||
<div>${LabelffmpegPathHelp}</div>
|
||||
|
||||
@@ -32,11 +32,13 @@ import alert from '../../components/alert';
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast;
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || '';
|
||||
page.querySelector('#txtH264Crf').value = config.H264Crf || '';
|
||||
page.querySelector('#txtH265Crf').value = config.H265Crf || '';
|
||||
@@ -86,11 +88,13 @@ import alert from '../../components/alert';
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
|
||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
|
||||
config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value;
|
||||
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
|
||||
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
|
||||
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;
|
||||
config.VppTonemappingContrast = form.querySelector('#txtVppTonemappingContrast').value;
|
||||
config.EncoderPreset = form.querySelector('#selectEncoderPreset').value;
|
||||
config.H264Crf = parseInt(form.querySelector('#txtH264Crf').value || '0');
|
||||
config.H265Crf = parseInt(form.querySelector('#txtH265Crf').value || '0');
|
||||
@@ -205,9 +209,9 @@ import alert from '../../components/alert';
|
||||
}
|
||||
|
||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
||||
page.querySelector('.fldVppTonemapping').classList.remove('hide');
|
||||
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldVppTonemapping').classList.add('hide');
|
||||
page.querySelector('.vppTonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv') {
|
||||
@@ -230,21 +234,6 @@ import alert from '../../components/alert';
|
||||
|
||||
setDecodingCodecsVisible(page, this.value);
|
||||
});
|
||||
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('.txtEncoderPath', page).val(path);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
|
||||
@@ -72,6 +72,12 @@
|
||||
<textarea is="emby-textarea" id="txtCustomCss" label="${LabelCustomCss}" class="textarea-mono"></textarea>
|
||||
<div class="fieldDescription">${LabelCustomCssHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSplashScreenAvailable" />
|
||||
<span>${EnableSplashScreen}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
|
||||
@@ -39,6 +39,7 @@ import alert from '../../components/alert';
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
|
||||
brandingConfig.LoginDisclaimer = form.querySelector('#txtLoginDisclaimer').value;
|
||||
brandingConfig.CustomCss = form.querySelector('#txtCustomCss').value;
|
||||
brandingConfig.SplashscreenEnabled = form.querySelector('#chkSplashScreenAvailable').checked;
|
||||
|
||||
ApiClient.updateNamedConfiguration(brandingConfigKey, brandingConfig).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
@@ -106,6 +107,7 @@ import alert from '../../components/alert';
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function (config) {
|
||||
view.querySelector('#txtLoginDisclaimer').value = config.LoginDisclaimer || '';
|
||||
view.querySelector('#txtCustomCss').value = config.CustomCss || '';
|
||||
view.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import alert from '../../components/alert';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function onSubmit() {
|
||||
function onSubmit(event) {
|
||||
event.preventDefault();
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
|
||||
@@ -38,7 +38,7 @@ function reload(page) {
|
||||
$('.monitorUsers', page).hide();
|
||||
}
|
||||
|
||||
$('.notificationType', page).html(escapeHtml(typeInfo.Name) || 'Unknown Notification');
|
||||
$('.notificationType', page).html(escapeHtml(typeInfo.Name || '') || 'Unknown Notification');
|
||||
|
||||
if (!notificationConfig) {
|
||||
notificationConfig = {
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<div class="readOnlyContent">
|
||||
<div is="emby-collapse" title="${HeaderDeveloperInfo}">
|
||||
<div class="collapseContent">
|
||||
<p id="developer"></p>
|
||||
<p>${LabelDeveloper}: <span id="developer"></span></p>
|
||||
<p>${LabelRepositoryName}: <span id="repositoryName"></span></p>
|
||||
<p>${LabelRepositoryUrl}: <span id="repositoryUrl"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -53,24 +53,34 @@ function renderPackage(pkg, installedPlugins, page) {
|
||||
populateVersions(pkg, page, installedPlugin);
|
||||
populateHistory(pkg, page);
|
||||
|
||||
$('.pluginName', page).html(pkg.name);
|
||||
$('.pluginName', page).text(pkg.name);
|
||||
$('#btnInstallDiv', page).removeClass('hide');
|
||||
$('#pSelectVersion', page).removeClass('hide');
|
||||
|
||||
if (pkg.overview) {
|
||||
$('#overview', page).show().html(pkg.overview);
|
||||
$('#overview', page).show().text(pkg.overview);
|
||||
} else {
|
||||
$('#overview', page).hide();
|
||||
}
|
||||
|
||||
$('#description', page).html(pkg.description);
|
||||
$('#developer', page).html(pkg.owner);
|
||||
$('#description', page).text(pkg.description);
|
||||
$('#developer', page).text(pkg.owner);
|
||||
// This is a hack; the repository name and URL should be part of the global values
|
||||
// for the plugin, not each individual version. So we just use the top (latest)
|
||||
// version to get this information. If it's missing (no versions), then say so.
|
||||
if (pkg.versions.length) {
|
||||
$('#repositoryName', page).text(pkg.versions[0].repositoryName);
|
||||
$('#repositoryUrl', page).text(pkg.versions[0].repositoryUrl);
|
||||
} else {
|
||||
$('#repositoryName', page).text(globalize.translate('Unknown'));
|
||||
$('#repositoryUrl', page).text(globalize.translate('Unknown'));
|
||||
}
|
||||
|
||||
if (installedPlugin) {
|
||||
const currentVersionText = globalize.translate('MessageYouHaveVersionInstalled', '<strong>' + installedPlugin.Version + '</strong>');
|
||||
$('#pCurrentVersion', page).show().html(currentVersionText);
|
||||
} else {
|
||||
$('#pCurrentVersion', page).hide().html('');
|
||||
$('#pCurrentVersion', page).hide().text('');
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
@@ -81,7 +91,7 @@ function alertText(options) {
|
||||
}
|
||||
|
||||
function performInstallation(page, name, guid, version) {
|
||||
const developer = $('#developer', page).html().toLowerCase();
|
||||
const repositoryUrl = $('#repositoryUrl', page).html().toLowerCase();
|
||||
|
||||
const alertCallback = function () {
|
||||
loading.show();
|
||||
@@ -94,7 +104,9 @@ function performInstallation(page, name, guid, version) {
|
||||
});
|
||||
};
|
||||
|
||||
if (developer !== 'jellyfin') {
|
||||
// Check the repository URL for the official Jellyfin repository domain, or
|
||||
// present the warning for 3rd party plugins.
|
||||
if (!repositoryUrl.startsWith('https://repo.jellyfin.org/')) {
|
||||
loading.hide();
|
||||
let msg = globalize.translate('MessagePluginInstallDisclaimer');
|
||||
msg += '<br/>';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import escapeHTML from 'escape-html';
|
||||
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
@@ -73,7 +75,7 @@ function populateList(options) {
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + category + '</h2>';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + escapeHTML(category) + '</h2>';
|
||||
html += '<div class="itemsContainer vertical-wrap">';
|
||||
currentCategory = category;
|
||||
}
|
||||
@@ -107,7 +109,7 @@ function getPluginHtml(plugin, options, installedPlugins) {
|
||||
html += `<a class="cardImageContainer" is="emby-linkbutton" style="margin:0;padding:0" href="${href}" ${target}>`;
|
||||
|
||||
if (plugin.imageUrl) {
|
||||
html += `<img src="${plugin.imageUrl}" style="width:100%" />`;
|
||||
html += `<img src="${escapeHTML(plugin.imageUrl)}" style="width:100%" />`;
|
||||
} else {
|
||||
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>';
|
||||
@@ -119,11 +121,9 @@ function getPluginHtml(plugin, options, installedPlugins) {
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter">';
|
||||
html += "<div class='cardText'>";
|
||||
html += plugin.name;
|
||||
html += escapeHTML(plugin.name);
|
||||
html += '</div>';
|
||||
const installedPlugin = installedPlugins.filter(function (ip) {
|
||||
return ip.Id == plugin.guid;
|
||||
})[0];
|
||||
const installedPlugin = installedPlugins.find(installed => installed.Id === plugin.guid);
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += installedPlugin ? globalize.translate('LabelVersionInstalled', installedPlugin.Version) : ' ';
|
||||
html += '</div>';
|
||||
|
||||
@@ -2,9 +2,12 @@ import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import dialogHelper from '../../../../components/dialogHelper/dialogHelper';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../../../elements/emby-select/emby-select';
|
||||
|
||||
import '../../../../components/formdialog.scss';
|
||||
import '../../../../components/listview/listview.scss';
|
||||
|
||||
@@ -19,8 +22,8 @@ function reloadList(page) {
|
||||
noneElement: page.querySelector('#none'),
|
||||
repositories: repositories
|
||||
});
|
||||
}).catch(() => {
|
||||
console.error('error loading repositories');
|
||||
}).catch(e => {
|
||||
console.error('error loading repositories', e);
|
||||
page.querySelector('#none').classList.remove('hide');
|
||||
loading.hide();
|
||||
});
|
||||
@@ -35,46 +38,69 @@ function saveList(page) {
|
||||
contentType: 'application/json'
|
||||
}).then(() => {
|
||||
reloadList(page);
|
||||
}).catch(() => {
|
||||
console.error('error saving repositories');
|
||||
}).catch(e => {
|
||||
console.error('error saving repositories', e);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function populateList(options) {
|
||||
let html = '';
|
||||
const paperList = document.createElement('div');
|
||||
paperList.className = 'paperList';
|
||||
|
||||
html += '<div class="paperList">';
|
||||
for (let i = 0; i < options.repositories.length; i++) {
|
||||
html += getRepositoryHtml(options.repositories[i]);
|
||||
}
|
||||
options.repositories.forEach(repo => {
|
||||
paperList.appendChild(getRepositoryElement(repo));
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
if (!options.repositories.length) {
|
||||
options.noneElement.classList.remove('hide');
|
||||
} else {
|
||||
options.noneElement.classList.add('hide');
|
||||
}
|
||||
|
||||
options.listElement.innerHTML = html;
|
||||
options.listElement.innerHTML = '';
|
||||
options.listElement.appendChild(paperList);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function getRepositoryHtml(repository) {
|
||||
let html = '';
|
||||
function getRepositoryElement(repository) {
|
||||
const listItem = document.createElement('div');
|
||||
listItem.className = 'listItem listItem-border';
|
||||
|
||||
html += '<div class="listItem listItem-border">';
|
||||
html += `<a is="emby-linkbutton" style="margin:0;padding:0" class="clearLink listItemIconContainer" href="${repository.Url}" rel="noopener noreferrer" target="_blank">`;
|
||||
html += '<span class="material-icons listItemIcon open_in_new" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += `<h3 class="listItemBodyText">${repository.Name}</h3>`;
|
||||
html += `<div class="listItemBodyText secondary">${repository.Url}</div>`;
|
||||
html += '</div>';
|
||||
html += `<button type="button" is="paper-icon-button-light" id="${repository.Url}" class="btnDelete" title="${globalize.translate('Delete')}"><span class="material-icons delete" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
const repoLink = document.createElement('a', 'emby-linkbutton');
|
||||
repoLink.classList.add('clearLink', 'listItemIconContainer');
|
||||
repoLink.style.margin = '0';
|
||||
repoLink.style.padding = '0';
|
||||
repoLink.rel = 'noopener noreferrer';
|
||||
repoLink.target = '_blank';
|
||||
repoLink.href = repository.Url;
|
||||
repoLink.innerHTML = '<span class="material-icons listItemIcon open_in_new" aria-hidden="true"></span>';
|
||||
listItem.appendChild(repoLink);
|
||||
|
||||
return html;
|
||||
const body = document.createElement('div');
|
||||
body.className = 'listItemBody two-line';
|
||||
|
||||
const name = document.createElement('h3');
|
||||
name.className = 'listItemBodyText';
|
||||
name.innerText = repository.Name;
|
||||
body.appendChild(name);
|
||||
|
||||
const url = document.createElement('div');
|
||||
url.className = 'listItemBodyText secondary';
|
||||
url.innerText = repository.Url;
|
||||
body.appendChild(url);
|
||||
|
||||
listItem.appendChild(body);
|
||||
|
||||
const button = document.createElement('button', 'paper-icon-button-light');
|
||||
button.type = 'button';
|
||||
button.classList.add('btnDelete');
|
||||
button.id = repository.Url;
|
||||
button.title = globalize.translate('Delete');
|
||||
button.innerHTML = '<span class="material-icons delete" aria-hidden="true"></span>';
|
||||
listItem.appendChild(button);
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
@@ -141,14 +167,36 @@ export default function(view) {
|
||||
dialog.querySelector('.newPluginForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
|
||||
repositories.push({
|
||||
Name: dialog.querySelector('#txtRepositoryName').value,
|
||||
Url: dialog.querySelector('#txtRepositoryUrl').value,
|
||||
Enabled: true
|
||||
});
|
||||
const repositoryUrl = dialog.querySelector('#txtRepositoryUrl').value.toLowerCase();
|
||||
|
||||
const alertCallback = function () {
|
||||
repositories.push({
|
||||
Name: dialog.querySelector('#txtRepositoryName').value,
|
||||
Url: dialog.querySelector('#txtRepositoryUrl').value,
|
||||
Enabled: true
|
||||
});
|
||||
saveList(view);
|
||||
dialogHelper.close(dialog);
|
||||
};
|
||||
|
||||
// Check the repository URL for the official Jellyfin repository domain, or
|
||||
// present the warning for 3rd party plugins.
|
||||
if (!repositoryUrl.startsWith('https://repo.jellyfin.org/')) {
|
||||
let msg = globalize.translate('MessageRepositoryInstallDisclaimer');
|
||||
msg += '<br/>';
|
||||
msg += '<br/>';
|
||||
msg += globalize.translate('PleaseConfirmRepositoryInstallation');
|
||||
|
||||
confirm(msg, globalize.translate('HeaderConfirmRepositoryInstallation')).then(function () {
|
||||
alertCallback();
|
||||
}).catch(() => {
|
||||
console.debug('repository not installed');
|
||||
dialogHelper.close(dialog);
|
||||
});
|
||||
} else {
|
||||
alertCallback();
|
||||
}
|
||||
|
||||
saveList(view);
|
||||
dialogHelper.close(dialog);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ import confirm from '../../../components/confirm/confirm';
|
||||
|
||||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||
if (trigger.MaxRuntimeMs) {
|
||||
if (trigger.MaxRuntimeTicks) {
|
||||
html += '<div class="listItemBody two-line">';
|
||||
} else {
|
||||
html += '<div class="listItemBody">';
|
||||
}
|
||||
html += "<div class='listItemBodyText'>" + ScheduledTaskPage.getTriggerFriendlyName(trigger) + '</div>';
|
||||
if (trigger.MaxRuntimeMs) {
|
||||
if (trigger.MaxRuntimeTicks) {
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
const hours = trigger.MaxRuntimeTicks / 36e9;
|
||||
if (hours == 1) {
|
||||
@@ -202,7 +202,7 @@ import confirm from '../../../components/confirm/confirm';
|
||||
let timeLimit = $('#txtTimeLimit', page).val() || '0';
|
||||
timeLimit = parseFloat(timeLimit) * 3600000;
|
||||
|
||||
trigger.MaxRuntimeMs = timeLimit || null;
|
||||
trigger.MaxRuntimeTicks = timeLimit * 1e4 || null;
|
||||
|
||||
return trigger;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Events } from 'jellyfin-apiclient';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import serverNotifications from '../../../scripts/serverNotifications';
|
||||
import { formatDistance, formatDistanceToNow } from 'date-fns';
|
||||
import { getLocale, localeWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import { getLocale, getLocaleWithSuffix } from '../../../scripts/dfnshelper';
|
||||
import '../../../components/listview/listview.scss';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
|
||||
@@ -77,7 +77,7 @@ import '../../../elements/emby-button/emby-button';
|
||||
if (task.LastExecutionResult) {
|
||||
const endtime = Date.parse(task.LastExecutionResult.EndTimeUtc);
|
||||
const starttime = Date.parse(task.LastExecutionResult.StartTimeUtc);
|
||||
html += globalize.translate('LabelScheduledTaskLastRan', formatDistanceToNow(endtime, localeWithSuffix),
|
||||
html += globalize.translate('LabelScheduledTaskLastRan', formatDistanceToNow(endtime, getLocaleWithSuffix()),
|
||||
formatDistance(starttime, endtime, { locale: getLocale() }));
|
||||
if (task.LastExecutionResult.Status === 'Failed') {
|
||||
html += " <span style='color:#FF0000;'>(" + globalize.translate('LabelFailed') + ')</span>';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mainDetailButtons focuscontainer-x">
|
||||
<button is="emby-button" type="button" class="button-flat btnPlay hide detailButton raised" title="${ButtonResume}" data-action="resume">
|
||||
<button is="emby-button" type="button" class="button-flat btnPlay hide detailButton" title="${ButtonResume}" data-action="resume">
|
||||
<div class="detailButton-content">
|
||||
<span class="material-icons detailButton-icon play_arrow" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ function renderTrackSelections(page, instance, item, forceReload) {
|
||||
});
|
||||
|
||||
resolutionNames.sort((a, b) => parseInt(b.Name, 10) - parseInt(a.Name, 10));
|
||||
sourceNames.sort(function(a, b) {
|
||||
sourceNames.sort((a, b) => {
|
||||
const nameA = a.Name.toUpperCase();
|
||||
const nameB = b.Name.toUpperCase();
|
||||
if (nameA < nameB) {
|
||||
@@ -274,6 +274,7 @@ function renderAudioSelections(page, mediaSources) {
|
||||
const tracks = mediaSource.MediaStreams.filter(function (m) {
|
||||
return m.Type === 'Audio';
|
||||
});
|
||||
tracks.sort(itemHelper.sortTracks);
|
||||
const select = page.querySelector('.selectAudio');
|
||||
select.setLabel(globalize.translate('Audio'));
|
||||
const selectedId = mediaSource.DefaultAudioStreamIndex;
|
||||
@@ -303,31 +304,26 @@ function renderSubtitleSelections(page, mediaSources) {
|
||||
const tracks = mediaSource.MediaStreams.filter(function (m) {
|
||||
return m.Type === 'Subtitle';
|
||||
});
|
||||
tracks.sort(itemHelper.sortTracks);
|
||||
const select = page.querySelector('.selectSubtitles');
|
||||
select.setLabel(globalize.translate('Subtitles'));
|
||||
const selectedId = mediaSource.DefaultSubtitleStreamIndex == null ? -1 : mediaSource.DefaultSubtitleStreamIndex;
|
||||
|
||||
const videoTracks = mediaSource.MediaStreams.filter(function (m) {
|
||||
return m.Type === 'Video';
|
||||
});
|
||||
let selected = selectedId === -1 ? ' selected' : '';
|
||||
select.innerHTML = '<option value="-1">' + globalize.translate('Off') + '</option>' + tracks.map(function (v) {
|
||||
selected = v.Index === selectedId ? ' selected' : '';
|
||||
return '<option value="' + v.Index + '" ' + selected + '>' + v.DisplayTitle + '</option>';
|
||||
}).join('');
|
||||
|
||||
// This only makes sense on Video items
|
||||
if (videoTracks.length) {
|
||||
let selected = selectedId === -1 ? ' selected' : '';
|
||||
select.innerHTML = '<option value="-1">' + globalize.translate('Off') + '</option>' + tracks.map(function (v) {
|
||||
selected = v.Index === selectedId ? ' selected' : '';
|
||||
return '<option value="' + v.Index + '" ' + selected + '>' + v.DisplayTitle + '</option>';
|
||||
}).join('');
|
||||
|
||||
if (tracks.length > 0) {
|
||||
select.removeAttribute('disabled');
|
||||
} else {
|
||||
select.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
if (tracks.length > 0) {
|
||||
select.removeAttribute('disabled');
|
||||
} else {
|
||||
select.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
if (tracks.length) {
|
||||
page.querySelector('.selectSubtitlesContainer').classList.remove('hide');
|
||||
} else {
|
||||
select.innerHTML = '';
|
||||
page.querySelector('.selectSubtitlesContainer').classList.add('hide');
|
||||
}
|
||||
}
|
||||
@@ -450,7 +446,7 @@ function renderName(item, container, context) {
|
||||
} else if (item.ParentIndexNumber != null && item.Type === 'Episode') {
|
||||
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.SeasonId}" data-serverid="${item.ServerId}" data-type="Season" data-isfolder="true">${escapeHtml(item.SeasonName)}</a>`);
|
||||
} else if (item.ParentIndexNumber != null && item.IsSeries) {
|
||||
parentNameHtml.push(escapeHtml(item.SeasonName) || 'S' + item.ParentIndexNumber);
|
||||
parentNameHtml.push(escapeHtml(item.SeasonName || 'S' + item.ParentIndexNumber));
|
||||
} else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) {
|
||||
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.AlbumId}" data-serverid="${item.ServerId}" data-type="MusicAlbum" data-isfolder="true">${escapeHtml(item.Album)}</a>`);
|
||||
} else if (item.Album) {
|
||||
@@ -513,7 +509,7 @@ function setTrailerButtonVisibility(page, item) {
|
||||
}
|
||||
|
||||
function renderBackdrop(item) {
|
||||
if (dom.getWindowSize().innerWidth >= 1000) {
|
||||
if (!layoutManager.mobile && dom.getWindowSize().innerWidth >= 1000) {
|
||||
backdrop.setBackdrops([item]);
|
||||
} else {
|
||||
backdrop.clearBackdrop();
|
||||
@@ -1175,9 +1171,9 @@ function renderMoreFromArtist(view, item, apiClient) {
|
||||
};
|
||||
|
||||
if (item.Type === 'MusicArtist') {
|
||||
query.AlbumArtistIds = item.Id;
|
||||
query.ContributingArtistIds = item.Id;
|
||||
} else {
|
||||
query.AlbumArtistIds = item.AlbumArtists[0].Id;
|
||||
query.ContributingArtistIds = item.AlbumArtists.map(artist => artist.Id).join(',');
|
||||
}
|
||||
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) {
|
||||
@@ -1926,7 +1922,15 @@ export default function (view, params) {
|
||||
}
|
||||
|
||||
function onPlayClick() {
|
||||
playCurrentItem(this, this.getAttribute('data-action'));
|
||||
let actionElem = this;
|
||||
let action = actionElem.getAttribute('data-action');
|
||||
|
||||
if (!action) {
|
||||
actionElem = actionElem.querySelector('[data-action]') || actionElem;
|
||||
action = actionElem.getAttribute('data-action');
|
||||
}
|
||||
|
||||
playCurrentItem(actionElem, action);
|
||||
}
|
||||
|
||||
function onInstantMixClick() {
|
||||
@@ -1962,7 +1966,9 @@ export default function (view, params) {
|
||||
download([{
|
||||
url: downloadHref,
|
||||
itemId: currentItem.Id,
|
||||
serverId: currentItem.serverId
|
||||
serverId: currentItem.ServerId,
|
||||
title: currentItem.Name,
|
||||
filename: currentItem.Path.replace(/^.*[\\/]/, '')
|
||||
}]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import remotecontrolFactory from '../../../components/remotecontrol/remotecontrol';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../../elements/emby-collapse/emby-collapse';
|
||||
import '../../../elements/emby-input/emby-input';
|
||||
import '../../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../../elements/emby-slider/emby-slider';
|
||||
|
||||
export default function (view) {
|
||||
const remoteControl = new remotecontrolFactory();
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<input is="emby-slider" type="range" step="1" min="0" max="100" value="0" class="osdVolumeSlider" />
|
||||
</div>
|
||||
</div>
|
||||
<button is="paper-icon-button-light" class="btnVideoOsdSettings hide autoSize" title="${Settings}">
|
||||
<button is="paper-icon-button-light" class="btnVideoOsdSettings autoSize" title="${Settings}">
|
||||
<span class="largePaperIconButton material-icons settings" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button is="paper-icon-button-light" class="btnAirPlay hide autoSize" title="${AirPlay}">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { playbackManager } from '../../../components/playback/playbackmanager';
|
||||
import SyncPlay from '../../../components/syncPlay/core';
|
||||
import browser from '../../../scripts/browser';
|
||||
@@ -217,9 +218,9 @@ import { appRouter } from '../../../components/appRouter';
|
||||
|
||||
let mouseIsDown = false;
|
||||
|
||||
function showOsd() {
|
||||
function showOsd(focusElement) {
|
||||
slideDownToShow(headerElement);
|
||||
showMainOsdControls();
|
||||
showMainOsdControls(focusElement);
|
||||
resetIdle();
|
||||
}
|
||||
|
||||
@@ -273,7 +274,9 @@ import { appRouter } from '../../../components/appRouter';
|
||||
});
|
||||
}
|
||||
|
||||
function showMainOsdControls() {
|
||||
const _focus = debounce((focusElement) => focusManager.focus(focusElement), 50);
|
||||
|
||||
function showMainOsdControls(focusElement) {
|
||||
if (!currentVisibleMenu) {
|
||||
const elem = osdBottomElement;
|
||||
currentVisibleMenu = 'osd';
|
||||
@@ -281,12 +284,14 @@ import { appRouter } from '../../../components/appRouter';
|
||||
elem.classList.remove('hide');
|
||||
elem.classList.remove('videoOsdBottom-hidden');
|
||||
|
||||
focusElement ||= elem.querySelector('.btnPause');
|
||||
|
||||
if (!layoutManager.mobile) {
|
||||
setTimeout(function () {
|
||||
focusManager.focus(elem.querySelector('.btnPause'));
|
||||
}, 50);
|
||||
_focus(focusElement);
|
||||
}
|
||||
toggleSubtitleSync();
|
||||
} else if (currentVisibleMenu === 'osd' && focusElement && !layoutManager.mobile) {
|
||||
_focus(focusElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,11 +515,11 @@ import { appRouter } from '../../../components/appRouter';
|
||||
}
|
||||
|
||||
function onBeginFetch() {
|
||||
document.querySelector('.osdMediaStatus').classList.remove('hide');
|
||||
view.querySelector('.osdMediaStatus').classList.remove('hide');
|
||||
}
|
||||
|
||||
function onEndFetch() {
|
||||
document.querySelector('.osdMediaStatus').classList.add('hide');
|
||||
view.querySelector('.osdMediaStatus').classList.add('hide');
|
||||
}
|
||||
|
||||
function bindToPlayer(player) {
|
||||
@@ -677,12 +682,6 @@ import { appRouter } from '../../../components/appRouter';
|
||||
updateTimeDisplay(playState.PositionTicks, nowPlayingItem.RunTimeTicks, playState.PlaybackStartTimeTicks, playState.PlaybackRate, playState.BufferedRanges || []);
|
||||
updateNowPlayingInfo(player, state);
|
||||
|
||||
if (state.MediaSource && state.MediaSource.SupportsTranscoding && supportedCommands.indexOf('SetMaxStreamingBitrate') !== -1) {
|
||||
view.querySelector('.btnVideoOsdSettings').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.btnVideoOsdSettings').classList.add('hide');
|
||||
}
|
||||
|
||||
const isProgressClear = state.MediaSource && state.MediaSource.RunTimeTicks == null;
|
||||
nowPlayingPositionSlider.setIsClear(isProgressClear);
|
||||
|
||||
@@ -860,6 +859,8 @@ import { appRouter } from '../../../components/appRouter';
|
||||
const player = currentPlayer;
|
||||
|
||||
if (player) {
|
||||
const state = playbackManager.getPlayerState(player);
|
||||
|
||||
// show subtitle offset feature only if player and media support it
|
||||
const showSubOffset = playbackManager.supportSubtitleOffset(player) &&
|
||||
playbackManager.canHandleOffsetOnCurrentSubtitle(player);
|
||||
@@ -868,6 +869,7 @@ import { appRouter } from '../../../components/appRouter';
|
||||
mediaType: 'Video',
|
||||
player: player,
|
||||
positionTo: btn,
|
||||
quality: state.MediaSource?.SupportsTranscoding,
|
||||
stats: true,
|
||||
suboffset: showSubOffset,
|
||||
onOption: onSettingsOption
|
||||
@@ -1038,18 +1040,37 @@ import { appRouter } from '../../../components/appRouter';
|
||||
const key = keyboardnavigation.getKeyName(e);
|
||||
const isKeyModified = e.ctrlKey || e.altKey || e.metaKey;
|
||||
|
||||
const btnPlayPause = osdBottomElement.querySelector('.btnPause');
|
||||
|
||||
if (e.keyCode === 32) {
|
||||
if (e.target.tagName !== 'BUTTON' || !layoutManager.tv) {
|
||||
playbackManager.playPause(currentPlayer);
|
||||
showOsd(btnPlayPause);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Trick Firefox with a null element to skip next click
|
||||
clickedElement = null;
|
||||
} else {
|
||||
showOsd();
|
||||
}
|
||||
showOsd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutManager.tv && !currentVisibleMenu) {
|
||||
// Change the behavior of some keys when the OSD is hidden
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
showOsd(nowPlayingPositionSlider);
|
||||
nowPlayingPositionSlider.dispatchEvent(new KeyboardEvent(e.type, e));
|
||||
return;
|
||||
case 'Enter':
|
||||
playbackManager.playPause(currentPlayer);
|
||||
showOsd(btnPlayPause);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutManager.tv && keyboardnavigation.isNavigationKey(key)) {
|
||||
showOsd();
|
||||
return;
|
||||
@@ -1069,7 +1090,7 @@ import { appRouter } from '../../../components/appRouter';
|
||||
break;
|
||||
case 'k':
|
||||
playbackManager.playPause(currentPlayer);
|
||||
showOsd();
|
||||
showOsd(btnPlayPause);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'Up':
|
||||
@@ -1083,23 +1104,21 @@ import { appRouter } from '../../../components/appRouter';
|
||||
case 'ArrowRight':
|
||||
case 'Right':
|
||||
playbackManager.fastForward(currentPlayer);
|
||||
showOsd();
|
||||
showOsd(btnFastForward);
|
||||
break;
|
||||
case 'j':
|
||||
case 'ArrowLeft':
|
||||
case 'Left':
|
||||
playbackManager.rewind(currentPlayer);
|
||||
showOsd();
|
||||
showOsd(btnRewind);
|
||||
break;
|
||||
case 'f':
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
playbackManager.toggleFullscreen(currentPlayer);
|
||||
showOsd();
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
playbackManager.toggleMute(currentPlayer);
|
||||
showOsd();
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
@@ -1119,7 +1138,7 @@ import { appRouter } from '../../../components/appRouter';
|
||||
// Ignores gamepad events that are always triggered, even when not focused.
|
||||
if (document.hasFocus()) { /* eslint-disable-line compat/compat */
|
||||
playbackManager.rewind(currentPlayer);
|
||||
showOsd();
|
||||
showOsd(btnRewind);
|
||||
}
|
||||
break;
|
||||
case 'NavigationRight':
|
||||
@@ -1128,7 +1147,7 @@ import { appRouter } from '../../../components/appRouter';
|
||||
// Ignores gamepad events that are always triggered, even when not focused.
|
||||
if (document.hasFocus()) { /* eslint-disable-line compat/compat */
|
||||
playbackManager.fastForward(currentPlayer);
|
||||
showOsd();
|
||||
showOsd(btnFastForward);
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
@@ -1311,7 +1330,7 @@ import { appRouter } from '../../../components/appRouter';
|
||||
const btnFastForward = view.querySelector('.btnFastForward');
|
||||
const transitionEndEventName = dom.whichTransitionEvent();
|
||||
const headerElement = document.querySelector('.skinHeader');
|
||||
const osdBottomElement = document.querySelector('.videoOsdBottom-maincontrols');
|
||||
const osdBottomElement = view.querySelector('.videoOsdBottom-maincontrols');
|
||||
|
||||
nowPlayingPositionSlider.enableKeyboardDragging();
|
||||
nowPlayingVolumeSlider.enableKeyboardDragging();
|
||||
|
||||
@@ -289,6 +289,7 @@ import './login.scss';
|
||||
disclaimer.innerHTML = DOMPurify.sanitize(marked(options.LoginDisclaimer || ''));
|
||||
|
||||
for (const elem of disclaimer.querySelectorAll('a')) {
|
||||
elem.rel = 'noopener noreferrer';
|
||||
elem.target = '_blank';
|
||||
elem.classList.add('button-link');
|
||||
elem.setAttribute('is', 'emby-linkbutton');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div>${QuickConnectDescription}</div>
|
||||
<br />
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" min="0" max="999999" required id="txtQuickConnectCode" label="${LabelQuickConnectCode}" autocomplete="off" />
|
||||
<input is="emby-input" type="text" inputmode="numeric" pattern="[0-9\s]*" minlength="6" required id="txtQuickConnectCode" label="${LabelQuickConnectCode}" autocomplete="off" />
|
||||
</div>
|
||||
<button id="btnQuickConnectAuthorize" is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Authorize}</span>
|
||||
|
||||
@@ -15,7 +15,9 @@ export default function (view) {
|
||||
return;
|
||||
}
|
||||
|
||||
authorize(codeElement.value);
|
||||
// Remove spaces from code
|
||||
const normalizedCode = codeElement.value.replace(/\s/g, '');
|
||||
authorize(normalizedCode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,20 +123,20 @@
|
||||
|
||||
@keyframes repaintChrome {
|
||||
from {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes repaintChrome {
|
||||
from {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,27 @@ import '../emby-input/emby-input';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns normalized slider step.
|
||||
*
|
||||
* @param {HTMLInputElement} range slider itself
|
||||
* @param {number|undefined} step step
|
||||
* @returns {number} normalized slider step.
|
||||
*/
|
||||
function normalizeSliderStep(range, step) {
|
||||
if (step > 0) {
|
||||
return step;
|
||||
}
|
||||
|
||||
step = parseFloat(range.step);
|
||||
|
||||
if (step > 0) {
|
||||
return step;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns slider fraction corresponding to client position.
|
||||
*
|
||||
@@ -35,7 +56,7 @@ import '../emby-input/emby-input';
|
||||
// Snap to step
|
||||
const valueRange = range.max - range.min;
|
||||
if (range.step !== 'any' && valueRange !== 0) {
|
||||
const step = (range.step || 1) / valueRange;
|
||||
const step = normalizeSliderStep(range) / valueRange;
|
||||
fraction = Math.round(fraction / step) * step;
|
||||
}
|
||||
|
||||
@@ -54,7 +75,7 @@ import '../emby-input/emby-input';
|
||||
|
||||
// Snap to step
|
||||
if (range.step !== 'any') {
|
||||
const step = range.step || 1;
|
||||
const step = normalizeSliderStep(range);
|
||||
value = Math.round(value / step) * step;
|
||||
}
|
||||
|
||||
@@ -305,6 +326,8 @@ import '../emby-input/emby-input';
|
||||
} else {
|
||||
startInterval(this);
|
||||
}
|
||||
|
||||
updateValues.call(this);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -375,16 +398,23 @@ import '../emby-input/emby-input';
|
||||
switch (keyboardnavigation.getKeyName(e)) {
|
||||
case 'ArrowLeft':
|
||||
case 'Left':
|
||||
stepKeyboard(this, -this.keyboardStepDown || -1);
|
||||
stepKeyboard(this, -normalizeSliderStep(this, this.keyboardStepDown));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'Right':
|
||||
stepKeyboard(this, this.keyboardStepUp || 1);
|
||||
stepKeyboard(this, normalizeSliderStep(this, this.keyboardStepUp));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 'Enter':
|
||||
if (this.keyboardDragging) {
|
||||
finishKeyboardDragging(this);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sliderBubbleText {
|
||||
|
||||
@@ -75,13 +75,27 @@
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.mouseIdle,
|
||||
.mouseIdle button,
|
||||
.mouseIdle select,
|
||||
.mouseIdle input,
|
||||
.mouseIdle textarea,
|
||||
.mouseIdle a,
|
||||
.mouseIdle label {
|
||||
.layout-tv .mouseIdle,
|
||||
.layout-tv .mouseIdle button,
|
||||
.layout-tv .mouseIdle select,
|
||||
.layout-tv .mouseIdle input,
|
||||
.layout-tv .mouseIdle textarea,
|
||||
.layout-tv .mouseIdle a,
|
||||
.layout-tv .mouseIdle label,
|
||||
.transparentDocument .mouseIdle,
|
||||
.transparentDocument .mouseIdle button,
|
||||
.transparentDocument .mouseIdle select,
|
||||
.transparentDocument .mouseIdle input,
|
||||
.transparentDocument .mouseIdle textarea,
|
||||
.transparentDocument .mouseIdle a,
|
||||
.transparentDocument .mouseIdle label,
|
||||
.screensaver-noScroll.mouseIdle,
|
||||
.screensaver-noScroll.mouseIdle button,
|
||||
.screensaver-noScroll.mouseIdle select,
|
||||
.screensaver-noScroll.mouseIdle input,
|
||||
.screensaver-noScroll.mouseIdle textarea,
|
||||
.screensaver-noScroll.mouseIdle a,
|
||||
.screensaver-noScroll.mouseIdle label {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
|
||||
48
src/legacy/keyboardEvent.js
Normal file
48
src/legacy/keyboardEvent.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Polyfill for KeyboardEvent
|
||||
* - Constructor.
|
||||
*/
|
||||
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
new window.KeyboardEvent('event', { bubbles: true, cancelable: true });
|
||||
} catch (e) {
|
||||
// We can't use `KeyboardEvent` in old WebKit because `initKeyboardEvent`
|
||||
// doesn't seem to populate some properties (`keyCode`, `which`) that
|
||||
// are read-only.
|
||||
const KeyboardEventOriginal = window.Event;
|
||||
|
||||
const KeyboardEvent = function (eventName, options) {
|
||||
options = options || {};
|
||||
|
||||
const event = document.createEvent('Event');
|
||||
|
||||
event.initEvent(eventName, !!options.bubbles, !!options.cancelable);
|
||||
|
||||
event.view = options.view || document.defaultView;
|
||||
|
||||
event.key = options.key || options.keyIdentifier || '';
|
||||
event.keyCode = options.keyCode || 0;
|
||||
event.code = options.code || '';
|
||||
event.charCode = options.charCode || 0;
|
||||
event.char = options.char || '';
|
||||
event.which = options.which || 0;
|
||||
|
||||
event.location = options.location || options.keyLocation || 0;
|
||||
|
||||
event.ctrlKey = !!options.ctrlKey;
|
||||
event.altKey = !!options.altKey;
|
||||
event.shiftKey = !!options.shiftKey;
|
||||
event.metaKey = !!options.metaKey;
|
||||
|
||||
event.repeat = !!options.repeat;
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
KeyboardEvent.prototype = KeyboardEventOriginal.prototype;
|
||||
window.KeyboardEvent = KeyboardEvent;
|
||||
}
|
||||
}(window));
|
||||
@@ -116,7 +116,7 @@ function ScreenSaverManager() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getFunctionalEventIdleTime < getMinIdleTime()) {
|
||||
if (getFunctionalEventIdleTime() < getMinIdleTime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,9 @@ export class BookPlayer {
|
||||
width: '100%',
|
||||
height: renderHeight,
|
||||
// TODO: Add option for scrolled-doc
|
||||
flow: 'paginated'
|
||||
flow: 'paginated',
|
||||
// Scripted content is required to allow touch event passthrough in Safari
|
||||
allowScriptedContent: true
|
||||
});
|
||||
|
||||
this.currentSrc = downloadHref;
|
||||
|
||||
@@ -242,7 +242,7 @@ export class ComicsPlayer {
|
||||
}
|
||||
|
||||
// the comic book archive supports any kind of image format as it's just a zip archive
|
||||
const supportedFormats = ['jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi', 'png', 'avif', 'gif', 'bmp', 'dib', 'tiff', 'tif'];
|
||||
const supportedFormats = ['jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi', 'png', 'avif', 'gif', 'bmp', 'dib', 'tiff', 'tif', 'webp'];
|
||||
|
||||
class ArchiveSource {
|
||||
constructor(url) {
|
||||
|
||||
@@ -74,7 +74,7 @@ function enableHlsPlayer(url, item, mediaSource, mediaType) {
|
||||
type: 'HEAD'
|
||||
}).then(function (response) {
|
||||
const contentType = (response.headers.get('Content-Type') || '').toLowerCase();
|
||||
if (contentType === 'application/x-mpegurl') {
|
||||
if (contentType === 'application/vnd.apple.mpegurl' || contentType === 'application/x-mpegurl') {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import browser from '../../scripts/browser';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import { appHost } from '../../components/apphost';
|
||||
@@ -28,8 +30,29 @@ import itemHelper from '../../components/itemHelper';
|
||||
import Screenfull from 'screenfull';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ServerConnections from '../../components/ServerConnections';
|
||||
import profileBuilder from '../../scripts/browserDeviceProfile';
|
||||
import profileBuilder, { canPlaySecondaryAudio } from '../../scripts/browserDeviceProfile';
|
||||
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
|
||||
import { includesAny } from '../../utils/container.ts';
|
||||
|
||||
/**
|
||||
* Returns resolved URL.
|
||||
* @param {string} url - URL.
|
||||
* @returns {string} Resolved URL or `url` if resolving failed.
|
||||
*/
|
||||
function resolveUrl(url) {
|
||||
return new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('HEAD', url, true);
|
||||
xhr.onload = function () {
|
||||
resolve(xhr.responseURL || url);
|
||||
};
|
||||
xhr.onerror = function (e) {
|
||||
console.error(e);
|
||||
resolve(url);
|
||||
};
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
@@ -344,7 +367,7 @@ function tryRemoveElement(elem) {
|
||||
|
||||
this.#currentTime = null;
|
||||
|
||||
this.resetSubtitleOffset();
|
||||
if (options.resetSubtitleOffset !== false) this.resetSubtitleOffset();
|
||||
|
||||
return this.createMediaElement(options).then(elem => {
|
||||
return this.updateVideoUrl(options).then(() => {
|
||||
@@ -582,7 +605,7 @@ function tryRemoveElement(elem) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
isAudioStreamSupported(stream, deviceProfile) {
|
||||
isAudioStreamSupported(stream, deviceProfile, container) {
|
||||
const codec = (stream.Codec || '').toLowerCase();
|
||||
|
||||
if (!codec) {
|
||||
@@ -596,17 +619,11 @@ function tryRemoveElement(elem) {
|
||||
|
||||
const profiles = deviceProfile.DirectPlayProfiles || [];
|
||||
|
||||
return profiles.filter(function (p) {
|
||||
if (p.Type === 'Video') {
|
||||
if (!p.AudioCodec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return p.AudioCodec.toLowerCase().includes(codec);
|
||||
}
|
||||
|
||||
return false;
|
||||
}).length > 0;
|
||||
return profiles.some(function (p) {
|
||||
return p.Type === 'Video'
|
||||
&& includesAny((p.Container || '').toLowerCase(), container)
|
||||
&& includesAny((p.AudioCodec || '').toLowerCase(), codec);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,8 +632,11 @@ function tryRemoveElement(elem) {
|
||||
getSupportedAudioStreams() {
|
||||
const profile = this.#lastProfile;
|
||||
|
||||
return getMediaStreamAudioTracks(this._currentPlayOptions.mediaSource).filter((stream) => {
|
||||
return this.isAudioStreamSupported(stream, profile);
|
||||
const mediaSource = this._currentPlayOptions.mediaSource;
|
||||
const container = mediaSource.Container.toLowerCase();
|
||||
|
||||
return getMediaStreamAudioTracks(mediaSource).filter((stream) => {
|
||||
return this.isAudioStreamSupported(stream, profile, container);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1048,7 +1068,7 @@ function tryRemoveElement(elem) {
|
||||
* @private
|
||||
*/
|
||||
renderSsaAss(videoElement, track, item) {
|
||||
const supportedFonts = ['application/x-truetype-font', 'font/otf', 'font/ttf', 'font/woff', 'font/woff2'];
|
||||
const supportedFonts = ['application/vnd.ms-opentype', 'application/x-truetype-font', 'font/otf', 'font/ttf', 'font/woff', 'font/woff2'];
|
||||
const avaliableFonts = [];
|
||||
const attachments = this._currentPlayOptions.mediaSource.MediaAttachments || [];
|
||||
const apiClient = ServerConnections.getApiClient(item);
|
||||
@@ -1081,19 +1101,27 @@ function tryRemoveElement(elem) {
|
||||
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000,
|
||||
|
||||
// new octopus options; override all, even defaults
|
||||
renderMode: 'blend',
|
||||
renderMode: 'wasm-blend',
|
||||
dropAllAnimations: false,
|
||||
libassMemoryLimit: 40,
|
||||
libassGlyphLimit: 40,
|
||||
targetFps: 24,
|
||||
prescaleTradeoff: 0.8,
|
||||
softHeightLimit: 1080,
|
||||
hardHeightLimit: 2160,
|
||||
prescaleFactor: 0.8,
|
||||
prescaleHeightLimit: 1080,
|
||||
maxRenderHeight: 2160,
|
||||
resizeVariation: 0.2,
|
||||
renderAhead: 90
|
||||
};
|
||||
import('libass-wasm').then(({default: SubtitlesOctopus}) => {
|
||||
apiClient.getNamedConfiguration('encoding').then(config => {
|
||||
import('@jellyfin/libass-wasm').then(({default: SubtitlesOctopus}) => {
|
||||
Promise.all([
|
||||
apiClient.getNamedConfiguration('encoding'),
|
||||
// Worker in Tizen 5 doesn't resolve relative path with async request
|
||||
resolveUrl(options.workerUrl),
|
||||
resolveUrl(options.legacyWorkerUrl)
|
||||
]).then(([config, workerUrl, legacyWorkerUrl]) => {
|
||||
options.workerUrl = workerUrl;
|
||||
options.legacyWorkerUrl = legacyWorkerUrl;
|
||||
|
||||
if (config.EnableFallbackFont) {
|
||||
apiClient.getJSON(fallbackFontList).then((fontFiles = []) => {
|
||||
fontFiles.forEach(font => {
|
||||
@@ -1291,7 +1319,8 @@ function tryRemoveElement(elem) {
|
||||
}
|
||||
|
||||
if (selectedTrackEvent && selectedTrackEvent.Text) {
|
||||
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
|
||||
subtitleTextElement.innerHTML = DOMPurify.sanitize(
|
||||
normalizeTrackEventText(selectedTrackEvent.Text, true));
|
||||
subtitleTextElement.classList.remove('hide');
|
||||
} else {
|
||||
subtitleTextElement.classList.add('hide');
|
||||
@@ -1347,6 +1376,9 @@ function tryRemoveElement(elem) {
|
||||
// Can't autoplay in these browsers so we need to use the full controls, at least until playback starts
|
||||
if (!appHost.supports('htmlvideoautoplay')) {
|
||||
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" controls="controls" webkit-playsinline playsinline>';
|
||||
} else if (browser.web0s) {
|
||||
// in webOS, setting preload auto allows resuming videos
|
||||
html += '<video class="' + cssClass + '" preload="auto" autoplay="autoplay" webkit-playsinline playsinline>';
|
||||
} else {
|
||||
// Chrome 35 won't play with preload none
|
||||
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" webkit-playsinline playsinline>';
|
||||
@@ -1528,15 +1560,9 @@ function tryRemoveElement(elem) {
|
||||
}
|
||||
|
||||
canSetAudioStreamIndex() {
|
||||
if (browser.tizen || browser.orsay) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const video = this.#mediaElement;
|
||||
if (video) {
|
||||
if (video.audioTracks) {
|
||||
return true;
|
||||
}
|
||||
return canPlaySecondaryAudio(video);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -297,7 +297,9 @@ if (userAgent.toLowerCase().indexOf('xbox') !== -1) {
|
||||
browser.tv = true;
|
||||
}
|
||||
browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null;
|
||||
browser.hisense = userAgent.toLowerCase().includes('hisense');
|
||||
browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || window.tizen != null;
|
||||
browser.vidaa = userAgent.toLowerCase().includes('vidaa');
|
||||
browser.web0s = isWeb0s();
|
||||
browser.edgeUwp = browser.edge && (userAgent.toLowerCase().indexOf('msapphost') !== -1 || userAgent.toLowerCase().indexOf('webview') !== -1);
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ import browser from './browser';
|
||||
|
||||
typeString = 'audio/ogg; codecs="opus"';
|
||||
} else if (format === 'alac') {
|
||||
if (browser.iOS || browser.osx) {
|
||||
if (browser.iOS || browser.osx && browser.safari) {
|
||||
return true;
|
||||
}
|
||||
} else if (format === 'mp2') {
|
||||
@@ -294,19 +294,27 @@ import browser from './browser';
|
||||
(browser.tizen && isTizenFhd ? 20000000 : null)));
|
||||
}
|
||||
|
||||
let maxChannelCount = null;
|
||||
|
||||
function getSpeakerCount() {
|
||||
if (maxChannelCount != null) {
|
||||
return maxChannelCount;
|
||||
}
|
||||
|
||||
maxChannelCount = -1;
|
||||
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext || false; /* eslint-disable-line compat/compat */
|
||||
|
||||
if (AudioContext) {
|
||||
const audioCtx = new AudioContext();
|
||||
|
||||
return audioCtx.destination.maxChannelCount;
|
||||
maxChannelCount = audioCtx.destination.maxChannelCount;
|
||||
}
|
||||
|
||||
return -1;
|
||||
return maxChannelCount;
|
||||
}
|
||||
|
||||
function getPhysicalAudioChannels(options) {
|
||||
function getPhysicalAudioChannels(options, videoTestElement) {
|
||||
const allowedAudioChannels = parseInt(userSettings.allowedAudioChannels(), 10);
|
||||
|
||||
if (allowedAudioChannels > 0) {
|
||||
@@ -318,8 +326,14 @@ import browser from './browser';
|
||||
}
|
||||
|
||||
const isSurroundSoundSupportedBrowser = browser.safari || browser.chrome || browser.edgeChromium || browser.firefox || browser.tv || browser.ps4 || browser.xboxOne;
|
||||
const isAc3Eac3Supported = supportsAc3(videoTestElement) || supportsEac3(videoTestElement);
|
||||
const speakerCount = getSpeakerCount();
|
||||
|
||||
// AC3/EAC3 hinted that device is able to play dolby surround sound.
|
||||
if (isAc3Eac3Supported && isSurroundSoundSupportedBrowser) {
|
||||
return speakerCount > 6 ? speakerCount : 6;
|
||||
}
|
||||
|
||||
if (speakerCount > 2) {
|
||||
if (isSurroundSoundSupportedBrowser) {
|
||||
return speakerCount;
|
||||
@@ -339,15 +353,32 @@ import browser from './browser';
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the web engine supports secondary audio.
|
||||
* @param {HTMLVideoElement} videoTestElement The video test element
|
||||
* @returns {boolean} _true_ if the web engine supports secondary audio.
|
||||
*/
|
||||
export function canPlaySecondaryAudio(videoTestElement) {
|
||||
// We rely on HTMLMediaElement.audioTracks
|
||||
// It works in Chrome 79+ with "Experimental Web Platform features" enabled
|
||||
return !!videoTestElement.audioTracks
|
||||
// It doesn't work in Firefox 108 even with "media.track.enabled" enabled (it only sees the first audio track)
|
||||
&& !browser.firefox
|
||||
// It seems to work on Tizen 5.5+ (2020, Chrome 69+). See https://developer.tizen.org/forums/web-application-development/video-tag-not-work-audiotracks
|
||||
&& (browser.tizenVersion >= 5.5 || !browser.tizen)
|
||||
// Assume webOS 5+ (2020, Chrome 68+) supports secondary audio like Tizen 5.5+
|
||||
&& (browser.web0sVersion >= 5.0 || !browser.web0sVersion);
|
||||
}
|
||||
|
||||
export default function (options) {
|
||||
options = options || {};
|
||||
|
||||
const physicalAudioChannels = getPhysicalAudioChannels(options);
|
||||
|
||||
const bitrateSetting = getMaxBitrate();
|
||||
|
||||
const videoTestElement = document.createElement('video');
|
||||
|
||||
const physicalAudioChannels = getPhysicalAudioChannels(options, videoTestElement);
|
||||
|
||||
const canPlayVp8 = videoTestElement.canPlayType('video/webm; codecs="vp8"').replace(/no/, '');
|
||||
const canPlayVp9 = videoTestElement.canPlayType('video/webm; codecs="vp9"').replace(/no/, '');
|
||||
const webmAudioCodecs = ['vorbis'];
|
||||
@@ -466,7 +497,8 @@ import browser from './browser';
|
||||
}
|
||||
}
|
||||
|
||||
if (canPlayAudioFormat('flac')) {
|
||||
// FLAC audio in video plays with a delay on Tizen
|
||||
if (canPlayAudioFormat('flac') && !browser.tizen) {
|
||||
videoAudioCodecs.push('flac');
|
||||
hlsInFmp4VideoAudioCodecs.push('flac');
|
||||
}
|
||||
@@ -530,7 +562,6 @@ import browser from './browser';
|
||||
}
|
||||
|
||||
if (canPlayVp8) {
|
||||
mp4VideoCodecs.push('vp8');
|
||||
webmVideoCodecs.push('vp8');
|
||||
}
|
||||
|
||||
@@ -659,30 +690,6 @@ import browser from './browser';
|
||||
});
|
||||
});
|
||||
|
||||
if (canPlayMkv && !browser.tizen && options.enableMkvProgressive !== false) {
|
||||
profile.TranscodingProfiles.push({
|
||||
Container: 'mkv',
|
||||
Type: 'Video',
|
||||
AudioCodec: videoAudioCodecs.join(','),
|
||||
VideoCodec: mp4VideoCodecs.join(','),
|
||||
Context: 'Streaming',
|
||||
MaxAudioChannels: physicalAudioChannels.toString(),
|
||||
CopyTimestamps: true
|
||||
});
|
||||
}
|
||||
|
||||
if (canPlayMkv) {
|
||||
profile.TranscodingProfiles.push({
|
||||
Container: 'mkv',
|
||||
Type: 'Video',
|
||||
AudioCodec: videoAudioCodecs.join(','),
|
||||
VideoCodec: mp4VideoCodecs.join(','),
|
||||
Context: 'Static',
|
||||
MaxAudioChannels: physicalAudioChannels.toString(),
|
||||
CopyTimestamps: true
|
||||
});
|
||||
}
|
||||
|
||||
if (canPlayHls() && options.enableHls !== false) {
|
||||
if (hlsInFmp4VideoCodecs.length && hlsInFmp4VideoAudioCodecs.length && userSettings.preferFmp4HlsContainer() && (browser.safari || browser.tizen || browser.web0s)) {
|
||||
profile.TranscodingProfiles.push({
|
||||
@@ -713,35 +720,11 @@ import browser from './browser';
|
||||
}
|
||||
}
|
||||
|
||||
if (webmAudioCodecs.length && webmVideoCodecs.length) {
|
||||
profile.TranscodingProfiles.push({
|
||||
Container: 'webm',
|
||||
Type: 'Video',
|
||||
AudioCodec: webmAudioCodecs.join(','),
|
||||
// TODO: Remove workaround when servers migrate away from 'vpx' for transcoding profiles.
|
||||
VideoCodec: (canPlayVp8 ? webmVideoCodecs.concat('vpx') : webmVideoCodecs).join(','),
|
||||
Context: 'Streaming',
|
||||
Protocol: 'http',
|
||||
// If audio transcoding is needed, limit channels to number of physical audio channels
|
||||
// Trying to transcode to 5 channels when there are only 2 speakers generally does not sound good
|
||||
MaxAudioChannels: physicalAudioChannels.toString()
|
||||
});
|
||||
}
|
||||
|
||||
profile.TranscodingProfiles.push({
|
||||
Container: 'mp4',
|
||||
Type: 'Video',
|
||||
AudioCodec: videoAudioCodecs.join(','),
|
||||
VideoCodec: 'h264',
|
||||
Context: 'Static',
|
||||
Protocol: 'http'
|
||||
});
|
||||
|
||||
profile.ContainerProfiles = [];
|
||||
|
||||
profile.CodecProfiles = [];
|
||||
|
||||
const supportsSecondaryAudio = browser.tizen || videoTestElement.audioTracks;
|
||||
const supportsSecondaryAudio = canPlaySecondaryAudio(videoTestElement);
|
||||
|
||||
const aacCodecProfileConditions = [];
|
||||
|
||||
@@ -800,10 +783,9 @@ import browser from './browser';
|
||||
maxH264Level = 52;
|
||||
}
|
||||
|
||||
if (browser.tizen ||
|
||||
videoTestElement.canPlayType('video/mp4; codecs="avc1.6e0033"').replace(/no/, '')) {
|
||||
if (videoTestElement.canPlayType('video/mp4; codecs="avc1.6e0033"').replace(/no/, '')) {
|
||||
// These tests are passing in safari, but playback is failing
|
||||
if (!browser.safari && !browser.iOS && !browser.web0s && !browser.edge && !browser.mobile) {
|
||||
if (!browser.safari && !browser.iOS && !browser.web0s && !browser.edge && !browser.mobile && !browser.tizen) {
|
||||
h264Profiles += '|high 10';
|
||||
}
|
||||
}
|
||||
@@ -838,6 +820,30 @@ import browser from './browser';
|
||||
hevcProfiles = 'main|main 10';
|
||||
}
|
||||
|
||||
const h264VideoRangeTypes = 'SDR';
|
||||
let hevcVideoRangeTypes = 'SDR';
|
||||
let vp9VideoRangeTypes = 'SDR';
|
||||
let av1VideoRangeTypes = 'SDR';
|
||||
|
||||
if (browser.safari && ((browser.iOS && browser.iOSVersion >= 11) || browser.osx)) {
|
||||
hevcVideoRangeTypes += '|HDR10|HLG';
|
||||
if ((browser.iOS && browser.iOSVersion >= 13) || browser.osx) {
|
||||
hevcVideoRangeTypes += '|DOVI';
|
||||
}
|
||||
}
|
||||
|
||||
if (browser.tizen || browser.web0s) {
|
||||
hevcVideoRangeTypes += '|HDR10|HLG';
|
||||
if (browser.web0s) hevcVideoRangeTypes += '|DOVI';
|
||||
vp9VideoRangeTypes += '|HDR10|HLG';
|
||||
av1VideoRangeTypes += '|HDR10|HLG';
|
||||
}
|
||||
|
||||
if (browser.edgeChromium || browser.chrome || browser.firefox) {
|
||||
vp9VideoRangeTypes += '|HDR10|HLG';
|
||||
av1VideoRangeTypes += '|HDR10|HLG';
|
||||
}
|
||||
|
||||
const h264CodecProfileConditions = [
|
||||
{
|
||||
Condition: 'NotEquals',
|
||||
@@ -851,6 +857,12 @@ import browser from './browser';
|
||||
Value: h264Profiles,
|
||||
IsRequired: false
|
||||
},
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: h264VideoRangeTypes,
|
||||
IsRequired: false
|
||||
},
|
||||
{
|
||||
Condition: 'LessThanEqual',
|
||||
Property: 'VideoLevel',
|
||||
@@ -872,6 +884,12 @@ import browser from './browser';
|
||||
Value: hevcProfiles,
|
||||
IsRequired: false
|
||||
},
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: hevcVideoRangeTypes,
|
||||
IsRequired: false
|
||||
},
|
||||
{
|
||||
Condition: 'LessThanEqual',
|
||||
Property: 'VideoLevel',
|
||||
@@ -880,6 +898,24 @@ import browser from './browser';
|
||||
}
|
||||
];
|
||||
|
||||
const vp9CodecProfileConditions = [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: vp9VideoRangeTypes,
|
||||
IsRequired: false
|
||||
}
|
||||
];
|
||||
|
||||
const av1CodecProfileConditions = [
|
||||
{
|
||||
Condition: 'EqualsAny',
|
||||
Property: 'VideoRangeType',
|
||||
Value: av1VideoRangeTypes,
|
||||
IsRequired: false
|
||||
}
|
||||
];
|
||||
|
||||
if (!browser.edgeUwp && !browser.tizen && !browser.web0s) {
|
||||
h264CodecProfileConditions.push({
|
||||
Condition: 'NotEquals',
|
||||
@@ -969,6 +1005,18 @@ import browser from './browser';
|
||||
Conditions: hevcCodecProfileConditions
|
||||
});
|
||||
|
||||
profile.CodecProfiles.push({
|
||||
Type: 'Video',
|
||||
Codec: 'vp9',
|
||||
Conditions: vp9CodecProfileConditions
|
||||
});
|
||||
|
||||
profile.CodecProfiles.push({
|
||||
Type: 'Video',
|
||||
Codec: 'av1',
|
||||
Conditions: av1CodecProfileConditions
|
||||
});
|
||||
|
||||
const globalVideoConditions = [];
|
||||
|
||||
if (globalMaxVideoBitrate) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { af, arDZ, be, bg, bn, ca, cs, da, de, el, enGB, enUS, eo, es, faIR, fi, fr, frCA, he, hi, hr, hu, gl, id, is, it, ja, kk, ko, lt, ms, nb,
|
||||
nl, pl, ptBR, pt, ro, ru, sk, sl, sv, ta, th, tr, uk, vi, zhCN, zhTW } from 'date-fns/locale';
|
||||
import { af, arDZ, be, bg, bn, ca, cs, cy, da, de, el, enGB, enUS, eo, es, et, eu, faIR, fi, fr, frCA, gl, he, hi, hr, hu, id, is, it, ja, kk, ko, lt, lv, ms, nb,
|
||||
nl, nn, pl, ptBR, pt, ro, ru, sk, sl, sv, ta, th, tr, uk, vi, zhCN, zhTW } from 'date-fns/locale';
|
||||
import globalize from './globalize';
|
||||
|
||||
const dateLocales = (locale) => ({
|
||||
@@ -10,6 +10,7 @@ const dateLocales = (locale) => ({
|
||||
'bn': bn,
|
||||
'ca': ca,
|
||||
'cs': cs,
|
||||
'cy': cy,
|
||||
'da': da,
|
||||
'de': de,
|
||||
'el': el,
|
||||
@@ -20,6 +21,8 @@ const dateLocales = (locale) => ({
|
||||
'es-ar': es,
|
||||
'es-do': es,
|
||||
'es-mx': es,
|
||||
'et': et,
|
||||
'eu': eu,
|
||||
'fa': faIR,
|
||||
'fi': fi,
|
||||
'fr': fr,
|
||||
@@ -37,9 +40,11 @@ const dateLocales = (locale) => ({
|
||||
'kk': kk,
|
||||
'ko': ko,
|
||||
'lt-lt': lt,
|
||||
'lv': lv,
|
||||
'ms': ms,
|
||||
'nb': nb,
|
||||
'nl': nl,
|
||||
'nn': nn,
|
||||
'pl': pl,
|
||||
'pt': pt,
|
||||
'pt-br': ptBR,
|
||||
@@ -63,9 +68,14 @@ export function getLocale() {
|
||||
return dateLocales(globalize.getCurrentLocale()) || dateLocales(globalize.getCurrentLocale().replace(/-.*/, '')) || enUS;
|
||||
}
|
||||
|
||||
export const localeWithSuffix = { addSuffix: true, locale: getLocale() };
|
||||
export function getLocaleWithSuffix() {
|
||||
return {
|
||||
addSuffix: true,
|
||||
locale: getLocale()
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getLocale: getLocale,
|
||||
localeWithSuffix: localeWithSuffix
|
||||
getLocaleWithSuffix
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const _GAMEPAD_LEFT_THUMBSTICK_UP_KEY = 'GamepadLeftThumbStickUp';
|
||||
const _GAMEPAD_LEFT_THUMBSTICK_DOWN_KEY = 'GamepadLeftThumbStickDown';
|
||||
const _GAMEPAD_LEFT_THUMBSTICK_LEFT_KEY = 'GamepadLeftThumbStickLeft';
|
||||
const _GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEY = 'GamepadLeftThumbStickRight';
|
||||
const _GAMEPAD_A_KEYCODE = 0;
|
||||
const _GAMEPAD_A_KEYCODE = 13;
|
||||
const _GAMEPAD_B_KEYCODE = 27;
|
||||
const _GAMEPAD_DPAD_UP_KEYCODE = 38;
|
||||
const _GAMEPAD_DPAD_DOWN_KEYCODE = 40;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
|
||||
import * as userSettings from './settings/userSettings';
|
||||
import { currentSettings as userSettings } from './settings/userSettings';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
/* eslint-disable indent */
|
||||
// audit note: this module is expected to return safe text for use in HTML
|
||||
|
||||
export function getDeviceIcon(device) {
|
||||
const baseUrl = 'assets/img/devices/';
|
||||
@@ -11,12 +12,16 @@
|
||||
case 'Sony PS4':
|
||||
return baseUrl + 'playstation.svg';
|
||||
case 'Kodi':
|
||||
case 'Kodi JellyCon':
|
||||
return baseUrl + 'kodi.svg';
|
||||
case 'Jellyfin Android':
|
||||
case 'AndroidTV':
|
||||
case 'Android TV':
|
||||
return baseUrl + 'android.svg';
|
||||
case 'Jellyfin Mobile (iOS)':
|
||||
case 'Jellyfin Mobile (iPadOS)':
|
||||
case 'Jellyfin iOS':
|
||||
case 'Infuse':
|
||||
return baseUrl + 'apple.svg';
|
||||
case 'Jellyfin Web':
|
||||
switch (device.Name || device.DeviceName) {
|
||||
|
||||
@@ -265,6 +265,12 @@ function renderSection(page, item, element, type) {
|
||||
function loadItems(element, item, type, query, listOptions) {
|
||||
query = getQuery(query, item);
|
||||
getItemsFunction(query, item)(query.StartIndex, query.Limit, query.Fields).then(function (result) {
|
||||
// If results are empty, hide the section
|
||||
if (!result.Items?.length) {
|
||||
element.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
if (query.Limit && result.TotalRecordCount > query.Limit) {
|
||||
@@ -327,7 +333,7 @@ function addCurrentItemToQuery(query, item) {
|
||||
} else if (item.Type === 'Studio') {
|
||||
query.StudioIds = item.Id;
|
||||
} else if (item.Type === 'MusicArtist') {
|
||||
query.ArtistIds = item.Id;
|
||||
query.AlbumArtistIds = item.Id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @module components/input/keyboardnavigation
|
||||
*/
|
||||
|
||||
import browser from './browser';
|
||||
import inputManager from './inputManager';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import appSettings from './settings/appSettings';
|
||||
@@ -44,6 +45,16 @@ const KeyNames = {
|
||||
*/
|
||||
const NavigationKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
/**
|
||||
* Elements for which navigation should be constrained.
|
||||
*/
|
||||
const InteractiveElements = ['INPUT', 'TEXTAREA'];
|
||||
|
||||
/**
|
||||
* Types of INPUT element for which navigation shouldn't be constrained.
|
||||
*/
|
||||
const NonInteractiveInputElements = ['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'reset', 'submit'];
|
||||
|
||||
let hasFieldKey = false;
|
||||
try {
|
||||
hasFieldKey = 'key' in new KeyboardEvent('keydown');
|
||||
@@ -78,6 +89,24 @@ export function isNavigationKey(key) {
|
||||
return NavigationKeys.indexOf(key) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns _true_ if the element is interactive.
|
||||
*
|
||||
* @param {Element} element - Element.
|
||||
* @return {boolean} _true_ if the element is interactive.
|
||||
*/
|
||||
export function isInteractiveElement(element) {
|
||||
if (element && InteractiveElements.includes(element.tagName)) {
|
||||
if (element.tagName === 'INPUT') {
|
||||
return !NonInteractiveInputElements.includes(element.type);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function enable() {
|
||||
window.addEventListener('keydown', function (e) {
|
||||
const key = getKeyName(e);
|
||||
@@ -91,13 +120,21 @@ export function enable() {
|
||||
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
inputManager.handleCommand('left');
|
||||
if (!isInteractiveElement(document.activeElement)) {
|
||||
inputManager.handleCommand('left');
|
||||
} else {
|
||||
capture = false;
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
inputManager.handleCommand('up');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
inputManager.handleCommand('right');
|
||||
if (!isInteractiveElement(document.activeElement)) {
|
||||
inputManager.handleCommand('right');
|
||||
} else {
|
||||
capture = false;
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
inputManager.handleCommand('down');
|
||||
@@ -107,6 +144,15 @@ export function enable() {
|
||||
inputManager.handleCommand('back');
|
||||
break;
|
||||
|
||||
// HACK: Hisense TV (VIDAA OS) uses Backspace for Back action
|
||||
case 'Backspace':
|
||||
if (browser.tv && browser.hisense && browser.vidaa) {
|
||||
inputManager.handleCommand('back');
|
||||
} else {
|
||||
capture = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
if (layoutManager.tv) {
|
||||
inputManager.handleCommand('back');
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
import Headroom from 'headroom.js';
|
||||
|
||||
import dom from './dom';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import inputManager from './inputManager';
|
||||
@@ -12,13 +14,14 @@ import browser from './browser';
|
||||
import globalize from './globalize';
|
||||
import imageHelper from './imagehelper';
|
||||
import { getMenuLinks } from '../scripts/settings/webSettings';
|
||||
import Dashboard, { pageClassOn } from './clientUtils';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
|
||||
import '../elements/emby-button/paper-icon-button-light';
|
||||
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../assets/css/scrollstyles.scss';
|
||||
import '../assets/css/flexstyles.scss';
|
||||
import Dashboard, { pageClassOn } from './clientUtils';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
import Headroom from 'headroom.js';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
@@ -667,9 +670,8 @@ import Headroom from 'headroom.js';
|
||||
if (customMenuOptions) {
|
||||
getMenuLinks().then(links => {
|
||||
links.forEach(link => {
|
||||
const option = document.createElement('a');
|
||||
option.setAttribute('is', 'emby-linkbutton');
|
||||
option.className = 'navMenuOption lnkMediaFolder';
|
||||
const option = document.createElement('a', 'emby-linkbutton');
|
||||
option.classList.add('navMenuOption', 'lnkMediaFolder');
|
||||
option.rel = 'noopener noreferrer';
|
||||
option.target = '_blank';
|
||||
option.href = link.url;
|
||||
|
||||
@@ -92,6 +92,19 @@ class AppSettings {
|
||||
return val ? parseInt(val) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set 'Maximum video width'
|
||||
* @param {number|undefined} val - Maximum video width or undefined.
|
||||
* @return {number} Maximum video width.
|
||||
*/
|
||||
maxVideoWidth(val) {
|
||||
if (val !== undefined) {
|
||||
return this.set('maxVideoWidth', val.toString());
|
||||
}
|
||||
|
||||
return parseInt(this.get('maxVideoWidth') || '0', 10) || 0;
|
||||
}
|
||||
|
||||
set(name, value, userId) {
|
||||
const currentValue = this.get(name, userId);
|
||||
AppStorage.setItem(this.#getKey(name, userId), value);
|
||||
|
||||
@@ -166,19 +166,6 @@ export class UserSettings {
|
||||
return val !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set 'SetUsingLastTracks' state.
|
||||
* @param {boolean|undefined} val - Flag to enable 'SetUsingLastTracks' or undefined.
|
||||
* @return {boolean} 'SetUsingLastTracks' state.
|
||||
*/
|
||||
enableSetUsingLastTracks(val) {
|
||||
if (val !== undefined) {
|
||||
return this.set('enableSetUsingLastTracks', val.toString());
|
||||
}
|
||||
|
||||
return this.get('enableSetUsingLastTracks', false) !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set 'Theme Songs' state.
|
||||
* @param {boolean|undefined} val - Flag to enable 'Theme Songs' or undefined.
|
||||
@@ -468,7 +455,7 @@ export class UserSettings {
|
||||
return this.set('enableRewatchingInNextUp', val, false);
|
||||
}
|
||||
|
||||
return this.get('enableRewatchingInNextUp', false);
|
||||
return this.get('enableRewatchingInNextUp', false) === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -570,7 +557,6 @@ export const allowedAudioChannels = currentSettings.allowedAudioChannels.bind(cu
|
||||
export const preferFmp4HlsContainer = currentSettings.preferFmp4HlsContainer.bind(currentSettings);
|
||||
export const enableCinemaMode = currentSettings.enableCinemaMode.bind(currentSettings);
|
||||
export const enableNextVideoInfoOverlay = currentSettings.enableNextVideoInfoOverlay.bind(currentSettings);
|
||||
export const enableSetUsingLastTracks = currentSettings.enableSetUsingLastTracks.bind(currentSettings);
|
||||
export const enableThemeSongs = currentSettings.enableThemeSongs.bind(currentSettings);
|
||||
export const enableThemeVideos = currentSettings.enableThemeVideos.bind(currentSettings);
|
||||
export const enableFastFadein = currentSettings.enableFastFadein.bind(currentSettings);
|
||||
|
||||
@@ -32,6 +32,7 @@ import '../components/playback/playerSelectionMenu';
|
||||
import '../legacy/domParserTextHtml';
|
||||
import '../legacy/focusPreventScroll';
|
||||
import '../legacy/htmlMediaElement';
|
||||
import '../legacy/keyboardEvent';
|
||||
import '../legacy/vendorStyles';
|
||||
import SyncPlay from '../components/syncPlay/core';
|
||||
import { playbackManager } from '../components/playback/playbackmanager';
|
||||
@@ -71,7 +72,7 @@ window.getParameterByName = function(name, url) {
|
||||
};
|
||||
|
||||
function loadCoreDictionary() {
|
||||
const languages = ['af', 'ar', 'be-by', 'bg-bg', 'bn_bd', 'ca', 'cs', 'da', 'de', 'el', 'en-gb', 'en-us', 'eo', 'es', 'es-419', 'es-ar', 'es_do', 'es-mx', 'fa', 'fi', 'fil', 'fr', 'fr-ca', 'gl', 'gsw', 'he', 'hi-in', 'hr', 'hu', 'id', 'it', 'ja', 'kk', 'ko', 'lt-lt', 'mr', 'ms', 'nb', 'nl', 'pl', 'pr', 'pt', 'pt-br', 'pt-pt', 'ro', 'ru', 'sk', 'sl-si', 'sq', 'sv', 'ta', 'th', 'tr', 'uk', 'ur_pk', 'vi', 'zh-cn', 'zh-hk', 'zh-tw'];
|
||||
const languages = ['af', 'ar', 'be-by', 'bg-bg', 'bn_bd', 'ca', 'cs', 'cy', 'da', 'de', 'el', 'en-gb', 'en-us', 'eo', 'es', 'es_419', 'es-ar', 'es_do', 'es-mx', 'et', 'eu', 'fa', 'fi', 'fil', 'fr', 'fr-ca', 'gl', 'gsw', 'he', 'hi-in', 'hr', 'hu', 'id', 'it', 'ja', 'kk', 'ko', 'lt-lt', 'lv', 'mr', 'ms', 'nb', 'nl', 'nn', 'pl', 'pr', 'pt', 'pt-br', 'pt-pt', 'ro', 'ru', 'sk', 'sl-si', 'sq', 'sv', 'ta', 'th', 'tr', 'uk', 'ur_pk', 'vi', 'zh-cn', 'zh-hk', 'zh-tw'];
|
||||
const translations = languages.map(function (language) {
|
||||
return {
|
||||
lang: language,
|
||||
@@ -274,11 +275,24 @@ async function onAppReady() {
|
||||
}
|
||||
};
|
||||
|
||||
Events.on(ServerConnections, 'localusersignedin', handleStyleChange);
|
||||
Events.on(ServerConnections, 'localusersignedout', handleStyleChange);
|
||||
const handleLanguageChange = () => {
|
||||
const locale = globalize.getCurrentLocale();
|
||||
|
||||
document.documentElement.setAttribute('lang', locale);
|
||||
};
|
||||
|
||||
const handleUserChange = () => {
|
||||
handleStyleChange();
|
||||
handleLanguageChange();
|
||||
};
|
||||
|
||||
Events.on(ServerConnections, 'localusersignedin', handleUserChange);
|
||||
Events.on(ServerConnections, 'localusersignedout', handleUserChange);
|
||||
Events.on(currentSettings, 'change', (e, prop) => {
|
||||
if (prop == 'disableCustomCss' || prop == 'customCss') {
|
||||
handleStyleChange();
|
||||
} else if (prop == 'language') {
|
||||
handleLanguageChange();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"AllowOnTheFlySubtitleExtractionHelp": "Embedded subtitles can be extracted from videos and delivered to clients in plain text, in order to help prevent video transcoding. On some systems this can take a long time and cause video playback to stall during the extraction process. Disable this to have embedded subtitles burned in with video transcoding when they are not natively supported by the client device.",
|
||||
"AllowRemoteAccess": "Allow remote connections to this server",
|
||||
"AllowRemoteAccessHelp": "If unchecked, all remote connections will be blocked.",
|
||||
"AllowTonemappingHelp": "Tone-mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colors, which are very important information for representing the original scene. Currently works only with HDR10 or HLG videos. This requires the corresponding OpenCL or CUDA runtime.",
|
||||
"AllowTonemappingHelp": "Tone-mapping can transform the dynamic range of a video from HDR to SDR while maintaining image details and colors, which are very important information for representing the original scene. Currently works only with 10bit HDR10,HLG and DoVi videos. This requires the corresponding OpenCL or CUDA runtime.",
|
||||
"AlwaysPlaySubtitles": "Always Play",
|
||||
"AlwaysPlaySubtitlesHelp": "Subtitles matching the language preference will be loaded regardless of the audio language.",
|
||||
"AnyLanguage": "Any Language",
|
||||
@@ -333,6 +333,7 @@
|
||||
"HeaderCodecProfileHelp": "Codec profiles indicate the limitations of a device when playing specific codecs. If a limitation applies then the media will be transcoded, even if the codec is configured for direct playback.",
|
||||
"HeaderConfigureRemoteAccess": "Set up Remote Access",
|
||||
"HeaderConfirmPluginInstallation": "Confirm Plugin Installation",
|
||||
"HeaderConfirmRepositoryInstallation": "Confirm Plugin Repository Installation",
|
||||
"HeaderConfirmProfileDeletion": "Confirm Profile Deletion",
|
||||
"HeaderConfirmRevokeApiKey": "Revoke API Key",
|
||||
"HeaderConnectionFailure": "Connection Failure",
|
||||
@@ -359,7 +360,7 @@
|
||||
"HeaderDirectPlayProfile": "Direct Playback Profile",
|
||||
"HeaderDirectPlayProfileHelp": "Add direct playback profiles to indicate which formats the device can handle natively.",
|
||||
"HeaderDownloadSync": "Download & Sync",
|
||||
"HeaderDVR": "Digital Recorder",
|
||||
"HeaderDVR": "DVR",
|
||||
"HeaderEasyPinCode": "Easy PIN Code",
|
||||
"HeaderEditImages": "Edit Images",
|
||||
"HeaderEnabledFields": "Enabled Fields",
|
||||
@@ -602,6 +603,7 @@
|
||||
"LabelDefaultUser": "Default user:",
|
||||
"LabelDefaultUserHelp": "Determine which user library should be displayed on connected devices. This can be overridden for each device using profiles.",
|
||||
"LabelDeinterlaceMethod": "Deinterlacing method:",
|
||||
"LabelDeveloper": "Developer",
|
||||
"LabelDeviceDescription": "Device description:",
|
||||
"LabelDidlMode": "DIDL mode:",
|
||||
"LabelDisableCustomCss": "Disable custom CSS code for theming/branding provided from the server.",
|
||||
@@ -666,7 +668,7 @@
|
||||
"LabelFormat": "Format:",
|
||||
"LabelFriendlyName": "Friendly name:",
|
||||
"LabelGroupMoviesIntoCollections": "Group movies into collections",
|
||||
"LabelGroupMoviesIntoCollectionsHelp": "If pick the movies list view, then the box sets will be shown as items with grouped movies.",
|
||||
"LabelGroupMoviesIntoCollectionsHelp": "Movies in a collection will be displayed as one grouped item when displaying movie lists.",
|
||||
"LabelH264Crf": "H.264 encoding CRF:",
|
||||
"LabelH265Crf": "H.265 encoding CRF:",
|
||||
"LabelHardwareAccelerationType": "Hardware acceleration:",
|
||||
@@ -708,6 +710,7 @@
|
||||
"LabelLibraryPageSizeHelp": "Set the amount of items to show on a library page. Set to 0 in order to disable paging.",
|
||||
"LabelMaxDaysForNextUp": "Max days in 'Next Up':",
|
||||
"LabelMaxDaysForNextUpHelp": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
|
||||
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution",
|
||||
"LabelLineup": "Lineup:",
|
||||
"LabelLocalCustomCss": "Custom CSS code for styling which applies to this client only. You may want to disable server custom CSS code.",
|
||||
"LabelLocalHttpServerPortNumber": "Local HTTP port number:",
|
||||
@@ -1077,10 +1080,11 @@
|
||||
"MessagePleaseEnsureInternetMetadata": "Please ensure downloading of internet metadata is enabled.",
|
||||
"MessagePleaseWait": "Please wait. This may take a minute.",
|
||||
"MessagePluginConfigurationRequiresLocalAccess": "To set up this plugin please sign in to your local server directly.",
|
||||
"MessagePluginInstallDisclaimer": "Plugins built by community members are a great way to enhance your experience with additional features and benefits. Before installing, please be aware of the effects they may have on your server, such as longer library scans, additional background processing, and decreased system stability.",
|
||||
"MessagePluginInstallDisclaimer": "WARNING: Installing a third party plugin carries risks. It may contain unstable or malicious code, and may change at any time. Only install plugins from authors that you trust, and please be aware of the potential effects it may have, including external service queries, longer library scans, or additional background processing.",
|
||||
"MessagePluginInstalled": "The plugin has been successfully installed. The server will need to be restarted for changes to take effect.",
|
||||
"MessagePluginInstallError": "An error occurred while installing the plugin.",
|
||||
"MessageReenableUser": "See below to reenable",
|
||||
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
|
||||
"MessageSent": "Message sent.",
|
||||
"MessageSyncPlayCreateGroupDenied": "Permission required to create a group.",
|
||||
"MessageSyncPlayDisabled": "SyncPlay disabled.",
|
||||
@@ -1295,6 +1299,7 @@
|
||||
"PlayNextEpisodeAutomatically": "Play next episode automatically",
|
||||
"PleaseAddAtLeastOneFolder": "Please add at least one folder to this library by clicking the '+' button in 'Folders' section.",
|
||||
"PleaseConfirmPluginInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin installation.",
|
||||
"PleaseConfirmRepositoryInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin repository installation.",
|
||||
"PleaseEnterNameOrId": "Please enter a name or an external ID.",
|
||||
"PleaseRestartServerName": "Please restart Jellyfin on {0}.",
|
||||
"PleaseSelectTwoItems": "Please select at least two items.",
|
||||
@@ -1353,7 +1358,11 @@
|
||||
"RefreshQueued": "Refresh queued.",
|
||||
"ReleaseDate": "Release date",
|
||||
"ReleaseGroup": "Release Group",
|
||||
"RememberAudioSelections": "Set audio track based on previous item",
|
||||
"RememberAudioSelectionsHelp": "Try to set the audio track to the closest match to the last video.",
|
||||
"RememberMe": "Remember Me",
|
||||
"RememberSubtitleSelections": "Set subtitle track based on previous item",
|
||||
"RememberSubtitleSelectionsHelp": "Try to set the subtitle track to the closest match to the last video.",
|
||||
"Remixer": "Remixer",
|
||||
"RemoveFromCollection": "Remove from collection",
|
||||
"RemoveFromPlaylist": "Remove from playlist",
|
||||
@@ -1377,6 +1386,7 @@
|
||||
"ScanForNewAndUpdatedFiles": "Scan for new and updated files",
|
||||
"ScanLibrary": "Scan library",
|
||||
"Schedule": "Schedule",
|
||||
"ScreenResolution": "Screen Resolution",
|
||||
"Screenshot": "Screenshot",
|
||||
"Screenshots": "Screenshots",
|
||||
"Search": "Search",
|
||||
@@ -1401,8 +1411,6 @@
|
||||
"Settings": "Settings",
|
||||
"SettingsSaved": "Settings saved.",
|
||||
"SettingsWarning": "Changing these values may cause instability or connectivity failures. If you experience any problems, we recommend changing them back to default.",
|
||||
"SetUsingLastTracks": "Set Subtitle/Audio Tracks with Previous Item",
|
||||
"SetUsingLastTracksHelp": "Try to set the Subtitle/Audio track to the closest match to the last video.",
|
||||
"Share": "Share",
|
||||
"ShowAdvancedSettings": "Show advanced settings",
|
||||
"ShowIndicatorsFor": "Show indicators for:",
|
||||
@@ -1642,5 +1650,25 @@
|
||||
"Scene": "Scene",
|
||||
"Sample": "Sample",
|
||||
"ThemeSong": "Theme Song",
|
||||
"ThemeVideo": "Theme Video"
|
||||
"ThemeVideo": "Theme Video",
|
||||
"EnableSplashScreen": "Enable the splash screen",
|
||||
"LabelVppTonemappingBrightness": "VPP Tone mapping brightness gain:",
|
||||
"LabelVppTonemappingBrightnessHelp": "Apply brightness gain in VPP tone mapping. The recommended and default values are 16 and 0.",
|
||||
"LabelVppTonemappingContrast": "VPP Tone mapping contrast gain:",
|
||||
"LabelVppTonemappingContrastHelp": "Apply contrast gain in VPP tone mapping. Both recommended and default values are 1.",
|
||||
"VideoRangeTypeNotSupported": "The video's range type is not supported",
|
||||
"LabelVideoRangeType": "Video range type:",
|
||||
"MediaInfoVideoRangeType": "Video range type",
|
||||
"MediaInfoDoViTitle": "DV title",
|
||||
"MediaInfoDvVersionMajor": "DV version major",
|
||||
"MediaInfoDvVersionMinor": "DV version minor",
|
||||
"MediaInfoDvProfile": "DV profile",
|
||||
"MediaInfoDvLevel": "DV level",
|
||||
"MediaInfoRpuPresentFlag": "DV rpu preset flag",
|
||||
"MediaInfoElPresentFlag": "DV el preset flag",
|
||||
"MediaInfoBlPresentFlag": "DV bl preset flag",
|
||||
"MediaInfoDvBlSignalCompatibilityId": "DV bl signal compatibility id",
|
||||
"LabelTonemappingMode": "Tone mapping mode:",
|
||||
"TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode.",
|
||||
"Unknown": "Unknown"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user