1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge branch 'master' into chapter-markers

This commit is contained in:
Viperinius 2022-09-17 12:04:18 +02:00 committed by GitHub
commit 33fe2c51d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
160 changed files with 23903 additions and 4619 deletions

2
src/apiclient.d.ts vendored
View file

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

View file

@ -175,6 +175,9 @@
flex-direction: column;
contain: layout style paint;
transition: background ease-in-out 0.5s;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
}
.layout-tv .skinHeader {
@ -1146,10 +1149,12 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
.padded-left {
padding-left: 3.3%;
padding-left: max(env(safe-area-inset-left), 3.3%);
}
.padded-right {
padding-right: 3.3%;
padding-right: max(env(safe-area-inset-right), 3.3%);
}
.padded-top {
@ -1173,6 +1178,7 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
@media all and (min-height: 31.25em) {
.padded-right-withalphapicker {
padding-right: 7.5%;
padding-right: max(env(safe-area-inset-left), 7.5%);
}
}

View file

@ -84,6 +84,7 @@ div[data-role="page"] {
.pageWithAbsoluteTabs .pageTabContent {
/* provides room for the music controls */
padding-bottom: 5em !important;
padding-bottom: calc(env(safe-area-inset-bottom) + 5em) !important;
}
.readOnlyContent {

View file

@ -12,8 +12,11 @@
right: 0;
position: fixed;
background: linear-gradient(0deg, rgba(16, 16, 16, 0.75) 0%, rgba(16, 16, 16, 0) 100%);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: 7.5em;
padding-bottom: 1.75em;
padding-bottom: max(env(safe-area-inset-bottom), 1.75em);
display: flex;
flex-direction: row;
justify-content: center;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import alert from './alert';
import { appRouter } from './appRouter';
@ -33,7 +33,6 @@ type ConnectionRequiredProps = {
* If a condition fails, this component will navigate to the appropriate page.
*/
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
children,
isAdminRequired = false,
isUserRequired = true
}) => {
@ -147,12 +146,14 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
setIsLoading(false);
};
loading.show();
validateConnection();
}, [ isAdminRequired, isUserRequired, navigate ]);
// Show/hide the loading indicator
useEffect(() => {
if (!isLoading) {
if (isLoading) {
loading.show();
} else {
loading.hide();
}
}, [ isLoading ]);
@ -162,7 +163,9 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
}
return (
<>{children}</>
<div className='skinBody'>
<Outlet />
</div>
);
};

View file

@ -6,8 +6,10 @@ type PageProps = {
id: string, // id is required for libraryMenu
title?: string,
isBackButtonEnabled?: boolean,
isMenuButtonEnabled?: boolean,
isNowPlayingBarEnabled?: boolean,
isThemeMediaSupported?: boolean
isThemeMediaSupported?: boolean,
backDropType?: string
};
/**
@ -20,8 +22,10 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
className = '',
title,
isBackButtonEnabled = true,
isMenuButtonEnabled = false,
isNowPlayingBarEnabled = true,
isThemeMediaSupported = false
isThemeMediaSupported = false,
backDropType
}) => {
const element = useRef<HTMLDivElement>(null);
@ -59,7 +63,9 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
data-role='page'
className={`page ${className}`}
data-title={title}
data-backbutton={`${isBackButtonEnabled}`}
data-backbutton={isBackButtonEnabled}
data-menubutton={isMenuButtonEnabled}
data-backdroptype={backDropType}
>
{children}
</div>

View file

@ -301,7 +301,7 @@ export function show(options) {
resolve(selectedId);
} else {
reject();
reject('ActionSheet closed without resolving');
}
}
});

View file

@ -12,6 +12,7 @@
.alphaPicker-fixed {
position: fixed;
bottom: 5.5em;
bottom: max(env(safe-area-inset-bottom), 5.5em);
}
.alphaPickerRow {
@ -45,6 +46,7 @@
@media all and (max-height: 50em) {
.alphaPicker-fixed {
bottom: 5em;
bottom: max(env(safe-area-inset-bottom), 5em);
}
.alphaPickerButton-vertical {
@ -104,15 +106,18 @@
.alphaPicker-fixed.alphaPicker-tv {
bottom: 1%;
bottom: max(env(safe-area-inset-bottom), 1%);
}
.alphaPicker-fixed-right {
right: 0.4em;
right: max(env(safe-area-inset-right), 0.4em);
}
@media all and (min-width: 62.5em) {
.alphaPicker-fixed-right {
right: 1em;
right: max(env(safe-area-inset-right), 1em);
}
}

View file

@ -6,6 +6,13 @@
bottom: 0;
transition: transform 180ms linear;
contain: layout style;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.appfooter:empty {
padding: 0;
}
.appfooter.headroom--unpinned {

View file

@ -9,7 +9,6 @@ import loading from './loading/loading';
import viewManager from './viewManager/viewManager';
import ServerConnections from './ServerConnections';
import alert from './alert';
import reactControllerFactory from './reactControllerFactory';
export const history = createHashHistory();
@ -264,9 +263,7 @@ class AppRouter {
this.#sendRouteToViewManager(ctx, next, route, controllerFactory);
};
if (route.pageComponent) {
onInitComplete(reactControllerFactory);
} else if (route.controller) {
if (route.controller) {
import('../controllers/' + route.controller).then(onInitComplete);
} else {
onInitComplete();
@ -293,7 +290,6 @@ class AppRouter {
fullscreen: route.fullscreen,
controllerFactory: controllerFactory,
options: {
pageComponent: route.pageComponent,
supportsThemeMedia: route.supportsThemeMedia || false,
enableMediaControl: route.enableMediaControl !== false
},

View file

@ -821,7 +821,7 @@ import { appRouter } from '../appRouter';
if (isUsingLiveTvNaming(item)) {
lines.push(escapeHtml(item.Name));
if (!item.EpisodeTitle) {
if (!item.EpisodeTitle && !item.IndexNumber) {
titleAdded = true;
}
} else {
@ -1349,7 +1349,7 @@ import { appRouter } from '../appRouter';
cardImageContainerClose = '</div>';
} else {
const cardImageContainerAriaLabelAttribute = ` aria-label="${item.Name}"`;
const cardImageContainerAriaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
const url = appRouter.getRouteUrl(item);
// Don't use the IMG tag with safari because it puts a white border around it
@ -1433,7 +1433,7 @@ import { appRouter } from '../appRouter';
if (tagName === 'button') {
className += ' itemAction';
actionAttribute = ' data-action="' + action + '"';
ariaLabelAttribute = ` aria-label="${item.Name}"`;
ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
} else {
actionAttribute = '';
}

View file

@ -1,6 +1,6 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import CheckBoxElement from './CheckBoxElement';
import CheckBoxElement from '../../../elements/CheckBoxElement';
type IProps = {
containerClassName?: string;
@ -18,7 +18,11 @@ const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerT
return (
<div className={containerClassName}>
<h2>{globalize.translate(headerTitle)}</h2>
<CheckBoxElement labelClassName='checkboxContainer' type='checkbox' className={checkBoxClassName} title={checkBoxTitle} />
<CheckBoxElement
labelClassName='checkboxContainer'
className={checkBoxClassName}
title={checkBoxTitle}
/>
<div className={listContainerClassName}>
<div className={accessClassName}>
<h3 className='checkboxListLabel'>

View file

@ -1,21 +1,11 @@
import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize';
import IconButtonElement from '../../../elements/IconButtonElement';
const createButtonElement = (index: number) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='btnDelete listItemButton'
data-index='${index}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = {
type AccessScheduleListProps = {
index: number;
Id: number;
Id?: number;
DayOfWeek?: string;
StartHour?: number ;
EndHour?: number;
@ -32,7 +22,7 @@ function getDisplayTime(hours = 0) {
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
return (
<div
className='liSchedule listItem'
@ -48,8 +38,12 @@ const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartH
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
</div>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(index)}
<IconButtonElement
is='paper-icon-button-light'
className='btnDelete listItemButton'
title='Delete'
icon='delete'
dataIndex={index}
/>
</div>
);

View file

@ -1,15 +1,5 @@
import React, { FunctionComponent } from 'react';
const createButtonElement = (tag?: string) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='blockedTag btnDeleteTag listItemButton'
data-tag='${tag}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
import IconButtonElement from '../../../elements/IconButtonElement';
type IProps = {
tag?: string;
@ -24,11 +14,14 @@ const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
{tag}
</h3>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(tag)}
<IconButtonElement
is='paper-icon-button-light'
className='blockedTag btnDeleteTag listItemButton'
title='Delete'
icon='delete'
dataTag={tag}
/>
</div>
</div>
);
};

View file

@ -1,32 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button
is="emby-button"
type="${type}"
class="${className}"
>
<span>${title}</span>
</button>`
});
type IProps = {
type?: string;
className?: string;
title?: string
}
const ButtonElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default ButtonElement;

View file

@ -1,36 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
__html: `<label class="${labelClassName}">
<input
is="emby-checkbox"
type="${type}"
class="${className}"
/>
<span>${title}</span>
</label>`
});
type IProps = {
labelClassName?: string;
type?: string;
className?: string;
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, type, className, title }: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? labelClassName : '',
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default CheckBoxElement;

View file

@ -1,41 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
type IProps = {
className?: string;
Name?: string;
Id?: string;
ItemType?: string;
AppName?: string;
checkedAttribute?: string;
}
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label>
<input
type="checkbox"
is="emby-checkbox"
class="${className}"
${dataAttributes} ${checkedAttribute}
/>
<span>${escapeHtml(Name || '')} ${AppName}</span>
</label>`
});
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
className: className,
Name: Name,
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
AppName: AppName ? `- ${AppName}` : '',
checkedAttribute: checkedAttribute
})}
/>
);
};
export default CheckBoxListItem;

View file

@ -1,34 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
type IProps = {
title: string;
className?: string;
icon: string,
}
const createButtonElement = ({ className, title, icon }: { className?: string, title: string, icon: string }) => ({
__html: `<button
is="emby-button"
type="button"
class="${className}"
style="margin-left:1em;"
title="${title}"
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
className: className,
title: globalize.translate(title),
icon: icon
})}
/>
);
};
export default SectionTitleButtonElement;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import SectionTitleButtonElement from './SectionTitleButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
title: string;
isBtnVisible?: boolean;
titleLink?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({title, isBtnVisible = false, titleLink}: IProps) => {
return (
<div className='verticalSection'>
<div className='sectionTitleContainer flex align-items-center'>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <SectionTitleButtonElement
className='fab btnAddUser submit sectionTitleButton'
title='ButtonAddUser'
icon='add'
/>}
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={titleLink}
/>
</div>
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,44 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
${option}
</select>`
});
type ProvidersArr = {
Name?: string;
Id?: string;
}
type IProps = {
className?: string;
label?: string;
currentProviderId: string;
providers: ProvidersArr[]
}
const SelectElement: FunctionComponent<IProps> = ({ className, label, currentProviderId, providers }: IProps) => {
const renderOption = providers.map((provider) => {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + escapeHtml(provider.Name) + '</option>';
});
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption
})}
/>
);
};
export default SelectElement;

View file

@ -1,47 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
<option value=''></option>
${option}
</select>`
});
type RatingsArr = {
Name: string;
Value: number;
}
type IProps = {
className?: string;
label?: string;
parentalRatings: RatingsArr[];
}
const SelectMaxParentalRating: FunctionComponent<IProps> = ({ className, label, parentalRatings }: IProps) => {
const renderOption = () => {
let content = '';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHtml(rating.Name)}</option>`;
}
return content;
};
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption()
})}
/>
);
};
export default SelectMaxParentalRating;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
id="${id}"
label="${label}"
>
<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>
<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>
<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>
</select>`
});
type IProps = {
className?: string;
id?: string;
label?: string
}
const SelectSyncPlayAccessElement: FunctionComponent<IProps> = ({ className, id, label }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
id: id,
label: globalize.translate(label)
})}
/>
);
};
export default SelectSyncPlayAccessElement;

View file

@ -1,9 +1,11 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { localeWithSuffix } from '../../../scripts/dfnshelper';
import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder';
import IconButtonElement from '../../../elements/IconButtonElement';
import escapeHTML from 'escape-html';
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a
@ -15,16 +17,6 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
</a>`
});
const createButtonElement = () => ({
__html: `<button
is="paper-icon-button-light"
type="button"
class="btnUserMenu flex-shrink-zero"
>
<span class="material-icons more_vert" aria-hidden="true"></span>
</button>`
});
type IProps = {
user?: UserDto;
}
@ -81,16 +73,20 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
/>
</div>
<div className='cardFooter visualCardBox-cardFooter'>
<div className='cardText flex align-items-center'>
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>
{user.Name}
</div>
<div
dangerouslySetInnerHTML={createButtonElement()}
<div
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
>
<IconButtonElement
is='paper-icon-button-light'
className='btnUserMenu flex-shrink-zero'
icon='more_vert'
/>
</div>
<div className='cardText'>
<span>{escapeHTML(user.Name)}</span>
</div>
<div className='cardText cardText-secondary'>
{lastSeen != '' ? lastSeen : ''}
<span>{lastSeen != '' ? lastSeen : ''}</span>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import Dashboard from '../../../utils/dashboard';
import globalize from '../../../scripts/globalize';
@ -6,9 +6,9 @@ import LibraryMenu from '../../../scripts/libraryMenu';
import confirm from '../../confirm/confirm';
import loading from '../../loading/loading';
import toast from '../../toast/toast';
import ButtonElement from './ButtonElement';
import CheckBoxElement from './CheckBoxElement';
import InputElement from './InputElement';
import ButtonElement from '../../../elements/ButtonElement';
import CheckBoxElement from '../../../elements/CheckBoxElement';
import InputElement from '../../../elements/InputElement';
type IProps = {
userId: string;
@ -40,11 +40,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
let showLocalAccessSection = false;
if (user.HasConfiguredPassword) {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true;
} else {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
}
@ -65,11 +65,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
@ -206,8 +206,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
(page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.localAccessForm') as HTMLFormElement).addEventListener('submit', onLocalAccessSubmit);
(page.querySelector('.btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
(page.querySelector('.btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
(page.querySelector('#btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
(page.querySelector('#btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
}, [loadUser, userId]);
return (
@ -250,7 +250,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/>
<ButtonElement
type='button'
className='raised btnResetPassword button-cancel block hide'
id='btnResetPassword'
className='raised button-cancel block hide'
title='ResetPassword'
/>
</div>
@ -281,7 +282,6 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<br />
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
type='checkbox'
className='chkEnableLocalEasyPassword'
title='LabelInNetworkSignInWithEasyPassword'
/>
@ -297,7 +297,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/>
<ButtonElement
type='button'
className='raised btnResetEasyPassword button-cancel block hide'
id='btnResetEasyPassword'
className='raised button-cancel block hide'
title='ButtonResetEasyPassword'
/>
</div>

View file

@ -75,8 +75,7 @@
*/
function paramsToString(params) {
return Object.entries(params)
// eslint-disable-next-line no-unused-vars
.filter(([_, v]) => v !== null && v !== undefined && v !== '')
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}

View file

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

View file

@ -14,7 +14,13 @@
top: 0;
left: 0;
right: 0;
padding: 1em 0.5em;
padding-left: 0.5em;
padding-left: max(env(safe-area-inset-left), 0.5em);
padding-right: 0.5em;
padding-right: max(env(safe-area-inset-right), 0.5em);
padding-top: 1em;
padding-top: max(env(safe-area-inset-top), 1em);
padding-bottom: 1em;
display: flex;
align-items: center;
z-index: 99999;

View file

@ -238,6 +238,7 @@ function showWithUser(options, player, user) {
return actionsheet.show({
items: menuItems,
resolveOnClick: true,
positionTo: options.positionTo
}).then(function (id) {
return handleSelectedOption(id, options, player);

View file

@ -34,7 +34,7 @@ class PluginManager {
// translations won't be loaded for skins until needed
return plugin;
} else {
return await this.#loadStrings(plugin);
return this.#loadStrings(plugin);
}
}

View file

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
export default (view, params, { detail }) => {
if (detail.options?.pageComponent) {
// Fetch and render the page component to the view
import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`)
.then(({ default: component }) => {
ReactDOM.render(React.createElement(component, params), view);
});
// Unmount component when view is destroyed
view.addEventListener('viewdestroy', () => {
ReactDOM.unmountComponentAtNode(view);
});
}
};

View file

@ -139,7 +139,7 @@ function updateNowPlayingInfo(context, state, serverId) {
const displayName = item ? getNowPlayingNameHtml(item).replace('<br/>', ' - ') : '';
if (item) {
const nowPlayingServerId = (item.ServerId || serverId);
if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
if (item.Type == 'AudioBook' || item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') {
let artistsSeries = '';
let albumName = '';
if (item.Artists != null) {

View file

@ -195,7 +195,10 @@
height: 4.2em;
right: 0;
padding-left: 7.3%;
padding-left: max(env(safe-area-inset-left), 7.3%);
padding-right: 7.3%;
padding-right: max(env(safe-area-inset-right), 7.3%);
padding-bottom: env(safe-area-inset-bottom);
}
.layout-desktop .playlistSectionButton,

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
@ -33,6 +33,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -99,6 +100,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
setAudioBooks([]);
setBooks([]);
setPeople([]);
setCollections([]);
if (query) {
const apiClient = ServerConnections.getApiClient(serverId);
@ -166,6 +168,9 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items || []));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items || []));
}
}
}, [collectionType, parentId, query, serverId]);
@ -257,6 +262,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
title={globalize.translate('Books')}
items={books}
/>
<SearchResultsRow
title={globalize.translate('Collections')}
items={collections}
/>
<SearchResultsRow
title={globalize.translate('People')}
items={people}

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react';
import cardBuilder from '../cardbuilder/cardBuilder';

View file

@ -1,4 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import escapeHtml from 'escape-html';
import React, { FunctionComponent, useEffect, useState } from 'react';

View file

@ -2,7 +2,7 @@
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${Subtitles}</h3>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://docs.jellyfin.org/general/server/media/subtitles.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://jellyfin.org/docs/general/server/media/external-files.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
</div>
<div class="formDialogContent smoothScrollY">
<div class="dialogContentInner dialog-content-centered">

View file

@ -1,114 +0,0 @@
import { clearBackdrop } from '../backdrop/backdrop';
import * as mainTabsManager from '../maintabsmanager';
import layoutManager from '../layoutManager';
import '../../elements/emby-tabs/emby-tabs';
import LibraryMenu from '../../scripts/libraryMenu';
function onViewDestroy() {
const tabControllers = this.tabControllers;
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
this.tabControllers = null;
}
this.view = null;
this.params = null;
this.currentTabController = null;
this.initialTabIndex = null;
}
class TabbedView {
constructor(view, params) {
this.tabControllers = [];
this.view = view;
this.params = params;
const self = this;
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId));
this.initialTabIndex = currentTabIndex;
function validateTabLoad(index) {
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
}
function loadTab(index, previousIndex) {
validateTabLoad(index).then(function () {
self.getTabController(index).then(function (controller) {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex = index;
self.currentTabController = controller;
});
});
}
function getTabContainers() {
return view.querySelectorAll('.tabContent');
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}
view.addEventListener('viewbeforehide', this.onPause.bind(this));
view.addEventListener('viewbeforeshow', function () {
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
});
view.addEventListener('viewshow', function (e) {
self.onResume(e.detail);
});
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
}
onResume() {
this.setTitle();
clearBackdrop();
const currentTabController = this.currentTabController;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(this.initialTabIndex);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
}
onPause() {
const currentTabController = this.currentTabController;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
}
setTitle() {
LibraryMenu.setTitle('');
}
}
export default TabbedView;

View file

@ -4,7 +4,12 @@
bottom: 0;
pointer-events: none;
z-index: 9999999;
padding: 1em;
padding-left: 1em;
padding-left: max(env(safe-area-inset-left), 1em);
padding-right: 1em;
padding-top: 1em;
padding-bottom: 1em;
padding-bottom: max(env(safe-area-inset-bottom), 1em);
display: flex;
flex-direction: column;
}

View file

@ -21,9 +21,9 @@ viewContainer.setOnBeforeChange(function (newView, isRestored, options) {
newView.initComplete = true;
if (typeof options.controllerFactory === 'function') {
new options.controllerFactory(newView, eventDetail.detail.params, eventDetail);
new options.controllerFactory(newView, eventDetail.detail.params);
} else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') {
new options.controllerFactory.default(newView, eventDetail.detail.params, eventDetail);
new options.controllerFactory.default(newView, eventDetail.detail.params);
}
if (!options.controllerFactory || dispatchPageEvents) {

View file

@ -15,7 +15,6 @@ import alert from '../../components/alert';
page.querySelector('#txtServerName').value = systemInfo.ServerName;
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
page.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
@ -108,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;
});
});
}

View file

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

View file

@ -1,3 +0,0 @@
<div id="editUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="newUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userParentalControlPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
</div>

View file

@ -1,3 +0,0 @@
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
</div>

View file

@ -1,9 +0,0 @@
<div id="indexPage" style="outline: none;" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book">
<div class="tabContent pageTabContent" id="homeTab" data-index="0">
<div class="sections"></div>
</div>
<div class="tabContent pageTabContent" id="favoritesTab" data-index="1">
<div class="sections"></div>
</div>
</div>

View file

@ -1,69 +0,0 @@
import TabbedView from '../components/tabbedview/tabbedview';
import globalize from '../scripts/globalize';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller';
import LibraryMenu from '../scripts/libraryMenu';
class HomeView extends TabbedView {
constructor(view, params) {
super(view, params);
}
setTitle() {
LibraryMenu.setTitle(null);
}
onPause() {
super.onPause(this);
document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader');
}
onResume(options) {
super.onResume(this, options);
document.querySelector('.skinHeader').classList.add('noHomeButtonHeader');
}
getDefaultTabIndex() {
return 0;
}
getTabs() {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
}
getTabController(index) {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
const instance = this;
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = instance.tabControllers[index];
if (!controller) {
controller = new controllerFactory(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params);
instance.tabControllers[index] = controller;
}
return controller;
});
}
}
export default HomeView;

View file

@ -1985,7 +1985,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(/^.*[\\/]/, '')
}]);
}

View file

@ -62,6 +62,14 @@
<div class="fieldDescription checkboxFieldDescription">${EnableStreamLoopingHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldIgnoreDts hide">
<label>
<input type="checkbox" is="emby-checkbox" class="chkIgnoreDts" checked />
<span>${IgnoreDts}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${IgnoreDtsHelp}</div>
</div>
<p class="drmMessage hide">${DrmChannelsNotImported}</p>
<br />
<input type="hidden" class="fldDeviceId" />

View file

@ -61,6 +61,7 @@ function fillTunerHostInfo(view, info) {
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
}
@ -75,7 +76,8 @@ function submitForm(page) {
TunerCount: page.querySelector('.txtTunerCount').value || 0,
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
IgnoreDts: page.querySelector('.chkIgnoreDts').checked
};
if (isM3uVariant(info.Type)) {
@ -120,6 +122,7 @@ function onTypeChange() {
const supportsTunerIpAddress = value === 'hdhomerun';
const supportsTunerFileOrUrl = value === 'm3u';
const supportsStreamLooping = value === 'm3u';
const supportsIgnoreDts = value === 'm3u';
const supportsTunerCount = value === 'm3u';
const supportsUserAgent = value === 'm3u';
const suppportsSubmit = value !== 'other';
@ -168,6 +171,12 @@ function onTypeChange() {
view.querySelector('.fldStreamLoop').classList.add('hide');
}
if (supportsIgnoreDts) {
view.querySelector('.fldIgnoreDts').classList.remove('hide');
} else {
view.querySelector('.fldIgnoreDts').classList.add('hide');
}
if (supportsTunerCount) {
view.querySelector('.fldTunerCount').classList.remove('hide');
view.querySelector('.txtTunerCount').setAttribute('required', 'required');

View file

@ -92,6 +92,7 @@
<div class="pageTabContent" id="songsTab" data-index="5">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnShuffle autoSize" title="${Shuffle}"><span class="material-icons shuffle" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
</div>

View file

@ -8,197 +8,207 @@ import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import Dashboard from '../../utils/dashboard';
import {playbackManager} from '../../components/playback/playbackmanager';
/* eslint-disable indent */
export default function (view, params, tabContent) {
function getPageData(context) {
const key = getSavedQueryKey(context);
let pageData = data[key];
export default function (view, params, tabContent) {
function getPageData(context) {
const key = getSavedQueryKey(context);
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: true,
Fields: 'AudioInfo,ParentId',
StartIndex: 0,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary'
}
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'Album,SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Audio',
Recursive: true,
Fields: 'AudioInfo,ParentId',
StartIndex: 0,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary'
}
};
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
return pageData;
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
function getQuery(context) {
return getPageData(context).query;
}
function getSavedQueryKey(context) {
if (!context.savedQueryKey) {
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
}
return context.savedQueryKey;
}
function reloadItems(page) {
loading.show();
isLoading = true;
const query = getQuery(page);
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems(tabContent);
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems(tabContent);
}
window.scrollTo(0, 0);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const html = listView.getListViewHtml({
items: result.Items,
action: 'playallfromhere',
smallIcon: true,
artist: true,
addToListButton: true
});
let elems = tabContent.querySelectorAll('.paging');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onPreviousPageClick);
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
autoFocuser.autoFocus(page);
});
});
}
const self = this;
const data = {};
let isLoading = false;
self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: 'songs',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
});
filterDialog.show();
});
};
self.getCurrentViewStyle = function () {
return getPageData(tabContent).view;
};
function initPage(tabContent) {
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
self.showFilterMenu();
});
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('OptionTrackName'),
id: 'Name'
}, {
name: globalize.translate('Album'),
id: 'Album,SortName'
}, {
name: globalize.translate('AlbumArtist'),
id: 'AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Artist'),
id: 'Artist,Album,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,AlbumArtist,Album,SortName'
}],
callback: function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
},
query: getQuery(tabContent),
button: e.target
});
});
}
initPage(tabContent);
self.renderTab = function () {
reloadItems(tabContent);
};
return pageData;
}
/* eslint-enable indent */
function getQuery(context) {
return getPageData(context).query;
}
function getSavedQueryKey(context) {
if (!context.savedQueryKey) {
context.savedQueryKey = libraryBrowser.getSavedQueryKey('songs');
}
return context.savedQueryKey;
}
function reloadItems(page) {
loading.show();
isLoading = true;
const query = getQuery(page);
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems(tabContent);
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems(tabContent);
}
window.scrollTo(0, 0);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const html = listView.getListViewHtml({
items: result.Items,
action: 'playallfromhere',
smallIcon: true,
artist: true,
addToListButton: true
});
let elems = tabContent.querySelectorAll('.paging');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (let i = 0, length = elems.length; i < length; i++) {
elems[i].addEventListener('click', onPreviousPageClick);
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => {
autoFocuser.autoFocus(page);
});
});
}
const self = this;
const data = {};
let isLoading = false;
self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => {
const filterDialog = new filterDialogFactory({
query: getQuery(tabContent),
mode: 'songs',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
});
filterDialog.show();
});
};
function shuffle() {
ApiClient.getItem(ApiClient.getCurrentUserId(), params.topParentId).then(function (item) {
playbackManager.shuffle(item);
});
}
self.getCurrentViewStyle = function () {
return getPageData(tabContent).view;
};
function initPage(tabContent) {
tabContent.querySelector('.btnFilter').addEventListener('click', function () {
self.showFilterMenu();
});
tabContent.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('OptionTrackName'),
id: 'Name'
}, {
name: globalize.translate('Album'),
id: 'Album,SortName'
}, {
name: globalize.translate('AlbumArtist'),
id: 'AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Artist'),
id: 'Artist,Album,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,AlbumArtist,Album,SortName'
}, {
name: globalize.translate('OptionRandom'),
id: 'Random,SortName'
}],
callback: function () {
getQuery(tabContent).StartIndex = 0;
reloadItems(tabContent);
},
query: getQuery(tabContent),
button: e.target
});
});
tabContent.querySelector('.btnShuffle').addEventListener('click', shuffle);
}
initPage(tabContent);
self.renderTab = function () {
reloadItems(tabContent);
};
}

View file

@ -3,7 +3,7 @@
<div class="readOnlyContent" style="margin: 0 auto;">
<div class="verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkMyProfile listItem-border">
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkUserProfile listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span>
<div class="listItemBody">

View file

@ -26,7 +26,7 @@ export default function (view, params) {
const userId = params.userId || Dashboard.getCurrentUserId();
const page = this;
page.querySelector('.lnkMyProfile').setAttribute('href', '#/myprofile.html?userId=' + userId);
page.querySelector('.lnkUserProfile').setAttribute('href', '#/userprofile.html?userId=' + userId);
page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay.html?userId=' + userId);
page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId);
page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId);

View file

@ -1,3 +0,0 @@
<div id="userProfilePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
</div>

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createButtonElement = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => ({
__html: `<button
is="emby-button"
type="${type}"
${id}
class="${className}"
>
${leftIcon}
<span>${title}</span>
${rightIcon}
</button>`
});
type IProps = {
type?: string;
id?: string;
className?: string;
title?: string;
leftIcon?: string;
rightIcon?: string;
}
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
id: id ? `id="${id}"` : '',
className: className,
title: globalize.translate(title),
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
})}
/>
);
};
export default ButtonElement;

View file

@ -0,0 +1,57 @@
import escapeHTML from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, className, id, dataFilter, dataItemType, dataId, checkedAttribute, renderContent }: { labelClassName?: string, type?: string, className?: string, id?: string, dataFilter?: string, dataItemType?: string, dataId?: string, checkedAttribute?: string, renderContent?: string }) => ({
__html: `<label ${labelClassName}>
<input
is="emby-checkbox"
type="checkbox"
class="${className}"
${id}
${dataFilter}
${dataItemType}
${dataId}
${checkedAttribute}
/>
${renderContent}
</label>`
});
type IProps = {
labelClassName?: string;
className?: string;
elementId?: string;
dataFilter?: string;
itemType?: string;
itemId?: string;
itemAppName?: string;
itemCheckedAttribute?: string;
itemName?: string
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, className, elementId, dataFilter, itemType, itemId, itemAppName, itemCheckedAttribute, itemName, title }: IProps) => {
const appName = itemAppName ? `- ${itemAppName}` : '';
const renderContent = itemName ?
`<span>${escapeHTML(itemName || '')} ${appName}</span>` :
`<span>${globalize.translate(title)}</span>`;
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? `class='${labelClassName}'` : '',
className: className,
id: elementId ? `id='${elementId}'` : '',
dataFilter: dataFilter ? `data-filter='${dataFilter}'` : '',
dataItemType: itemType ? `data-itemtype='${itemType}'` : '',
dataId: itemId ? `data-id='${itemId}'` : '',
checkedAttribute: itemCheckedAttribute ? itemCheckedAttribute : '',
renderContent: renderContent
})}
/>
);
};
export default CheckBoxElement;

View file

@ -0,0 +1,47 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
type IProps = {
is?: string;
id?: string;
title?: string;
className?: string;
icon?: string,
dataIndex?: string | number;
dataTag?: string | number;
dataProfileid?: string | number;
}
const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({
__html: `<button
is="${is}"
type="button"
${id}
class="${className}"
${title}
${dataIndex}
${dataTag}
${dataProfileid}
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createIconButtonElement({
is: is,
id: id ? `id="${id}"` : '',
className: className,
title: title ? `title="${globalize.translate(title)}"` : '',
icon: icon,
dataIndex: dataIndex ? `data-index="${dataIndex}"` : '',
dataTag: dataTag ? `data-tag="${dataTag}"` : '',
dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : ''
})}
/>
);
};
export default IconButtonElement;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import globalize from '../scripts/globalize';
const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({
__html: `<input

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import IconButtonElement from './IconButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
SectionClassName?: string;
title?: string;
isBtnVisible?: boolean;
btnId?: string;
btnClassName?: string;
btnTitle?: string;
btnIcon?: string;
isLinkVisible?: boolean;
url?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({SectionClassName, title, isBtnVisible = false, btnId, btnClassName, btnTitle, btnIcon, isLinkVisible = true, url}: IProps) => {
return (
<div className={`${SectionClassName} sectionTitleContainer flex align-items-center`}>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <IconButtonElement
is='emby-button'
id={btnId}
className={btnClassName}
title={btnTitle}
icon={btnIcon}
/>}
{isLinkVisible && <SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={url}
/>}
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
import globalize from '../scripts/globalize';
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a

View file

@ -0,0 +1,38 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createSelectElement = ({ name, id, required, label, option }: { name?: string, id?: string, required?: string, label?: string, option?: React.ReactNode }) => ({
__html: `<select
is="emby-select"
${name}
id="${id}"
${required}
label="${label}"
>
${option}
</select>`
});
type IProps = {
name?: string;
id?: string;
required?: string;
label?: string;
children?: React.ReactNode
}
const SelectElement: FunctionComponent<IProps> = ({ name, id, required, label, children }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
name: name ? `name='${name}'` : '',
id: id,
required: required ? `required='${required}'` : '',
label: globalize.translate(label),
option: children
})}
/>
);
};
export default SelectElement;

View file

@ -45,6 +45,9 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype);
if (scrollWidth <= scrollSize + 20) {
scrollButtons.scrollButtonsLeft.classList.add('hide');
scrollButtons.scrollButtonsRight.classList.add('hide');
} else {
scrollButtons.scrollButtonsLeft.classList.remove('hide');
scrollButtons.scrollButtonsRight.classList.remove('hide');
}
if (scrollPos > 0) {

View file

@ -4,7 +4,9 @@
.emby-scroller {
margin-left: 3.3%;
margin-left: max(env(safe-area-inset-left), 3.3%);
margin-right: 3.3%;
margin-right: max(env(safe-area-inset-right), 3.3%);
}
/* align first card in scroller to heading */
@ -21,7 +23,9 @@
.layout-tv .emby-scroller,
.layout-mobile .emby-scroller {
padding-left: 3.3%;
padding-left: max(env(safe-area-inset-left), 3.3%);
padding-right: 3.3%;
padding-right: max(env(safe-area-inset-right), 3.3%);
margin-left: 0;
margin-right: 0;
}

View file

@ -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;
}
@ -102,6 +116,7 @@
bottom: 0;
z-index: 1;
width: 0.8em;
padding-left: env(safe-area-inset-left);
}
@-webkit-keyframes fadein {

View file

@ -4,6 +4,8 @@
top: 0;
bottom: 0;
contain: strict;
box-sizing: border-box;
padding-left: env(safe-area-inset-left);
}
.touch-menu-la {

View file

@ -42,6 +42,12 @@
#dialogToc {
background-color: white;
height: fit-content;
width: fit-content;
max-height: 80%;
max-width: 60%;
padding-right: 50px;
padding-bottom: 15px;
.bookplayerButtonIcon {
color: black;
@ -49,5 +55,19 @@
.toc li {
margin-bottom: 5px;
list-style-type: none;
font-size: 120%;
a:link {
color: #000;
text-decoration: none;
}
a:active,
a:hover {
color: #00a4dc;
text-decoration: none;
}
}
}

View file

@ -12,5 +12,6 @@
.swiper-slide-img {
max-height: 100%;
max-width: 100%;
}
}

View file

@ -1150,8 +1150,7 @@ function tryRemoveElement(elem) {
return true;
}
// This is unfortunate, but we're unable to remove the textTrack that gets added via addTextTrack
if (browser.firefox || browser.web0s) {
if (browser.web0s) {
return true;
}

View file

@ -7,6 +7,10 @@
display: flex;
align-items: center;
background: #000 !important;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.videoPlayerContainer-onTop {
@ -58,6 +62,9 @@ video[controls]::-webkit-media-controls {
right: 0;
color: #fff;
font-size: 170%;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.videoSubtitlesInner {

View file

@ -7,7 +7,6 @@ import { appRouter } from '../../components/appRouter';
import './style.scss';
import '../../elements/emby-button/paper-icon-button-light';
import { Events } from 'jellyfin-apiclient';
import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
export class PdfPlayer {
constructor() {
@ -200,14 +199,14 @@ export class PdfPlayer {
const serverId = item.ServerId;
const apiClient = ServerConnections.getApiClient(serverId);
return new Promise((resolve) => {
return import('pdfjs-dist').then(({ GlobalWorkerOptions, getDocument }) => {
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
this.bindEvents();
GlobalWorkerOptions.workerSrc = appRouter.baseUrl() + '/libraries/pdf.worker.js';
const downloadTask = getDocument(downloadHref);
downloadTask.promise.then(book => {
return downloadTask.promise.then(book => {
if (this.cancellationToken) return;
this.book = book;
this.loaded = true;
@ -219,8 +218,6 @@ export class PdfPlayer {
} else {
this.loadPage(1);
}
return resolve();
});
});
}

View file

@ -7,6 +7,10 @@
right: 0;
display: flex;
align-items: center;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.youtubePlayerContainer.onTop {

177
src/routes/home.tsx Normal file
View file

@ -0,0 +1,177 @@
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
import globalize from '../scripts/globalize';
import LibraryMenu from '../scripts/libraryMenu';
import { clearBackdrop } from '../components/backdrop/backdrop';
import layoutManager from '../components/layoutManager';
import * as mainTabsManager from '../components/maintabsmanager';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller';
import Page from '../components/Page';
type IProps = {
tab?: string;
}
type OnResumeOptions = {
autoFocus?: boolean;
refresh?: boolean
}
type ControllerProps = {
onResume: (
options: OnResumeOptions
) => void;
refreshed: boolean;
onPause: () => void;
destroy: () => void;
}
const Home: FunctionComponent<IProps> = (props: IProps) => {
const getDefaultTabIndex = () => {
return 0;
};
const tabController = useRef<ControllerProps | null>();
const currentTabIndex = useRef(parseInt(props.tab || getDefaultTabIndex().toString()));
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
const initialTabIndex = useRef<number | null>(currentTabIndex.current);
const element = useRef<HTMLDivElement>(null);
const setTitle = () => {
LibraryMenu.setTitle(null);
};
const getTabs = () => {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
};
const getTabContainers = () => {
return element.current?.querySelectorAll('.tabContent');
};
const getTabController = useCallback((index: number) => {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = tabControllers[index];
if (!controller) {
const tabContent = element.current?.querySelector(".tabContent[data-index='" + index + "']");
controller = new controllerFactory(tabContent, props);
tabControllers[index] = controller;
}
return controller;
});
}, [props, tabControllers]);
const onViewDestroy = useCallback(() => {
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
}
tabController.current = null;
initialTabIndex.current = null;
}, [tabControllers]);
const loadTab = useCallback((index: number, previousIndex: number | null) => {
getTabController(index).then((controller) => {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex.current = index;
tabController.current = controller;
});
}, [getTabController]);
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; previousIndex: number | null }; }) => {
const newIndex = parseInt(e.detail.selectedTabIndex);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}, [loadTab, tabControllers]);
const onResume = useCallback(() => {
setTitle();
clearBackdrop();
const currentTabController = tabController.current;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(initialTabIndex.current);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
}, []);
const onPause = useCallback(() => {
const currentTabController = tabController.current;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.remove('noHomeButtonHeader');
}, []);
useEffect(() => {
mainTabsManager.setTabs(element.current, currentTabIndex.current, getTabs, getTabContainers, null, onTabChange, false);
onResume();
return () => {
onPause();
onViewDestroy();
};
}, [onPause, onResume, onTabChange, onViewDestroy]);
return (
<div ref={element}>
<Page
id='indexPage'
className='mainAnimatedPage homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs'
isBackButtonEnabled={false}
backDropType='movie,series,book'
>
<div className='tabContent pageTabContent' id='homeTab' data-index='0'>
<div className='sections'></div>
</div>
<div className='tabContent pageTabContent' id='favoritesTab' data-index='1'>
<div className='sections'></div>
</div>
</Page>
</div>
);
};
export default Home;

View file

@ -2,19 +2,36 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../components/ConnectionRequired';
import SearchPage from './search';
import UserNew from './user/usernew';
import Search from './search';
import UserEdit from './user/useredit';
import UserLibraryAccess from './user/userlibraryaccess';
import UserParentalControl from './user/userparentalcontrol';
import UserPassword from './user/userpassword';
import UserProfile from './user/userprofile';
import UserProfiles from './user/userprofiles';
import Home from './home';
const AppRoutes = () => (
<Routes>
<Route path='/'>
<Route
path='search.html'
element={
<ConnectionRequired>
<SearchPage />
</ConnectionRequired>
}
/>
{/* User routes */}
<Route path='/' element={<ConnectionRequired />}>
<Route path='search.html' element={<Search />} />
<Route path='userprofile.html' element={<UserProfile />} />
<Route path='home.html' element={<Home />} />
</Route>
{/* Admin routes */}
<Route path='/' element={<ConnectionRequired isAdminRequired={true} />}>
<Route path='usernew.html' element={<UserNew />} />
<Route path='userprofiles.html' element={<UserProfiles />} />
<Route path='useredit.html' element={<UserEdit />} />
<Route path='userlibraryaccess.html' element={<UserLibraryAccess />} />
<Route path='userparentalcontrol.html' element={<UserParentalControl />} />
<Route path='userpassword.html' element={<UserPassword />} />
</Route>
{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
</Route>

View file

@ -8,7 +8,7 @@ import SearchSuggestions from '../components/search/SearchSuggestions';
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
import globalize from '../scripts/globalize';
const SearchPage: FunctionComponent = () => {
const Search: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>();
const [ searchParams ] = useSearchParams();
@ -41,4 +41,4 @@ const SearchPage: FunctionComponent = () => {
);
};
export default SearchPage;
export default Search;

View file

@ -1,32 +1,35 @@
import { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import ButtonElement from '../dashboard/users/ButtonElement';
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import InputElement from '../dashboard/users/InputElement';
import LinkEditUserPreferences from '../dashboard/users/LinkEditUserPreferences';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SelectElement from '../dashboard/users/SelectElement';
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import ButtonElement from '../../elements/ButtonElement';
import CheckBoxElement from '../../elements/CheckBoxElement';
import InputElement from '../../elements/InputElement';
import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import escapeHTML from 'escape-html';
import SelectElement from '../../elements/SelectElement';
import Page from '../../components/Page';
type ItemsArr = {
Name?: string;
Id?: string;
type ResetProvider = AuthProvider & {
checkedAttribute: string
}
const UserEditPage: FunctionComponent = () => {
type AuthProvider = {
Name?: string;
Id?: string;
}
const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]);
const [ authProviders, setAuthProviders ] = useState([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
@ -91,7 +94,7 @@ const UserEditPage: FunctionComponent = () => {
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
const itemsArr: ItemsArr[] = [];
const itemsArr: ResetProvider[] = [];
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
@ -172,7 +175,7 @@ const UserEditPage: FunctionComponent = () => {
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) {
(page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess;
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -227,8 +230,8 @@ const UserEditPage: FunctionComponent = () => {
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0');
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0');
user.Policy.AuthenticationProviderId = (page.querySelector('.selectLoginProvider') as HTMLInputElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('.selectPasswordResetProvider') as HTMLInputElement).value;
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
@ -236,7 +239,7 @@ const UserEditPage: FunctionComponent = () => {
return c.getAttribute('data-id');
});
if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value as SyncPlayUserAccessType;
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
}
window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
@ -263,25 +266,49 @@ 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');
});
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadData]);
const optionLoginProvider = authProviders.map((provider) => {
const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionPasswordResetProvider = passwordResetProviders.map((provider) => {
const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionSyncPlayAccess = () => {
let content = '';
content += `<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>`;
content += `<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>`;
content += `<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>`;
return content;
};
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='editUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='useredit'/>
<div
className='lnkEditUserPreferencesContainer'
@ -313,29 +340,29 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement
className= 'selectLoginProvider'
label= 'LabelAuthProvider'
currentProviderId={authenticationProviderId}
providers={authProviders}
/>
id='selectLoginProvider'
label='LabelAuthProvider'
>
{optionLoginProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')}
</div>
</div>
<div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement
className= 'selectPasswordResetProvider'
label= 'LabelPasswordResetProvider'
currentProviderId={passwordResetProviderId}
providers={passwordResetProviders}
/>
id='selectPasswordResetProvider'
label='LabelPasswordResetProvider'
>
{optionPasswordResetProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement
type='checkbox'
className='chkRemoteAccess'
title='AllowRemoteAccess'
/>
@ -345,7 +372,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<CheckBoxElement
labelClassName='checkboxContainer'
type='checkbox'
className='chkIsAdmin'
title='OptionAllowUserToManageServer'
/>
@ -355,12 +381,10 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv'
/>
<CheckBoxElement
type='checkbox'
className='chkManageLiveTv'
title='OptionAllowManageLiveTv'
/>
@ -372,27 +396,22 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding'
/>
<CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing'
/>
<CheckBoxElement
type='checkbox'
className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding'
/>
@ -420,11 +439,12 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'>
<SelectSyncPlayAccessElement
className='selectSyncPlayAccess'
<SelectElement
id='selectSyncPlayAccess'
label='LabelSyncPlayAccess'
/>
>
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')}
</div>
@ -437,18 +457,17 @@ const UserEditPage: FunctionComponent = () => {
<div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement
labelClassName='checkboxContainer'
type='checkbox'
className='chkEnableDeleteAllFolders'
title='AllLibraries'
/>
<div className='deleteAccess'>
{deleteFoldersAccess.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</div>
@ -460,12 +479,10 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement
type='checkbox'
className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers'
/>
<CheckBoxElement
type='checkbox'
className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices'
/>
@ -479,7 +496,6 @@ const UserEditPage: FunctionComponent = () => {
</h2>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
type='checkbox'
className='chkEnableDownloading'
title='OptionAllowContentDownload'
/>
@ -489,7 +505,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement
type='checkbox'
className='chkDisabled'
title='OptionDisableUser'
/>
@ -499,7 +514,6 @@ const UserEditPage: FunctionComponent = () => {
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement
type='checkbox'
className='chkIsHidden'
title='OptionHideUser'
/>
@ -550,14 +564,16 @@ const UserEditPage: FunctionComponent = () => {
/>
<ButtonElement
type='button'
className='raised button-cancel block btnCancel'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</div>
</Page>
);
};
export default UserEditPage;
export default UserEdit;

View file

@ -1,16 +1,17 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../loading/loading';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import toast from '../toast/toast';
import SectionTabs from '../dashboard/users/SectionTabs';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import ButtonElement from '../dashboard/users/ButtonElement';
import toast from '../../components/toast/toast';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import ButtonElement from '../../elements/ButtonElement';
import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import AccessContainer from '../dashboard/users/AccessContainer';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import AccessContainer from '../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type ItemsArr = {
Name?: string;
@ -19,7 +20,7 @@ type ItemsArr = {
checkedAttribute?: string
}
const UserLibraryAccessPage: FunctionComponent = () => {
const UserLibraryAccess: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@ -226,12 +227,17 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadData]);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userlibraryaccess'/>
<form className='userLibraryAccessForm'>
<AccessContainer
@ -245,12 +251,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -266,12 +272,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkChannel'
Id={Item.Id}
Name={Item.Name}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -287,13 +293,13 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='DeviceAccessHelp'
>
{devicesItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkDevice'
Id={Item.Id}
Name={Item.Name}
AppName={Item.AppName}
checkedAttribute={Item.checkedAttribute}
itemId={Item.Id}
itemName={Item.Name}
itemAppName={Item.AppName}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
@ -307,8 +313,9 @@ const UserLibraryAccessPage: FunctionComponent = () => {
</div>
</form>
</div>
</div>
</Page>
);
};
export default UserLibraryAccessPage;
export default UserLibraryAccess;

View file

@ -2,13 +2,14 @@ import React, { FunctionComponent, useCallback, useEffect, useState, useRef } fr
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import toast from '../toast/toast';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import InputElement from '../dashboard/users/InputElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import ButtonElement from '../dashboard/users/ButtonElement';
import AccessContainer from '../dashboard/users/AccessContainer';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import InputElement from '../../elements/InputElement';
import ButtonElement from '../../elements/ButtonElement';
import AccessContainer from '../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type userInput = {
Name?: string;
@ -20,7 +21,7 @@ type ItemsArr = {
Id?: string;
}
const NewUserPage: FunctionComponent = () => {
const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
@ -169,18 +170,24 @@ const NewUserPage: FunctionComponent = () => {
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadUser]);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='newUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<form className='newUserProfileForm'>
<div className='inputContainer'>
<InputElement
@ -208,12 +215,11 @@ const NewUserPage: FunctionComponent = () => {
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute=''
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
@ -229,12 +235,11 @@ const NewUserPage: FunctionComponent = () => {
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxListItem
<CheckBoxElement
key={Item.Id}
className='chkChannel'
Id={Item.Id}
Name={Item.Name}
checkedAttribute=''
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
@ -246,14 +251,16 @@ const NewUserPage: FunctionComponent = () => {
/>
<ButtonElement
type='button'
className='raised button-cancel block btnCancel'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</div>
</Page>
);
};
export default NewUserPage;
export default UserNew;

View file

@ -1,25 +1,22 @@
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import AccessScheduleList from '../dashboard/users/AccessScheduleList';
import BlockedTagList from '../dashboard/users/BlockedTagList';
import ButtonElement from '../dashboard/users/ButtonElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList';
import BlockedTagList from '../../components/dashboard/users/BlockedTagList';
import ButtonElement from '../../elements/ButtonElement';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import CheckBoxElement from '../../elements/CheckBoxElement';
import escapeHTML from 'escape-html';
import SelectElement from '../../elements/SelectElement';
import Page from '../../components/Page';
type RatingsArr = {
Name: string;
Value: number;
}
type ItemsArr = {
type UnratedItem = {
name: string;
value: string;
checkedAttribute: string
@ -27,8 +24,8 @@ type ItemsArr = {
const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]);
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
@ -36,7 +33,7 @@ const UserParentalControl: FunctionComponent = () => {
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: RatingsArr[] = [];
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
@ -90,7 +87,7 @@ const UserParentalControl: FunctionComponent = () => {
value: 'Series'
}];
const itemsArr: ItemsArr[] = [];
const itemsArr: UnratedItem[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
@ -181,7 +178,7 @@ const UserParentalControl: FunctionComponent = () => {
}
}
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue;
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
@ -226,7 +223,7 @@ const UserParentalControl: FunctionComponent = () => {
throw new Error('Unexpected null user.Policy');
}
user.Policy.MaxParentalRating = parseInt((page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value || '0', 10) || null;
user.Policy.MaxParentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value || '0', 10) || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked;
}).map(function (i) {
@ -299,7 +296,7 @@ const UserParentalControl: FunctionComponent = () => {
return false;
};
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
@ -309,28 +306,43 @@ const UserParentalControl: FunctionComponent = () => {
}, -1);
});
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
const optionMaxParentalRating = () => {
let content = '';
content += '<option value=\'\'></option>';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
}
return content;
};
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<Page
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectMaxParentalRating
className= 'selectMaxParentalRating'
label= 'LabelMaxParentalRating'
parentalRatings={parentalRatings}
/>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
@ -342,12 +354,12 @@ const UserParentalControl: FunctionComponent = () => {
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxListItem
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
ItemType={Item.value}
Name={Item.name}
checkedAttribute={Item.checkedAttribute}
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
@ -355,19 +367,16 @@ const UserParentalControl: FunctionComponent = () => {
</div>
<br />
<div className='verticalSection' style={{marginBottom: '2em'}}>
<div
className='detailSectionHeader sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('LabelBlockContentWithTags')}
</h2>
<SectionTitleButtonElement
className='fab btnAddBlockedTag submit'
title='Add'
icon='add'
/>
</div>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='blockedTags' style={{marginTop: '.5em'}}>
{blockedTags.map((tag, index) => {
return <BlockedTagList
@ -378,19 +387,15 @@ const UserParentalControl: FunctionComponent = () => {
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
<div
className='sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('HeaderAccessSchedule')}
</h2>
<SectionTitleButtonElement
className='fab btnAddSchedule submit'
title='Add'
icon='add'
/>
</div>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
@ -414,7 +419,8 @@ const UserParentalControl: FunctionComponent = () => {
</div>
</form>
</div>
</div>
</Page>
);
};

View file

@ -1,19 +1,23 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import SectionTabs from '../dashboard/users/SectionTabs';
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
import SectionTabs from '../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import Page from '../../components/Page';
import loading from '../../components/loading/loading';
const UserPasswordPage: FunctionComponent = () => {
const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => {
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
setUserName(user.Name);
loading.hide();
});
}, [userId]);
useEffect(() => {
@ -21,12 +25,17 @@ const UserPasswordPage: FunctionComponent = () => {
}, [loadUser]);
return (
<div>
<Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'>
<SectionTitleContainer
title={userName}
titleLink='https://docs.jellyfin.org/general/server/users/'
/>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
@ -34,8 +43,9 @@ const UserPasswordPage: FunctionComponent = () => {
/>
</div>
</div>
</div>
</Page>
);
};
export default UserPasswordPage;
export default UserPassword;

View file

@ -1,21 +1,21 @@
import { ImageType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import { appHost } from '../apphost';
import confirm from '../confirm/confirm';
import ButtonElement from '../dashboard/users/ButtonElement';
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
import loading from '../loading/loading';
import toast from '../toast/toast';
import { appHost } from '../../components/apphost';
import confirm from '../../components/confirm/confirm';
import ButtonElement from '../../elements/ButtonElement';
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import loading from '../../components/loading/loading';
import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import Page from '../../components/Page';
type IProps = {
userId: string;
}
const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
const UserProfile: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const element = useRef<HTMLDivElement>(null);
@ -57,11 +57,11 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}
if (user.PrimaryImageTag) {
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
} else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.remove('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
}
});
loading.hide();
@ -120,7 +120,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
reader.readAsDataURL(file);
};
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
confirm(
globalize.translate('DeleteImageConfirmation'),
globalize.translate('DeleteImage')
@ -133,7 +133,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
});
});
(page.querySelector('.btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
(page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement;
uploadImage.value = '';
uploadImage.click();
@ -145,13 +145,18 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}, [reloadUser, userId]);
return (
<div ref={element}>
<div className='padded-left padded-right padded-bottom-page'>
<Page
id='userProfilePage'
title={globalize.translate('Profile')}
className='mainAnimatedPage libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage'
>
<div ref={element} className='padded-left padded-right padded-bottom-page'>
<div
className='readOnlyContent'
style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}}
>
<div
className='imagePlaceHolder'
style={{position: 'relative', display: 'inline-block', maxWidth: 200 }}
>
<input
@ -172,12 +177,14 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
<br />
<ButtonElement
type='button'
className='raised btnAddImage hide'
id='btnAddImage'
className='raised button-submit hide'
title='ButtonAddImage'
/>
<ButtonElement
type='button'
className='raised btnDeleteImage hide'
id='btnDeleteImage'
className='raised hide'
title='DeleteImage'
/>
</div>
@ -186,8 +193,9 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
userId={userId}
/>
</div>
</div>
</Page>
);
};
export default UserProfilePage;
export default UserProfile;

View file

@ -1,17 +1,18 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import loading from '../../components/loading/loading';
import dom from '../../scripts/dom';
import confirm from '../../components/confirm/confirm';
import UserCardBox from '../dashboard/users/UserCardBox';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer';
import UserCardBox from '../../components/dashboard/users/UserCardBox';
import SectionTitleContainer from '../../elements/SectionTitleContainer';
import '../../elements/emby-button/emby-button';
import '../../elements/emby-button/paper-icon-button-light';
import '../../components/cardbuilder/card.scss';
import '../../components/indicators/indicators.scss';
import '../../assets/css/flexstyles.scss';
import Page from '../../components/Page';
type MenuEntry = {
name?: string;
@ -19,7 +20,7 @@ type MenuEntry = {
icon?: string;
}
const UserProfilesPage: FunctionComponent = () => {
const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);
@ -124,19 +125,28 @@ const UserProfilesPage: FunctionComponent = () => {
}
});
(page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);
return (
<div ref={element}>
<div className='content-primary'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
titleLink='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
<Page
id='userProfilesPage'
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
btnId='btnAddUser'
btnClassName='fab submit sectionTitleButton'
btnTitle='ButtonAddUser'
btnIcon='add'
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
@ -144,8 +154,9 @@ const UserProfilesPage: FunctionComponent = () => {
})}
</div>
</div>
</div>
</Page>
);
};
export default UserProfilesPage;
export default UserProfiles;

View file

@ -390,6 +390,9 @@ import browser from './browser';
if (supportsMp3VideoAudio && (browser.chrome || browser.edgeChromium || (browser.firefox && browser.versionMajor >= 83))) {
supportsMp2VideoAudio = true;
}
if (browser.android) {
supportsMp2VideoAudio = false;
}
}
/* eslint-disable compat/compat */

View file

@ -19,7 +19,7 @@ import globalize from './globalize';
// "00", "00", ".000", "Z", undefined, undefined, undefined]
if (!d) {
throw "Couldn't parse ISO 8601 date string '" + s + "'";
throw new Error("Couldn't parse ISO 8601 date string '" + s + "'");
}
// parse strings, leading zeros into proper ints

View file

@ -355,14 +355,14 @@ import '../assets/css/flexstyles.scss';
}
}
function refreshDashboardInfoInDrawer(apiClient) {
function refreshDashboardInfoInDrawer(page, apiClient) {
currentDrawerType = 'admin';
loadNavDrawer();
if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) {
updateDashboardMenuSelectedItem();
updateDashboardMenuSelectedItem(page);
} else {
createDashboardMenu(apiClient);
createDashboardMenu(page, apiClient);
}
}
@ -370,9 +370,9 @@ import '../assets/css/flexstyles.scss';
return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1;
}
function updateDashboardMenuSelectedItem() {
function updateDashboardMenuSelectedItem(page) {
const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption');
const currentViewId = viewManager.currentView().id;
const currentViewId = page.id;
for (let i = 0, length = links.length; i < length; i++) {
let link = links[i];
@ -590,7 +590,7 @@ import '../assets/css/flexstyles.scss';
});
}
function createDashboardMenu(apiClient) {
function createDashboardMenu(page, apiClient) {
return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) {
let html = '';
html += '<a class="adminDrawerLogo clearLink" is="emby-linkbutton" href="#/home.html">';
@ -598,7 +598,7 @@ import '../assets/css/flexstyles.scss';
html += '</a>';
html += toolsMenuHtml;
navDrawerScrollContainer.innerHTML = html;
updateDashboardMenuSelectedItem();
updateDashboardMenuSelectedItem(page);
});
}
@ -1017,7 +1017,7 @@ import '../assets/css/flexstyles.scss';
mainDrawerButton.classList.remove('hide');
}
refreshDashboardInfoInDrawer(apiClient);
refreshDashboardInfoInDrawer(page, apiClient);
} else {
if (mainDrawerButton) {
if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) {

View file

@ -77,13 +77,6 @@ import { appRouter } from '../components/appRouter';
controller: 'user/menu/index'
});
defineRoute({
alias: '/myprofile.html',
path: 'user/profile/index.html',
autoFocus: false,
pageComponent: 'UserProfilePage'
});
defineRoute({
alias: '/mypreferencescontrols.html',
path: 'user/controls/index.html',
@ -300,14 +293,6 @@ import { appRouter } from '../components/appRouter';
controller: 'dashboard/plugins/repositories/index'
});
defineRoute({
alias: '/home.html',
path: 'home.html',
autoFocus: false,
controller: 'home',
type: 'home'
});
defineRoute({
alias: '/list.html',
path: 'list.html',
@ -429,53 +414,6 @@ import { appRouter } from '../components/appRouter';
controller: 'shows/tvrecommended'
});
defineRoute({
alias: '/useredit.html',
path: 'dashboard/users/useredit.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserEditPage'
});
defineRoute({
alias: '/userlibraryaccess.html',
path: 'dashboard/users/userlibraryaccess.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserLibraryAccessPage'
});
defineRoute({
alias: '/usernew.html',
path: 'dashboard/users/usernew.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'NewUserPage'
});
defineRoute({
alias: '/userparentalcontrol.html',
path: 'dashboard/users/userparentalcontrol.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserParentalControl'
});
defineRoute({
alias: '/userpassword.html',
path: 'dashboard/users/userpassword.html',
autoFocus: false,
pageComponent: 'UserPasswordPage'
});
defineRoute({
alias: '/userprofiles.html',
path: 'dashboard/users/userprofiles.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserProfilesPage'
});
defineRoute({
alias: '/wizardremoteaccess.html',
path: 'wizard/remote/index.html',

View file

@ -116,7 +116,7 @@ function ScreenSaverManager() {
return;
}
if (getFunctionalEventIdleTime < getMinIdleTime()) {
if (getFunctionalEventIdleTime() < getMinIdleTime()) {
return;
}

View file

@ -772,7 +772,7 @@
"LabelHttpsPort": "Plaaslike HTTPS -poortnommer:",
"LabelHomeScreenSectionValue": "Tuisskermafdeling {0}:",
"LabelHomeNetworkQuality": "Tuisnetwerk kwaliteit:",
"LabelHDHomerunPortRangeHelp": "Beperk die HD Homerun UDP -poortreeks tot hierdie waarde. (Standaard is 1024 - 645535).",
"LabelHDHomerunPortRangeHelp": "Beperk die HD Homerun UDP -poortreeks tot hierdie waarde. (Standaard is 1024 - 65535).",
"LabelHDHomerunPortRange": "HD Homerun -poortreeks:",
"LabelHardwareAccelerationTypeHelp": "Hardewareversnelling vereis ekstra konfigurasie.",
"LabelHardwareAccelerationType": "Hardeware versnelling:",

View file

@ -309,7 +309,7 @@
"LabelFriendlyName": "اسم مخصوص لك:",
"LabelServerNameHelp": "هذا الاسم سيستخدم للتعرف على هذا الخادم، اسم الحاسوب سوف يستخدم بشكل افتراضي.",
"LabelGroupMoviesIntoCollections": "تجميع الأفلام إلى مجاميع",
"LabelGroupMoviesIntoCollectionsHelp": "الأفلام الموجودة في مجموعات ستظهر موحدة عندما تظهر في قوائم الأفلام",
"LabelGroupMoviesIntoCollectionsHelp": "سيتم عرض الأفلام في المجموعة كعنصر مجمع عند عرض قوائم الأفلام.",
"LabelH264Crf": "قيمة CRF لتشفير H264:",
"LabelEncoderPreset": "إعداد التشفير:",
"LabelHardwareAccelerationType": "التسريع بعتاد الحاسب:",
@ -667,7 +667,7 @@
"TabCatalog": "الكتالوج",
"TabCodecs": "الكودكات",
"TabContainers": "الحاويات",
"TabDashboard": "لوحة العدادات",
"TabDashboard": "لوحة التحكم",
"TabDirectPlay": "تشغيل مباشر",
"TabLatest": "الاخير",
"TabLogs": "الكشوفات",
@ -785,7 +785,7 @@
"AllEpisodes": "كل الحلقات",
"AllComplexFormats": "جميع الصيغ المعقدة (ASS, SSA, VobSub, PGS, SUB, IDX, …)",
"AllChannels": "كل القنوات",
"Albums": "ألبومات",
"Albums": "البومات",
"Aired": "عرضت",
"AirDate": "تاريخ العرض",
"AddedOnValue": "تم إضافة {0}",
@ -803,7 +803,7 @@
"ConfirmDeleteItems": "حذف هذه العناصر سوف يحذفها من نظام الملفات ومن مكتبة الوسائط. هل ترغب حقاً فى الاستمرار؟",
"EveryNDays": "كل {0} يوم",
"ConfirmDeleteItem": "حذف هذا العنصر سوف يحذفه من نظام الملفات ومن مكتبة الوسائط. هل ترغب حقاً فى الاستمرار؟",
"DropShadow": "شبح الهبوط",
"DropShadow": "ظل الهبوط",
"LabelDropShadow": "اسقاط الظل:",
"EditSubtitles": "تعديل الترجمات",
"EditMetadata": "تعديل البيانات التعريفية",
@ -875,7 +875,7 @@
"DeinterlaceMethodHelp": "حدد طريقة فك التشابك لاستخدامها عند تحويل محتوى متشابك. اذا كان فك التشابك عن طريق العتاد الصلب فعال سيتم استخدامه بدل هذا الاعداد.",
"DefaultSubtitlesHelp": "يتم عرض الترجمات استنادًا إلى العلامات الافتراضية والقسرية في البيانات التعريفية المضمنة. سيتم إعتبار تفضيلات اللغة عند توفر خيارات متعددة.",
"DefaultMetadataLangaugeDescription": "هذه هي إعداداتك الافتراضية ويمكن تعديلها على أساس كل مكتبة.",
"Default": "إفتراضي",
"Default": "افتراضي",
"CopyStreamURL": "نسخ عنوان رابط البث",
"Continuing": "مستمر",
"CopyStreamURLSuccess": "تم نسخ عنوان الرابط بنجاح.",
@ -886,7 +886,7 @@
"ButtonTogglePlaylist": "قائمة التشغيل",
"BoxSet": "طقم",
"ButtonSplit": "تقسيم",
"AllowFfmpegThrottlingHelp": "عندما يتقدم التحويل أو الريمكس بعيدًا بما يكفي عن موضع التشغيل الحالي ، أوقف العملية مؤقتًا حتى تستهلك موارد أقل. يكون هذا مفيدًا للغاية عند المشاهدة دون البحث كثيرًا. قم بإيقاف تشغيل هذا إذا كنت تواجه مشكلات في التشغيل.",
"AllowFfmpegThrottlingHelp": "عندما تقوم بتفعيلها؛ سوف تتوقف عملية الترميز transcoding توقفا مؤقتا كلما تقدمت العملية عن موضع التشغيل بنسبة كافية، تهدف هذه الخاصية إلى التقليل من استهلاك الموارد، وتكون ذات منفعة كبيرة عندما تتم عملية المشاهدة بانتظام دون القفز عدة دقائق في المشاهدة ما بين الحينة والأخرى. كما ينطبق الأمر ذاته على عملية نسخ الملف إلى حاوية أخرى لتتوافق مع الجهاز remuxing.",
"InstallingPackage": "تثبيت {0} (الإصدار {1})",
"Images": "الصور",
"Identify": "التعرف على الوسائط",
@ -1049,7 +1049,7 @@
"DashboardVersionNumber": "النسخة: {0}",
"DashboardServerName": "الخادم: {0}",
"DashboardOperatingSystem": "نظام التشغيل: {0}",
"DashboardArchitecture": "المعمارية: {0}",
"DashboardArchitecture": "نوع المعمارية: {0}",
"DailyAt": "يومياً على {0}",
"ClearQueue": "مسح القائمة المؤقتة",
"Bwdif": "BWDIF",
@ -1225,7 +1225,7 @@
"LabelUserLoginAttemptsBeforeLockout": "محاولات تسجيل الدخول الفاشلة قبل حظر المستخدم:",
"LabelUserAgent": "وكيل المستخدم:",
"LabelUnstable": "غير مستقر",
"LabelUDPPortRangeHelp": "تقييد Jellyfin لاستخدام نطاق المنفذ هذا عند إجراء اتصالات UDP. (الافتراضي هو 1024 - 645535). <br/> ملاحظة: تتطلب وظيفة معينة منافذ ثابتة قد تكون خارج هذا النطاق.",
"LabelUDPPortRangeHelp": "تقييد Jellyfin لاستخدام نطاق المنفذ هذا عند إجراء اتصالات UDP. (الافتراضي هو 1024 - 65535). <br/> ملاحظة: تتطلب وظيفة معينة منافذ ثابتة قد تكون خارج هذا النطاق.",
"LabelUDPPortRange": "نطاق اتصالات UDP:",
"LabelTranscodingFramerate": "معدل إطارات التحويل:",
"LabelTranscodePath": "مسار التحويل:",
@ -1537,7 +1537,7 @@
"LabelKeepUpTo": "حافظ على ما يصل إلى:",
"LabelIsForced": "مجبر",
"LabelIdentificationFieldHelp": "سلسلة فرعية أو تعبير regex غير حساس لحالة الأحرف.",
"LabelHDHomerunPortRangeHelp": "يقصر نطاق منفذ HD Homerun UDP على هذه القيمة. (الافتراضي هو 1024-645535).",
"LabelHDHomerunPortRangeHelp": "يقصر نطاق منفذ HD Homerun UDP على هذه القيمة. (الافتراضي هو 1024-65535).",
"LabelHDHomerunPortRange": "نطاق منفذ HD Homerun:",
"LabelHardwareEncoding": "ترميز الأجهزة:",
"LabelH265Crf": "H.265 ترميز CRF:",
@ -1554,7 +1554,7 @@
"HeaderSyncPlayPlaybackSettings": "التشغيل",
"HeaderNewRepository": "مستودع جديد",
"DirectPlayHelp": "الملف المصدر متوافق تمامًا مع هذا العميل ، وتستقبل الجلسة الملف بدون تعديلات.",
"LabelDashboardTheme": "سمة لوحة تحكم الخادم:",
"LabelDashboardTheme": "قالب لوحة تحكم الخادم:",
"LabelTonemappingParamHelp": "ضبط خوارزمية تعيين النغمة. القيم الموصى بها والافتراضية هي NaN. اتركه فارغًا بشكل عام.",
"LabelTonemappingParam": "معلمة تعيين النغمة:",
"LabelTonemappingDesat": "تم حفظ تعيين النغمة:",
@ -1642,7 +1642,7 @@
"EnableEnhancedNvdecDecoderHelp": "تنفيذ NVDEC التجريبي ، لا تقم بتمكين هذا الخيار إلا إذا واجهت أخطاء في فك التشفير.",
"StoryArc": "قصة القوس",
"Production": "إنتاج",
"OriginalAirDate": "تاريخ الهواء الأصلي",
"OriginalAirDate": "تاريخ البث الأصلي",
"MessageUnauthorizedUser": "غير مصرح لك بالوصول إلى الخادم في هذا الوقت. يرجى الاتصال بمسؤول الخادم الخاص بك لمزيد من المعلومات.",
"Localization": "تحديد الموقع",
"ItemDetails": "تفاصيل العنصر",
@ -1650,5 +1650,27 @@
"Bold": "عريض",
"LabelTextWeight": "سمك الخط:",
"HomeVideosPhotos": "مقاطع الفيديو والصور",
"EnableSplashScreen": "قم بتفعيل شاشة البداية"
"EnableSplashScreen": "قم بتفعيل شاشة البداية",
"MediaInfoDvBlSignalCompatibilityId": "معرف توافق إشارة DV bl",
"MediaInfoBlPresentFlag": "علامة الضبط المسبق لـ DV bl",
"MediaInfoElPresentFlag": "DV el علم مسبق الضبط",
"MediaInfoRpuPresentFlag": "علامة الضبط المسبق لـ DV Rpu",
"MediaInfoDvLevel": "مستوى DV",
"MediaInfoDvProfile": "الملف الشخصي DV",
"MediaInfoDvVersionMinor": "نسخة DV طفيفة",
"MediaInfoDvVersionMajor": "إصدار DV الرئيسي",
"MediaInfoDoViTitle": "عنوان DV",
"MediaInfoVideoRangeType": "نوع نطاق الفيديو",
"LabelVideoRangeType": "نوع نطاق الفيديو:",
"VideoRangeTypeNotSupported": "نوع نطاق الفيديو غير مدعوم",
"LabelVppTonemappingContrastHelp": "تطبيق كسب التباين في تعيين نغمة VPP. القيم الموصى بها والافتراضية هي 1.2 و 1.",
"LabelVppTonemappingContrast": "كسب تباين تعيين نغمة VPP:",
"LabelVppTonemappingBrightnessHelp": "تطبيق كسب السطوع في تعيين نغمة VPP. كل من القيم الموصى بها والافتراضية هي 0.",
"LabelVppTonemappingBrightness": "كسب سطوع رسم الخرائط VPP نغمة:",
"ScreenResolution": "تعيين مسار الترجمة على أساس البند السابق",
"RememberSubtitleSelectionsHelp": "تعيين مسار الترجمة على أساس البند السابق.",
"RememberSubtitleSelections": "تعيين مسار الترجمة على أساس البند السابق",
"RememberAudioSelectionsHelp": "حاول ضبط المسار الصوتي على أقرب تطابق للفيديو الأخير.",
"RememberAudioSelections": "تعيين مسار الصوت على أساس البند السابق",
"LabelMaxVideoResolution": "الحد الأقصى المسموح به لقرار تحويل ترميز الفيديو"
}

View file

@ -313,7 +313,7 @@
"LabelNewPassword": "Нова парола:",
"LabelNewPasswordConfirm": "Нова парола (отново):",
"LabelNumberOfGuideDays": "Брой дни за които да се свали програма:",
"LabelNumberOfGuideDaysHelp": "Изтеглянето на програма заповече дни дава възможност да планирате по-нататъшните записи предварително, но и отнема повече време, за да се изтегли. Автомат ще избере въз основа на броя на каналите.",
"LabelNumberOfGuideDaysHelp": "Изтеглянето на програма за повече дни дава възможност да планирате по-нататъшните записи предварително, но отнема повече време, за да се изтегли. \"Автоматично\" ще избере въз основа на броя на каналите.",
"LabelOptionalNetworkPath": "Споделена мрежова папка:",
"LabelOriginalAspectRatio": "Оригинално съотношение:",
"LabelOriginalTitle": "Оригинално заглавие:",

View file

@ -1095,7 +1095,7 @@
"LabelUserLibraryHelp": "Seleccioneu el qual la biblioteca d'usuaris per visualitzar al dispositiu. Deixar en blanc per heretar la configuració predeterminada.",
"LabelUserAgent": "Agent d'usuari:",
"LabelUnstable": "Inestable",
"LabelUDPPortRangeHelp": "Restringir Jellyfin utilitzar aquest rang de ports a fer les connexions UDP. (Per defecte és 1.024-645.535) Nota <br/>: Certes funcions requereixen ports que poden estar fora d'aquest rang fix ..",
"LabelUDPPortRangeHelp": "Restringir Jellyfin utilitzar aquest rang de ports a fer les connexions UDP. (Per defecte és 1024-65535) Nota <br/>: Certes funcions requereixen ports que poden estar fora d'aquest rang fix ..",
"LabelUDPPortRange": "UDP Comunicacions de Camp:",
"LabelTypeText": "Text",
"LabelTypeMetadataDownloaders": "Descarregadors de metadades ({0}):",
@ -1255,7 +1255,7 @@
"LabelIconMaxResHelp": "La resolució màxima d'icones exposades a través de la propietat 'upnp:icon'.",
"LabelHttpsPortHelp": "El número de port TCP per al servidor HTTPS.",
"LabelHomeNetworkQuality": "Qualitat de la xarxa domèstica:",
"LabelHDHomerunPortRangeHelp": "Restringeix el rang de ports UDP HDHomeRun a aquest valor. (Per defecte és 1024-645535).",
"LabelHDHomerunPortRangeHelp": "Restringeix el rang de ports UDP HDHomeRun a aquest valor. (Per defecte és 1024-65535).",
"LabelHDHomerunPortRange": "HDHomeRun rang de ports:",
"LabelHardwareAccelerationTypeHelp": "L'acceleració de maquinari requereix una configuració addicional.",
"LabelHardwareAccelerationType": "L'acceleració de maquinari:",

View file

@ -708,7 +708,7 @@
"MinutesBefore": "minut předem",
"Mobile": "Mobilní",
"Monday": "Pondělí",
"MoreFromValue": "Více z {0}",
"MoreFromValue": "Více od {0}",
"MoreUsersCanBeAddedLater": "Další uživatele můžete přidat později na nástěnce serveru.",
"MoveLeft": "Posunout vlevo",
"MoveRight": "Posunout vpravo",
@ -1057,7 +1057,7 @@
"Genre": "Žánr",
"GroupBySeries": "Seskupit podle série",
"HeaderAllowMediaDeletionFrom": "Povolit mazání médií z:",
"HeaderAppearsOn": "Objeví se",
"HeaderAppearsOn": "Viz také",
"HeaderBlockItemsWithNoRating": "Blokovat položky s žádnými nebo nerozpoznanými informacemi o hodnocení:",
"HeaderChapterImages": "Obrázky kapitol",
"HeaderConfigureRemoteAccess": "Nastavit vzdálený přístup",
@ -1458,14 +1458,14 @@
"AspectRatioFill": "Vyplnit",
"AspectRatioCover": "Obal",
"PluginFromRepo": "{0} z repozitáře {1}",
"LabelUDPPortRangeHelp": "Omezí UDP připojení serveru Jellyfin na tento rozsah. (Výchozí hodnota je 1024-645535).<br/>Poznámka: Některé funkce vyžadují určité porty, které se mohou nacházet mimo tento rozsah.",
"LabelUDPPortRangeHelp": "Omezí UDP připojení serveru Jellyfin na tento rozsah. (Výchozí hodnota je 1024-65535).<br/>Poznámka: Některé funkce vyžadují určité porty, které se mohou nacházet mimo tento rozsah.",
"LabelUDPPortRange": "Rozsah pro komunikaci UDP:",
"LabelSSDPTracingFilterHelp": "Nepovinná IP adresa, pomocí které se má filtrovat zaznamenaná komunikace SSDP.",
"LabelSSDPTracingFilter": "Filtr SSDP:",
"LabelPublishedServerUriHelp": "Přepíše URI používanou serverem Jellyfin v závislosti na rozhraní nebo IP adrese klienta.",
"LabelPublishedServerUri": "Veřejné URI serveru:",
"LabelIsForced": "Vynucené",
"LabelHDHomerunPortRangeHelp": "Omezí rozsah UDP portů HDHomeRun na tuto hodnotu. (Výchozí hodnota je 1024-645535).",
"LabelHDHomerunPortRangeHelp": "Omezí rozsah UDP portů HDHomeRun na tuto hodnotu. (Výchozí hodnota je 1024-65535).",
"LabelHDHomerunPortRange": "Rozsah portů HDHomeRun:",
"LabelH265Crf": "H.265 kódování CRF:",
"LabelEnableSSDPTracingHelp": "Povolí zaznamenávání podrobností o trasování sítě SSDP.<br/><b>VAROVÁNÍ:</b> Způsobuje závažné snížení výkonu.",
@ -1675,5 +1675,7 @@
"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í"
"LabelMaxVideoResolution": "Maximální rozlišení videa pro překódování",
"IgnoreDtsHelp": "Vypnutím se mohou vyřešit některé problémy, např. chybějící zvuk u kanálů s oddělenými zvukovými a video stopami.",
"IgnoreDts": "Ignorovat DTS (časové razítko dekódování)"
}

View file

@ -1467,7 +1467,7 @@
"MediaInfoVideoRange": "Video rækkevidde",
"LabelVideoRange": "Video rækkevidde:",
"LabelUserMaxActiveSessions": "Maksimum antal af samtidige bruger sessioner:",
"LabelUDPPortRangeHelp": "Begræns Jellyfin til at bruge denne part rækkevidde når der oprettes UDP forbindelser. (Default er 1024-645535).<br/> Note: Nogle funktioner kan kræve bestemte porte uden for denne rækkevidde.",
"LabelUDPPortRangeHelp": "Begræns Jellyfin til at bruge denne part rækkevidde når der oprettes UDP forbindelser. (Default er 1024-65535).<br/> Note: Nogle funktioner kan kræve bestemte porte uden for denne rækkevidde.",
"LabelUDPPortRange": "UDP Kommunikations Rækkevidde:",
"LabelTonemappingParam": "Tonemapping parameter:",
"LabelTonemappingAlgorithm": "Vælg Tone mapping algorytme der skal bruges:",
@ -1486,7 +1486,7 @@
"LabelMaxAudiobookResumeHelp": "Titler bliver talt som fuldt afspillet hvis stoppet mens tilbageværende tid er mindre end denne værdi.",
"LabelKnownProxies": "Kendte proxier:",
"LabelIconMaxResHelp": "Maksimum opløsning af ikoner gjort tilgængelig via 'upnp:icon' værdien.",
"LabelHDHomerunPortRangeHelp": "Begræns HDHomeRun UDP port vidden til denne værdi. (Standard er 1024 - 645535).",
"LabelHDHomerunPortRangeHelp": "Begræns HDHomeRun UDP port vidden til denne værdi. (Standard er 1024 - 65535).",
"LabelHDHomerunPortRange": "HDHomeRun port vidde:",
"LabelEnableSSDPTracingHelp": "Aktiver detaljer om SSDP netværk sporing til logning.<br/><b>ADVARSEL:</b> Dette vil forårsage seriøse fald i effektivitet.",
"LabelDropSubtitleHere": "Smid undertekst her, eller klik for at gennemse.",

View file

@ -495,7 +495,7 @@
"LabelFriendlyName": "Benutzerfreundlicher Name:",
"LabelServerNameHelp": "Dieser Name wird benutzt, um den Server zu identifizieren, standardmäßig wird der Hostname des Servers verwendet.",
"LabelGroupMoviesIntoCollections": "Gruppiere Filme in Sammlungen",
"LabelGroupMoviesIntoCollectionsHelp": "Wenn für Filme die Listenansicht ausgewählt ist, werden Sammlungen als Einträge mit gruppierten Filmen angezeigt.",
"LabelGroupMoviesIntoCollectionsHelp": "Filme in einer Sammlung werden bei der Anzeige von Filmlisten als ein gruppiertes Element angezeigt.",
"LabelEncoderPreset": "Kodierungsvoreinstellung:",
"LabelHardwareAccelerationType": "Hardwarebeschleunigung:",
"LabelHardwareAccelerationTypeHelp": "Hardwarebeschleunigung benötigt zusätzliche Konfiguration.",
@ -557,7 +557,7 @@
"LabelMethod": "Methode:",
"LabelMinBackdropDownloadWidth": "Minimale Breite der zu herunterladenden Hintergründe:",
"LabelMinResumeDuration": "Minimale Dauer für Wiederaufnahme:",
"LabelMinResumeDurationHelp": "Die kürzeste Videolänge in Sekunden, die die Wiedergabeposition speichert und dich fortsetzen lässt.",
"LabelMinResumeDurationHelp": "Die Mindestfilmlänge in Sekunden, welche die Abspielzeit speichert und dich fortsetzen lässt.",
"LabelMinResumePercentage": "Minimale Prozent für Wiederaufnahme:",
"LabelMinResumePercentageHelp": "Titel werden als ungesehen eingetragen, wenn sie vor dieser Zeit gestoppt werden.",
"LabelMinScreenshotDownloadWidth": "Minimale Breite für zu herunterladende Screenshot:",
@ -910,7 +910,7 @@
"PlayAllFromHere": "Alles ab hier abspielen",
"PlayCount": "Wiedergabezähler",
"PlayFromBeginning": "Von Beginn abspielen",
"PlayNext": "Nächste abspielen",
"PlayNext": "Als nächstes abspielen",
"PlayNextEpisodeAutomatically": "Nächste Episode automatisch abspielen",
"Played": "Gespielt",
"Playlists": "Wiedergabelisten",
@ -934,8 +934,8 @@
"Raised": "Erhöht",
"Rate": "Bewertung",
"RecentlyWatched": "Kürzlich gesehen",
"RecommendationBecauseYouLike": "Weil du auch {0} magst",
"RecommendationBecauseYouWatched": "Weil du auch {0} angesehen hast",
"RecommendationBecauseYouLike": "Weil du {0} magst",
"RecommendationBecauseYouWatched": "Weil du {0} angesehen hast",
"RecommendationDirectedBy": "Unter der Regie von {0}",
"RecommendationStarring": "In der Hauptrolle {0}",
"Record": "Aufnehmen",
@ -1157,7 +1157,7 @@
"OptionProtocolHls": "HTTP-Live-Streaming (HLS)",
"OptionProtocolHttp": "HTTP",
"OptionRegex": "Reguläre Ausdrücke",
"OptionSpecialEpisode": "Extras",
"OptionSpecialEpisode": "Special Features",
"OptionTrackName": "Titel",
"Screenshots": "Bildschirmfotos",
"Studios": "Studios",
@ -1401,19 +1401,19 @@
"MediaInfoColorTransfer": "Farbübertragung",
"MediaInfoVideoRange": "Videobereich",
"ThumbCard": "Miniaturansichtkarte",
"QuickConnectNotActive": "Schnellverbindung ist auf diesem Server nicht aktiv",
"QuickConnectNotAvailable": "Frag deinen Server-Administrator, ob er Schnellverbindung erlaubt",
"QuickConnectInvalidCode": "Falscher Schnellverbindungs-Code",
"QuickConnectDescription": "Um mit Schnellverbindung einzuloggen, wähle den Schnellverbindungs-Knopf auf dem gewünschten Gerät aus und gib den unten angezeigten Code ein.",
"QuickConnectDeactivated": "Schnellverbindung war deaktiviert bevor der Login verifiziert werden konnte",
"QuickConnectAuthorizeFail": "Unbekannter Schnellverbindungs-Code",
"QuickConnectNotActive": "Quick Connect ist auf diesem Server nicht aktiv",
"QuickConnectNotAvailable": "Frag deinen Server-Administrator, ob er Quick Connect erlaubt",
"QuickConnectInvalidCode": "Quick Connect Code ungültig",
"QuickConnectDescription": "Um mit Quick Connect einzuloggen, wähle den Quick Connect-Knopf auf einem angemeldeten Gerät und gib den unten angezeigten Code ein.",
"QuickConnectDeactivated": "Quick Connect wurde deaktiviert bevor der Login verifiziert werden konnte",
"QuickConnectAuthorizeFail": "Unbekannter Quick Connect-Code",
"QuickConnectAuthorizeSuccess": "Anfrage autorisiert",
"QuickConnectAuthorizeCode": "Login Code {0} eingeben",
"QuickConnectActivationSuccessful": "Erfolgreich aktiviert",
"EnableQuickConnect": "Schnellverbindung auf diesem Server aktivieren",
"QuickConnect": "Schnellverbindung",
"EnableQuickConnect": "Quick Connect auf diesem Server aktivieren",
"QuickConnect": "Quick Connect",
"PosterCard": "Posterkarte",
"LabelQuickConnectCode": "Schnellverbindungs-Code:",
"LabelQuickConnectCode": "Quick Connect-Code:",
"LabelCurrentStatus": "Aktueller Status:",
"EnableAutoCast": "Als Standard festlegen",
"ButtonUseQuickConnect": "Quick Connect nutzen",
@ -1480,7 +1480,7 @@
"PluginFromRepo": "{0} aus dem Repository {1}",
"LabelUDPPortRangeHelp": "Beschränkt Jellyfin auf die Verwendung dieses Portbereichs beim Herstellen von UDP-Verbindungen. (Standard ist 1024 - 645535).<br/> Hinweis: Für bestimmte Funktionen sind feste Ports erforderlich, die möglicherweise außerhalb dieses Bereichs liegen.",
"LabelUDPPortRange": "UDP-Kommunikationsbereich:",
"LabelHDHomerunPortRangeHelp": "Beschränkt den HDHomeRun UDP-Portbereich zu diesem Wert. (Standard ist 1024 - 645535).",
"LabelHDHomerunPortRangeHelp": "Beschränkt den HDHomeRun UDP-Portbereich zu diesem Wert. (Standard ist 1024 - 65535).",
"LabelHDHomerunPortRange": "HDHomeRun Portbereich:",
"LabelSyncPlayInfo": "SyncPlay-Info",
"LabelOriginalMediaInfo": "Originale Medieninformation",
@ -1669,5 +1669,12 @@
"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"
"LabelMaxVideoResolution": "Maximal erlaubte Video Transcodierungs-Auflösung",
"VideoRangeTypeNotSupported": "Dieses Video-Spektrum ist nicht unterstützt",
"MediaInfoDvBlSignalCompatibilityId": "Dolby Vision BL Signal-Kompatibilitäts-ID",
"MediaInfoBlPresentFlag": "DV BL verfügbar Marker",
"MediaInfoElPresentFlag": "DV el verfügbar Marker",
"MediaInfoVideoRangeType": "Spektrum",
"LabelVideoRangeType": "Spektrum:",
"IgnoreDtsHelp": "Die Deaktivierung dieser Option könnte Probleme beheben, z. B. kein Ton auf Filmen mit getrennten Audio- und Video-Streams."
}

View file

@ -5,7 +5,7 @@
"Add": "Προσθήκη",
"AddToCollection": "Προσθήκη στη συλλογή",
"AddToPlayQueue": "Προσθήκη στην ουρά αναπαραγωγής",
"AddToPlaylist": "Πρόσθεσε σε λίστα",
"AddToPlaylist": "Προσθήκη σε λίστα",
"AddedOnValue": "Προστέθηκε {0}",
"AdditionalNotificationServices": "Περιηγηθείτε στον κατάλογο plugin για να εγκαταστήσετε πρόσθετες υπηρεσίες ειδοποίησης.",
"AirDate": "Ημερομηνία προβολής",
@ -148,12 +148,12 @@
"Download": "Λήψη",
"DownloadsValue": "Λήψεις: {0}",
"DropShadow": "Σκίαση",
"EasyPasswordHelp": "Ο εύκολος κωδικός PIN σας χρησιμοποιείται για πρόσβαση εκτός σύνδεσης σε υποστηριζόμενους διαμεσολαβητές αναπαραγωγής και μπορεί επίσης να χρησιμοποιηθεί για εύκολη σύνδεση στο δίκτυο.",
"EasyPasswordHelp": "Ο κωδικός σας Easy PIN χρησιμοποιείται για πρόσβαση εκτός σύνδεσης σε υποστηριζόμενους διαμεσολαβητές αναπαραγωγής και μπορεί επίσης να χρησιμοποιηθεί για εύκολη σύνδεση στο δίκτυο.",
"Edit": "Επεξεργασία",
"EditImages": "Επεξεργασία εικόνων",
"EditMetadata": "Επεξεργασία μεταδεδομένων",
"EditSubtitles": "Επεξεργασία υποτίτλων",
"EnableBackdropsHelp": "Eμφάνιση φόντων στο παρασκήνιο ορισμένων σελίδων κατά την περιήγηση στη βιβλιοθήκη.",
"EnableBackdropsHelp": "Eμφάνιση του φόντου στο παρασκήνιο ορισμένων σελίδων κατά την περιήγηση στη βιβλιοθήκη.",
"EnableCinemaMode": "Λειτουργία Κινηματογράφου",
"EnableColorCodedBackgrounds": "Έγχρωμα κωδικοποιημένα φόντα",
"EnableDisplayMirroring": "αντικατοπτρισμός οθόνης",
@ -289,7 +289,7 @@
"HeaderPasswordReset": "Επαναφορά του κωδικού πρόσβασης",
"HeaderPaths": "Διαδρομή",
"HeaderPhotoAlbums": "Άλμπουμ φωτογραφιών",
"HeaderPinCodeReset": "Επαναφορά PIN Code",
"HeaderPinCodeReset": "Επαναφορά κωδικού PIN",
"HeaderPlayAll": "Αναπαραγωγή Όλων",
"HeaderPlayOn": "Συνέχισε να παίζεις",
"HeaderPlayback": "Αναπαραγωγή πολυμέσων:",
@ -1053,9 +1053,9 @@
"ChangingMetadataImageSettingsNewContent": "Οι αλλαγές στις ρυθμίσεις λήψης μεταδεδομένων ή εικόνων θα εφαρμοστούν μόνο στο νέο περιεχόμενο που προστίθεται στη βιβλιοθήκη σας. Για να εφαρμόσετε τις αλλαγές στους υπάρχοντες τίτλους, θα πρέπει να ανανεώσετε τα μεταδεδομένα τους χειροκίνητα.",
"ButtonActivate": "Ενεργοποίηση",
"Authorize": "Εξουσιοδότηση",
"EnableQuickConnect": "Ενεργοποιήστε τη γρήγορη σύνδεση σε αυτόν τον διακομιστή",
"EnableDecodingColorDepth10Vp9": "Ενεργοποίηση αποκωδικοποίησης υλικού 10-bit για το VP9",
"EnableDecodingColorDepth10Hevc": "Ενεργοποίηση αποκωδικοποίησης υλικού 10-bit για HEVC",
"EnableQuickConnect": "Ενεργοποιήστε την \"Γρήγορη Σύνδεση\" σε αυτόν τον διακομιστή",
"EnableDecodingColorDepth10Vp9": "Ενεργοποίηση αποκωδικοποίησης υλικού 10-bit για βίντεο VP9",
"EnableDecodingColorDepth10Hevc": "Ενεργοποίηση αποκωδικοποίησης υλικού 10-bit για βίντεο HEVC",
"EnableAutoCast": "Ορίσετε ως προεπιλογή",
"ButtonUseQuickConnect": "Χρήση Γρήγορης Σύνδεσης",
"EnableDetailsBanner": "Πανό Λεπτομερειών",
@ -1169,5 +1169,21 @@
"GuideProviderSelectListings": "Επιλογή Συλλογών",
"ErrorPleaseSelectLineup": "Παρακαλώ διαλέξτε μία συλλογή και ξαναπροσπαθήστε. Αν δεν υπάρχουν διαθέσιμες συλλογές, βεβαιωθείτε ότι το όνομα, ο κωδικός και η διεύθυνση ηλεκτρονικού ταχυδρομείου είναι σωστά.",
"ErrorAddingListingsToSchedulesDirect": "Εμφανίστηκε σφάλμα κατά την εισαγωγή της συλλογής στον Schedules Direct λογαριασμό σας. Η υπηρεσία Schedules Direct επιτρέπει περιορισμένο αριθμό συλλογών ανά λογαριασμό. Θα χρειαστεί να συνδεθείτε στην ιστοσελίδα της υπηρεσίας Schedules Direct και να διαγράψετε επί πλέον συλλογές για να συνεχίσετε.",
"Digital": "Ψηφιακός"
"Digital": "Ψηφιακός",
"ShowLess": "Εμφάνισε λιγότερα",
"ShowMore": "Εμφάνισε περισσότερα",
"ItemDetails": "Λεπτομέρειες αντικειμένου",
"ImportFavoriteChannelsHelp": "Μόνο τα κανάλια που έχουν επισημανθεί ως αγαπημένα στη συσκευή δέκτη θα εισάγονται.",
"Image": "Εικόνα",
"IgnoreDtsHelp": "Η απενεργοποίηση αυτής της επιλογής μπορεί να επιλύσει ορισμένα προβλήματα, π.χ. λείπει ήχος σε κανάλια με ξεχωριστές ροές ήχου και βίντεο.",
"IgnoreDts": "Παράβλεψη DTS (χρονοσήμανση αποκωδικοποίησης)",
"HttpsRequiresCert": "Για να ενεργοποιήσετε ασφαλείς συνδέσεις, θα χρειαστεί να παρέχετε ένα αξιόπιστο πιστοποιητικό SSL, όπως το Let's Encrypt. Παρέχετε ένα πιστοποιητικό ή απενεργοποιήστε τις ασφαλείς συνδέσεις.",
"HeaderUploadSubtitle": "Ανεβάστε υπότιτλους",
"HeaderTypeImageFetchers": "Λήπτες εικόνων ({0}):",
"HeaderSyncPlayTimeSyncSettings": "Συγχρονισμός χρόνου",
"HeaderSyncPlayPlaybackSettings": "Αναπαραγωγή",
"HeaderSyncPlaySettings": "Ρυθμίσεις",
"HeaderSyncPlaySelectGroup": "Εγγραφείτε σε μια ομάδα",
"HeaderSyncPlayEnabled": "ενεργοποιημένη",
"HeaderServerAddressSettings": "Ρυθμίσεις διεύθυνσης διακομιστή"
}

View file

@ -1451,9 +1451,9 @@
"LabelAutoDiscoveryTracingHelp": "When enabled, packets received on the auto discovery port will be logged.",
"HeaderPortRanges": "Firewall and Proxy Settings",
"LabelUDPPortRange": "UDP Communication Range:",
"LabelUDPPortRangeHelp": "Restrict Jellyfin to use this port range when making UDP connections. (Default is 1024 - 645535).<br/> Note: Certain function require fixed ports that may be outside of this range.",
"LabelUDPPortRangeHelp": "Restrict Jellyfin to use this port range when making UDP connections. (Default is 1024 - 65535).<br/> Note: Certain function require fixed ports that may be outside of this range.",
"LabelHDHomerunPortRange": "HDHomeRun port range:",
"LabelHDHomerunPortRangeHelp": "Restricts the HDHomeRun UDP port range to this value. (Default is 1024 - 645535).",
"LabelHDHomerunPortRangeHelp": "Restricts the HDHomeRun UDP port range to this value. (Default is 1024 - 65535).",
"LabelPublishedServerUri": "Published Server URIs:",
"LabelPublishedServerUriHelp": "Override the URI used by Jellyfin, based on the interface, or client IP address.",
"HeaderDebugging": "Debugging and Tracing",

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