Merge branch 'jellyfin:master' into chapter-markers

This commit is contained in:
Viperinius 2022-07-03 17:08:49 +02:00 committed by GitHub
commit ca84407884
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 933 additions and 179 deletions

1
.gitignore vendored
View File

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

5
.vscode/extensions.json vendored Normal file
View File

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

7
.vscode/settings.json vendored Normal file
View File

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

View File

@ -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

View File

@ -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)

View File

@ -4,7 +4,7 @@ Name: jellyfin-web
Version: 10.8.0
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,22 +24,26 @@ 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>

17
package-lock.json generated
View File

@ -10760,6 +10760,23 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": {
"history": "^5.2.0"
}
},
"react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": {
"history": "^5.2.0",
"react-router": "6.3.0"
}
},
"read-file-stdin": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",

View File

@ -96,6 +96,7 @@
"pdfjs-dist": "2.12.313",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "6.3.0",
"resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.0",
"sortablejs": "1.14.0",

View File

@ -0,0 +1,169 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import alert from './alert';
import { appRouter } from './appRouter';
import loading from './loading/loading';
import ServerConnections from './ServerConnections';
import globalize from '../scripts/globalize';
enum BounceRoutes {
Home = '/home.html',
Login = '/login.html',
SelectServer = '/selectserver.html',
StartWizard = '/wizardstart.html'
}
// TODO: This should probably be in the SDK
enum ConnectionState {
SignedIn = 'SignedIn',
ServerSignIn = 'ServerSignIn',
ServerSelection = 'ServerSelection',
ServerUpdateNeeded = 'ServerUpdateNeeded'
}
type ConnectionRequiredProps = {
isAdminRequired?: boolean,
isUserRequired?: boolean
};
/**
* A component that ensures a server connection has been established.
* Additional parameters exist to verify a user or admin have authenticated.
* If a condition fails, this component will navigate to the appropriate page.
*/
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
children,
isAdminRequired = false,
isUserRequired = true
}) => {
const navigate = useNavigate();
const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bounce = async (connectionResponse: any) => {
switch (connectionResponse.State) {
case ConnectionState.SignedIn:
// Already logged in, bounce to the home page
console.debug('[ConnectionRequired] already logged in, redirecting to home');
navigate(BounceRoutes.Home);
return;
case ConnectionState.ServerSignIn:
// Bounce to the login page
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
navigate(BounceRoutes.Login, {
state: {
serverid: connectionResponse.ApiClient.serverId()
}
});
return;
case ConnectionState.ServerSelection:
// Bounce to select server page
console.debug('[ConnectionRequired] redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
case ConnectionState.ServerUpdateNeeded:
// Show update needed message and bounce to select server page
try {
await alert({
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
});
} catch (ex) {
console.warn('[ConnectionRequired] failed to show alert', ex);
}
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
navigate(BounceRoutes.SelectServer);
return;
}
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
};
const validateConnection = async () => {
// Check connection status on initial page load
const firstConnection = appRouter.firstConnectionResult;
appRouter.firstConnectionResult = null;
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
if (firstConnection.State === ConnectionState.ServerSignIn) {
// Verify the wizard is complete
try {
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
if (!infoResponse.ok) {
throw new Error('Public system info request failed');
}
const systemInfo = await infoResponse.json();
if (!systemInfo?.StartupWizardCompleted) {
// Bounce to the wizard
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
navigate(BounceRoutes.StartWizard);
return;
}
} catch (ex) {
console.error('[ConnectionRequired] checking wizard status failed', ex);
return;
}
}
// Bounce to the correct page in the login flow
bounce(firstConnection);
return;
}
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
// This case will need to be handled elsewhere before appRouter can be killed.
const client = ServerConnections.currentApiClient();
// If this is a user route, ensure a user is logged in
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
try {
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
bounce(await ServerConnections.connect());
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from user route', ex);
}
return;
}
// If this is an admin route, ensure the user has access
if (isAdminRequired) {
try {
const user = await client.getCurrentUser();
if (!user.Policy.IsAdministrator) {
console.warn('[ConnectionRequired] normal user attempted to access admin route');
bounce(await ServerConnections.connect());
return;
}
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
return;
}
}
setIsLoading(false);
};
loading.show();
validateConnection();
}, [ isAdminRequired, isUserRequired, navigate ]);
useEffect(() => {
if (!isLoading) {
loading.hide();
}
}, [ isLoading ]);
if (isLoading) {
return null;
}
return (
<>{children}</>
);
};
export default ConnectionRequired;

View File

@ -0,0 +1,48 @@
import React, { useLayoutEffect } from 'react';
import { HistoryRouterProps, Router } from 'react-router-dom';
import { Update } from 'history';
/** Strips leading "!" from paths */
const normalizePath = (pathname: string) => pathname.replace(/^!/, '');
/**
* A slightly customized version of the HistoryRouter from react-router-dom.
* We need to use HistoryRouter to have a shared history state between react-router and appRouter, but it does not seem
* to be properly exported in the upstream package.
* We also needed some customizations to handle #! routes.
* Refs: https://github.com/remix-run/react-router/blob/v6.3.0/packages/react-router-dom/index.tsx#L222
*/
export function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
const [state, setState] = React.useState<Update>({
action: history.action,
location: history.location
});
useLayoutEffect(() => {
const onHistoryChange = (update: Update) => {
if (update.location.pathname.startsWith('!')) {
// When the location changes, we need to check for #! paths and replace the location with the "!" stripped
history.replace(normalizePath(update.location.pathname), update.location.state);
} else {
setState(update);
}
};
history.listen(onHistoryChange);
}, [ history ]);
return (
<Router
basename={basename}
// eslint-disable-next-line react/no-children-prop
children={children}
location={{
...state.location,
// The original location does not get replaced with the normalized version, so we need to strip it here
pathname: normalizePath(state.location.pathname)
}}
navigationType={state.action}
navigator={history}
/>
);
}

69
src/components/Page.tsx Normal file
View File

@ -0,0 +1,69 @@
import React, { FunctionComponent, HTMLAttributes, useEffect, useRef } from 'react';
import viewManager from './viewManager/viewManager';
type PageProps = {
id: string, // id is required for libraryMenu
title?: string,
isBackButtonEnabled?: boolean,
isNowPlayingBarEnabled?: boolean,
isThemeMediaSupported?: boolean
};
/**
* Page component that handles hiding active non-react views, triggering the required events for
* navigation and appRouter state updates, and setting the correct classes and data attributes.
*/
const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
children,
id,
className = '',
title,
isBackButtonEnabled = true,
isNowPlayingBarEnabled = true,
isThemeMediaSupported = false
}) => {
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
// hide active non-react views
viewManager.hideView();
}, []);
useEffect(() => {
const event = {
bubbles: true,
cancelable: false,
detail: {
isRestored: false,
options: {
enableMediaControl: isNowPlayingBarEnabled,
supportsThemeMedia: isThemeMediaSupported
}
}
};
// viewbeforeshow - switches between the admin dashboard and standard themes
element.current?.dispatchEvent(new CustomEvent('viewbeforeshow', event));
// pagebeforeshow - hides tabs on tables pages in libraryMenu
element.current?.dispatchEvent(new CustomEvent('pagebeforeshow', event));
// viewshow - updates state of appRouter
element.current?.dispatchEvent(new CustomEvent('viewshow', event));
// pageshow - updates header/navigation in libraryMenu
element.current?.dispatchEvent(new CustomEvent('pageshow', event));
}, [ element, isNowPlayingBarEnabled, isThemeMediaSupported ]);
return (
<div
ref={element}
id={id}
data-role='page'
className={`page ${className}`}
data-title={title}
data-backbutton={`${isBackButtonEnabled}`}
>
{children}
</div>
);
};
export default Page;

View File

@ -122,7 +122,11 @@ class AppRouter {
isBack: action === Action.Pop
});
} else {
console.warn('[appRouter] "%s" route not found', normalizedPath, location);
console.info('[appRouter] "%s" route not found', normalizedPath, location);
this.currentRouteInfo = {
route: {},
path: normalizedPath + location.search
};
}
}
@ -139,7 +143,7 @@ class AppRouter {
Events.on(apiClient, 'requestfail', this.onRequestFail);
});
ServerConnections.connect().then(result => {
return ServerConnections.connect().then(result => {
this.firstConnectionResult = result;
// Handle the initial route

View File

@ -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) => {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -113,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) {
@ -128,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) {
@ -137,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) {
@ -149,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));
}

View File

@ -1,42 +0,0 @@
import React, { FunctionComponent, useState } from 'react';
import SearchFields from '../search/SearchFields';
import SearchResults from '../search/SearchResults';
import SearchSuggestions from '../search/SearchSuggestions';
import LiveTVSearchResults from '../search/LiveTVSearchResults';
type SearchProps = {
serverId?: string,
parentId?: string,
collectionType?: string
};
const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => {
const [ query, setQuery ] = useState<string>();
return (
<>
<SearchFields onSearch={setQuery} />
{!query &&
<SearchSuggestions
serverId={serverId || window.ApiClient.serverId()}
parentId={parentId}
/>
}
<SearchResults
serverId={serverId || window.ApiClient.serverId()}
parentId={parentId}
collectionType={collectionType}
query={query}
/>
<LiveTVSearchResults
serverId={serverId || window.ApiClient.serverId()}
parentId={parentId}
collectionType={collectionType}
query={query}
/>
</>
);
};
export default SearchPage;

View File

@ -2281,7 +2281,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;
@ -2306,7 +2306,7 @@ class PlaybackManager {
}
}
function autoSetNextTracks(prevSource, mediaSource) {
function autoSetNextTracks(prevSource, mediaSource, audio, subtitle) {
try {
if (!prevSource) return;
@ -2315,18 +2315,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');
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
}
} catch (e) {
console.error(`AutoSet - Caught unexpected error: ${e}`);
}
@ -2390,9 +2385,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);

View File

@ -41,7 +41,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 +52,8 @@ import template from './playbackSettings.template.html';
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
enableAuto: true
enableAuto: true,
maxVideoWidth
});
@ -60,6 +61,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 +73,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 => {
@ -180,7 +186,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 +199,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();
@ -209,6 +219,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 +233,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 +259,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 +316,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) {

View File

@ -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">
@ -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">

View File

@ -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
});
}

View File

@ -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 = [];

View File

@ -21,8 +21,8 @@ const CARD_OPTIONS = {
type LiveTVSearchResultsProps = {
serverId?: string;
parentId?: string;
collectionType?: string;
parentId?: string | null;
collectionType?: string | null;
query?: string;
}

View File

@ -9,8 +9,8 @@ import SearchResultsRow from './SearchResultsRow';
type SearchResultsProps = {
serverId?: string;
parentId?: string;
collectionType?: string;
parentId?: string | null;
collectionType?: string | null;
query?: string;
}

View File

@ -22,7 +22,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
type SearchSuggestionsProps = {
serverId?: string;
parentId?: string;
parentId?: string | null;
}
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {

View File

@ -32,7 +32,7 @@ function getSubtitleAppearanceObject(context) {
appearanceSettings.dropShadow = context.querySelector('#selectDropShadow').value;
appearanceSettings.font = context.querySelector('#selectFont').value;
appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value;
appearanceSettings.textColor = context.querySelector('#inputTextColor').value;
appearanceSettings.textColor = layoutManager.tv ? context.querySelector('#selectTextColor').value : context.querySelector('#inputTextColor').value;
appearanceSettings.verticalPosition = context.querySelector('#sliderVerticalPosition').value;
return appearanceSettings;
@ -57,6 +57,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#selectTextWeight').value = appearanceSettings.textWeight || 'normal';
context.querySelector('#selectDropShadow').value = appearanceSettings.dropShadow || '';
context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent';
context.querySelector('#selectTextColor').value = appearanceSettings.textColor || '#ffffff';
context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff';
context.querySelector('#selectFont').value = appearanceSettings.font || '';
context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition;
@ -171,6 +172,7 @@ function embed(options, self) {
options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#selectTextColor').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange);
options.element.querySelector('#inputTextBackground').addEventListener('change', onAppearanceFieldChange);
@ -201,6 +203,10 @@ function embed(options, self) {
sliderVerticalPosition.classList.add('focusable');
sliderVerticalPosition.enableKeyboardDragging();
}, 0);
// Replace color picker
dom.parentWithTag(options.element.querySelector('#inputTextColor'), 'DIV').classList.add('hide');
dom.parentWithTag(options.element.querySelector('#selectTextColor'), 'DIV').classList.remove('hide');
}
options.element.querySelector('.chkPreview').addEventListener('change', (e) => {

View File

@ -95,8 +95,21 @@
<input is="emby-input" id="inputTextBackground" label="${LabelTextBackgroundColor}" type="text" />
</div>
<div class="inputContainer hide">
<input is="emby-input" id="inputTextColor" label="${LabelTextColor}" type="text" />
<div class="selectContainer">
<input is="emby-input" id="inputTextColor" label="${LabelTextColor}" type="color" />
</div>
<div class="selectContainer hide">
<select is="emby-select" id="selectTextColor" label="${LabelTextColor}">
<option value="#ffffff">${White}</option>
<option value="#ffff00">${Yellow}</option>
<option value="#008000">${Green}</option>
<option value="#00ffff">${Cyan}</option>
<option value="#0000ff">${Blue}</option>
<option value="#ff00ff">${Magenta}</option>
<option value="#ff0000">${Red}</option>
<option value="#000000">${Black}</option>
</select>
</div>
<div class="selectContainer">

View File

@ -147,6 +147,15 @@ class ViewManager {
});
}
hideView() {
if (currentView) {
dispatchViewEvent(currentView, null, 'viewbeforehide');
dispatchViewEvent(currentView, null, 'viewhide');
currentView.classList.add('hide');
currentView = null;
}
}
tryRestoreView(options, onViewChanging) {
if (options.cancel) {
return Promise.reject({ cancelled: true });

View File

@ -126,13 +126,24 @@
</div>
</div>
<div class="checkboxListContainer checkboxContainer-withDescription fldVppTonemapping hide">
<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>

View File

@ -37,6 +37,8 @@ import alert from '../../components/alert';
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 || '';
@ -91,6 +93,8 @@ import alert from '../../components/alert';
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') {

View File

@ -1,2 +0,0 @@
<div id="searchPage" data-role="page" class="page libraryPage allLibraryPage noSecondaryNavPage" data-title="${Search}" data-backbutton="true">
</div>

View File

@ -17,6 +17,11 @@
width: 100%;
}
.emby-input[type=color] {
height: 2.5em;
padding: 0;
}
.emby-input::-moz-focus-inner {
border: 0;
}

View File

@ -158,6 +158,7 @@
<div class="mainAnimatedPages skinBody">
<div class="splashLogo"></div>
</div>
<div id="reactRoot"></div>
<div class="mainDrawerHandle"></div>
</body>
</html>

View File

@ -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();

View File

@ -1376,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>';

24
src/routes/index.tsx Normal file
View File

@ -0,0 +1,24 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../components/ConnectionRequired';
import SearchPage from './search';
const AppRoutes = () => (
<Routes>
<Route path='/'>
<Route
path='search.html'
element={
<ConnectionRequired>
<SearchPage />
</ConnectionRequired>
}
/>
{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
</Route>
</Routes>
);
export default AppRoutes;

44
src/routes/search.tsx Normal file
View File

@ -0,0 +1,44 @@
import React, { FunctionComponent, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import Page from '../components/Page';
import SearchFields from '../components/search/SearchFields';
import SearchResults from '../components/search/SearchResults';
import SearchSuggestions from '../components/search/SearchSuggestions';
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
import globalize from '../scripts/globalize';
const SearchPage: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>();
const [ searchParams ] = useSearchParams();
return (
<Page
id='searchPage'
title={globalize.translate('Search')}
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
>
<SearchFields onSearch={setQuery} />
{!query &&
<SearchSuggestions
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
/>
}
<SearchResults
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
collectionType={searchParams.get('collectionType')}
query={query}
/>
<LiveTVSearchResults
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
collectionType={searchParams.get('collectionType')}
query={query}
/>
</Page>
);
};
export default SearchPage;

View File

@ -312,7 +312,7 @@ import browser from './browser';
return -1;
}
function getPhysicalAudioChannels(options) {
function getPhysicalAudioChannels(options, videoTestElement) {
const allowedAudioChannels = parseInt(userSettings.allowedAudioChannels(), 10);
if (allowedAudioChannels > 0) {
@ -324,8 +324,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;
@ -348,12 +354,12 @@ import browser from './browser';
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'];
@ -851,6 +857,29 @@ 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|DOVI';
vp9VideoRangeTypes += '|HDR10|HLG';
av1VideoRangeTypes += '|HDR10|HLG';
}
if (browser.edgeChromium || browser.chrome || browser.firefox) {
vp9VideoRangeTypes += '|HDR10|HLG';
av1VideoRangeTypes += '|HDR10|HLG';
}
const h264CodecProfileConditions = [
{
Condition: 'NotEquals',
@ -864,6 +893,12 @@ import browser from './browser';
Value: h264Profiles,
IsRequired: false
},
{
Condition: 'EqualsAny',
Property: 'VideoRangeType',
Value: h264VideoRangeTypes,
IsRequired: false
},
{
Condition: 'LessThanEqual',
Property: 'VideoLevel',
@ -885,6 +920,12 @@ import browser from './browser';
Value: hevcProfiles,
IsRequired: false
},
{
Condition: 'EqualsAny',
Property: 'VideoRangeType',
Value: hevcVideoRangeTypes,
IsRequired: false
},
{
Condition: 'LessThanEqual',
Property: 'VideoLevel',
@ -893,6 +934,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',
@ -982,6 +1041,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) {

View File

@ -11,6 +11,7 @@
case 'Sony PS4':
return baseUrl + 'playstation.svg';
case 'Kodi':
case 'Kodi JellyCon':
return baseUrl + 'kodi.svg';
case 'Jellyfin Android':
case 'AndroidTV':
@ -18,6 +19,8 @@
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) {

View File

@ -308,12 +308,6 @@ import { appRouter } from '../components/appRouter';
type: 'home'
});
defineRoute({
alias: '/search.html',
path: 'search.html',
pageComponent: 'SearchPage'
});
defineRoute({
alias: '/list.html',
path: 'list.html',

View File

@ -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);

View File

@ -164,19 +164,6 @@ export class UserSettings {
return toBoolean(this.get('enableNextVideoInfoOverlay', false), true);
}
/**
* 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 toBoolean(this.get('enableSetUsingLastTracks', false), true);
}
/**
* Get or set 'Theme Songs' state.
* @param {boolean|undefined} val - Flag to enable 'Theme Songs' or undefined.
@ -561,7 +548,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);

View File

@ -7,6 +7,8 @@ import 'classlist.js';
import 'whatwg-fetch';
import 'resize-observer-polyfill';
import '../assets/css/site.scss';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { Events } from 'jellyfin-apiclient';
import ServerConnections from '../components/ServerConnections';
import globalize from './globalize';
@ -18,7 +20,7 @@ import { appHost } from '../components/apphost';
import { getPlugins } from './settings/webSettings';
import { pluginManager } from '../components/pluginManager';
import packageManager from '../components/packageManager';
import { appRouter } from '../components/appRouter';
import { appRouter, history } from '../components/appRouter';
import '../elements/emby-button/emby-button';
import './autoThemes';
import './libraryMenu';
@ -40,6 +42,8 @@ import SyncPlayHtmlVideoPlayer from '../components/syncPlay/ui/players/HtmlVideo
import SyncPlayHtmlAudioPlayer from '../components/syncPlay/ui/players/HtmlAudioPlayer';
import { currentSettings } from './settings/userSettings';
import taskButton from './taskbutton';
import { HistoryRouter } from '../components/HistoryRouter.tsx';
import AppRoutes from '../routes/index.tsx';
function loadCoreDictionary() {
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', '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'];
@ -167,7 +171,14 @@ async function onAppReady() {
ServerConnections.currentApiClient()?.ensureWebSocket();
});
appRouter.start();
await appRouter.start();
ReactDOM.render(
<HistoryRouter history={history}>
<AppRoutes />
</HistoryRouter>,
document.getElementById('reactRoot')
);
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
import('../components/nowPlayingBar/nowPlayingBar');

View File

@ -1653,5 +1653,27 @@
"HomeVideosPhotos": "Domácí videa a fotky",
"Bold": "Tučné",
"LabelTextWeight": "Tloušťka textu:",
"EnableSplashScreen": "Povolit úvodní obrazovku"
"EnableSplashScreen": "Povolit úvodní obrazovku",
"MediaInfoDvBlSignalCompatibilityId": "ID kompatibility signálu DV Bl",
"MediaInfoBlPresentFlag": "Natavení předvolby DV Bl",
"MediaInfoElPresentFlag": "Nastavení předvolby DV El",
"MediaInfoRpuPresentFlag": "Nastavení předvolby DV RPU",
"MediaInfoDvLevel": "Úroveň DV",
"MediaInfoDvProfile": "Profil DV",
"MediaInfoDvVersionMinor": "Vedlejší verze DV",
"MediaInfoDvVersionMajor": "Hlavní verze DV",
"MediaInfoDoViTitle": "Název DV",
"MediaInfoVideoRangeType": "Typ rozsahu videa",
"LabelVideoRangeType": "Typ rozsahu videa:",
"VideoRangeTypeNotSupported": "Typ rozsahu videa není podporován",
"LabelVppTonemappingContrastHelp": "Zvýší kontrast při mapování tonů VPP. Doporučená hodnota je 1.2, výchozí hodnota je 1.",
"LabelVppTonemappingContrast": "Zvýšení kontrastu mapování tónů VPP:",
"LabelVppTonemappingBrightnessHelp": "Zvýší jas při mapování tonů VPP. Doporučená i výchozí hodnota je 0.",
"LabelVppTonemappingBrightness": "Zvýšení jasu mapování tónů VPP:",
"ScreenResolution": "Rozlišení obrazovky",
"RememberSubtitleSelectionsHelp": "Pokusí se nastavit titulkovou stopu co nejpodobněji předchozímu videu.",
"RememberSubtitleSelections": "Nastavit titulkovou stopu podle předchozí položky",
"RememberAudioSelectionsHelp": "Pokusí se nastavit zvukovou stopu co nejpodobněji předchozímu videu.",
"RememberAudioSelections": "Nastavit zvukovou stopu podle předchozí položky",
"LabelMaxVideoResolution": "Maximální rozlišení videa pro překódování"
}

View File

@ -1653,5 +1653,21 @@
"HomeVideosPhotos": "Heimvideos und -bilder",
"Bold": "Fett",
"LabelTextWeight": "Schriftstärke:",
"EnableSplashScreen": "Aktiviere den Splash Screen"
"EnableSplashScreen": "Aktiviere den Splash Screen",
"MediaInfoRpuPresentFlag": "DV rpu Flag anwesend",
"MediaInfoDvLevel": "DV Level",
"MediaInfoDvProfile": "DV Profil",
"MediaInfoDvVersionMinor": "DV Nebenversion",
"MediaInfoDvVersionMajor": "DV Hauptversion",
"MediaInfoDoViTitle": "DV Titel",
"LabelVppTonemappingContrastHelp": "VPP Tonemapping Kontrast anwenden. Empfohlene und Standartwerte sind 1,2 und 1.",
"LabelVppTonemappingContrast": "VPP Tonemapping Kontrast:",
"LabelVppTonemappingBrightnessHelp": "VPP Tonemapping Helligkeit anwenden. Empfohlener und Standartwert sind 0.",
"LabelVppTonemappingBrightness": "VPP Tonemapping Helligkeit:",
"ScreenResolution": "Bildschirmauflösung",
"RememberSubtitleSelectionsHelp": "Versuche den zum letzten Video ähnlichsten Untertitel zu setzen.",
"RememberSubtitleSelections": "Setze den Untertitel auf Basis des letzten Objekts",
"RememberAudioSelectionsHelp": "Versuchen die ähnlichste Tonspur zum letzten Video zu setzen.",
"RememberAudioSelections": "Tonspur auf Basis des letzten Objekt auswählen",
"LabelMaxVideoResolution": "Maximal erlaubte Video Transcodierungs-Auflösung"
}

View File

@ -1653,5 +1653,27 @@
"EnableEnhancedNvdecDecoderHelp": "Experimental NVDEC implementation, do not enable this option unless you encounter decoding errors.",
"EnableSplashScreen": "Enable the splash screen",
"Bold": "Bold",
"HomeVideosPhotos": "Home Videos and Photos"
"HomeVideosPhotos": "Home Videos and Photos",
"MediaInfoDvBlSignalCompatibilityId": "DV bl signal compatibility id",
"MediaInfoBlPresentFlag": "DV bl preset flag",
"MediaInfoElPresentFlag": "DV el preset flag",
"MediaInfoRpuPresentFlag": "DV rpu preset flag",
"MediaInfoDvLevel": "DV level",
"MediaInfoDvProfile": "DV profile",
"MediaInfoDvVersionMinor": "DV version minor",
"MediaInfoDvVersionMajor": "DV version major",
"MediaInfoDoViTitle": "DV title",
"MediaInfoVideoRangeType": "Video range type",
"LabelVideoRangeType": "Video range type:",
"VideoRangeTypeNotSupported": "The video's range type is not supported",
"LabelVppTonemappingContrastHelp": "Apply contrast gain in VPP tone mapping. The recommended and default values are 1.2 and 1.",
"LabelVppTonemappingContrast": "VPP Tone mapping contrast gain:",
"LabelVppTonemappingBrightnessHelp": "Apply brightness gain in VPP tone mapping. Both recommended and default values are 0.",
"LabelVppTonemappingBrightness": "VPP Tone mapping brightness gain:",
"ScreenResolution": "Screen Resolution",
"RememberSubtitleSelectionsHelp": "Try to set the subtitle track to the closest match to the last video.",
"RememberSubtitleSelections": "Set subtitle track based on previous item",
"RememberAudioSelectionsHelp": "Try to set the audio track to the closest match to the last video.",
"RememberAudioSelections": "Set audio track based on previous item",
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution"
}

View File

@ -709,6 +709,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:",
@ -1354,7 +1355,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",
@ -1378,6 +1383,7 @@
"ScanForNewAndUpdatedFiles": "Scan for new and updated files",
"ScanLibrary": "Scan library",
"Schedule": "Schedule",
"ScreenResolution": "Screen Resolution",
"Search": "Search",
"SearchForCollectionInternetMetadata": "Search the internet for artwork and metadata",
"SearchForMissingMetadata": "Search for missing metadata",
@ -1400,8 +1406,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:",
@ -1643,5 +1647,21 @@
"ThemeSong": "Theme Song",
"ThemeVideo": "Theme Video",
"EnableEnhancedNvdecDecoderHelp": "Experimental NVDEC implementation, do not enable this option unless you encounter decoding errors.",
"EnableSplashScreen": "Enable the splash screen"
"EnableSplashScreen": "Enable the splash screen",
"LabelVppTonemappingBrightness": "VPP Tone mapping brightness gain:",
"LabelVppTonemappingBrightnessHelp": "Apply brightness gain in VPP tone mapping. Both recommended and default values are 0.",
"LabelVppTonemappingContrast": "VPP Tone mapping contrast gain:",
"LabelVppTonemappingContrastHelp": "Apply contrast gain in VPP tone mapping. The recommended and default values are 1.2 and 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"
}

View File

@ -1649,5 +1649,6 @@
"HomeVideosPhotos": "Hejmaj Videoj kaj Fotoj",
"Bold": "Grasa",
"LabelTextWeight": "Teksta pezo:",
"EnableSplashScreen": "Ebligi la salutŝildon"
"EnableSplashScreen": "Ebligi la salutŝildon",
"MediaInfoDoViTitle": "DV titolo"
}

View File

@ -1645,5 +1645,6 @@
"EnableRewatchingNextUpHelp": "Habilite 'Mostrar episodios ya vistos' en las secciones 'Siguiente'.",
"EnableRewatchingNextUp": "Habilitar \"Volver a mirar\" en \"Siguiente\"",
"Bold": "Audaz",
"LabelTextWeight": "Peso del texto:"
"LabelTextWeight": "Peso del texto:",
"EnableSplashScreen": "Habilitar pantalla de inicio"
}

View File

@ -1651,5 +1651,21 @@
"HomeVideosPhotos": "Kotivideot ja valokuvat",
"Bold": "Lihavoitu",
"LabelTextWeight": "Tekstin vahvuus:",
"EnableSplashScreen": "Käytä aloitusruutua"
"EnableSplashScreen": "Käytä aloitusruutua",
"MediaInfoDvLevel": "DV-taso",
"MediaInfoDvProfile": "DV-profiili",
"MediaInfoDoViTitle": "DV-nimike",
"ScreenResolution": "Näyttötarkkuus",
"RememberAudioSelectionsHelp": "Pyri valitsemaan parhaiten edellisen videon ääniraitaa vastaava ääniraita.",
"RememberSubtitleSelectionsHelp": "Pyri valitsemaan parhaiten edellisen videon tekstitysraitaa vastaava tekstitysraita.",
"RememberSubtitleSelections": "Valitse tekstitysraita edellisen kohteen perusteella",
"RememberAudioSelections": "Valitse ääniraita edellisen kohteen perusteella",
"LabelMaxVideoResolution": "Suurin sallittu videon transkoodaustarkkuus",
"MediaInfoVideoRangeType": "Videon aluetyyppi",
"LabelVideoRangeType": "Videon aluetyyppi:",
"VideoRangeTypeNotSupported": "Videon aluetyyppiä ei tueta",
"LabelVppTonemappingContrastHelp": "Käytä kontrastin vahvistusta VPP-sävykartoituksen kanssa. Molemmat ovat suositeltavia ja oletusarvot ovat 0.",
"LabelVppTonemappingBrightnessHelp": "Käytä kirkkauden vahvistusta VPP-sävykartoituksen kanssa. Molemmat ovat suositeltavia ja oletusarvot ovat 0.",
"LabelVppTonemappingContrast": "VPP-sävykartoituksen kontrastin vahvistus:",
"LabelVppTonemappingBrightness": "VPP-sävykartoituksen kirkkauden vahvistus:"
}

View File

@ -77,7 +77,7 @@
"ButtonRemove": "Supprimer",
"ButtonRename": "Renommer",
"ButtonResetEasyPassword": "Réinitialiser le code Easy PIN",
"ButtonResume": "Reprendre",
"ButtonResume": "Reprise",
"ButtonRevoke": "Révoquer",
"ButtonScanAllLibraries": "Actualiser toutes les médiathèques",
"ButtonSelectDirectory": "Sélectionner le répertoire",
@ -1653,5 +1653,27 @@
"HomeVideosPhotos": "Vidéos et photos personnelles",
"LabelTextWeight": "Poids de la police :",
"Bold": "Gras",
"EnableSplashScreen": "Activer l'écran de démarrage"
"EnableSplashScreen": "Activer l'écran de démarrage",
"LabelMaxVideoResolution": "Résolution maximale du transcodage vidéo :",
"MediaInfoDvBlSignalCompatibilityId": "ID de compatibilité du signal DV bl",
"MediaInfoBlPresentFlag": "Indicateur de préréglage DV bl",
"MediaInfoElPresentFlag": "Indicateur de préréglage DV el",
"MediaInfoRpuPresentFlag": "Indicateur de préréglage DV rpu",
"MediaInfoDvLevel": "Niveau de DV",
"MediaInfoDvProfile": "Profile DV",
"MediaInfoDvVersionMinor": "Version DV mineure",
"MediaInfoDvVersionMajor": "Version DV majeure",
"MediaInfoDoViTitle": "Titre DV",
"MediaInfoVideoRangeType": "Type de plage vidéo",
"LabelVideoRangeType": "Type de plage vidéo :",
"VideoRangeTypeNotSupported": "Le type de plage de la vidéo n'est pas pris en charge",
"LabelVppTonemappingContrastHelp": "Appliquer le gain de contraste dans le mappage de tonalité VPP. Les valeurs recommandées et par défaut sont 1.2 et 1.",
"LabelVppTonemappingContrast": "Gain de contraste de mappage de tonalité VPP :",
"LabelVppTonemappingBrightnessHelp": "Appliquer le gain de luminosité dans le mappage de tonalité VPP. Les valeurs recommandées et par défaut sont 0.",
"LabelVppTonemappingBrightness": "Gain de luminosité du mappage VPP :",
"ScreenResolution": "Résolution d'écran",
"RememberSubtitleSelectionsHelp": "Choisir la piste de sous-titres la plus proche de la dernière vidéo.",
"RememberSubtitleSelections": "Définir la piste de sous-titre en fonction de l'élément précédent",
"RememberAudioSelectionsHelp": "Choisir la piste audio la plus proche de la dernière vidéo.",
"RememberAudioSelections": "Définir la piste audio en fonction de l'élément précédent"
}

View File

@ -1653,5 +1653,23 @@
"HomeVideosPhotos": "Otthoni videók és fotók",
"Bold": "Félkövér",
"LabelTextWeight": "Betűvastagság:",
"EnableSplashScreen": "Indítóképernyő engedélyezése"
"EnableSplashScreen": "Indítóképernyő engedélyezése",
"MediaInfoDvLevel": "DV Szint",
"MediaInfoDvProfile": "DV Profil",
"MediaInfoDvVersionMinor": "DV Mellék Verzió",
"MediaInfoDvVersionMajor": "DV Fő Verzió",
"MediaInfoDoViTitle": "DV cím",
"MediaInfoVideoRangeType": "Videó tartomány típusa",
"LabelVideoRangeType": "Videó tartomány típusa:",
"VideoRangeTypeNotSupported": "A videó tartománytípusa nem támogatott",
"LabelVppTonemappingContrastHelp": "Alkalmazzon kontraszterősítést a VPP tónusleképezésben. Az ajánlott és alapértelmezett értékek 1.2 és 1.",
"LabelVppTonemappingContrast": "VPP Tone leképezés kontraszt erőssége:",
"LabelVppTonemappingBrightnessHelp": "Fényerőbeállítása a VPP hangszínleképezésben. Az ajánlott és az alapértelmezett érték is 0.",
"LabelVppTonemappingBrightness": "VPP Tone leképezési fényerőnövekedés:",
"ScreenResolution": "Képernyőfelbontás",
"RememberSubtitleSelectionsHelp": "Próbálja meg úgy beállítani a feliratsávot, hogy a legközelebb legyen az utolsó videóhoz.",
"RememberSubtitleSelections": "Feliratsáv beállítása az előző elem alapján",
"RememberAudioSelectionsHelp": "Próbálja meg úgy beállítani a hangsávot, hogy a legközelebb legyen az utolsó videóhoz.",
"RememberAudioSelections": "Hangsáv beállítása az előző elem alapján",
"LabelMaxVideoResolution": "Maximálisan engedélyezett felbontás, Video Transzkódolás alatt"
}

View File

@ -1651,5 +1651,10 @@
"AllowEmbeddedSubtitlesHelp": "Desabilitar legendas que estiverem empacotadas nos contêineres de mídia. Precisa de uma atualização total das bibliotecas.",
"AllowEmbeddedSubtitles": "Desabilitar diferentes tipos de legendas embutidas",
"Bold": "Negrito",
"LabelTextWeight": "Intensidade do texto:"
"LabelTextWeight": "Intensidade do texto:",
"EnableSplashScreen": "Ativar tela de abertura",
"ScreenResolution": "Resolução de tela",
"RememberAudioSelectionsHelp": "Tentar definir a faixa de áudio com a correspondência mais próxima do último vídeo.",
"RememberAudioSelections": "Definir faixa de áudio baseado no item anterior",
"LabelMaxVideoResolution": "Resolução máxima de transcodificação de vídeo permitida"
}

View File

@ -1653,5 +1653,27 @@
"HomeVideosPhotos": "Домашние видео и фото",
"Bold": "Жирный",
"LabelTextWeight": "Насыщенность шрифта:",
"EnableSplashScreen": "Включить экран-заставку"
"EnableSplashScreen": "Включить экран-заставку",
"MediaInfoDvLevel": "Уровень DV",
"MediaInfoDvProfile": "DV-профиль",
"MediaInfoDvVersionMinor": "Дополнительная версия DV",
"MediaInfoDvVersionMajor": "Основная версия DV",
"MediaInfoDoViTitle": "Название DV",
"MediaInfoVideoRangeType": "Тип диапазона видео",
"LabelVideoRangeType": "Тип диапазона видео:",
"ScreenResolution": "Разрешение экрана",
"RememberAudioSelectionsHelp": "Попытка задать аудиодорожку по ближайшему совпадению с предыдущим видео.",
"RememberSubtitleSelectionsHelp": "Попытка задать дорожку субтитров по ближайшему совпадению с предыдущим видео.",
"RememberSubtitleSelections": "Задать дорожку субтитров на основе предыдущего элемента",
"RememberAudioSelections": "Задать аудиодорожку на основе предыдущего элемента",
"LabelMaxVideoResolution": "Максимально допустимое разрешение перекодирующегося видео",
"LabelVppTonemappingContrastHelp": "Применяется усиление контрастности при VPP-тонмаппинге. Значения рекомендованные и по умолчанию равны 1.2 и 1.",
"MediaInfoDvBlSignalCompatibilityId": "ID совместимости сигнала DV bl",
"MediaInfoBlPresentFlag": "Флаг предустановки DV bl",
"MediaInfoElPresentFlag": "Флаг предустановки DV el",
"MediaInfoRpuPresentFlag": "Флаг предустановки DV rpu",
"VideoRangeTypeNotSupported": "Тип диапазона видео не поддерживается",
"LabelVppTonemappingBrightnessHelp": "Применяется усиление яркости при VPP-тонмаппинге. Значения рекомендованные и по умолчанию равны 0.",
"LabelVppTonemappingContrast": "Усиление контрастности VPP-тонмаппинга:",
"LabelVppTonemappingBrightness": "Усиление яркости VPP-тонмаппинга:"
}

View File

@ -207,7 +207,7 @@
"HeaderAddToPlaylist": "Lägg till i spellista",
"HeaderAddUpdateImage": "Lägg till/uppdatera bild",
"HeaderAdditionalParts": "Ytterligare delar",
"HeaderAlbumArtists": "Albumsartister",
"HeaderAlbumArtists": "Albumartister",
"HeaderAlert": "Varning",
"HeaderAllowMediaDeletionFrom": "Tillåt mediaborttagning från:",
"HeaderApiKey": "API-nyckel",

View File

@ -1644,5 +1644,24 @@
"HomeVideosPhotos": "Videos và Ảnh Gia Đình",
"Bold": "In đậm",
"LabelTextWeight": "Trọng lượng văn bản:",
"EnableSplashScreen": "Bật màn hình giật gân"
"EnableSplashScreen": "Bật màn hình giật gân",
"MediaInfoDvLevel": "Mức DV",
"MediaInfoDvProfile": "Hồ sơ DV",
"MediaInfoDvVersionMinor": "Phiên bản DV nhỏ",
"MediaInfoDvVersionMajor": "Phiên bản DV chính",
"MediaInfoDoViTitle": "Tiêu đề DV",
"MediaInfoVideoRangeType": "Loại phạm vi video",
"LabelVideoRangeType": "Loại phạm vi video:",
"LabelVppTonemappingContrastHelp": "Áp dụng độ tăng tương phản trong ánh xạ tông màu VPP. Giá trị đề xuất và mặc định là 1,2 và 1.",
"LabelVppTonemappingContrast": "Độ tương phản ánh xạ tông màu VPP:",
"LabelVppTonemappingBrightnessHelp": "Áp dụng tăng độ sáng trong ánh xạ tông màu VPP. Giá trị đề xuất và giá trị mặc định đều là 0.",
"LabelVppTonemappingBrightness": "Tăng độ sáng ánh xạ tông màu VPP:",
"ScreenResolution": "Độ Phân Giải Màn Hình",
"RememberSubtitleSelectionsHelp": "Cố gắng đặt phụ đề phù hợp nhất với video cuối cùng.",
"RememberSubtitleSelections": "Đặt phụ đề dựa trên mục trước",
"RememberAudioSelectionsHelp": "Cố gắng đặt bản âm thanh phù hợp nhất với video cuối cùng.",
"RememberAudioSelections": "Đặt bản nhạc dựa trên mục trước đó",
"LabelMaxVideoResolution": "Độ Phân Giải Chuyển Mã Video Tối Đa Được Phép",
"VideoRangeTypeNotSupported": "Loại phạm vi của video không được hỗ trợ",
"Interview": "Phỏng vấn"
}

View File

@ -1653,5 +1653,23 @@
"HomeVideosPhotos": "主页视频及照片",
"Bold": "粗体",
"LabelTextWeight": "文字大小:",
"EnableSplashScreen": "显示启动画面"
"EnableSplashScreen": "显示启动画面",
"MediaInfoDvBlSignalCompatibilityId": "杜比视界 BL 兼容性序号",
"MediaInfoBlPresentFlag": "杜比视界 BL 存在标记",
"MediaInfoElPresentFlag": "杜比视界 EL 存在标记",
"MediaInfoRpuPresentFlag": "杜比视界 RPU 存在标记",
"MediaInfoDvLevel": "杜比视界等级",
"MediaInfoDvProfile": "杜比视界配置",
"MediaInfoDvVersionMinor": "杜比视界次要版本",
"MediaInfoDvVersionMajor": "杜比视界主要版本",
"MediaInfoDoViTitle": "杜比视界标题",
"MediaInfoVideoRangeType": "动态范围类型",
"LabelVideoRangeType": "动态范围类型:",
"VideoRangeTypeNotSupported": "视频动态范围不支持",
"LabelVppTonemappingContrastHelp": "在 VPP 色调映射中应用对比度增益。推荐值和默认值分别为 1.2 和 1。",
"LabelVppTonemappingBrightness": "VPP 色调映射亮度增益:",
"LabelVppTonemappingContrast": "VPP 色调映射对比度增益:",
"LabelVppTonemappingBrightnessHelp": "在 VPP 色调映射中应用亮度增益。推荐值和默认值均为 0。",
"ScreenResolution": "屏幕分辨率",
"LabelMaxVideoResolution": "允许的最大视频转码分辨率"
}

View File

@ -96,7 +96,7 @@ module.exports = {
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|blurhash|date-fns|epubjs|flv.js|libarchive.js|marked|screenfull)/,
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|blurhash|date-fns|epubjs|flv.js|libarchive.js|marked|react-router|screenfull)/,
use: [{
loader: 'babel-loader'
}]