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

Merge branch 'master' into burn-subtitle-transcoding

This commit is contained in:
gnattu 2024-10-09 06:53:18 +08:00 committed by GitHub
commit da4265eb46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 3292 additions and 1480 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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