Compare commits
37 commits
smithflix
...
release-12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b1f89036 | ||
|
|
8e63a4d19e | ||
|
|
572c5b7ec2 | ||
|
|
f4bfac091b | ||
|
|
eedd461948 | ||
|
|
9974e5acff | ||
|
|
f0b1704226 | ||
|
|
0eaa6e6939 | ||
|
|
42e4153610 | ||
|
|
cf5f4bbb12 | ||
|
|
88de8d5a9a | ||
|
|
75a8d33fe8 | ||
|
|
0983ce70ca | ||
|
|
d1bd735876 | ||
|
|
83c9422703 | ||
|
|
656667fc72 | ||
|
|
972a2e9418 | ||
|
|
4a47914aea | ||
|
|
ab61f96622 | ||
|
|
21585dc615 | ||
|
|
ab8cdad5e0 | ||
|
|
5dfccb9260 | ||
|
|
4cffcb7d34 | ||
|
|
8a2e3f5c67 | ||
|
|
008fbeefac | ||
|
|
7a2050fb75 | ||
|
|
b634438f7a | ||
|
|
cfbaf2b83f | ||
|
|
2c14c06904 | ||
|
|
0f87efad60 | ||
|
|
351f87be32 | ||
|
|
48bf0dba9a | ||
|
|
56933f723d | ||
|
|
e99f756ecf | ||
|
|
97cc9e9322 | ||
|
|
2389ae1d98 | ||
|
|
21add08619 |
29 changed files with 348 additions and 272 deletions
|
|
@ -110,6 +110,7 @@
|
|||
- [austinhardaway](https://github.com/austinhardaway)
|
||||
- [Alex Dickens](https://github.com/alex-dicko)
|
||||
- [shindouj](https://github.com/shindouj)
|
||||
- [TheDreadPirate](https://github.com/thedreaddpirate)
|
||||
|
||||
## Emby Contributors
|
||||
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "12.1.0",
|
||||
"version": "12.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jellyfin-web",
|
||||
"version": "12.1.0",
|
||||
"version": "12.2.0",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.14.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jellyfin-web",
|
||||
"version": "12.1.0",
|
||||
"version": "12.2.0",
|
||||
"description": "Web interface for Jellyfin",
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ function renderVideoSelections(page, mediaSources) {
|
|||
titleParts.push(v.Codec.toUpperCase());
|
||||
}
|
||||
|
||||
return '<option value="' + v.Index + '" ' + selected + '>' + (v.DisplayTitle || titleParts.join(' ')) + '</option>';
|
||||
return `<option value="${v.Index}" ${selected}>${escapeHtml(v.DisplayTitle || titleParts.join(' '))}</option>`;
|
||||
}).join('');
|
||||
select.setAttribute('disabled', 'disabled');
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ function renderAudioSelections(page, mediaSources) {
|
|||
const selectedId = mediaSource.DefaultAudioStreamIndex;
|
||||
select.innerHTML = tracks.map(function (v) {
|
||||
const selected = v.Index === selectedId ? ' selected' : '';
|
||||
return '<option value="' + v.Index + '" ' + selected + '>' + v.DisplayTitle + '</option>';
|
||||
return `<option value="${v.Index}" ${selected}>${escapeHtml(v.DisplayTitle)}</option>`;
|
||||
}).join('');
|
||||
|
||||
if (tracks.length > 1) {
|
||||
|
|
@ -309,7 +309,7 @@ function renderSubtitleSelections(page, mediaSources) {
|
|||
let selected = selectedId === -1 ? ' selected' : '';
|
||||
select.innerHTML = '<option value="-1">' + globalize.translate('Off') + '</option>' + tracks.map(function (v) {
|
||||
selected = v.Index === selectedId ? ' selected' : '';
|
||||
return '<option value="' + v.Index + '" ' + selected + '>' + v.DisplayTitle + '</option>';
|
||||
return `<option value="${v.Index}" ${selected}>${escapeHtml(v.DisplayTitle)}</option>`;
|
||||
}).join('');
|
||||
|
||||
if (tracks.length > 0) {
|
||||
|
|
@ -729,12 +729,22 @@ function renderLinks(page, item) {
|
|||
const links = [];
|
||||
|
||||
if (!layoutManager.tv && item.HomePageUrl) {
|
||||
links.push(`<a is="emby-linkbutton" class="button-link" href="${item.HomePageUrl}" target="_blank">${globalize.translate('ButtonWebsite')}</a>`);
|
||||
try {
|
||||
const parsedUrl = new URL(item.HomePageUrl);
|
||||
links.push(`<a is="emby-linkbutton" class="button-link" href="${parsedUrl.href}" target="_blank">${globalize.translate('ButtonWebsite')}</a>`);
|
||||
} catch {
|
||||
console.error('[renderLinks] Failed to parse home page URL', item.HomePageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.ExternalUrls) {
|
||||
for (const url of item.ExternalUrls) {
|
||||
links.push(`<a is="emby-linkbutton" class="button-link" href="${url.Url}" target="_blank">${escapeHtml(url.Name)}</a>`);
|
||||
try {
|
||||
const parsedUrl = new URL(url.Url);
|
||||
links.push(`<a is="emby-linkbutton" class="button-link" href="${parsedUrl.href}" target="_blank">${escapeHtml(url.Name)}</a>`);
|
||||
} catch {
|
||||
console.error('[renderLinks] Failed to parse external URL', url.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ const SearchFields: FC<SearchFieldsProps> = ({
|
|||
data-keyboard='true'
|
||||
placeholder={globalize.translate('Search')}
|
||||
autoComplete='off'
|
||||
maxLength={40}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
value={query}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import Theaters from '@mui/icons-material/Theaters';
|
|||
import MusicVideo from '@mui/icons-material/MusicVideo';
|
||||
import Book from '@mui/icons-material/Book';
|
||||
import Queue from '@mui/icons-material/Queue';
|
||||
import Quiz from '@mui/icons-material/Quiz';
|
||||
import VideoLibrary from '@mui/icons-material/VideoLibrary';
|
||||
import Folder from '@mui/icons-material/Folder';
|
||||
import OndemandVideo from '@mui/icons-material/OndemandVideo';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { MetaView } from '../constants/metaView';
|
||||
|
|
@ -51,7 +51,7 @@ const LibraryIcon: FC<LibraryIconProps> = ({
|
|||
case CollectionType.Playlists:
|
||||
return <Queue />;
|
||||
case undefined:
|
||||
return <Quiz />;
|
||||
return <OndemandVideo />;
|
||||
default:
|
||||
return <Folder />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import FilterAltOff from '@mui/icons-material/FilterAltOff';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
|
@ -62,9 +64,8 @@ const LibraryToolbar: FC = () => {
|
|||
const isPending = itemsResult?.isPending ?? true;
|
||||
const totalRecordCount = itemsResult?.data?.TotalRecordCount ?? 0;
|
||||
const items = itemsResult?.data?.Items ?? [];
|
||||
const hasFilters = Object.values(viewSettings?.Filters ?? {}).some(
|
||||
(filter) => !!filter
|
||||
);
|
||||
const hasFilterOptions = Object.values(viewSettings?.Filters ?? {}).some(f => !!f);
|
||||
const hasFilters = !!viewSettings?.Alphabet || hasFilterOptions;
|
||||
const { user } = useApi();
|
||||
const canCreateCollections = user?.Policy?.IsAdministrator || user?.Policy?.EnableCollectionManagement;
|
||||
|
||||
|
|
@ -106,141 +107,172 @@ const LibraryToolbar: FC = () => {
|
|||
<LibraryViewMenu />
|
||||
|
||||
{!MENU_ONLY_VIEWS.includes(viewType) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
},
|
||||
justifyContent: 'flex-end',
|
||||
marginLeft: 1
|
||||
}}
|
||||
>
|
||||
<Chip label={itemCountDisplay} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!MENU_ONLY_VIEWS.includes(viewType) && (
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={1}
|
||||
sx={{
|
||||
justifyContent: {
|
||||
xs: 'auto',
|
||||
sm: 'flex-end'
|
||||
},
|
||||
flexBasis: {
|
||||
xs: '100%',
|
||||
sm: 'auto'
|
||||
},
|
||||
flexGrow: 1,
|
||||
marginTop: {
|
||||
xs: 1,
|
||||
sm: 0.5
|
||||
},
|
||||
marginBottom: 0.5
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: {
|
||||
xs: 'row-reverse',
|
||||
sm: 'row'
|
||||
},
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
}
|
||||
},
|
||||
justifyContent: 'flex-start',
|
||||
marginLeft: 1
|
||||
}}
|
||||
>
|
||||
<Chip label={itemCountDisplay} />
|
||||
|
||||
{hasFilters && (
|
||||
<Button
|
||||
variant='text'
|
||||
color='primary'
|
||||
startIcon={<FilterAltOff />}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => setLibraryViewSettings(prevState => ({
|
||||
...prevState,
|
||||
StartIndex: 0,
|
||||
Alphabet: undefined,
|
||||
Filters: undefined
|
||||
}))}
|
||||
sx={{
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
sm: 1
|
||||
},
|
||||
marginRight: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ResetFilters')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Stack
|
||||
direction='row'
|
||||
spacing={1}
|
||||
sx={{
|
||||
justifyContent: {
|
||||
xs: 'auto',
|
||||
sm: 'flex-end'
|
||||
},
|
||||
flexBasis: {
|
||||
xs: '100%',
|
||||
sm: 'auto'
|
||||
},
|
||||
flexGrow: 1,
|
||||
marginTop: {
|
||||
xs: 1,
|
||||
sm: 0.5
|
||||
},
|
||||
marginBottom: 0.5
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexGrow: {
|
||||
xs: 1,
|
||||
sm: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
size={isSmallScreen ? undefined : 'small'}
|
||||
>
|
||||
{isBtnPlayAllEnabled && (
|
||||
<PlayAllButton
|
||||
item={item}
|
||||
items={items}
|
||||
viewType={viewType}
|
||||
collectionType={collectionType}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnShuffleEnabled && (
|
||||
<ShuffleButton
|
||||
item={item}
|
||||
items={items}
|
||||
viewType={viewType}
|
||||
collectionType={collectionType}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
|
||||
<QueueButton
|
||||
item={item}
|
||||
items={items}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{isBtnNewCollectionEnabled && canCreateCollections && (
|
||||
<NewCollectionButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
|
||||
)}
|
||||
{isBtnNewPlaylistEnabled && (
|
||||
<NewPlaylistButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ButtonGroup
|
||||
variant='contained'
|
||||
color='inherit'
|
||||
variant='text'
|
||||
size={isSmallScreen ? undefined : 'small'}
|
||||
>
|
||||
{isBtnPlayAllEnabled && (
|
||||
<PlayAllButton
|
||||
item={item}
|
||||
items={items}
|
||||
{isBtnFilterEnabled && (
|
||||
<FilterButton
|
||||
parentId={parentId}
|
||||
itemType={itemType ?? []}
|
||||
viewType={viewType}
|
||||
collectionType={collectionType}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen}
|
||||
hasFilters={hasFilterOptions}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnShuffleEnabled && (
|
||||
<ShuffleButton
|
||||
item={item}
|
||||
items={items}
|
||||
{isBtnSortEnabled && (
|
||||
<SortButton
|
||||
viewType={viewType}
|
||||
collectionType={collectionType}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnQueueEnabled && item && playbackManager.canQueue(item) && (
|
||||
<QueueButton
|
||||
item={item}
|
||||
items={items}
|
||||
hasFilters={hasFilters}
|
||||
isTextVisible={isSmallScreen && !isBtnPlayAllEnabled && !isBtnShuffleEnabled}
|
||||
{isBtnGridListEnabled && (
|
||||
<ViewSettingsButton
|
||||
viewType={viewType}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{isBtnNewCollectionEnabled && canCreateCollections && (
|
||||
<NewCollectionButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
|
||||
)}
|
||||
{isBtnNewPlaylistEnabled && (
|
||||
<NewPlaylistButton isTextVisible={isSmallScreen} queryKey={allItemsQueryKey} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ButtonGroup
|
||||
color='inherit'
|
||||
variant='text'
|
||||
size={isSmallScreen ? undefined : 'small'}
|
||||
>
|
||||
{isBtnFilterEnabled && (
|
||||
<FilterButton
|
||||
parentId={parentId}
|
||||
itemType={itemType ?? []}
|
||||
viewType={viewType}
|
||||
hasFilters={hasFilters}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
{isPaginationEnabled && isUserPaginationEnabled && (
|
||||
<Pagination
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
index={startIndex}
|
||||
pageSize={paginationLimit}
|
||||
total={totalRecordCount}
|
||||
disabled={isPending || !isPaginationRequired || itemsResult?.isPlaceholderData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnSortEnabled && (
|
||||
<SortButton
|
||||
viewType={viewType}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBtnGridListEnabled && (
|
||||
<ViewSettingsButton
|
||||
viewType={viewType}
|
||||
libraryViewSettings={libraryViewSettings}
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
{isPaginationEnabled && isUserPaginationEnabled && (
|
||||
<Pagination
|
||||
setLibraryViewSettings={setLibraryViewSettings}
|
||||
index={startIndex}
|
||||
pageSize={paginationLimit}
|
||||
total={totalRecordCount}
|
||||
disabled={isPending || !isPaginationRequired || itemsResult?.isPlaceholderData}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite
|
|||
import React, { FC, useCallback } from 'react';
|
||||
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
|
||||
import FilterAlt from '@mui/icons-material/FilterAlt';
|
||||
import Clear from '@mui/icons-material/Clear';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import Popover from '@mui/material/Popover';
|
||||
|
|
@ -115,16 +114,6 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
setAnchorEl(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleResetFiltersClick = useCallback(() => {
|
||||
if (hasFilters) {
|
||||
setLibraryViewSettings((prevState) => ({
|
||||
...prevState,
|
||||
StartIndex: 0,
|
||||
Filters: {}
|
||||
}));
|
||||
}
|
||||
}, [hasFilters, setLibraryViewSettings]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, []);
|
||||
|
|
@ -518,19 +507,6 @@ const FilterButton: FC<FilterButtonProps> = ({
|
|||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)}
|
||||
<Button
|
||||
disabled={!hasFilters}
|
||||
title={globalize.translate('ResetFilters')}
|
||||
aria-describedby={id}
|
||||
onClick={handleResetFiltersClick}
|
||||
fullWidth={true}
|
||||
startIcon={<Clear />}
|
||||
sx={{
|
||||
justifyContent: 'right'
|
||||
}}
|
||||
>
|
||||
{globalize.translate('ResetFilters')}
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ const Backdrop = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className='backdropContainer' />
|
||||
<div className='backdropContainer'>
|
||||
<div id='backdropImage0' className='backdropImage' />
|
||||
<div id='backdropImage1' className='backdropImage' />
|
||||
</div>
|
||||
<div className='backgroundContainer' />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,24 @@ const QueryClientEventHandler: FC = () => {
|
|||
const queryClient = useQueryClient();
|
||||
const { user } = useApi();
|
||||
|
||||
const clearQueries = useCallback(() => (
|
||||
queryClient.clear()
|
||||
), [queryClient]);
|
||||
|
||||
const invalidateItemQueries = useCallback(() => (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['User', user?.Id, 'Items']
|
||||
})
|
||||
), [queryClient, user?.Id]);
|
||||
|
||||
useEffect(() => {
|
||||
Events.on(document, EventType.LANGUAGE_CHANGE, clearQueries);
|
||||
|
||||
return () => {
|
||||
Events.off(document, EventType.LANGUAGE_CHANGE, clearQueries);
|
||||
};
|
||||
}, [clearQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
Events.on(document, EventType.REFRESH_NEEDED, invalidateItemQueries);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { type FC, useEffect, useState } from 'react';
|
|||
|
||||
import { useUserTheme } from 'hooks/useUserTheme';
|
||||
import { getDefaultTheme } from 'scripts/settings/webSettings';
|
||||
import themeManager from 'scripts/themeManager';
|
||||
|
||||
interface ThemeCssProps {
|
||||
dashboard?: boolean
|
||||
|
|
@ -19,7 +20,10 @@ const ThemeCss: FC<ThemeCssProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
const id = dashboard ? dashboardTheme : theme;
|
||||
if (id) setThemeUrl(getThemeUrl(id));
|
||||
if (id) {
|
||||
setThemeUrl(getThemeUrl(id));
|
||||
void themeManager.setTheme(id);
|
||||
}
|
||||
}, [dashboard, dashboardTheme, theme]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function enableRotation() {
|
|||
}
|
||||
|
||||
class Backdrop {
|
||||
load(url, parent) {
|
||||
load(url, slot) {
|
||||
const img = new Image();
|
||||
const self = this;
|
||||
|
||||
|
|
@ -25,14 +25,13 @@ class Backdrop {
|
|||
return;
|
||||
}
|
||||
|
||||
const backdropImage = document.createElement('div');
|
||||
backdropImage.classList.add('backdropImage');
|
||||
backdropImage.classList.add('displayingBackdropImage');
|
||||
const backdropImage = document.getElementById(`backdropImage${slot}`);
|
||||
backdropImage.style.backgroundImage = `url('${url}')`;
|
||||
backdropImage.setAttribute('data-url', url);
|
||||
backdropImage.classList.add('displayingBackdropImage', 'backdropImageFadeIn');
|
||||
self.currentAnimatingElement = backdropImage;
|
||||
|
||||
backdropImage.classList.add('backdropImageFadeIn');
|
||||
parent.appendChild(backdropImage);
|
||||
document.getElementById(`backdropImage${1 - slot}`)?.classList.remove('displayingBackdropImage');
|
||||
|
||||
if (!enableAnimation()) {
|
||||
internalBackdrop(true);
|
||||
|
|
@ -46,6 +45,8 @@ class Backdrop {
|
|||
if (backdropImage === self.currentAnimatingElement) {
|
||||
self.currentAnimatingElement = null;
|
||||
}
|
||||
|
||||
backdropImage?.classList.remove('backdropImageFadeIn');
|
||||
};
|
||||
|
||||
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
|
|
@ -95,8 +96,12 @@ export function clearBackdrop(clearAll) {
|
|||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
elem.innerHTML = '';
|
||||
// Remove backdrop images from each slot element
|
||||
document.querySelectorAll('.backdropImage').forEach(elem => {
|
||||
elem.classList.remove('displayingBackdropImage', 'backdropImageFadeIn');
|
||||
elem.style.backgroundImage = 'none';
|
||||
elem.removeAttribute('data-url');
|
||||
});
|
||||
|
||||
if (clearAll) {
|
||||
hasExternalBackdrop = false;
|
||||
|
|
@ -134,12 +139,8 @@ export function externalBackdrop(isEnabled) {
|
|||
}
|
||||
|
||||
let currentLoadingBackdrop;
|
||||
let currentBackdropSlot = 0;
|
||||
function setBackdropImage(url) {
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
|
||||
// If the current backdrop image is the same as the new one, do nothing
|
||||
|
|
@ -148,8 +149,9 @@ function setBackdropImage(url) {
|
|||
}
|
||||
|
||||
const instance = new Backdrop();
|
||||
instance.load(url, elem);
|
||||
instance.load(url, currentBackdropSlot);
|
||||
currentLoadingBackdrop = instance;
|
||||
currentBackdropSlot = 1 - currentBackdropSlot;
|
||||
}
|
||||
|
||||
function getItemImageUrls(item, imageOptions) {
|
||||
|
|
@ -244,14 +246,6 @@ function onRotationInterval() {
|
|||
currentRotationIndex = newIndex;
|
||||
const currentImage = currentRotatingImages[newIndex];
|
||||
setBackdropImage(currentImage);
|
||||
|
||||
// Remove old images after a delay to allow fade-in animation (800ms) to complete
|
||||
setTimeout(() => {
|
||||
const oldImages = getBackdropContainer().querySelectorAll(`.backdropImage:not([data-url="${currentImage}"])`);
|
||||
oldImages.forEach(img => {
|
||||
img.remove();
|
||||
});
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
function clearRotation() {
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.displayingBackdropImage {
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,21 +27,27 @@ const Image: FC<ImageProps> = ({
|
|||
containImage
|
||||
}) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isTransitionComplete, setIsTransitionComplete] = useState(false);
|
||||
const [isLoadStarted, setIsLoadStarted] = useState(false);
|
||||
|
||||
const fadeinDuration = userSettings.enableFastFadein() ? '0.1s' : '0.5s';
|
||||
const transitionDuration = isLoaded ? fadeinDuration : 'none';
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleTransitionEnd = useCallback(() => {
|
||||
setIsTransitionComplete(true);
|
||||
}, []);
|
||||
|
||||
const handleLoadStarted = useCallback(() => {
|
||||
setIsLoadStarted(true);
|
||||
}, []);
|
||||
|
||||
const fadeinDuration = userSettings.enableFastFadein() ? '0.1s' : '0.5s';
|
||||
const transitionDuration = isLoaded ? fadeinDuration : 'none';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoaded && isLoadStarted && blurhash && userSettings.enableBlurhash() && (
|
||||
{!isTransitionComplete && isLoadStarted && blurhash && userSettings.enableBlurhash() && (
|
||||
<BlurhashCanvas
|
||||
hash={blurhash}
|
||||
width= {20}
|
||||
|
|
@ -64,6 +70,7 @@ const Image: FC<ImageProps> = ({
|
|||
transition: transitionDuration
|
||||
}}
|
||||
onLoad={handleLoad}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
beforeLoad={handleLoadStarted}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -216,15 +216,6 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 50em) {
|
||||
.newTvProgram,
|
||||
.liveTvProgram,
|
||||
.premiereTvProgram,
|
||||
.guideHdIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.channelPrograms + .channelPrograms,
|
||||
.guide-channelHeaderCell + .guide-channelHeaderCell {
|
||||
margin-top: -1px;
|
||||
|
|
@ -271,6 +262,15 @@
|
|||
contain: strict;
|
||||
flex-grow: 1;
|
||||
margin: 0 !important;
|
||||
|
||||
@media all and (max-width: 50em) {
|
||||
.newTvProgram,
|
||||
.liveTvProgram,
|
||||
.premiereTvProgram,
|
||||
.guideHdIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guideProgramName {
|
||||
|
|
|
|||
|
|
@ -306,28 +306,29 @@ export async function getCommands(options) {
|
|||
});
|
||||
}
|
||||
|
||||
// Only show playlist edit actions if the user can modify the playlist
|
||||
if (item.PlaylistItemId && options.playlistId && options.canEditPlaylist) {
|
||||
commands.push({
|
||||
name: globalize.translate('RemoveFromPlaylist'),
|
||||
id: 'removefromplaylist',
|
||||
icon: 'playlist_remove'
|
||||
});
|
||||
}
|
||||
|
||||
if (item.PlaylistItemId && options.playlistId && item.PlaylistIndex > 0) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveToTop'),
|
||||
id: 'movetotop',
|
||||
icon: 'vertical_align_top'
|
||||
});
|
||||
}
|
||||
if (item.PlaylistIndex > 0) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveToTop'),
|
||||
id: 'movetotop',
|
||||
icon: 'vertical_align_top'
|
||||
});
|
||||
}
|
||||
|
||||
if (item.PlaylistItemId && options.playlistId && item.PlaylistIndex < (item.PlaylistItemCount - 1)) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveToBottom'),
|
||||
id: 'movetobottom',
|
||||
icon: 'vertical_align_bottom'
|
||||
});
|
||||
if (item.PlaylistIndex < (item.PlaylistItemCount - 1)) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveToBottom'),
|
||||
id: 'movetobottom',
|
||||
icon: 'vertical_align_bottom'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.collectionId) {
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ function populateMetadataSettings(parent, contentType) {
|
|||
renderMediaSegmentProviders(parent, availableOptions, {});
|
||||
renderImageFetchers(parent, availableOptions, {});
|
||||
renderSimilarItemProviders(parent, availableOptions, {});
|
||||
availableOptions.SubtitleFetchers.length ? parent.querySelector('.subtitleDownloadSettings').classList.remove('hide') : parent.querySelector('.subtitleDownloadSettings').classList.add('hide');
|
||||
availableOptions.SubtitleFetchers.length ? parent.querySelector('.subtitleProviderSettings').classList.remove('hide') : parent.querySelector('.subtitleProviderSettings').classList.add('hide');
|
||||
}).catch(() => {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
|
@ -557,8 +557,10 @@ export function setContentType(parent, contentType) {
|
|||
|
||||
if (contentType === 'tvshows' || contentType === 'movies' || contentType === 'musicvideos' || contentType === 'mixed') {
|
||||
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.remove('hide');
|
||||
parent.querySelector('.subtitleDownloadSettings').classList.remove('hide');
|
||||
} else {
|
||||
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.add('hide');
|
||||
parent.querySelector('.subtitleDownloadSettings').classList.add('hide');
|
||||
}
|
||||
|
||||
if (contentType === 'music') {
|
||||
|
|
|
|||
|
|
@ -166,45 +166,48 @@ <h2>${HeaderChapterImages}</h2>
|
|||
|
||||
<div class="subtitleDownloadSettings hide">
|
||||
<h2>${HeaderSubtitleDownloads}</h2>
|
||||
<div class="subtitleProviderSettings hide">
|
||||
<div>
|
||||
<h3 class="checkboxListLabel">${LabelDownloadLanguages}</h3>
|
||||
<div class="subtitleDownloadLanguages paperList checkboxList" style="max-height: 10.5em; overflow-y: auto; padding: .5em 1em;">
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<h3 class="checkboxListLabel">${LabelDownloadLanguages}</h3>
|
||||
<div class="subtitleDownloadLanguages paperList checkboxList" style="max-height: 10.5em; overflow-y: auto; padding: .5em 1em;">
|
||||
<div class="subtitleFetchers advanced" style="margin-bottom: 2em;">
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkRequirePerfectMatch" checked />
|
||||
<span>${OptionRequirePerfectSubtitleMatch}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionRequirePerfectSubtitleMatchHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSkipIfAudioTrackPresent" />
|
||||
<span>${LabelSkipIfAudioTrackPresent}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSkipIfAudioTrackPresentHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSkipIfGraphicalSubsPresent" />
|
||||
<span>${LabelSkipIfGraphicalSubsPresent}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSkipIfGraphicalSubsPresentHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="subtitleFetchers advanced" style="margin-bottom: 2em;">
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkRequirePerfectMatch" checked />
|
||||
<span>${OptionRequirePerfectSubtitleMatch}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionRequirePerfectSubtitleMatchHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSkipIfAudioTrackPresent" />
|
||||
<span>${LabelSkipIfAudioTrackPresent}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSkipIfAudioTrackPresentHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSkipIfGraphicalSubsPresent" />
|
||||
<span>${LabelSkipIfGraphicalSubsPresent}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSkipIfGraphicalSubsPresentHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription advanced">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSaveSubtitlesLocally" checked />
|
||||
<span>${SaveSubtitlesIntoMediaFolders}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${SaveSubtitlesIntoMediaFoldersHelp}</div>
|
||||
<div class="fldSaveSubtitlesSettings">
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSaveSubtitlesLocally" checked />
|
||||
<span>${SaveSubtitlesIntoMediaFolders}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${SaveSubtitlesIntoMediaFoldersHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import './elements/emby-button/emby-button';
|
|||
import './components/playback/displayMirrorManager';
|
||||
import './components/playback/playerSelectionMenu';
|
||||
import './components/themeMediaPlayer';
|
||||
import './scripts/autoThemes';
|
||||
import './scripts/mouseManager';
|
||||
import './scripts/screensavermanager';
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ function getTextTrackUrl(track, item, format) {
|
|||
}
|
||||
|
||||
function getSubtitleFileNameHint(track) {
|
||||
const candidates = [track?.Path, track?.DeliveryUrl];
|
||||
const candidates = [track?.Path];
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ export default class PhotoPlayer {
|
|||
interactive: true,
|
||||
// playbackManager.shuffle has no options. So treat 'shuffle' as a 'play' action
|
||||
autoplay: {
|
||||
delay: userSettings.slideshowInterval() * 1000
|
||||
delay: userSettings.slideshowInterval() * 1000,
|
||||
enabled: !!(options.autoplay || options.shuffle)
|
||||
},
|
||||
user: result,
|
||||
onClose: () => screenSaverManager.unblock()
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import * as userSettings from './settings/userSettings';
|
||||
import skinManager from './themeManager';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { pageClassOn } from 'utils/dashboard';
|
||||
import Events from 'utils/events.ts';
|
||||
|
||||
// Set the default theme when loading
|
||||
skinManager.setTheme(userSettings.theme())
|
||||
/* this keeps the scrollbar always present in all pages, so we avoid clipping while switching between pages
|
||||
that need the scrollbar and pages that don't.
|
||||
*/
|
||||
.then(() => document.body.classList.add('force-scroll'));
|
||||
|
||||
// set the saved theme once a user authenticates
|
||||
Events.on(ServerConnections, 'localusersignedin', () => {
|
||||
skinManager.setTheme(userSettings.theme());
|
||||
});
|
||||
|
||||
pageClassOn('viewbeforeshow', 'page', function () {
|
||||
if (this.classList.contains('type-interior')) {
|
||||
skinManager.setTheme(userSettings.dashboardTheme());
|
||||
} else {
|
||||
skinManager.setTheme(userSettings.theme());
|
||||
}
|
||||
});
|
||||
|
|
@ -10,10 +10,6 @@ function isTv(userAgent) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (userAgent.includes('samsungbrowser')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (userAgent.includes('viera')) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,22 @@ describe('Browser', () => {
|
|||
expect(browser.xboxOne).toBe(true);
|
||||
expect(browser.tv).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Samsung Tizen TV devices', () => {
|
||||
// 2024 Samsung TV (Tizen 8.0)
|
||||
const browser = detectBrowser('Mozilla/5.0 (SMART-TV; Linux; Tizen 8.0) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.0 Chrome/108.0.5359.1 TV Safari/537.36');
|
||||
expect(browser.tizen).toBe(true);
|
||||
expect(browser.tizenVersion).toBe(8);
|
||||
expect(browser.tv).toBe(true);
|
||||
expect(browser.mobile).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not identify Samsung Browser (Android) as a TV', () => {
|
||||
// Ref: https://developer.samsung.com/browser/user-agent-string-format.html
|
||||
const browser = detectBrowser('Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/117.0.0.0 Mobile Safari/537.36');
|
||||
expect(browser.android).toBe(true);
|
||||
expect(browser.mobile).toBe(true);
|
||||
expect(browser.tizen).toBeFalsy();
|
||||
expect(browser.tv).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -526,6 +526,7 @@ export default function (options) {
|
|||
};
|
||||
|
||||
let videoAudioCodecs = [];
|
||||
let mkvAudioCodecs = [];
|
||||
let hlsInTsVideoAudioCodecs = [];
|
||||
let hlsInFmp4VideoAudioCodecs = [];
|
||||
|
||||
|
|
@ -562,12 +563,14 @@ export default function (options) {
|
|||
// Prefer AAC, MP3 to other codecs when audio transcoding.
|
||||
if (canPlayAacVideoAudio) {
|
||||
videoAudioCodecs.push('aac');
|
||||
mkvAudioCodecs.push('aac');
|
||||
hlsInTsVideoAudioCodecs.push('aac');
|
||||
hlsInFmp4VideoAudioCodecs.push('aac');
|
||||
}
|
||||
|
||||
if (supportsMp3VideoAudio) {
|
||||
videoAudioCodecs.push('mp3');
|
||||
mkvAudioCodecs.push('mp3');
|
||||
}
|
||||
|
||||
// Safari supports mp3 with HLS, but only in mpegts container, and the supportsMp3VideoAudio will return false.
|
||||
|
|
@ -584,12 +587,14 @@ export default function (options) {
|
|||
// Do not use AC3 for audio transcoding unless AAC and MP3 are not supported.
|
||||
if (canPlayAc3VideoAudio) {
|
||||
videoAudioCodecs.push('ac3');
|
||||
mkvAudioCodecs.push('ac3');
|
||||
if (browser.edgeChromium) {
|
||||
hlsInFmp4VideoAudioCodecs.push('ac3');
|
||||
}
|
||||
|
||||
if (canPlayEac3VideoAudio) {
|
||||
videoAudioCodecs.push('eac3');
|
||||
mkvAudioCodecs.push('eac3');
|
||||
if (browser.edgeChromium) {
|
||||
hlsInFmp4VideoAudioCodecs.push('eac3');
|
||||
}
|
||||
|
|
@ -608,6 +613,7 @@ export default function (options) {
|
|||
|
||||
if (supportsMp2VideoAudio) {
|
||||
videoAudioCodecs.push('mp2');
|
||||
mkvAudioCodecs.push('mp2');
|
||||
hlsInTsVideoAudioCodecs.push('mp2');
|
||||
hlsInFmp4VideoAudioCodecs.push('mp2');
|
||||
}
|
||||
|
|
@ -620,30 +626,37 @@ export default function (options) {
|
|||
if (supportsDts) {
|
||||
videoAudioCodecs.push('dca');
|
||||
videoAudioCodecs.push('dts');
|
||||
mkvAudioCodecs.push('dca', 'dts');
|
||||
}
|
||||
|
||||
if (browser.tizen || browser.web0s) {
|
||||
videoAudioCodecs.push('pcm_s16le');
|
||||
videoAudioCodecs.push('pcm_s24le');
|
||||
mkvAudioCodecs.push('pcm_s16le', 'pcm_s24le');
|
||||
}
|
||||
|
||||
if (appSettings.enableTrueHd() || options.supportsTrueHd) {
|
||||
videoAudioCodecs.push('truehd');
|
||||
mkvAudioCodecs.push('truehd');
|
||||
}
|
||||
|
||||
if (browser.tizen) {
|
||||
videoAudioCodecs.push('aac_latm');
|
||||
mkvAudioCodecs.push('aac_latm');
|
||||
}
|
||||
|
||||
if (canPlayAudioFormat('opus')) {
|
||||
videoAudioCodecs.push('opus');
|
||||
webmAudioCodecs.push('opus');
|
||||
if (browser.tizen) {
|
||||
hlsInTsVideoAudioCodecs.push('opus');
|
||||
// On Tizen 3/4/5, OPUS is supported only in MKV/WebM
|
||||
// On Tizen 6, OPUS appears to be supported in MP4 as well, but not in TS (that requires refactoring of TS profile generation)
|
||||
if (!browser.tizen) {
|
||||
videoAudioCodecs.push('opus');
|
||||
}
|
||||
mkvAudioCodecs.push('opus');
|
||||
webmAudioCodecs.push('opus');
|
||||
hlsInFmp4VideoAudioCodecs.push('opus');
|
||||
} else if (safariSupportsOpus) {
|
||||
videoAudioCodecs.push('opus');
|
||||
mkvAudioCodecs.push('opus');
|
||||
webmAudioCodecs.push('opus');
|
||||
hlsInFmp4VideoAudioCodecs.push('opus');
|
||||
}
|
||||
|
|
@ -651,11 +664,13 @@ export default function (options) {
|
|||
// FLAC audio in video plays with a delay on Tizen
|
||||
if (canPlayAudioFormat('flac') && !browser.tizen) {
|
||||
videoAudioCodecs.push('flac');
|
||||
mkvAudioCodecs.push('flac');
|
||||
hlsInFmp4VideoAudioCodecs.push('flac');
|
||||
}
|
||||
|
||||
if (canPlayAudioFormat('alac')) {
|
||||
videoAudioCodecs.push('alac');
|
||||
mkvAudioCodecs.push('alac');
|
||||
hlsInFmp4VideoAudioCodecs.push('alac');
|
||||
}
|
||||
|
||||
|
|
@ -663,6 +678,8 @@ export default function (options) {
|
|||
return (options.disableVideoAudioCodecs || []).indexOf(c) === -1;
|
||||
});
|
||||
|
||||
mkvAudioCodecs = mkvAudioCodecs.filter(c => !(options.disableVideoAudioCodecs || []).includes(c));
|
||||
|
||||
hlsInTsVideoAudioCodecs = hlsInTsVideoAudioCodecs.filter(function (c) {
|
||||
return (options.disableHlsVideoAudioCodecs || []).indexOf(c) === -1;
|
||||
});
|
||||
|
|
@ -750,6 +767,7 @@ export default function (options) {
|
|||
|
||||
if ((!browser.safari && canPlayVp8) || browser.tizen) {
|
||||
videoAudioCodecs.push('vorbis');
|
||||
mkvAudioCodecs.push('vorbis');
|
||||
}
|
||||
|
||||
if (webmVideoCodecs.length) {
|
||||
|
|
@ -775,7 +793,7 @@ export default function (options) {
|
|||
Container: 'mkv',
|
||||
Type: 'Video',
|
||||
VideoCodec: mp4VideoCodecs.join(','),
|
||||
AudioCodec: videoAudioCodecs.join(',')
|
||||
AudioCodec: mkvAudioCodecs.join(',')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const _GAMEPAD_LEFT_THUMBSTICK_LEFT_KEYCODE = 37;
|
|||
const _GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEYCODE = 39;
|
||||
const _THUMB_STICK_THRESHOLD = 0.75;
|
||||
|
||||
let _lastGamepad = -1;
|
||||
let _leftThumbstickUpPressed = false;
|
||||
let _leftThumbstickDownPressed = false;
|
||||
let _leftThumbstickLeftPressed = false;
|
||||
|
|
@ -257,22 +258,38 @@ let inputLoopTimer;
|
|||
function runInputLoop() {
|
||||
// Get the latest gamepad state.
|
||||
const gamepads = navigator.getGamepads(); /* eslint-disable-line compat/compat */
|
||||
|
||||
let lastGamepad = _lastGamepad;
|
||||
_lastGamepad = -1;
|
||||
|
||||
if (!gamepads[lastGamepad]) lastGamepad = -1;
|
||||
|
||||
for (let i = 0, len = gamepads.length; i < len; i++) {
|
||||
if (lastGamepad >= 0 && i !== lastGamepad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gamepad = gamepads[i];
|
||||
|
||||
if (!gamepad) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Iterate through the axes
|
||||
const axes = gamepad.axes;
|
||||
const leftStickX = axes[0];
|
||||
const leftStickY = axes[1];
|
||||
if (leftStickX > _THUMB_STICK_THRESHOLD) { // Right
|
||||
_lastGamepad = i;
|
||||
_ButtonPressedState.setleftThumbstickRight(true);
|
||||
} else if (leftStickX < -_THUMB_STICK_THRESHOLD) { // Left
|
||||
_lastGamepad = i;
|
||||
_ButtonPressedState.setleftThumbstickLeft(true);
|
||||
} else if (leftStickY < -_THUMB_STICK_THRESHOLD) { // Up
|
||||
_lastGamepad = i;
|
||||
_ButtonPressedState.setleftThumbstickUp(true);
|
||||
} else if (leftStickY > _THUMB_STICK_THRESHOLD) { // Down
|
||||
_lastGamepad = i;
|
||||
_ButtonPressedState.setleftThumbstickDown(true);
|
||||
} else {
|
||||
_ButtonPressedState.setleftThumbstickLeft(false);
|
||||
|
|
@ -285,6 +302,8 @@ function runInputLoop() {
|
|||
for (let j = 0, buttonsLen = buttons.length; j < buttonsLen; j++) {
|
||||
if (ProcessedButtons.indexOf(j) !== -1) {
|
||||
if (buttons[j].pressed) {
|
||||
_lastGamepad = i;
|
||||
|
||||
switch (j) {
|
||||
case _GAMEPAD_DPAD_UP_BUTTON_INDEX:
|
||||
_ButtonPressedState.setdPadUp(true);
|
||||
|
|
@ -347,6 +366,8 @@ function runInputLoop() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_lastGamepad >= 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -583,7 +583,7 @@ export class UserSettings {
|
|||
return this.set('stillWatchingPrompt', val, false);
|
||||
}
|
||||
|
||||
return this.get('stillWatchingPrompt', false) || StillWatchingOptions.Default;
|
||||
return this.get('stillWatchingPrompt', false) || StillWatchingOptions.Disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ describe('getLibraryIcon()', () => {
|
|||
|
||||
it('Should return the correct icon for nonstandard types', () => {
|
||||
expect(getLibraryIcon(undefined))
|
||||
.toBe('quiz');
|
||||
.toBe('ondemand_video');
|
||||
expect(getLibraryIcon('channels'))
|
||||
.toBe('videocam');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function getLibraryIcon(library: CollectionType | string | null | undefin
|
|||
case 'channels':
|
||||
return 'videocam';
|
||||
case undefined:
|
||||
return 'quiz';
|
||||
return 'ondemand_video';
|
||||
default:
|
||||
return 'folder';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue