mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into burn-subtitle-transcoding
This commit is contained in:
commit
da4265eb46
89 changed files with 3292 additions and 1480 deletions
|
@ -4,6 +4,7 @@
|
|||
* @module components/cardBuilder/cardBuilder
|
||||
*/
|
||||
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
|
@ -12,7 +13,7 @@ import datetime from 'scripts/datetime';
|
|||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
import imageHelper from 'utils/image';
|
||||
import { getItemTypeIcon, getLibraryIcon } from 'utils/image';
|
||||
|
||||
import focusManager from '../focusManager';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
|
@ -1053,7 +1054,7 @@ function buildCard(index, item, apiClient, options) {
|
|||
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
|
||||
}
|
||||
|
||||
if (item.Type === 'CollectionFolder' || item.CollectionType) {
|
||||
if (item.Type === BaseItemKind.CollectionFolder || item.CollectionType) {
|
||||
const refreshClass = item.RefreshProgress ? '' : ' class="hide"';
|
||||
indicatorsHtml += '<div is="emby-itemrefreshindicator"' + refreshClass + ' data-progress="' + (item.RefreshProgress || 0) + '" data-status="' + item.RefreshStatus + '"></div>';
|
||||
importRefreshIndicator();
|
||||
|
@ -1180,41 +1181,18 @@ function getHoverMenuHtml(item, action) {
|
|||
* @returns {string} HTML markup of the card overlay.
|
||||
*/
|
||||
export function getDefaultText(item, options) {
|
||||
if (item.CollectionType) {
|
||||
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
|
||||
let icon;
|
||||
|
||||
if (item.Type === BaseItemKind.CollectionFolder || item.CollectionType) {
|
||||
icon = getLibraryIcon(item.CollectionType);
|
||||
}
|
||||
|
||||
switch (item.Type) {
|
||||
case 'MusicAlbum':
|
||||
return '<span class="cardImageIcon material-icons album" aria-hidden="true"></span>';
|
||||
case 'MusicArtist':
|
||||
case 'Person':
|
||||
return '<span class="cardImageIcon material-icons person" aria-hidden="true"></span>';
|
||||
case 'Audio':
|
||||
return '<span class="cardImageIcon material-icons audiotrack" aria-hidden="true"></span>';
|
||||
case 'Movie':
|
||||
return '<span class="cardImageIcon material-icons movie" aria-hidden="true"></span>';
|
||||
case 'Episode':
|
||||
case 'Series':
|
||||
return '<span class="cardImageIcon material-icons tv" aria-hidden="true"></span>';
|
||||
case 'Program':
|
||||
return '<span class="cardImageIcon material-icons live_tv" aria-hidden="true"></span>';
|
||||
case 'Book':
|
||||
return '<span class="cardImageIcon material-icons book" aria-hidden="true"></span>';
|
||||
case 'Folder':
|
||||
return '<span class="cardImageIcon material-icons folder" aria-hidden="true"></span>';
|
||||
case 'BoxSet':
|
||||
return '<span class="cardImageIcon material-icons collections" aria-hidden="true"></span>';
|
||||
case 'Playlist':
|
||||
return '<span class="cardImageIcon material-icons view_list" aria-hidden="true"></span>';
|
||||
case 'Photo':
|
||||
return '<span class="cardImageIcon material-icons photo" aria-hidden="true"></span>';
|
||||
case 'PhotoAlbum':
|
||||
return '<span class="cardImageIcon material-icons photo_album" aria-hidden="true"></span>';
|
||||
if (!icon) {
|
||||
icon = getItemTypeIcon(item.Type, options?.defaultCardImageIcon);
|
||||
}
|
||||
|
||||
if (options?.defaultCardImageIcon) {
|
||||
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>';
|
||||
if (icon) {
|
||||
return `<span class="cardImageIcon material-icons ${icon}" aria-hidden="true"></span>`;
|
||||
}
|
||||
|
||||
const defaultName = isUsingLiveTvNaming(item.Type) ? item.Name : itemHelper.getDisplayName(item);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import React, { type FC } from 'react';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import imageHelper from 'utils/image';
|
||||
import { getItemTypeIcon, getLibraryIcon } from 'utils/image';
|
||||
import DefaultName from './DefaultName';
|
||||
import type { ItemDto } from 'types/base/models/item-dto';
|
||||
|
||||
|
@ -14,38 +14,24 @@ const DefaultIconText: FC<DefaultIconTextProps> = ({
|
|||
item,
|
||||
defaultCardImageIcon
|
||||
}) => {
|
||||
if (item.CollectionType) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{imageHelper.getLibraryIcon(item.CollectionType)}
|
||||
</Icon>
|
||||
);
|
||||
let icon;
|
||||
|
||||
if (item.Type === BaseItemKind.CollectionFolder || item.CollectionType) {
|
||||
icon = getLibraryIcon(item.CollectionType);
|
||||
}
|
||||
|
||||
if (item.Type && !(item.Type === BaseItemKind.TvChannel || item.Type === BaseItemKind.Studio )) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{imageHelper.getItemTypeIcon(item.Type)}
|
||||
</Icon>
|
||||
);
|
||||
if (!icon) {
|
||||
icon = getItemTypeIcon(item.Type, defaultCardImageIcon);
|
||||
}
|
||||
|
||||
if (defaultCardImageIcon) {
|
||||
if (icon) {
|
||||
return (
|
||||
<Icon
|
||||
className='cardImageIcon'
|
||||
sx={{ color: 'inherit', fontSize: '5em' }}
|
||||
aria-hidden='true'
|
||||
>
|
||||
{defaultCardImageIcon}
|
||||
{icon}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const createLinkElement = ({ className, title }: IProps) => ({
|
||||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="${className}"
|
||||
href='#'
|
||||
>
|
||||
${title}
|
||||
</a>`
|
||||
});
|
||||
|
||||
const LinkEditUserPreferences: FunctionComponent<IProps> = ({ className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkEditUserPreferences;
|
|
@ -1,49 +1,60 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import { navigate } from '../../../utils/dashboard';
|
||||
import LinkButton from '../../../elements/emby-button/LinkButton';
|
||||
|
||||
type IProps = {
|
||||
activeTab: string;
|
||||
};
|
||||
|
||||
const createLinkElement = (activeTab: string) => ({
|
||||
__html: `<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('/dashboard/users/profile', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('/dashboard/users/access', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('/dashboard/users/password', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
function useNavigate(url: string): () => void {
|
||||
return React.useCallback(() => {
|
||||
navigate(url, true).catch(err => {
|
||||
console.warn('Error navigating to dashboard url', err);
|
||||
});
|
||||
}, [url]);
|
||||
}
|
||||
|
||||
const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => {
|
||||
const onClickProfile = useNavigate('/dashboard/users/profile');
|
||||
const onClickAccess = useNavigate('/dashboard/users/access');
|
||||
const onClickParentalControl = useNavigate('/dashboard/users/parentalcontrol');
|
||||
const clickPassword = useNavigate('/dashboard/users/password');
|
||||
return (
|
||||
<div
|
||||
data-role='controlgroup'
|
||||
data-type='horizontal'
|
||||
className='localnav'
|
||||
dangerouslySetInnerHTML={createLinkElement(activeTab)}
|
||||
/>
|
||||
className='localnav'>
|
||||
<LinkButton
|
||||
href='#'
|
||||
data-role='button'
|
||||
className={activeTab === 'useredit' ? 'ui-btn-active' : ''}
|
||||
onClick={onClickProfile}>
|
||||
{globalize.translate('Profile')}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
href='#'
|
||||
data-role='button'
|
||||
className={activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}
|
||||
onClick={onClickAccess}>
|
||||
{globalize.translate('TabAccess')}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
href='#'
|
||||
data-role='button'
|
||||
className={activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}
|
||||
onClick={onClickParentalControl}>
|
||||
{globalize.translate('TabParentalControl')}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
href='#'
|
||||
data-role='button'
|
||||
className={activeTab === 'userpassword' ? 'ui-btn-active' : ''}
|
||||
onClick={clickPassword}>
|
||||
{globalize.translate('HeaderPassword')}
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -4,19 +4,9 @@ import { formatDistanceToNow } from 'date-fns';
|
|||
import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale';
|
||||
import globalize from '../../../lib/globalize';
|
||||
import IconButtonElement from '../../../elements/IconButtonElement';
|
||||
import escapeHTML from 'escape-html';
|
||||
import LinkButton from '../../../elements/emby-button/LinkButton';
|
||||
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
|
||||
|
||||
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
|
||||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="cardContent"
|
||||
href="#/dashboard/users/profile?userId=${user.Id}"
|
||||
>
|
||||
${renderImgUrl}
|
||||
</a>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
user?: UserDto;
|
||||
};
|
||||
|
@ -55,22 +45,21 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
|||
const lastSeen = getLastSeenText(user.LastActivityDate);
|
||||
|
||||
const renderImgUrl = imgUrl ?
|
||||
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` :
|
||||
`<div class='${imageClass} ${getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'>
|
||||
<span class='material-icons cardImageIcon person' aria-hidden='true'></span>
|
||||
</div>`;
|
||||
<div className={imageClass} style={{ backgroundImage: `url(${imgUrl})` }} /> :
|
||||
<div className={`${imageClass} ${getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center`}>
|
||||
<span className='material-icons cardImageIcon person' aria-hidden='true'></span>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div data-userid={user.Id} data-username={user.Name} className={cssClass}>
|
||||
<div className='cardBox visualCardBox'>
|
||||
<div className='cardScalable visualCardBox-cardScalable'>
|
||||
<div className='cardPadder cardPadder-square'></div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
user: user,
|
||||
renderImgUrl: renderImgUrl
|
||||
})}
|
||||
/>
|
||||
<LinkButton
|
||||
className='cardContent'
|
||||
href={`#/dashboard/users/profile?userId=${user.Id}`}>
|
||||
{renderImgUrl}
|
||||
</LinkButton>
|
||||
</div>
|
||||
<div className='cardFooter visualCardBox-cardFooter'>
|
||||
<div
|
||||
|
@ -83,7 +72,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
|||
/>
|
||||
</div>
|
||||
<div className='cardText'>
|
||||
<span>{escapeHTML(user.Name)}</span>
|
||||
<span>{user.Name}</span>
|
||||
</div>
|
||||
<div className='cardText cardText-secondary'>
|
||||
<span>{lastSeen != '' ? lastSeen : ''}</span>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import globalize from '../../../lib/globalize';
|
||||
import LibraryMenu from '../../../scripts/libraryMenu';
|
||||
import confirm from '../../confirm/confirm';
|
||||
import loading from '../../loading/loading';
|
||||
import toast from '../../toast/toast';
|
||||
|
@ -16,6 +15,7 @@ type IProps = {
|
|||
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
const user = useRef<UserDto>();
|
||||
const libraryMenu = useMemo(async () => ((await import('../../../scripts/libraryMenu')).default), []);
|
||||
|
||||
const loadUser = useCallback(async () => {
|
||||
const page = element.current;
|
||||
|
@ -37,7 +37,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
throw new Error('Unexpected null user policy or configuration');
|
||||
}
|
||||
|
||||
LibraryMenu.setTitle(user.current.Name);
|
||||
(await libraryMenu).setTitle(user.current.Name);
|
||||
|
||||
if (user.current.HasConfiguredPassword) {
|
||||
if (!user.current.Policy?.IsAdministrator) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import toast from '../toast/toast';
|
|||
import template from './homeScreenSettings.template.html';
|
||||
import { LibraryTab } from '../../types/libraryTab.ts';
|
||||
|
||||
const numConfigurableSections = 7;
|
||||
const numConfigurableSections = 10;
|
||||
|
||||
function renderViews(page, user, result) {
|
||||
let folderHtml = '';
|
||||
|
@ -204,15 +204,15 @@ function renderViewOrder(context, user, result) {
|
|||
}
|
||||
|
||||
function updateHomeSectionValues(context, userSettings) {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
for (let i = 1; i <= numConfigurableSections; i++) {
|
||||
const select = context.querySelector(`#selectHomeSection${i}`);
|
||||
const defaultValue = homeSections.getDefaultSection(i - 1);
|
||||
|
||||
const option = select.querySelector(`option[value=${defaultValue}]`) || select.querySelector('option[value=""]');
|
||||
const option = select.querySelector(`option[value="${defaultValue}"]`) || select.querySelector('option[value=""]');
|
||||
|
||||
const userValue = userSettings.get(`homesection${i - 1}`);
|
||||
|
||||
option.value = '';
|
||||
if (option) option.value = '';
|
||||
|
||||
if (userValue === defaultValue || !userValue) {
|
||||
select.value = '';
|
||||
|
@ -390,6 +390,9 @@ function saveUser(context, user, userSettingsInstance, apiClient) {
|
|||
userSettingsInstance.set('homesection4', context.querySelector('#selectHomeSection5').value);
|
||||
userSettingsInstance.set('homesection5', context.querySelector('#selectHomeSection6').value);
|
||||
userSettingsInstance.set('homesection6', context.querySelector('#selectHomeSection7').value);
|
||||
userSettingsInstance.set('homesection7', context.querySelector('#selectHomeSection8').value);
|
||||
userSettingsInstance.set('homesection8', context.querySelector('#selectHomeSection9').value);
|
||||
userSettingsInstance.set('homesection9', context.querySelector('#selectHomeSection10').value);
|
||||
|
||||
const selectLandings = context.querySelectorAll('.selectLanding');
|
||||
for (i = 0, length = selectLandings.length; i < length; i++) {
|
||||
|
|
|
@ -115,6 +115,48 @@
|
|||
<option value="none">${None}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectHomeSection8" label="{section8label}">
|
||||
<option value="smalllibrarytiles">${HeaderMyMedia}</option>
|
||||
<option value="librarybuttons">${HeaderMyMediaSmall}</option>
|
||||
<option value="activerecordings">${HeaderActiveRecordings}</option>
|
||||
<option value="resume">${HeaderContinueWatching}</option>
|
||||
<option value="resumeaudio">${HeaderContinueListening}</option>
|
||||
<option value="resumebook">${HeaderContinueReading}</option>
|
||||
<option value="latestmedia">${HeaderLatestMedia}</option>
|
||||
<option value="nextup">${NextUp}</option>
|
||||
<option value="livetv">${LiveTV}</option>
|
||||
<option value="none">${None}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectHomeSection9" label="{section9label}">
|
||||
<option value="smalllibrarytiles">${HeaderMyMedia}</option>
|
||||
<option value="librarybuttons">${HeaderMyMediaSmall}</option>
|
||||
<option value="activerecordings">${HeaderActiveRecordings}</option>
|
||||
<option value="resume">${HeaderContinueWatching}</option>
|
||||
<option value="resumeaudio">${HeaderContinueListening}</option>
|
||||
<option value="resumebook">${HeaderContinueReading}</option>
|
||||
<option value="latestmedia">${HeaderLatestMedia}</option>
|
||||
<option value="nextup">${NextUp}</option>
|
||||
<option value="livetv">${LiveTV}</option>
|
||||
<option value="none">${None}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectHomeSection10" label="{section10label}">
|
||||
<option value="smalllibrarytiles">${HeaderMyMedia}</option>
|
||||
<option value="librarybuttons">${HeaderMyMediaSmall}</option>
|
||||
<option value="activerecordings">${HeaderActiveRecordings}</option>
|
||||
<option value="resume">${HeaderContinueWatching}</option>
|
||||
<option value="resumeaudio">${HeaderContinueListening}</option>
|
||||
<option value="resumebook">${HeaderContinueReading}</option>
|
||||
<option value="latestmedia">${HeaderLatestMedia}</option>
|
||||
<option value="nextup">${NextUp}</option>
|
||||
<option value="livetv">${LiveTV}</option>
|
||||
<option value="none">${None}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
|
|
|
@ -61,7 +61,7 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
|||
let html = '';
|
||||
|
||||
if (userViews.length) {
|
||||
const userSectionCount = 7;
|
||||
const userSectionCount = 10;
|
||||
// TV layout can have an extra section to ensure libraries are visible
|
||||
const totalSectionCount = layoutManager.tv ? userSectionCount + 1 : userSectionCount;
|
||||
for (let i = 0; i < totalSectionCount; i++) {
|
||||
|
|
|
@ -351,12 +351,13 @@ export function getCommands(options) {
|
|||
return commands;
|
||||
}
|
||||
|
||||
function getResolveFunction(resolve, id, changed, deleted) {
|
||||
function getResolveFunction(resolve, commandId, changed, deleted, itemId) {
|
||||
return function () {
|
||||
resolve({
|
||||
command: id,
|
||||
command: commandId,
|
||||
updated: changed,
|
||||
deleted: deleted
|
||||
deleted: deleted,
|
||||
itemId: itemId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -533,7 +534,7 @@ function executeCommand(item, id, options) {
|
|||
getResolveFunction(resolve, id)();
|
||||
break;
|
||||
case 'delete':
|
||||
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id));
|
||||
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true, itemId), getResolveFunction(resolve, id));
|
||||
break;
|
||||
case 'share':
|
||||
navigator.share({
|
||||
|
|
|
@ -12,6 +12,7 @@ import dom from '../../scripts/dom';
|
|||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-textarea/emby-textarea';
|
||||
import './style.scss';
|
||||
import template from './libraryoptionseditor.template.html';
|
||||
|
||||
|
@ -473,8 +474,10 @@ export function setContentType(parent, contentType) {
|
|||
|
||||
if (contentType === 'music') {
|
||||
parent.querySelector('.lyricSettingsSection').classList.remove('hide');
|
||||
parent.querySelector('.audioTagSettingsSection').classList.remove('hide');
|
||||
} else {
|
||||
parent.querySelector('.lyricSettingsSection').classList.add('hide');
|
||||
parent.querySelector('.audioTagSettingsSection').classList.add('hide');
|
||||
}
|
||||
|
||||
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies' && contentType !== 'mixed');
|
||||
|
@ -597,6 +600,8 @@ export function getLibraryOptions(parent) {
|
|||
SaveLyricsWithMedia: parent.querySelector('#chkSaveLyricsLocally').checked,
|
||||
RequirePerfectSubtitleMatch: parent.querySelector('#chkRequirePerfectMatch').checked,
|
||||
AutomaticallyAddToCollection: parent.querySelector('#chkAutomaticallyAddToCollection').checked,
|
||||
PreferNonstandardArtistsTag: parent.querySelector('#chkPreferNonstandardArtistsTag').checked,
|
||||
UseCustomTagDelimiters: parent.querySelector('#chkUseCustomTagDelimiters').checked,
|
||||
MetadataSavers: Array.prototype.map.call(Array.prototype.filter.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
||||
return elem.checked;
|
||||
}), elem => {
|
||||
|
@ -613,6 +618,8 @@ export function getLibraryOptions(parent) {
|
|||
}), elem => {
|
||||
return elem.getAttribute('data-lang');
|
||||
});
|
||||
options.CustomTagDelimiters = parent.querySelector('#customTagDelimitersInput').value.split('');
|
||||
options.DelimiterWhitelist = parent.querySelector('#tagDelimiterWhitelist').value.split('\n').filter(item => item.trim());
|
||||
setSubtitleFetchersIntoOptions(parent, options);
|
||||
setLyricFetchersIntoOptions(parent, options);
|
||||
setMetadataFetchersIntoOptions(parent, options);
|
||||
|
@ -661,12 +668,16 @@ export function setLibraryOptions(parent, options) {
|
|||
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;
|
||||
parent.querySelector('#chkRequirePerfectMatch').checked = options.RequirePerfectSubtitleMatch;
|
||||
parent.querySelector('#chkAutomaticallyAddToCollection').checked = options.AutomaticallyAddToCollection;
|
||||
parent.querySelector('#chkPreferNonstandardArtistsTag').checked = options.PreferNonstandardArtistsTag;
|
||||
parent.querySelector('#chkUseCustomTagDelimiters').checked = options.UseCustomTagDelimiters;
|
||||
Array.prototype.forEach.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
|
||||
elem.checked = options.MetadataSavers ? options.MetadataSavers.includes(elem.getAttribute('data-pluginname')) : elem.getAttribute('data-defaultenabled') === 'true';
|
||||
});
|
||||
Array.prototype.forEach.call(parent.querySelectorAll('.chkSubtitleLanguage'), elem => {
|
||||
elem.checked = !!options.SubtitleDownloadLanguages && options.SubtitleDownloadLanguages.includes(elem.getAttribute('data-lang'));
|
||||
});
|
||||
parent.querySelector('#customTagDelimitersInput').value = options.CustomTagDelimiters.join('');
|
||||
parent.querySelector('#tagDelimiterWhitelist').value = options.DelimiterWhitelist.filter(item => item.trim()).join('\n');
|
||||
renderMetadataReaders(parent, getOrderedPlugins(parent.availableOptions.MetadataReaders, options.LocalMetadataReaderOrder || []));
|
||||
renderMetadataFetchers(parent, parent.availableOptions, options);
|
||||
renderImageFetchers(parent, parent.availableOptions, options);
|
||||
|
|
|
@ -216,3 +216,29 @@
|
|||
<div class="fieldDescription checkboxFieldDescription">${SaveLyricsIntoMediaFoldersHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audioTagSettingsSection hide">
|
||||
<h2>${LabelAudioTagSettings}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkPreferNonstandardArtistsTag" />
|
||||
<span>${PreferNonStandardArtistsTag}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${PreferNonstandardArtistsTagHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkUseCustomTagDelimiters" />
|
||||
<span>${UseCustomTagDelimiters}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${UseCustomTagDelimitersHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input type="text" is="emby-input" id="customTagDelimitersInput" label="${LabelCustomTagDelimiters}" value="/|;\"/>
|
||||
<div class="fieldDescription">${LabelCustomTagDelimitersHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<textarea is="emby-textarea" id="tagDelimiterWhitelist" label="${LabelDelimiterWhitelist}" class="textarea-mono" style="resize: none;min-height:2.5em"></textarea>
|
||||
<div class="fieldDescription">${LabelDelimiterWhitelistHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,8 @@ import ServerConnections from '../ServerConnections';
|
|||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import template from './metadataEditor.template.html';
|
||||
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status';
|
||||
|
||||
let currentContext;
|
||||
let metadataEditorInfo;
|
||||
|
@ -541,7 +542,7 @@ function setFieldVisibilities(context, item) {
|
|||
hideElement('#fldPath', context);
|
||||
}
|
||||
|
||||
if (item.Type === 'Series' || item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Person') {
|
||||
if ([BaseItemKind.Series, BaseItemKind.Season, BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Trailer, BaseItemKind.Person].includes(item.Type)) {
|
||||
showElement('#fldOriginalName', context);
|
||||
} else {
|
||||
hideElement('#fldOriginalName', context);
|
||||
|
@ -717,7 +718,7 @@ function setFieldVisibilities(context, item) {
|
|||
showElement('#fldDisplayOrder', context);
|
||||
hideElement('.seriesDisplayOrderDescription', context);
|
||||
|
||||
context.querySelector('#selectDisplayOrder').innerHTML = '<option value="SortName">' + globalize.translate('SortName') + '</option><option value="PremiereDate">' + globalize.translate('ReleaseDate') + '</option>';
|
||||
context.querySelector('#selectDisplayOrder').innerHTML = '<option value="Default">' + globalize.translate('DateModified') + '<option value="SortName">' + globalize.translate('SortName') + '</option><option value="PremiereDate">' + globalize.translate('ReleaseDate') + '</option>';
|
||||
} else if (item.Type === 'Series') {
|
||||
showElement('#fldDisplayOrder', context);
|
||||
showElement('.seriesDisplayOrderDescription', context);
|
||||
|
|
|
@ -87,7 +87,7 @@ function onSelectionChange() {
|
|||
updateItemSelection(this, this.checked);
|
||||
}
|
||||
|
||||
function showSelection(item, isChecked) {
|
||||
function showSelection(item, isChecked, addInitialCheck) {
|
||||
let itemSelectionPanel = item.querySelector('.itemSelectionPanel');
|
||||
|
||||
if (!itemSelectionPanel) {
|
||||
|
@ -99,7 +99,7 @@ function showSelection(item, isChecked) {
|
|||
parent.appendChild(itemSelectionPanel);
|
||||
|
||||
let cssClass = 'chkItemSelect';
|
||||
if (isChecked) {
|
||||
if (isChecked && addInitialCheck) {
|
||||
cssClass += ' checkedInitial';
|
||||
}
|
||||
const checkedAttribute = isChecked ? ' checked' : '';
|
||||
|
@ -361,11 +361,11 @@ function combineVersions(apiClient, selection) {
|
|||
});
|
||||
}
|
||||
|
||||
function showSelections(initialCard) {
|
||||
function showSelections(initialCard, addInitialCheck) {
|
||||
import('../../elements/emby-checkbox/emby-checkbox').then(() => {
|
||||
const cards = document.querySelectorAll('.card');
|
||||
for (let i = 0, length = cards.length; i < length; i++) {
|
||||
showSelection(cards[i], initialCard === cards[i]);
|
||||
showSelection(cards[i], initialCard === cards[i], addInitialCheck);
|
||||
}
|
||||
|
||||
showSelectionCommands();
|
||||
|
@ -402,7 +402,7 @@ export default function (options) {
|
|||
const card = dom.parentWithClass(e.target, 'card');
|
||||
|
||||
if (card) {
|
||||
showSelections(card);
|
||||
showSelections(card, true);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
@ -500,7 +500,7 @@ export default function (options) {
|
|||
touchTarget = null;
|
||||
|
||||
if (card) {
|
||||
showSelections(card);
|
||||
showSelections(card, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -569,7 +569,7 @@ export default function (options) {
|
|||
}
|
||||
|
||||
export const startMultiSelect = (card) => {
|
||||
showSelections(card);
|
||||
showSelections(card, false);
|
||||
};
|
||||
|
||||
export const stopMultiSelect = () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'
|
|||
import { MediaError } from 'types/mediaError';
|
||||
import { getMediaError } from 'utils/mediaError';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
|
||||
|
||||
const UNLIMITED_ITEMS = -1;
|
||||
|
||||
|
@ -697,7 +698,7 @@ function sortPlayerTargets(a, b) {
|
|||
return aVal.localeCompare(bVal);
|
||||
}
|
||||
|
||||
class PlaybackManager {
|
||||
export class PlaybackManager {
|
||||
constructor() {
|
||||
const self = this;
|
||||
|
||||
|
@ -2417,20 +2418,20 @@ class PlaybackManager {
|
|||
});
|
||||
}
|
||||
|
||||
function rankStreamType(prevIndex, prevSource, mediaSource, streamType, isSecondarySubtitle) {
|
||||
function rankStreamType(prevIndex, prevSource, mediaStreams, trackOptions, streamType, isSecondarySubtitle) {
|
||||
if (prevIndex == -1) {
|
||||
console.debug(`AutoSet ${streamType} - No Stream Set`);
|
||||
if (streamType == 'Subtitle') {
|
||||
if (isSecondarySubtitle) {
|
||||
mediaSource.DefaultSecondarySubtitleStreamIndex = -1;
|
||||
trackOptions.DefaultSecondarySubtitleStreamIndex = -1;
|
||||
} else {
|
||||
mediaSource.DefaultSubtitleStreamIndex = -1;
|
||||
trackOptions.DefaultSubtitleStreamIndex = -1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prevSource.MediaStreams || !mediaSource.MediaStreams) {
|
||||
if (!prevSource.MediaStreams || !mediaStreams) {
|
||||
console.debug(`AutoSet ${streamType} - No MediaStreams`);
|
||||
return;
|
||||
}
|
||||
|
@ -2456,7 +2457,7 @@ class PlaybackManager {
|
|||
}
|
||||
|
||||
let newRelIndex = 0;
|
||||
for (const stream of mediaSource.MediaStreams) {
|
||||
for (const stream of mediaStreams) {
|
||||
if (stream.Type != streamType) continue;
|
||||
|
||||
let score = 0;
|
||||
|
@ -2479,38 +2480,38 @@ class PlaybackManager {
|
|||
console.debug(`AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.`);
|
||||
if (streamType == 'Subtitle') {
|
||||
if (isSecondarySubtitle) {
|
||||
mediaSource.DefaultSecondarySubtitleStreamIndex = bestStreamIndex;
|
||||
trackOptions.DefaultSecondarySubtitleStreamIndex = bestStreamIndex;
|
||||
} else {
|
||||
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
|
||||
trackOptions.DefaultSubtitleStreamIndex = bestStreamIndex;
|
||||
}
|
||||
}
|
||||
if (streamType == 'Audio') {
|
||||
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
|
||||
trackOptions.DefaultAudioStreamIndex = bestStreamIndex;
|
||||
}
|
||||
} else {
|
||||
console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`);
|
||||
}
|
||||
}
|
||||
|
||||
function autoSetNextTracks(prevSource, mediaSource, audio, subtitle) {
|
||||
function autoSetNextTracks(prevSource, mediaStreams, trackOptions, audio, subtitle) {
|
||||
try {
|
||||
if (!prevSource) return;
|
||||
|
||||
if (!mediaSource) {
|
||||
console.warn('AutoSet - No mediaSource');
|
||||
if (!mediaStreams) {
|
||||
console.warn('AutoSet - No mediaStreams');
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio && typeof prevSource.DefaultAudioStreamIndex == 'number') {
|
||||
rankStreamType(prevSource.DefaultAudioStreamIndex, prevSource, mediaSource, 'Audio');
|
||||
rankStreamType(prevSource.DefaultAudioStreamIndex, prevSource, mediaStreams, trackOptions, 'Audio');
|
||||
}
|
||||
|
||||
if (subtitle && typeof prevSource.DefaultSubtitleStreamIndex == 'number') {
|
||||
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaSource, 'Subtitle');
|
||||
rankStreamType(prevSource.DefaultSubtitleStreamIndex, prevSource, mediaStreams, trackOptions, 'Subtitle');
|
||||
}
|
||||
|
||||
if (subtitle && typeof prevSource.DefaultSecondarySubtitleStreamIndex == 'number') {
|
||||
rankStreamType(prevSource.DefaultSecondarySubtitleStreamIndex, prevSource, mediaSource, 'Subtitle', true);
|
||||
rankStreamType(prevSource.DefaultSecondarySubtitleStreamIndex, prevSource, mediaStreams, trackOptions, 'Subtitle', true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`AutoSet - Caught unexpected error: ${e}`);
|
||||
|
@ -2582,12 +2583,25 @@ class PlaybackManager {
|
|||
});
|
||||
}
|
||||
|
||||
return Promise.all([promise, player.getDeviceProfile(item)]).then(function (responses) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
let mediaSourceId;
|
||||
|
||||
const isLiveTv = [BaseItemKind.TvChannel, BaseItemKind.LiveTvChannel].includes(item.Type);
|
||||
|
||||
if (!isLiveTv) {
|
||||
mediaSourceId = playOptions.mediaSourceId || item.Id;
|
||||
}
|
||||
|
||||
const getMediaStreams = isLiveTv ? Promise.resolve([]) : apiClient.getItem(apiClient.getCurrentUserId(), mediaSourceId)
|
||||
.then(fullItem => {
|
||||
return fullItem.MediaStreams;
|
||||
});
|
||||
|
||||
return Promise.all([promise, player.getDeviceProfile(item), apiClient.getCurrentUser(), getMediaStreams]).then(function (responses) {
|
||||
const deviceProfile = responses[1];
|
||||
const user = responses[2];
|
||||
const mediaStreams = responses[3];
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
const mediaSourceId = playOptions.mediaSourceId;
|
||||
const audioStreamIndex = playOptions.audioStreamIndex;
|
||||
const subtitleStreamIndex = playOptions.subtitleStreamIndex;
|
||||
const options = {
|
||||
|
@ -2610,9 +2624,20 @@ class PlaybackManager {
|
|||
// this reference was only needed by sendPlaybackListToPlayer
|
||||
playOptions.items = null;
|
||||
|
||||
const trackOptions = {};
|
||||
|
||||
autoSetNextTracks(prevSource, mediaStreams, trackOptions, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
|
||||
if (trackOptions.DefaultAudioStreamIndex != null) {
|
||||
options.audioStreamIndex = trackOptions.DefaultAudioStreamIndex;
|
||||
}
|
||||
if (trackOptions.DefaultSubtitleStreamIndex != null) {
|
||||
options.subtitleStreamIndex = trackOptions.DefaultSubtitleStreamIndex;
|
||||
}
|
||||
|
||||
return getPlaybackMediaSource(player, apiClient, deviceProfile, item, mediaSourceId, options).then(async (mediaSource) => {
|
||||
const user = await apiClient.getCurrentUser();
|
||||
autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections);
|
||||
if (trackOptions.DefaultSecondarySubtitleStreamIndex != null) {
|
||||
mediaSource.DefaultSecondarySubtitleStreamIndex = trackOptions.DefaultSecondarySubtitleStreamIndex;
|
||||
}
|
||||
|
||||
if (mediaSource.DefaultSubtitleStreamIndex == null || mediaSource.DefaultSubtitleStreamIndex < 0) {
|
||||
mediaSource.DefaultSubtitleStreamIndex = mediaSource.DefaultSecondarySubtitleStreamIndex;
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import React, { type FC } from 'react';
|
||||
import { useSearchSuggestions } from 'hooks/searchHook';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../lib/globalize';
|
||||
import LinkButton from 'elements/emby-button/LinkButton';
|
||||
import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions';
|
||||
import globalize from 'lib/globalize';
|
||||
import LinkButton from '../../elements/emby-button/LinkButton';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
|
||||
interface SearchSuggestionsProps {
|
||||
parentId?: string;
|
||||
}
|
||||
type SearchSuggestionsProps = {
|
||||
parentId?: string | null;
|
||||
};
|
||||
|
||||
const SearchSuggestions: FC<SearchSuggestionsProps> = ({ parentId }) => {
|
||||
const { isLoading, data: suggestions } = useSearchSuggestions(parentId);
|
||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => {
|
||||
const { isLoading, data: suggestions } = useSearchSuggestions(parentId || undefined);
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
|
@ -27,15 +29,12 @@ const SearchSuggestions: FC<SearchSuggestionsProps> = ({ parentId }) => {
|
|||
</div>
|
||||
|
||||
<div className='searchSuggestionsList padded-left padded-right'>
|
||||
{suggestions?.map((item) => (
|
||||
<div key={`suggestion-${item.Id}`}>
|
||||
{suggestions?.map(item => (
|
||||
<div key={item.Id}>
|
||||
<LinkButton
|
||||
className='button-link'
|
||||
style={{ display: 'inline-block', padding: '0.5em 1em' }}
|
||||
href={appRouter.getRouteUrl(item)}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.5em 1em'
|
||||
}}
|
||||
>
|
||||
{item.Name}
|
||||
</LinkButton>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue