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:
commit
33fe2c51d6
160 changed files with 23903 additions and 4619 deletions
2
src/apiclient.d.ts
vendored
2
src/apiclient.d.ts
vendored
|
@ -1,7 +1,7 @@
|
|||
// TODO: Move to jellyfin-apiclient
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module 'jellyfin-apiclient' {
|
||||
import {
|
||||
import type {
|
||||
AllThemeMediaResult,
|
||||
AuthenticationResult,
|
||||
BaseItemDto,
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -301,7 +301,7 @@ export function show(options) {
|
|||
|
||||
resolve(selectedId);
|
||||
} else {
|
||||
reject();
|
||||
reject('ActionSheet closed without resolving');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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 = '';
|
||||
}
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('&');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<div id="editUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="newUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userParentalControlPage" data-role="page" class="page type-interior">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
|
||||
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
|
||||
|
||||
</div>
|
|
@ -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>
|
|
@ -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;
|
|
@ -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(/^.*[\\/]/, '')
|
||||
}]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<div id="userProfilePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
|
||||
|
||||
</div>
|
41
src/elements/ButtonElement.tsx
Normal file
41
src/elements/ButtonElement.tsx
Normal 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;
|
57
src/elements/CheckBoxElement.tsx
Normal file
57
src/elements/CheckBoxElement.tsx
Normal 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;
|
47
src/elements/IconButtonElement.tsx
Normal file
47
src/elements/IconButtonElement.tsx
Normal 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;
|
|
@ -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
|
41
src/elements/SectionTitleContainer.tsx
Normal file
41
src/elements/SectionTitleContainer.tsx
Normal 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;
|
|
@ -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
|
38
src/elements/SelectElement.tsx
Normal file
38
src/elements/SelectElement.tsx
Normal 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;
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
contain: strict;
|
||||
box-sizing: border-box;
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
.touch-menu-la {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
|
||||
.swiper-slide-img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
177
src/routes/home.tsx
Normal 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;
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 */
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -116,7 +116,7 @@ function ScreenSaverManager() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getFunctionalEventIdleTime < getMinIdleTime()) {
|
||||
if (getFunctionalEventIdleTime() < getMinIdleTime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -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:",
|
||||
|
|
|
@ -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": "الحد الأقصى المسموح به لقرار تحويل ترميز الفيديو"
|
||||
}
|
||||
|
|
|
@ -313,7 +313,7 @@
|
|||
"LabelNewPassword": "Нова парола:",
|
||||
"LabelNewPasswordConfirm": "Нова парола (отново):",
|
||||
"LabelNumberOfGuideDays": "Брой дни за които да се свали програма:",
|
||||
"LabelNumberOfGuideDaysHelp": "Изтеглянето на програма заповече дни дава възможност да планирате по-нататъшните записи предварително, но и отнема повече време, за да се изтегли. Автомат ще избере въз основа на броя на каналите.",
|
||||
"LabelNumberOfGuideDaysHelp": "Изтеглянето на програма за повече дни дава възможност да планирате по-нататъшните записи предварително, но отнема повече време, за да се изтегли. \"Автоматично\" ще избере въз основа на броя на каналите.",
|
||||
"LabelOptionalNetworkPath": "Споделена мрежова папка:",
|
||||
"LabelOriginalAspectRatio": "Оригинално съотношение:",
|
||||
"LabelOriginalTitle": "Оригинално заглавие:",
|
||||
|
|
|
@ -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:",
|
||||
|
|
|
@ -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í)"
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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": "Ρυθμίσεις διεύθυνσης διακομιστή"
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue