mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into subtitle-improvements-2
This commit is contained in:
commit
f872f604f9
59 changed files with 1693 additions and 489 deletions
|
@ -30,6 +30,7 @@ module.exports = {
|
||||||
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
'brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
||||||
'comma-dangle': ['error', 'never'],
|
'comma-dangle': ['error', 'never'],
|
||||||
'comma-spacing': ['error'],
|
'comma-spacing': ['error'],
|
||||||
|
'curly': ['error', 'multi-line', 'consistent'],
|
||||||
'default-case-last': ['error'],
|
'default-case-last': ['error'],
|
||||||
'eol-last': ['error'],
|
'eol-last': ['error'],
|
||||||
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
'indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||||
|
@ -48,6 +49,7 @@ module.exports = {
|
||||||
'no-empty-function': ['error'],
|
'no-empty-function': ['error'],
|
||||||
'no-extend-native': ['error'],
|
'no-extend-native': ['error'],
|
||||||
'no-floating-decimal': ['error'],
|
'no-floating-decimal': ['error'],
|
||||||
|
'no-lonely-if': ['error'],
|
||||||
'no-multi-spaces': ['error'],
|
'no-multi-spaces': ['error'],
|
||||||
'no-multiple-empty-lines': ['error', { 'max': 1 }],
|
'no-multiple-empty-lines': ['error', { 'max': 1 }],
|
||||||
'no-nested-ternary': ['error'],
|
'no-nested-ternary': ['error'],
|
||||||
|
@ -63,6 +65,7 @@ module.exports = {
|
||||||
'no-trailing-spaces': ['error'],
|
'no-trailing-spaces': ['error'],
|
||||||
'no-unused-expressions': ['off'],
|
'no-unused-expressions': ['off'],
|
||||||
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
'@typescript-eslint/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
|
||||||
|
'no-unused-private-class-members': ['error'],
|
||||||
'no-useless-constructor': ['off'],
|
'no-useless-constructor': ['off'],
|
||||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||||
'no-var': ['error'],
|
'no-var': ['error'],
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
- [Merlin Sievers](https://github.com/dann-merlin)
|
- [Merlin Sievers](https://github.com/dann-merlin)
|
||||||
- [Fishbigger](https://github.com/fishbigger)
|
- [Fishbigger](https://github.com/fishbigger)
|
||||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||||
|
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|
899
package-lock.json
generated
899
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -38,6 +38,7 @@
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-sonarjs": "0.19.0",
|
"eslint-plugin-sonarjs": "0.19.0",
|
||||||
"expose-loader": "4.1.0",
|
"expose-loader": "4.1.0",
|
||||||
|
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||||
"html-loader": "4.2.0",
|
"html-loader": "4.2.0",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.5.3",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
"sass": "1.62.1",
|
"sass": "1.62.1",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "13.3.2",
|
||||||
"source-map-loader": "4.0.1",
|
"source-map-loader": "4.0.1",
|
||||||
|
"speed-measure-webpack-plugin": "1.5.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"stylelint": "15.6.2",
|
"stylelint": "15.6.2",
|
||||||
"stylelint-config-rational-order": "0.1.2",
|
"stylelint-config-rational-order": "0.1.2",
|
||||||
|
@ -57,6 +59,7 @@
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.4.4",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"webpack": "5.88.1",
|
"webpack": "5.88.1",
|
||||||
|
"webpack-bundle-analyzer": "4.9.1",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "4.15.1",
|
"webpack-dev-server": "4.15.1",
|
||||||
"webpack-merge": "5.9.0",
|
"webpack-merge": "5.9.0",
|
||||||
|
@ -136,6 +139,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run serve",
|
"start": "npm run serve",
|
||||||
"serve": "webpack serve --config webpack.dev.js",
|
"serve": "webpack serve --config webpack.dev.js",
|
||||||
|
"build:analyze": "cross-env NODE_ENV=\"production\" webpack --config webpack.analyze.js",
|
||||||
"build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js",
|
"build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js",
|
||||||
"build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js",
|
"build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js",
|
||||||
"build:check": "tsc --noEmit",
|
"build:check": "tsc --noEmit",
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { ButtonGroup, IconButton } from '@mui/material';
|
||||||
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
|
import ViewListIcon from '@mui/icons-material/ViewList';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { LibraryViewSettings, ViewMode } from 'types/library';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
import ViewSettingsButton from './ViewSettingsButton';
|
||||||
|
|
||||||
|
interface GridListViewButtonProps {
|
||||||
|
viewType: LibraryTab;
|
||||||
|
libraryViewSettings: LibraryViewSettings;
|
||||||
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridListViewButton: FC<GridListViewButtonProps> = ({
|
||||||
|
viewType,
|
||||||
|
libraryViewSettings,
|
||||||
|
setLibraryViewSettings
|
||||||
|
}) => {
|
||||||
|
const handleToggleCurrentView = useCallback(() => {
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
ViewMode:
|
||||||
|
prevState.ViewMode === ViewMode.ListView ? ViewMode.GridView : ViewMode.ListView
|
||||||
|
}));
|
||||||
|
}, [setLibraryViewSettings]);
|
||||||
|
|
||||||
|
const isGridView = libraryViewSettings.ViewMode === ViewMode.GridView;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
{isGridView ? (
|
||||||
|
<ViewSettingsButton
|
||||||
|
viewType={viewType}
|
||||||
|
libraryViewSettings={libraryViewSettings}
|
||||||
|
setLibraryViewSettings={setLibraryViewSettings}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
title={globalize.translate('GridView')}
|
||||||
|
className='paper-icon-button-light autoSize'
|
||||||
|
disabled={isGridView}
|
||||||
|
onClick={handleToggleCurrentView}
|
||||||
|
>
|
||||||
|
<ViewModuleIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={globalize.translate('ListView')}
|
||||||
|
className='paper-icon-button-light autoSize'
|
||||||
|
disabled={!isGridView}
|
||||||
|
onClick={handleToggleCurrentView}
|
||||||
|
>
|
||||||
|
<ViewListIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GridListViewButton;
|
198
src/apps/experimental/components/library/ViewSettingsButton.tsx
Normal file
198
src/apps/experimental/components/library/ViewSettingsButton.tsx
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import Popover from '@mui/material/Popover';
|
||||||
|
import ViewComfyIcon from '@mui/icons-material/ViewComfy';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { LibraryViewSettings, ViewMode } from 'types/library';
|
||||||
|
import { LibraryTab } from 'types/libraryTab';
|
||||||
|
|
||||||
|
const imageTypesOptions = [
|
||||||
|
{ label: 'Primary', value: ImageType.Primary },
|
||||||
|
{ label: 'Banner', value: ImageType.Banner },
|
||||||
|
{ label: 'Disc', value: ImageType.Disc },
|
||||||
|
{ label: 'Logo', value: ImageType.Logo },
|
||||||
|
{ label: 'Thumb', value: ImageType.Thumb }
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ViewSettingsButtonProps {
|
||||||
|
viewType: LibraryTab;
|
||||||
|
libraryViewSettings: LibraryViewSettings;
|
||||||
|
setLibraryViewSettings: React.Dispatch<React.SetStateAction<LibraryViewSettings>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewSettingsButton: FC<ViewSettingsButtonProps> = ({
|
||||||
|
viewType,
|
||||||
|
libraryViewSettings,
|
||||||
|
setLibraryViewSettings
|
||||||
|
}) => {
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const id = open ? 'selectview-popover' : undefined;
|
||||||
|
|
||||||
|
const handleClick = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const name = event.target.name;
|
||||||
|
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: event.target.checked
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setLibraryViewSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectChange = useCallback(
|
||||||
|
(event: SelectChangeEvent) => {
|
||||||
|
setLibraryViewSettings((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
ImageType: event.target.value as ImageType
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[setLibraryViewSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getVisibleImageType = () => {
|
||||||
|
const visibleImageType: ImageType[] = [ImageType.Primary];
|
||||||
|
|
||||||
|
if (
|
||||||
|
viewType !== LibraryTab.Episodes
|
||||||
|
&& viewType !== LibraryTab.Artists
|
||||||
|
&& viewType !== LibraryTab.AlbumArtists
|
||||||
|
&& viewType !== LibraryTab.Albums
|
||||||
|
) {
|
||||||
|
visibleImageType.push(ImageType.Banner);
|
||||||
|
visibleImageType.push(ImageType.Disc);
|
||||||
|
visibleImageType.push(ImageType.Logo);
|
||||||
|
visibleImageType.push(ImageType.Thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleImageType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isViewSettingsEnabled = () => {
|
||||||
|
return libraryViewSettings.ViewMode !== ViewMode.ListView;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<IconButton
|
||||||
|
title={globalize.translate('ButtonSelectView')}
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
aria-describedby={id}
|
||||||
|
className='paper-icon-button-light btnShuffle autoSize'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<ViewComfyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'& .MuiFormControl-root': { m: 1, width: 220 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel id='select-sort-label'>
|
||||||
|
<Typography component='span'>
|
||||||
|
{globalize.translate('LabelImageType')}
|
||||||
|
</Typography>
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={libraryViewSettings.ImageType}
|
||||||
|
label={globalize.translate('LabelImageType')}
|
||||||
|
onChange={onSelectChange}
|
||||||
|
>
|
||||||
|
{imageTypesOptions
|
||||||
|
.filter((imageType) => getVisibleImageType().includes(imageType.value))
|
||||||
|
.map((imageType) => (
|
||||||
|
<MenuItem
|
||||||
|
key={imageType.value}
|
||||||
|
value={imageType.value}
|
||||||
|
>
|
||||||
|
<Typography component='span'>
|
||||||
|
{globalize.translate(imageType.label)}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{isViewSettingsEnabled() && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<FormControl>
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={libraryViewSettings.ShowTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='ShowTitle'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ShowTitle')}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={libraryViewSettings.ShowYear}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='ShowYear'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate('ShowYear')}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
libraryViewSettings.CardLayout
|
||||||
|
}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='CardLayout'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={globalize.translate(
|
||||||
|
'EnableCardLayout'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewSettingsButton;
|
|
@ -3,41 +3,32 @@ import browser from '../scripts/browser';
|
||||||
import dialog from './dialog/dialog';
|
import dialog from './dialog/dialog';
|
||||||
import globalize from '../scripts/globalize';
|
import globalize from '../scripts/globalize';
|
||||||
|
|
||||||
function useNativeAlert() {
|
|
||||||
// webOS seems to block modals
|
|
||||||
// Tizen 2.x seems to block modals
|
|
||||||
return !browser.web0s
|
|
||||||
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
|
||||||
&& browser.tv
|
|
||||||
&& window.alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function (text, title) {
|
export default async function (text, title) {
|
||||||
let options;
|
// Modals seem to be blocked on Web OS and Tizen 2.x
|
||||||
if (typeof text === 'string') {
|
const canUseNativeAlert = !!(
|
||||||
options = {
|
!browser.web0s
|
||||||
title: title,
|
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
||||||
text: text
|
&& browser.tv
|
||||||
};
|
&& window.alert
|
||||||
} else {
|
);
|
||||||
options = text;
|
|
||||||
}
|
const options = typeof text === 'string' ? { title, text } : text;
|
||||||
|
|
||||||
await appRouter.ready();
|
await appRouter.ready();
|
||||||
|
|
||||||
if (useNativeAlert()) {
|
if (canUseNativeAlert) {
|
||||||
alert((options.text || '').replaceAll('<br/>', '\n'));
|
alert((options.text || '').replaceAll('<br/>', '\n'));
|
||||||
return Promise.resolve();
|
|
||||||
} else {
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
items.push({
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
options.buttons = [
|
||||||
|
{
|
||||||
name: globalize.translate('ButtonGotIt'),
|
name: globalize.translate('ButtonGotIt'),
|
||||||
id: 'ok',
|
id: 'ok',
|
||||||
type: 'submit'
|
type: 'submit'
|
||||||
});
|
}
|
||||||
|
];
|
||||||
|
|
||||||
options.buttons = items;
|
return dialog.show(options);
|
||||||
return dialog.show(options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -803,19 +803,17 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
||||||
} else {
|
} else {
|
||||||
lines.push(escapeHtml(item.SeriesName));
|
lines.push(escapeHtml(item.SeriesName));
|
||||||
}
|
}
|
||||||
|
} else if (isUsingLiveTvNaming(item)) {
|
||||||
|
lines.push(escapeHtml(item.Name));
|
||||||
|
|
||||||
|
if (!item.EpisodeTitle && !item.IndexNumber) {
|
||||||
|
titleAdded = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isUsingLiveTvNaming(item)) {
|
const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || '';
|
||||||
lines.push(escapeHtml(item.Name));
|
|
||||||
|
|
||||||
if (!item.EpisodeTitle && !item.IndexNumber) {
|
if (parentTitle || showTitle) {
|
||||||
titleAdded = true;
|
lines.push(escapeHtml(parentTitle));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || '';
|
|
||||||
|
|
||||||
if (parentTitle || showTitle) {
|
|
||||||
lines.push(escapeHtml(parentTitle));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -898,13 +896,11 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
||||||
if (item.Type === 'Series') {
|
if (item.Type === 'Series') {
|
||||||
if (item.Status === 'Continuing') {
|
if (item.Status === 'Continuing') {
|
||||||
lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
|
lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
|
||||||
|
} else if (item.EndDate && item.ProductionYear) {
|
||||||
|
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
|
||||||
|
lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
|
||||||
} else {
|
} else {
|
||||||
if (item.EndDate && item.ProductionYear) {
|
lines.push(productionYear || '');
|
||||||
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
|
|
||||||
lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
|
|
||||||
} else {
|
|
||||||
lines.push(productionYear || '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(productionYear || '');
|
lines.push(productionYear || '');
|
||||||
|
|
|
@ -762,12 +762,10 @@ function Guide(options) {
|
||||||
} else {
|
} else {
|
||||||
container.scrollTo(0, pos);
|
container.scrollTo(0, pos);
|
||||||
}
|
}
|
||||||
|
} else if (horizontal) {
|
||||||
|
container.scrollLeft = Math.round(pos);
|
||||||
} else {
|
} else {
|
||||||
if (horizontal) {
|
container.scrollTop = Math.round(pos);
|
||||||
container.scrollLeft = Math.round(pos);
|
|
||||||
} else {
|
|
||||||
container.scrollTop = Math.round(pos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -216,14 +216,12 @@ function getFetchLatestItemsFn(serverId, parentId, collectionType) {
|
||||||
if (collectionType === 'music') {
|
if (collectionType === 'music') {
|
||||||
limit = 30;
|
limit = 30;
|
||||||
}
|
}
|
||||||
|
} else if (collectionType === 'tvshows') {
|
||||||
|
limit = 5;
|
||||||
|
} else if (collectionType === 'music') {
|
||||||
|
limit = 9;
|
||||||
} else {
|
} else {
|
||||||
if (collectionType === 'tvshows') {
|
limit = 8;
|
||||||
limit = 5;
|
|
||||||
} else if (collectionType === 'music') {
|
|
||||||
limit = 9;
|
|
||||||
} else {
|
|
||||||
limit = 8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
@ -76,20 +76,18 @@ export function handleHlsJsMediaError(instance, reject) {
|
||||||
recoverDecodingErrorDate = now;
|
recoverDecodingErrorDate = now;
|
||||||
console.debug('try to recover media Error ...');
|
console.debug('try to recover media Error ...');
|
||||||
hlsPlayer.recoverMediaError();
|
hlsPlayer.recoverMediaError();
|
||||||
|
} else if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
|
||||||
|
recoverSwapAudioCodecDate = now;
|
||||||
|
console.debug('try to swap Audio Codec and recover media Error ...');
|
||||||
|
hlsPlayer.swapAudioCodec();
|
||||||
|
hlsPlayer.recoverMediaError();
|
||||||
} else {
|
} else {
|
||||||
if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
|
console.error('cannot recover, last media error recovery failed ...');
|
||||||
recoverSwapAudioCodecDate = now;
|
|
||||||
console.debug('try to swap Audio Codec and recover media Error ...');
|
|
||||||
hlsPlayer.swapAudioCodec();
|
|
||||||
hlsPlayer.recoverMediaError();
|
|
||||||
} else {
|
|
||||||
console.error('cannot recover, last media error recovery failed ...');
|
|
||||||
|
|
||||||
if (reject) {
|
if (reject) {
|
||||||
reject();
|
reject();
|
||||||
} else {
|
} else {
|
||||||
onErrorInternal(instance, 'mediadecodeerror');
|
onErrorInternal(instance, 'mediadecodeerror');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,14 +171,12 @@ function getRemoteImageHtml(image, imageType) {
|
||||||
shape = 'banner';
|
shape = 'banner';
|
||||||
} else if (imageType === 'Disc') {
|
} else if (imageType === 'Disc') {
|
||||||
shape = 'square';
|
shape = 'square';
|
||||||
|
} else if (currentItemType === 'Episode') {
|
||||||
|
shape = 'backdrop';
|
||||||
|
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
||||||
|
shape = 'square';
|
||||||
} else {
|
} else {
|
||||||
if (currentItemType === 'Episode') {
|
shape = 'portrait';
|
||||||
shape = 'backdrop';
|
|
||||||
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
|
||||||
shape = 'square';
|
|
||||||
} else {
|
|
||||||
shape = 'portrait';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
|
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
|
||||||
|
@ -230,10 +228,8 @@ function getRemoteImageHtml(image, imageType) {
|
||||||
if (image.Language) {
|
if (image.Language) {
|
||||||
html += ' • ' + image.Language;
|
html += ' • ' + image.Language;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (image.Language) {
|
||||||
if (image.Language) {
|
html += image.Language;
|
||||||
html += image.Language;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
@ -244,16 +240,14 @@ function getRemoteImageHtml(image, imageType) {
|
||||||
|
|
||||||
if (image.RatingType === 'Likes') {
|
if (image.RatingType === 'Likes') {
|
||||||
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
|
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
|
||||||
} else {
|
} else if (image.CommunityRating) {
|
||||||
if (image.CommunityRating) {
|
html += image.CommunityRating.toFixed(1);
|
||||||
html += image.CommunityRating.toFixed(1);
|
|
||||||
|
|
||||||
if (image.VoteCount) {
|
if (image.VoteCount) {
|
||||||
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
|
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
html += 'Unrated';
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
html += 'Unrated';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
|
@ -164,10 +164,8 @@ function getCardHtml(image, apiClient, options) {
|
||||||
} else {
|
} else {
|
||||||
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
||||||
}
|
}
|
||||||
} else {
|
} else if (options.imageProviders.length) {
|
||||||
if (options.imageProviders.length) {
|
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
|
||||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||||
|
|
|
@ -374,14 +374,12 @@ export function getListViewHtml(options) {
|
||||||
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
|
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
|
||||||
textlines.push(item.AlbumArtist);
|
textlines.push(item.AlbumArtist);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (options.artist) {
|
||||||
if (options.artist) {
|
const artistItems = item.ArtistItems;
|
||||||
const artistItems = item.ArtistItems;
|
if (artistItems && item.Type !== 'MusicAlbum') {
|
||||||
if (artistItems && item.Type !== 'MusicAlbum') {
|
textlines.push(artistItems.map(a => {
|
||||||
textlines.push(artistItems.map(a => {
|
return a.Name;
|
||||||
return a.Name;
|
}).join(', '));
|
||||||
}).join(', '));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -649,10 +649,8 @@ function onPlaybackStopped(e, state) {
|
||||||
if (state.NextMediaType !== 'Audio') {
|
if (state.NextMediaType !== 'Audio') {
|
||||||
hideNowPlayingBar();
|
hideNowPlayingBar();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!state.NextMediaType) {
|
||||||
if (!state.NextMediaType) {
|
hideNowPlayingBar();
|
||||||
hideNowPlayingBar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import alert from '../alert';
|
||||||
import { PluginType } from '../../types/plugin.ts';
|
import { PluginType } from '../../types/plugin.ts';
|
||||||
import { includesAny } from '../../utils/container.ts';
|
import { includesAny } from '../../utils/container.ts';
|
||||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||||
|
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage';
|
||||||
|
|
||||||
const UNLIMITED_ITEMS = -1;
|
const UNLIMITED_ITEMS = -1;
|
||||||
|
|
||||||
|
@ -154,28 +155,6 @@ function mergePlaybackQueries(obj1, obj2) {
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
function backdropImageUrl(apiClient, item, options) {
|
|
||||||
options = options || {};
|
|
||||||
options.type = options.type || 'Backdrop';
|
|
||||||
|
|
||||||
// If not resizing, get the original image
|
|
||||||
if (!options.maxWidth && !options.width && !options.maxHeight && !options.height && !options.fillWidth && !options.fillHeight) {
|
|
||||||
options.quality = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.BackdropImageTags?.length) {
|
|
||||||
options.tag = item.BackdropImageTags[0];
|
|
||||||
return apiClient.getScaledImageUrl(item.Id, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.ParentBackdropImageTags?.length) {
|
|
||||||
options.tag = item.ParentBackdropImageTags[0];
|
|
||||||
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMimeType(type, container) {
|
function getMimeType(type, container) {
|
||||||
container = (container || '').toLowerCase();
|
container = (container || '').toLowerCase();
|
||||||
|
|
||||||
|
@ -1437,15 +1416,13 @@ class PlaybackManager {
|
||||||
|
|
||||||
if (Screenfull.isEnabled) {
|
if (Screenfull.isEnabled) {
|
||||||
Screenfull.toggle();
|
Screenfull.toggle();
|
||||||
} else {
|
} else if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
|
||||||
// iOS Safari
|
// iOS Safari
|
||||||
if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
|
document.webkitCancelFullscreen();
|
||||||
document.webkitCancelFullscreen();
|
} else {
|
||||||
} else {
|
const elem = document.querySelector('video');
|
||||||
const elem = document.querySelector('video');
|
if (elem?.webkitEnterFullscreen) {
|
||||||
if (elem?.webkitEnterFullscreen) {
|
elem.webkitEnterFullscreen();
|
||||||
elem.webkitEnterFullscreen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2345,30 +2322,23 @@ class PlaybackManager {
|
||||||
|
|
||||||
let prevRelIndex = 0;
|
let prevRelIndex = 0;
|
||||||
for (const stream of prevSource.MediaStreams) {
|
for (const stream of prevSource.MediaStreams) {
|
||||||
if (stream.Type != streamType)
|
if (stream.Type != streamType) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (stream.Index == prevIndex)
|
if (stream.Index == prevIndex) break;
|
||||||
break;
|
|
||||||
|
|
||||||
prevRelIndex += 1;
|
prevRelIndex += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newRelIndex = 0;
|
let newRelIndex = 0;
|
||||||
for (const stream of mediaSource.MediaStreams) {
|
for (const stream of mediaSource.MediaStreams) {
|
||||||
if (stream.Type != streamType)
|
if (stream.Type != streamType) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
if (prevStream.Codec == stream.Codec)
|
if (prevStream.Codec == stream.Codec) score += 1;
|
||||||
score += 1;
|
if (prevRelIndex == newRelIndex) score += 1;
|
||||||
if (prevRelIndex == newRelIndex)
|
if (prevStream.DisplayTitle && prevStream.DisplayTitle == stream.DisplayTitle) score += 2;
|
||||||
score += 1;
|
if (prevStream.Language && prevStream.Language != 'und' && prevStream.Language == stream.Language) score += 2;
|
||||||
if (prevStream.DisplayTitle && prevStream.DisplayTitle == stream.DisplayTitle)
|
|
||||||
score += 2;
|
|
||||||
if (prevStream.Language && prevStream.Language != 'und' && prevStream.Language == stream.Language)
|
|
||||||
score += 2;
|
|
||||||
|
|
||||||
console.debug(`AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`);
|
console.debug(`AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}`);
|
||||||
if (score > bestStreamScore && score >= 3) {
|
if (score > bestStreamScore && score >= 3) {
|
||||||
|
@ -2388,8 +2358,9 @@ class PlaybackManager {
|
||||||
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
|
mediaSource.DefaultSubtitleStreamIndex = bestStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (streamType == 'Audio')
|
if (streamType == 'Audio') {
|
||||||
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
|
mediaSource.DefaultAudioStreamIndex = bestStreamIndex;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`);
|
console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`);
|
||||||
}
|
}
|
||||||
|
@ -2693,7 +2664,7 @@ class PlaybackManager {
|
||||||
title: item.Name
|
title: item.Name
|
||||||
};
|
};
|
||||||
|
|
||||||
const backdropUrl = backdropImageUrl(apiClient, item, {});
|
const backdropUrl = getItemBackdropImageUrl(apiClient, item, {}, true);
|
||||||
if (backdropUrl) {
|
if (backdropUrl) {
|
||||||
resultInfo.backdropUrl = backdropUrl;
|
resultInfo.backdropUrl = backdropUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,22 +129,6 @@ class PluginManager {
|
||||||
.sort((p1, p2) => (p1.priority || 0) - (p2.priority || 0))[0];
|
.sort((p1, p2) => (p1.priority || 0) - (p2.priority || 0))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
#mapRoute(plugin, route) {
|
|
||||||
if (typeof plugin === 'string') {
|
|
||||||
plugin = this.pluginsList.filter((p) => {
|
|
||||||
return (p.id || p.packageName) === plugin;
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
route = route.path || route;
|
|
||||||
|
|
||||||
if (route.toLowerCase().startsWith('http')) {
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '/plugins/' + plugin.id + '/' + route;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapPath(plugin, path, addCacheParam) {
|
mapPath(plugin, path, addCacheParam) {
|
||||||
if (typeof plugin === 'string') {
|
if (typeof plugin === 'string') {
|
||||||
plugin = this.pluginsList.filter((p) => {
|
plugin = this.pluginsList.filter((p) => {
|
||||||
|
|
|
@ -191,15 +191,13 @@ function onRecordChange(e) {
|
||||||
loading.hide();
|
loading.hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (hasEnabledTimer) {
|
||||||
if (hasEnabledTimer) {
|
loading.show();
|
||||||
loading.show();
|
recordingHelper.cancelTimer(apiClient, this.TimerId, true).then(function () {
|
||||||
recordingHelper.cancelTimer(apiClient, this.TimerId, true).then(function () {
|
Events.trigger(self, 'recordingchanged');
|
||||||
Events.trigger(self, 'recordingchanged');
|
fetchData(self);
|
||||||
fetchData(self);
|
loading.hide();
|
||||||
loading.hide();
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,13 +221,11 @@ function onRecordSeriesChange(e) {
|
||||||
fetchData(self);
|
fetchData(self);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (this.SeriesTimerId) {
|
||||||
if (this.SeriesTimerId) {
|
apiClient.cancelLiveTvSeriesTimer(this.SeriesTimerId).then(function () {
|
||||||
apiClient.cancelLiveTvSeriesTimer(this.SeriesTimerId).then(function () {
|
toast(globalize.translate('RecordingCancelled'));
|
||||||
toast(globalize.translate('RecordingCancelled'));
|
fetchData(self);
|
||||||
fetchData(self);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -514,10 +514,12 @@ export default function (options) {
|
||||||
function toggleFullscreenButtons(isFullscreen) {
|
function toggleFullscreenButtons(isFullscreen) {
|
||||||
const btnFullscreen = dialog.querySelector('.btnFullscreen');
|
const btnFullscreen = dialog.querySelector('.btnFullscreen');
|
||||||
const btnFullscreenExit = dialog.querySelector('.btnFullscreenExit');
|
const btnFullscreenExit = dialog.querySelector('.btnFullscreenExit');
|
||||||
if (btnFullscreen)
|
if (btnFullscreen) {
|
||||||
btnFullscreen.classList.toggle('hide', isFullscreen);
|
btnFullscreen.classList.toggle('hide', isFullscreen);
|
||||||
if (btnFullscreenExit)
|
}
|
||||||
|
if (btnFullscreenExit) {
|
||||||
btnFullscreenExit.classList.toggle('hide', !isFullscreen);
|
btnFullscreenExit.classList.toggle('hide', !isFullscreen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -194,7 +194,8 @@ function renderSearchResults(context, results) {
|
||||||
|
|
||||||
html += '<span class="listItemIcon material-icons closed_caption" aria-hidden="true"></span>';
|
html += '<span class="listItemIcon material-icons closed_caption" aria-hidden="true"></span>';
|
||||||
|
|
||||||
const bodyClass = result.Comment || result.IsHashMatch ? 'three-line' : 'two-line';
|
const hasAnyFlags = result.IsHashMatch || result.AiTranslated || result.MachineTranslated || result.Forced || result.HearingImpaired;
|
||||||
|
const bodyClass = result.Comment || hasAnyFlags ? 'three-line' : 'two-line';
|
||||||
|
|
||||||
html += '<div class="listItemBody ' + bodyClass + '">';
|
html += '<div class="listItemBody ' + bodyClass + '">';
|
||||||
|
|
||||||
|
@ -206,16 +207,45 @@ function renderSearchResults(context, results) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.DownloadCount != null) {
|
if (result.DownloadCount != null) {
|
||||||
html += '<span>' + globalize.translate('DownloadsValue', result.DownloadCount) + '</span>';
|
html += '<span style="margin-right:1em;">' + globalize.translate('DownloadsValue', result.DownloadCount) + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.FrameRate) {
|
||||||
|
html += '<span>' + globalize.translate('Framerate') + ': ' + result.FrameRate + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
if (result.Comment) {
|
if (result.Comment) {
|
||||||
html += '<div class="secondary listItemBodyText">' + escapeHtml(result.Comment) + '</div>';
|
html += '<div class="secondary listItemBodyText" style="white-space:pre-line;">' + escapeHtml(result.Comment) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsHashMatch) {
|
if (hasAnyFlags) {
|
||||||
html += '<div class="secondary listItemBodyText"><div class="inline-flex align-items-center justify-content-center" style="background:#3388cc;color:#fff;padding: .3em 1em;border-radius:1000em;">' + globalize.translate('PerfectMatch') + '</div></div>';
|
html += '<div class="secondary listItemBodyText">';
|
||||||
|
|
||||||
|
const spanOpen = '<span class="inline-flex align-items-center justify-content-center subtitleFeaturePillow">';
|
||||||
|
|
||||||
|
if (result.IsHashMatch) {
|
||||||
|
html += spanOpen + globalize.translate('PerfectMatch') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.AiTranslated) {
|
||||||
|
html += spanOpen + globalize.translate('AiTranslated') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.MachineTranslated) {
|
||||||
|
html += spanOpen + globalize.translate('MachineTranslated') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Forced) {
|
||||||
|
html += spanOpen + globalize.translate('ForeignPartsOnly') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.HearingImpaired) {
|
||||||
|
html += spanOpen + globalize.translate('HearingImpairedShort') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
.originalSubtitleFileLabel {
|
.originalSubtitleFileLabel {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtitleFeaturePillow {
|
||||||
|
background: #00a4dc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
border-radius: 1000em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
|
@ -73,12 +73,10 @@ export function loadView(options) {
|
||||||
} else {
|
} else {
|
||||||
mainAnimatedPages.replaceChild(view, currentPage);
|
mainAnimatedPages.replaceChild(view, currentPage);
|
||||||
}
|
}
|
||||||
|
} else if (newViewInfo.hasScript && window.$) {
|
||||||
|
view = $(view).appendTo(mainAnimatedPages)[0];
|
||||||
} else {
|
} else {
|
||||||
if (newViewInfo.hasScript && window.$) {
|
mainAnimatedPages.appendChild(view);
|
||||||
view = $(view).appendTo(mainAnimatedPages)[0];
|
|
||||||
} else {
|
|
||||||
mainAnimatedPages.appendChild(view);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.type) {
|
if (options.type) {
|
||||||
|
|
|
@ -484,13 +484,11 @@ window.DashboardPage = {
|
||||||
if (nowPlayingItem.Artists?.length) {
|
if (nowPlayingItem.Artists?.length) {
|
||||||
bottomText = topText;
|
bottomText = topText;
|
||||||
topText = escapeHtml(nowPlayingItem.Artists[0]);
|
topText = escapeHtml(nowPlayingItem.Artists[0]);
|
||||||
} else {
|
} else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) {
|
||||||
if (nowPlayingItem.SeriesName || nowPlayingItem.Album) {
|
bottomText = topText;
|
||||||
bottomText = topText;
|
topText = escapeHtml(nowPlayingItem.SeriesName || nowPlayingItem.Album);
|
||||||
topText = escapeHtml(nowPlayingItem.SeriesName || nowPlayingItem.Album);
|
} else if (nowPlayingItem.ProductionYear) {
|
||||||
} else if (nowPlayingItem.ProductionYear) {
|
bottomText = nowPlayingItem.ProductionYear;
|
||||||
bottomText = nowPlayingItem.ProductionYear;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nowPlayingItem.ImageTags?.Logo) {
|
if (nowPlayingItem.ImageTags?.Logo) {
|
||||||
|
|
|
@ -110,10 +110,11 @@ function load(page, devices) {
|
||||||
deviceHtml += '<div class="cardFooter">';
|
deviceHtml += '<div class="cardFooter">';
|
||||||
|
|
||||||
if (canDelete(device.Id)) {
|
if (canDelete(device.Id)) {
|
||||||
if (globalize.getIsRTL())
|
if (globalize.getIsRTL()) {
|
||||||
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||||
else
|
} else {
|
||||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||||
|
}
|
||||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||||
deviceHtml += '</div>';
|
deviceHtml += '</div>';
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,10 +272,8 @@ function renderDirectPlayProfiles(page, profiles) {
|
||||||
if (profile.Type == 'Video') {
|
if (profile.Type == 'Video') {
|
||||||
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
} else {
|
} else if (profile.Type == 'Audio') {
|
||||||
if (profile.Type == 'Audio') {
|
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</a>';
|
html += '</a>';
|
||||||
|
@ -333,10 +331,8 @@ function renderTranscodingProfiles(page, profiles) {
|
||||||
if (profile.Type == 'Video') {
|
if (profile.Type == 'Video') {
|
||||||
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
} else {
|
} else if (profile.Type == 'Audio') {
|
||||||
if (profile.Type == 'Audio') {
|
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</a>';
|
html += '</a>';
|
||||||
|
@ -561,10 +557,8 @@ function renderResponseProfiles(page, profiles) {
|
||||||
if (profile.Type == 'Video') {
|
if (profile.Type == 'Video') {
|
||||||
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueVideoCodec', profile.VideoCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
html += '<p>' + globalize.translate('ValueAudioCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
} else {
|
} else if (profile.Type == 'Audio') {
|
||||||
if (profile.Type == 'Audio') {
|
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
||||||
html += '<p>' + globalize.translate('ValueCodec', profile.AudioCodec || allText) + '</p>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.Conditions?.length) {
|
if (profile.Conditions?.length) {
|
||||||
|
|
|
@ -309,9 +309,7 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||||
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
|
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
|
||||||
|
|
||||||
if (virtualFolder.showMenu !== false) {
|
if (virtualFolder.showMenu !== false) {
|
||||||
let dirTextAlign = 'right';
|
const dirTextAlign = globalize.getIsRTL() ? 'left' : 'right';
|
||||||
if (globalize.getIsRTL())
|
|
||||||
dirTextAlign = 'left';
|
|
||||||
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
|
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
|
||||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
|
@ -83,10 +83,11 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) {
|
||||||
html += '<div class="cardFooter">';
|
html += '<div class="cardFooter">';
|
||||||
|
|
||||||
if (configPage || plugin.CanUninstall) {
|
if (configPage || plugin.CanUninstall) {
|
||||||
if (globalize.getIsRTL())
|
if (globalize.getIsRTL()) {
|
||||||
html += '<div style="text-align:left; float:left;padding-top:5px;">';
|
html += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||||
else
|
} else {
|
||||||
html += '<div style="text-align:right; float:right;padding-top:5px;">';
|
html += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||||
|
}
|
||||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,7 @@ function populateList(page, tasks) {
|
||||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||||
html += '</a>';
|
html += '</a>';
|
||||||
html += '<div class="listItemBody two-line">';
|
html += '<div class="listItemBody two-line">';
|
||||||
let textAlignStyle = 'left';
|
const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left';
|
||||||
if (globalize.getIsRTL())
|
|
||||||
textAlignStyle = 'right';
|
|
||||||
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='scheduledtask.html?id=" + task.Id + "'>";
|
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='scheduledtask.html?id=" + task.Id + "'>";
|
||||||
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
|
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
|
||||||
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
||||||
|
|
|
@ -36,6 +36,7 @@ import Dashboard from '../../utils/dashboard';
|
||||||
import ServerConnections from '../../components/ServerConnections';
|
import ServerConnections from '../../components/ServerConnections';
|
||||||
import confirm from '../../components/confirm/confirm';
|
import confirm from '../../components/confirm/confirm';
|
||||||
import { download } from '../../scripts/fileDownloader';
|
import { download } from '../../scripts/fileDownloader';
|
||||||
|
import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage';
|
||||||
|
|
||||||
function autoFocus(container) {
|
function autoFocus(container) {
|
||||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||||
|
@ -501,34 +502,12 @@ function renderDetailPageBackdrop(page, item, apiClient) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imgUrl;
|
|
||||||
let hasbackdrop = false;
|
let hasbackdrop = false;
|
||||||
const itemBackdropElement = page.querySelector('#itemBackdrop');
|
const itemBackdropElement = page.querySelector('#itemBackdrop');
|
||||||
|
|
||||||
if (item.BackdropImageTags?.length) {
|
const imgUrl = getItemBackdropImageUrl(apiClient, item, { maxWitdh: dom.getScreenWidth() }, false);
|
||||||
imgUrl = apiClient.getScaledImageUrl(item.Id, {
|
|
||||||
type: 'Backdrop',
|
if (imgUrl) {
|
||||||
maxWidth: dom.getScreenWidth(),
|
|
||||||
index: 0,
|
|
||||||
tag: item.BackdropImageTags[0]
|
|
||||||
});
|
|
||||||
imageLoader.lazyImage(itemBackdropElement, imgUrl);
|
|
||||||
hasbackdrop = true;
|
|
||||||
} else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
|
|
||||||
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
|
|
||||||
type: 'Backdrop',
|
|
||||||
maxWidth: dom.getScreenWidth(),
|
|
||||||
index: 0,
|
|
||||||
tag: item.ParentBackdropImageTags[0]
|
|
||||||
});
|
|
||||||
imageLoader.lazyImage(itemBackdropElement, imgUrl);
|
|
||||||
hasbackdrop = true;
|
|
||||||
} else if (item.ImageTags?.Primary) {
|
|
||||||
imgUrl = apiClient.getScaledImageUrl(item.Id, {
|
|
||||||
type: 'Primary',
|
|
||||||
maxWidth: dom.getScreenWidth(),
|
|
||||||
tag: item.ImageTags.Primary
|
|
||||||
});
|
|
||||||
imageLoader.lazyImage(itemBackdropElement, imgUrl);
|
imageLoader.lazyImage(itemBackdropElement, imgUrl);
|
||||||
hasbackdrop = true;
|
hasbackdrop = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -306,8 +306,7 @@ export default function (view) {
|
||||||
|
|
||||||
function onHideAnimationComplete(e) {
|
function onHideAnimationComplete(e) {
|
||||||
const elem = e.target;
|
const elem = e.target;
|
||||||
if (elem != osdBottomElement)
|
if (elem != osdBottomElement) return;
|
||||||
return;
|
|
||||||
elem.classList.add('hide');
|
elem.classList.add('hide');
|
||||||
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
|
dom.removeEventListener(elem, transitionEndEventName, onHideAnimationComplete, {
|
||||||
once: true
|
once: true
|
||||||
|
@ -392,11 +391,9 @@ export default function (view) {
|
||||||
case 'left':
|
case 'left':
|
||||||
if (currentVisibleMenu === 'osd') {
|
if (currentVisibleMenu === 'osd') {
|
||||||
showOsd();
|
showOsd();
|
||||||
} else {
|
} else if (!currentVisibleMenu) {
|
||||||
if (!currentVisibleMenu) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
playbackManager.rewind(player);
|
||||||
playbackManager.rewind(player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -285,10 +285,8 @@ ItemsContainerPrototype.attachedCallback = function () {
|
||||||
|
|
||||||
if (browser.touch) {
|
if (browser.touch) {
|
||||||
this.addEventListener('contextmenu', disableEvent);
|
this.addEventListener('contextmenu', disableEvent);
|
||||||
} else {
|
} else if (this.getAttribute('data-contextmenu') !== 'false') {
|
||||||
if (this.getAttribute('data-contextmenu') !== 'false') {
|
this.addEventListener('contextmenu', onContextMenu);
|
||||||
this.addEventListener('contextmenu', onContextMenu);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layoutManager.desktop || layoutManager.mobile && this.getAttribute('data-multiselect') !== 'false') {
|
if (layoutManager.desktop || layoutManager.mobile && this.getAttribute('data-multiselect') !== 'false') {
|
||||||
|
|
|
@ -30,8 +30,9 @@ function mapClientToFraction(range, clientX) {
|
||||||
const rect = range.sliderBubbleTrack.getBoundingClientRect();
|
const rect = range.sliderBubbleTrack.getBoundingClientRect();
|
||||||
|
|
||||||
let fraction = (clientX - rect.left) / rect.width;
|
let fraction = (clientX - rect.left) / rect.width;
|
||||||
if (globalize.getIsElementRTL(range))
|
if (globalize.getIsElementRTL(range)) {
|
||||||
fraction = (rect.right - clientX) / rect.width;
|
fraction = (rect.right - clientX) / rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
// Snap to step
|
// Snap to step
|
||||||
const valueRange = range.max - range.min;
|
const valueRange = range.max - range.min;
|
||||||
|
@ -490,10 +491,11 @@ EmbySliderPrototype.setKeyboardSteps = function (stepDown, stepUp) {
|
||||||
|
|
||||||
function setRange(elem, startPercent, endPercent) {
|
function setRange(elem, startPercent, endPercent) {
|
||||||
const style = elem.style;
|
const style = elem.style;
|
||||||
if (globalize.getIsRTL())
|
if (globalize.getIsRTL()) {
|
||||||
style.right = Math.max(startPercent, 0) + '%';
|
style.right = Math.max(startPercent, 0) + '%';
|
||||||
else
|
} else {
|
||||||
style.left = Math.max(startPercent, 0) + '%';
|
style.left = Math.max(startPercent, 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
const widthPercent = endPercent - startPercent;
|
const widthPercent = endPercent - startPercent;
|
||||||
style.width = Math.max(Math.min(widthPercent, 100), 0) + '%';
|
style.width = Math.max(Math.min(widthPercent, 100), 0) + '%';
|
||||||
|
|
|
@ -227,10 +227,8 @@ async function onAppReady() {
|
||||||
document.body.appendChild(localStyle);
|
document.body.appendChild(localStyle);
|
||||||
}
|
}
|
||||||
localStyle.textContent = localCss;
|
localStyle.textContent = localCss;
|
||||||
} else {
|
} else if (localStyle) {
|
||||||
if (localStyle) {
|
localStyle.textContent = '';
|
||||||
localStyle.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -130,17 +130,15 @@ class NavDrawer {
|
||||||
|
|
||||||
if (this.isPeeking) {
|
if (this.isPeeking) {
|
||||||
this.onMenuTouchMove(e);
|
this.onMenuTouchMove(e);
|
||||||
} else {
|
} else if ((getTouches(e)[0]?.clientX || 0) <= options.handleSize) {
|
||||||
if ((getTouches(e)[0]?.clientX || 0) <= options.handleSize) {
|
this.isPeeking = true;
|
||||||
this.isPeeking = true;
|
|
||||||
|
|
||||||
if (e.type === 'touchstart') {
|
if (e.type === 'touchstart') {
|
||||||
dom.removeEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {});
|
dom.removeEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {});
|
||||||
dom.addEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {});
|
dom.addEventListener(this.edgeContainer, 'touchmove', this.onEdgeTouchMove, {});
|
||||||
}
|
|
||||||
|
|
||||||
this.onMenuTouchStart(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onMenuTouchStart(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -206,10 +204,11 @@ class NavDrawer {
|
||||||
|
|
||||||
options.target.classList.add('touch-menu-la');
|
options.target.classList.add('touch-menu-la');
|
||||||
options.target.style.width = options.width + 'px';
|
options.target.style.width = options.width + 'px';
|
||||||
if (globalize.getIsRTL())
|
if (globalize.getIsRTL()) {
|
||||||
options.target.style.right = -options.width + 'px';
|
options.target.style.right = -options.width + 'px';
|
||||||
else
|
} else {
|
||||||
options.target.style.left = -options.width + 'px';
|
options.target.style.left = -options.width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.disableMask) {
|
if (!options.disableMask) {
|
||||||
this.mask = document.createElement('div');
|
this.mask = document.createElement('div');
|
||||||
|
@ -246,14 +245,10 @@ class NavDrawer {
|
||||||
} else {
|
} else {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (this.newPos >= 100) {
|
||||||
if (this.newPos >= 100) {
|
this.open();
|
||||||
this.open();
|
} else if (this.newPos) {
|
||||||
} else {
|
this.close();
|
||||||
if (this.newPos) {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,19 +315,17 @@ class NavDrawer {
|
||||||
passive: true
|
passive: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (this._edgeSwipeEnabled) {
|
||||||
if (this._edgeSwipeEnabled) {
|
this._edgeSwipeEnabled = false;
|
||||||
this._edgeSwipeEnabled = false;
|
dom.removeEventListener(this.edgeContainer, 'touchstart', this.onEdgeTouchStart, {
|
||||||
dom.removeEventListener(this.edgeContainer, 'touchstart', this.onEdgeTouchStart, {
|
passive: true
|
||||||
passive: true
|
});
|
||||||
});
|
dom.removeEventListener(this.edgeContainer, 'touchend', this.onEdgeTouchEnd, {
|
||||||
dom.removeEventListener(this.edgeContainer, 'touchend', this.onEdgeTouchEnd, {
|
passive: true
|
||||||
passive: true
|
});
|
||||||
});
|
dom.removeEventListener(this.edgeContainer, 'touchcancel', this.onEdgeTouchEnd, {
|
||||||
dom.removeEventListener(this.edgeContainer, 'touchcancel', this.onEdgeTouchEnd, {
|
passive: true
|
||||||
passive: true
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,8 +189,9 @@ const scrollerFactory = function (frame, options) {
|
||||||
|
|
||||||
// Set position limits & relatives
|
// Set position limits & relatives
|
||||||
self._pos.end = Math.max(slideeSize - frameSize, 0);
|
self._pos.end = Math.max(slideeSize - frameSize, 0);
|
||||||
if (globalize.getIsRTL())
|
if (globalize.getIsRTL()) {
|
||||||
self._pos.end *= -1;
|
self._pos.end *= -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,12 +258,10 @@ const scrollerFactory = function (frame, options) {
|
||||||
} else {
|
} else {
|
||||||
container.scrollTo(0, Math.round(pos));
|
container.scrollTo(0, Math.round(pos));
|
||||||
}
|
}
|
||||||
|
} else if (o.horizontal) {
|
||||||
|
container.scrollLeft = Math.round(pos);
|
||||||
} else {
|
} else {
|
||||||
if (o.horizontal) {
|
container.scrollTop = Math.round(pos);
|
||||||
container.scrollLeft = Math.round(pos);
|
|
||||||
} else {
|
|
||||||
container.scrollTop = Math.round(pos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,14 +505,12 @@ const scrollerFactory = function (frame, options) {
|
||||||
// If the pointer was released, the path will not become longer and it's
|
// If the pointer was released, the path will not become longer and it's
|
||||||
// definitely not a drag. If not released yet, decide on next iteration
|
// definitely not a drag. If not released yet, decide on next iteration
|
||||||
return dragging.released ? dragEnd() : undefined;
|
return dragging.released ? dragEnd() : undefined;
|
||||||
} else {
|
} else if (o.horizontal ? Math.abs(dragging.pathX) > Math.abs(dragging.pathY) : Math.abs(dragging.pathX) < Math.abs(dragging.pathY)) {
|
||||||
// If dragging path is sufficiently long we can confidently start a drag
|
// If dragging path is sufficiently long we can confidently start a drag
|
||||||
// if drag is in different direction than scroll, ignore it
|
// if drag is in different direction than scroll, ignore it
|
||||||
if (o.horizontal ? Math.abs(dragging.pathX) > Math.abs(dragging.pathY) : Math.abs(dragging.pathX) < Math.abs(dragging.pathY)) {
|
dragging.init = 1;
|
||||||
dragging.init = 1;
|
} else {
|
||||||
} else {
|
return dragEnd();
|
||||||
return dragEnd();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,6 @@
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerToc" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<button is="paper-icon-button-light" id="btnBookplayerToc" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||||
<span class="material-icons bookplayerButtonIcon toc" aria-hidden="true"></span>
|
<span class="material-icons bookplayerButtonIcon toc" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<!-- <button is="paper-icon-button-light" id="btnBookplayerPrev" class="autoSize bookplayerButton pageButton hide-mouse-idle-tv" tabindex="-1">
|
|
||||||
<span class="material-icons bookplayerButtonIcon navigate_before" aria-hidden="true"></span> ${Previous}
|
|
||||||
</button>
|
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerNext" class="autoSize bookplayerButton pageButton hide-mouse-idle-tv" tabindex="-1">
|
|
||||||
${Next} <span class="material-icons bookplayerButtonIcon navigate_next" aria-hidden="true"></span>
|
|
||||||
</button> -->
|
|
||||||
<button is="paper-icon-button-light" id="btnBookplayerExit" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
<button is="paper-icon-button-light" id="btnBookplayerExit" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1">
|
||||||
<span class="material-icons bookplayerButtonIcon close" aria-hidden="true"></span>
|
<span class="material-icons bookplayerButtonIcon close" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -27,12 +27,10 @@ function sendConnectionResult(isOk) {
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
} else if (reject) {
|
||||||
|
reject();
|
||||||
} else {
|
} else {
|
||||||
if (reject) {
|
playbackManager.removeActivePlayer(PlayerName);
|
||||||
reject();
|
|
||||||
} else {
|
|
||||||
playbackManager.removeActivePlayer(PlayerName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,7 +292,7 @@ class CastPlayer {
|
||||||
loadMedia(options, command) {
|
loadMedia(options, command) {
|
||||||
if (!this.session) {
|
if (!this.session) {
|
||||||
console.debug('no session');
|
console.debug('no session');
|
||||||
return Promise.reject();
|
return Promise.reject(new Error('no session'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert items to smaller stubs to send minimal amount of information
|
// convert items to smaller stubs to send minimal amount of information
|
||||||
|
@ -335,16 +333,27 @@ class CastPlayer {
|
||||||
apiClient = ServerConnections.currentApiClient();
|
apiClient = ServerConnections.currentApiClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If serverAddress is localhost,this address can not be used for the cast receiver device.
|
||||||
|
* Use the local address (ULA, Unique Local Address) in that case.
|
||||||
|
*/
|
||||||
|
const serverAddress = apiClient.serverAddress();
|
||||||
|
// eslint-disable-next-line compat/compat
|
||||||
|
const hostname = (new URL(serverAddress)).hostname;
|
||||||
|
const isLocalhost = hostname === 'localhost' || hostname.startsWith('127.') || hostname === '[::1]';
|
||||||
|
const serverLocalAddress = isLocalhost ? apiClient.serverInfo().LocalAddress : serverAddress;
|
||||||
|
|
||||||
message = Object.assign(message, {
|
message = Object.assign(message, {
|
||||||
userId: apiClient.getCurrentUserId(),
|
userId: apiClient.getCurrentUserId(),
|
||||||
deviceId: apiClient.deviceId(),
|
deviceId: apiClient.deviceId(),
|
||||||
accessToken: apiClient.accessToken(),
|
accessToken: apiClient.accessToken(),
|
||||||
serverAddress: apiClient.serverAddress(),
|
serverAddress: serverLocalAddress,
|
||||||
serverId: apiClient.serverId(),
|
serverId: apiClient.serverId(),
|
||||||
serverVersion: apiClient.serverVersion(),
|
serverVersion: apiClient.serverVersion(),
|
||||||
receiverName: receiverName
|
receiverName: receiverName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.debug('[chromecastPlayer] message{' + message.command + '; ' + serverAddress + ' -> ' + serverLocalAddress + '}');
|
||||||
|
|
||||||
const bitrateSetting = appSettings.maxChromecastBitrate();
|
const bitrateSetting = appSettings.maxChromecastBitrate();
|
||||||
if (bitrateSetting) {
|
if (bitrateSetting) {
|
||||||
message.maxBitrate = bitrateSetting;
|
message.maxBitrate = bitrateSetting;
|
||||||
|
@ -592,7 +601,7 @@ class ChromecastPlayer {
|
||||||
currentResolve = null;
|
currentResolve = null;
|
||||||
currentReject = null;
|
currentReject = null;
|
||||||
|
|
||||||
return Promise.reject();
|
return Promise.reject(new Error('tryPair failed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,10 +95,11 @@ export class ComicsPlayer {
|
||||||
onDirChanged = () => {
|
onDirChanged = () => {
|
||||||
let langDir = this.comicsPlayerSettings.langDir;
|
let langDir = this.comicsPlayerSettings.langDir;
|
||||||
|
|
||||||
if (!langDir || langDir === 'ltr')
|
if (!langDir || langDir === 'ltr') {
|
||||||
langDir = 'rtl';
|
langDir = 'rtl';
|
||||||
else
|
} else {
|
||||||
langDir = 'ltr';
|
langDir = 'ltr';
|
||||||
|
}
|
||||||
|
|
||||||
this.changeLanguageDirection(langDir);
|
this.changeLanguageDirection(langDir);
|
||||||
|
|
||||||
|
@ -125,10 +126,11 @@ export class ComicsPlayer {
|
||||||
onViewChanged = () => {
|
onViewChanged = () => {
|
||||||
let view = this.comicsPlayerSettings.pagesPerView;
|
let view = this.comicsPlayerSettings.pagesPerView;
|
||||||
|
|
||||||
if (!view || view === 1)
|
if (!view || view === 1) {
|
||||||
view = 2;
|
view = 2;
|
||||||
else
|
} else {
|
||||||
view = 1;
|
view = 1;
|
||||||
|
}
|
||||||
|
|
||||||
this.changeView(view);
|
this.changeView(view);
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/ba
|
||||||
import { PluginType } from '../../types/plugin.ts';
|
import { PluginType } from '../../types/plugin.ts';
|
||||||
import Events from '../../utils/events.ts';
|
import Events from '../../utils/events.ts';
|
||||||
import { includesAny } from '../../utils/container.ts';
|
import { includesAny } from '../../utils/container.ts';
|
||||||
|
import { isHls } from '../../utils/mediaSource.ts';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,12 +70,12 @@ function tryRemoveElement(elem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableNativeTrackSupport(currentSrc, track) {
|
function enableNativeTrackSupport(mediaSource, track) {
|
||||||
if (track?.DeliveryMethod === 'Embed') {
|
if (track?.DeliveryMethod === 'Embed') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browser.firefox && (currentSrc || '').toLowerCase().includes('.m3u8')) {
|
if (browser.firefox && isHls(mediaSource)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,137 +169,140 @@ const SECONDARY_TEXT_TRACK_INDEX = 1;
|
||||||
|
|
||||||
export class HtmlVideoPlayer {
|
export class HtmlVideoPlayer {
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
name;
|
name;
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
type = PluginType.MediaPlayer;
|
type = PluginType.MediaPlayer;
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
id = 'htmlvideoplayer';
|
id = 'htmlvideoplayer';
|
||||||
/**
|
/**
|
||||||
* Let any players created by plugins take priority
|
* Let any players created by plugins take priority
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
priority = 1;
|
priority = 1;
|
||||||
/**
|
/**
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLDivElement | null | undefined}
|
* @type {HTMLDivElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#videoDialog;
|
#videoDialog;
|
||||||
/**
|
/**
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#subtitleTrackIndexToSetOnPlaying;
|
#subtitleTrackIndexToSetOnPlaying;
|
||||||
/**
|
/**
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#secondarySubtitleTrackIndexToSetOnPlaying;
|
#secondarySubtitleTrackIndexToSetOnPlaying;
|
||||||
/**
|
/**
|
||||||
* @type {number | null}
|
* @type {number | null}
|
||||||
*/
|
*/
|
||||||
#audioTrackIndexToSetOnPlaying;
|
#audioTrackIndexToSetOnPlaying;
|
||||||
/**
|
/**
|
||||||
* @type {null | undefined}
|
* @type {null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentClock;
|
#currentClock;
|
||||||
/**
|
/**
|
||||||
* @type {any | null | undefined}
|
* @type {any | null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentAssRenderer;
|
#currentAssRenderer;
|
||||||
/**
|
/**
|
||||||
* @type {null | undefined}
|
* @type {null | undefined}
|
||||||
*/
|
*/
|
||||||
#customTrackIndex;
|
#customTrackIndex;
|
||||||
/**
|
/**
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#customSecondaryTrackIndex;
|
#customSecondaryTrackIndex;
|
||||||
/**
|
/**
|
||||||
* @type {boolean | undefined}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
#showTrackOffset;
|
#showTrackOffset;
|
||||||
/**
|
/**
|
||||||
* @type {number | undefined}
|
* @type {number | undefined}
|
||||||
*/
|
*/
|
||||||
#currentTrackOffset;
|
#currentTrackOffset;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLElement | null | undefined}
|
* @type {HTMLElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#secondaryTrackOffset;
|
#secondaryTrackOffset;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLElement | null | undefined}
|
* @type {HTMLElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#videoSubtitlesElem;
|
#videoSubtitlesElem;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLElement | null | undefined}
|
* @type {HTMLElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#videoSecondarySubtitlesElem;
|
#videoSecondarySubtitlesElem;
|
||||||
/**
|
/**
|
||||||
* @type {any | null | undefined}
|
* @type {any | null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentTrackEvents;
|
#currentTrackEvents;
|
||||||
/**
|
/**
|
||||||
* @type {any | null | undefined}
|
* @type {any | null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentSecondaryTrackEvents;
|
#currentSecondaryTrackEvents;
|
||||||
/**
|
/**
|
||||||
* @type {string[] | undefined}
|
* @type {string[] | undefined}
|
||||||
*/
|
*/
|
||||||
#supportedFeatures;
|
#supportedFeatures;
|
||||||
/**
|
/**
|
||||||
* @type {HTMLVideoElement | null | undefined}
|
* @type {HTMLVideoElement | null | undefined}
|
||||||
*/
|
*/
|
||||||
#mediaElement;
|
#mediaElement;
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
#fetchQueue = 0;
|
#fetchQueue = 0;
|
||||||
/**
|
/**
|
||||||
* @type {string | undefined}
|
* @type {string | undefined}
|
||||||
*/
|
*/
|
||||||
#currentSrc;
|
#currentSrc;
|
||||||
/**
|
/**
|
||||||
* @type {boolean | undefined}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
#started;
|
#started;
|
||||||
/**
|
/**
|
||||||
* @type {boolean | undefined}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
#timeUpdated;
|
#timeUpdated;
|
||||||
/**
|
/**
|
||||||
* @type {number | null | undefined}
|
* @type {number | null | undefined}
|
||||||
*/
|
*/
|
||||||
#currentTime;
|
#currentTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {any | undefined}
|
* @private (used in other files)
|
||||||
*/
|
* @type {any | undefined}
|
||||||
#flvPlayer;
|
*/
|
||||||
|
_flvPlayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private (used in other files)
|
* @private (used in other files)
|
||||||
* @type {any | undefined}
|
* @type {any | undefined}
|
||||||
*/
|
*/
|
||||||
_hlsPlayer;
|
_hlsPlayer;
|
||||||
/**
|
/**
|
||||||
* @private (used in other files)
|
* @private (used in other files)
|
||||||
* @type {any | null | undefined}
|
* @type {any | null | undefined}
|
||||||
*/
|
*/
|
||||||
_castPlayer;
|
_castPlayer;
|
||||||
/**
|
/**
|
||||||
* @private (used in other files)
|
* @private (used in other files)
|
||||||
* @type {any | undefined}
|
* @type {any | undefined}
|
||||||
*/
|
*/
|
||||||
_currentPlayOptions;
|
_currentPlayOptions;
|
||||||
/**
|
/**
|
||||||
* @type {any | undefined}
|
* @type {any | undefined}
|
||||||
*/
|
*/
|
||||||
#lastProfile;
|
#lastProfile;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -341,15 +345,13 @@ export class HtmlVideoPlayer {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
updateVideoUrl(streamInfo) {
|
updateVideoUrl(streamInfo) {
|
||||||
const isHls = streamInfo.url.toLowerCase().includes('.m3u8');
|
|
||||||
|
|
||||||
const mediaSource = streamInfo.mediaSource;
|
const mediaSource = streamInfo.mediaSource;
|
||||||
const item = streamInfo.item;
|
const item = streamInfo.item;
|
||||||
|
|
||||||
// Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts
|
// Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts
|
||||||
// This will start the transcoding process before actually feeding the video url into the player
|
// This will start the transcoding process before actually feeding the video url into the player
|
||||||
// Edit: Also seeing stalls from hls.js
|
// Edit: Also seeing stalls from hls.js
|
||||||
if (mediaSource && item && !mediaSource.RunTimeTicks && isHls && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) {
|
if (mediaSource && item && !mediaSource.RunTimeTicks && isHls(mediaSource) && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) {
|
||||||
const hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8');
|
const hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8');
|
||||||
|
|
||||||
loading.show();
|
loading.show();
|
||||||
|
@ -408,7 +410,7 @@ export class HtmlVideoPlayer {
|
||||||
flvPlayer.attachMediaElement(elem);
|
flvPlayer.attachMediaElement(elem);
|
||||||
flvPlayer.load();
|
flvPlayer.load();
|
||||||
|
|
||||||
this.#flvPlayer = flvPlayer;
|
this._flvPlayer = flvPlayer;
|
||||||
|
|
||||||
// This is needed in setCurrentTrackElement
|
// This is needed in setCurrentTrackElement
|
||||||
this.#currentSrc = url;
|
this.#currentSrc = url;
|
||||||
|
@ -513,7 +515,7 @@ export class HtmlVideoPlayer {
|
||||||
elem.crossOrigin = crossOrigin;
|
elem.crossOrigin = crossOrigin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && val.includes('.m3u8')) {
|
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && isHls(options.mediaSource)) {
|
||||||
return this.setSrcWithHlsJs(elem, options, val);
|
return this.setSrcWithHlsJs(elem, options, val);
|
||||||
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') {
|
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') {
|
||||||
return this.setSrcWithFlvJs(elem, options, val);
|
return this.setSrcWithFlvJs(elem, options, val);
|
||||||
|
@ -864,11 +866,9 @@ export class HtmlVideoPlayer {
|
||||||
|
|
||||||
if (Screenfull.isEnabled) {
|
if (Screenfull.isEnabled) {
|
||||||
Screenfull.exit();
|
Screenfull.exit();
|
||||||
} else {
|
} else if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
|
||||||
// iOS Safari
|
// iOS Safari
|
||||||
if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
|
document.webkitCancelFullscreen();
|
||||||
document.webkitCancelFullscreen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,15 +1106,14 @@ export class HtmlVideoPlayer {
|
||||||
tryRemoveElement(this.#videoSecondarySubtitlesElem);
|
tryRemoveElement(this.#videoSecondarySubtitlesElem);
|
||||||
this.#videoSecondarySubtitlesElem = null;
|
this.#videoSecondarySubtitlesElem = null;
|
||||||
}
|
}
|
||||||
} else { // destroy all
|
} else if (this.#videoSubtitlesElem) {
|
||||||
if (this.#videoSubtitlesElem) {
|
// destroy all
|
||||||
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
|
||||||
if (subtitlesContainer) {
|
if (subtitlesContainer) {
|
||||||
tryRemoveElement(subtitlesContainer);
|
tryRemoveElement(subtitlesContainer);
|
||||||
}
|
|
||||||
this.#videoSubtitlesElem = null;
|
|
||||||
this.#videoSecondarySubtitlesElem = null;
|
|
||||||
}
|
}
|
||||||
|
this.#videoSubtitlesElem = null;
|
||||||
|
this.#videoSecondarySubtitlesElem = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1561,7 +1560,7 @@ export class HtmlVideoPlayer {
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
|
this.setTrackForDisplay(this.#mediaElement, track, targetTextTrackIndex);
|
||||||
if (enableNativeTrackSupport(this.#currentSrc, track)) {
|
if (enableNativeTrackSupport(this._currentPlayOptions?.mediaSource, track)) {
|
||||||
if (streamIndex !== -1) {
|
if (streamIndex !== -1) {
|
||||||
this.setCueAppearance();
|
this.setCueAppearance();
|
||||||
}
|
}
|
||||||
|
@ -1665,7 +1664,13 @@ export class HtmlVideoPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(dlg.querySelector('video'));
|
const videoElement = dlg.querySelector('video');
|
||||||
|
if (options.backdropUrl) {
|
||||||
|
// update backdrop image
|
||||||
|
videoElement.poster = options.backdropUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(videoElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1812,10 +1817,8 @@ export class HtmlVideoPlayer {
|
||||||
} else {
|
} else {
|
||||||
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default);
|
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (video?.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') {
|
||||||
if (video?.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') {
|
video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline');
|
||||||
video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -352,10 +352,8 @@ class YoutubePlayer {
|
||||||
if (currentYoutubePlayer) {
|
if (currentYoutubePlayer) {
|
||||||
currentYoutubePlayer.mute();
|
currentYoutubePlayer.mute();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (currentYoutubePlayer) {
|
||||||
if (currentYoutubePlayer) {
|
currentYoutubePlayer.unMute();
|
||||||
currentYoutubePlayer.unMute();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMuted() {
|
isMuted() {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function isEnabled() {
|
||||||
const playerId = localStorage.getItem('autocastPlayerId');
|
const playerId = localStorage.getItem('autocastPlayerId');
|
||||||
const currentPlayerInfo = playbackManager.getPlayerInfo();
|
const currentPlayerInfo = playbackManager.getPlayerInfo();
|
||||||
|
|
||||||
return (currentPlayerInfo && playerId && currentPlayerInfo.id === playerId);
|
return playerId && currentPlayerInfo?.id === playerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
|
|
|
@ -148,14 +148,11 @@ let _supportsCssAnimation;
|
||||||
let _supportsCssAnimationWithPrefix;
|
let _supportsCssAnimationWithPrefix;
|
||||||
function supportsCssAnimation(allowPrefix) {
|
function supportsCssAnimation(allowPrefix) {
|
||||||
// TODO: Assess if this is still needed, as all of our targets should natively support CSS animations.
|
// TODO: Assess if this is still needed, as all of our targets should natively support CSS animations.
|
||||||
if (allowPrefix) {
|
if (allowPrefix && (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false)) {
|
||||||
if (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false) {
|
return _supportsCssAnimationWithPrefix;
|
||||||
return _supportsCssAnimationWithPrefix;
|
}
|
||||||
}
|
if (_supportsCssAnimation === true || _supportsCssAnimation === false) {
|
||||||
} else {
|
return _supportsCssAnimation;
|
||||||
if (_supportsCssAnimation === true || _supportsCssAnimation === false) {
|
|
||||||
return _supportsCssAnimation;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let animation = false;
|
let animation = false;
|
||||||
|
@ -187,13 +184,13 @@ function supportsCssAnimation(allowPrefix) {
|
||||||
const uaMatch = function (ua) {
|
const uaMatch = function (ua) {
|
||||||
ua = ua.toLowerCase();
|
ua = ua.toLowerCase();
|
||||||
|
|
||||||
const match = /(edg)[ /]([\w.]+)/.exec(ua)
|
const match = /(chrome)[ /]([\w.]+)/.exec(ua)
|
||||||
|
|| /(edg)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(edga)[ /]([\w.]+)/.exec(ua)
|
|| /(edga)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(edgios)[ /]([\w.]+)/.exec(ua)
|
|| /(edgios)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(edge)[ /]([\w.]+)/.exec(ua)
|
|| /(edge)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(opera)[ /]([\w.]+)/.exec(ua)
|
|| /(opera)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(opr)[ /]([\w.]+)/.exec(ua)
|
|| /(opr)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(chrome)[ /]([\w.]+)/.exec(ua)
|
|
||||||
|| /(safari)[ /]([\w.]+)/.exec(ua)
|
|| /(safari)[ /]([\w.]+)/.exec(ua)
|
||||||
|| /(firefox)[ /]([\w.]+)/.exec(ua)
|
|| /(firefox)[ /]([\w.]+)/.exec(ua)
|
||||||
|| ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua)
|
|| ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua)
|
||||||
|
|
|
@ -718,6 +718,15 @@ export default function (options) {
|
||||||
enableFmp4Hls = false;
|
enableFmp4Hls = false;
|
||||||
}
|
}
|
||||||
if (hlsInFmp4VideoCodecs.length && hlsInFmp4VideoAudioCodecs.length && enableFmp4Hls) {
|
if (hlsInFmp4VideoCodecs.length && hlsInFmp4VideoAudioCodecs.length && enableFmp4Hls) {
|
||||||
|
// HACK: Since there is no filter for TS/MP4 in the API, specify HLS support in general and rely on retry after DirectPlay error
|
||||||
|
// FIXME: Need support for {Container: 'mp4', Protocol: 'hls'} or {Container: 'hls', SubContainer: 'mp4'}
|
||||||
|
profile.DirectPlayProfiles.push({
|
||||||
|
Container: 'hls',
|
||||||
|
Type: 'Video',
|
||||||
|
VideoCodec: hlsInFmp4VideoCodecs.join(','),
|
||||||
|
AudioCodec: hlsInFmp4VideoAudioCodecs.join(',')
|
||||||
|
});
|
||||||
|
|
||||||
profile.TranscodingProfiles.push({
|
profile.TranscodingProfiles.push({
|
||||||
Container: 'mp4',
|
Container: 'mp4',
|
||||||
Type: 'Video',
|
Type: 'Video',
|
||||||
|
@ -732,6 +741,15 @@ export default function (options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hlsInTsVideoCodecs.length && hlsInTsVideoAudioCodecs.length) {
|
if (hlsInTsVideoCodecs.length && hlsInTsVideoAudioCodecs.length) {
|
||||||
|
// HACK: Since there is no filter for TS/MP4 in the API, specify HLS support in general and rely on retry after DirectPlay error
|
||||||
|
// FIXME: Need support for {Container: 'ts', Protocol: 'hls'} or {Container: 'hls', SubContainer: 'ts'}
|
||||||
|
profile.DirectPlayProfiles.push({
|
||||||
|
Container: 'hls',
|
||||||
|
Type: 'Video',
|
||||||
|
VideoCodec: hlsInTsVideoCodecs.join(','),
|
||||||
|
AudioCodec: hlsInTsVideoAudioCodecs.join(',')
|
||||||
|
});
|
||||||
|
|
||||||
profile.TranscodingProfiles.push({
|
profile.TranscodingProfiles.push({
|
||||||
Container: 'ts',
|
Container: 'ts',
|
||||||
Type: 'Video',
|
Type: 'Video',
|
||||||
|
|
|
@ -230,7 +230,6 @@ export function getDisplayTime(date) {
|
||||||
const timeLower = time.toLowerCase();
|
const timeLower = time.toLowerCase();
|
||||||
|
|
||||||
if (timeLower.indexOf('am') !== -1 || timeLower.indexOf('pm') !== -1) {
|
if (timeLower.indexOf('am') !== -1 || timeLower.indexOf('pm') !== -1) {
|
||||||
time = timeLower;
|
|
||||||
let hour = date.getHours() % 12;
|
let hour = date.getHours() % 12;
|
||||||
const suffix = date.getHours() > 11 ? 'pm' : 'am';
|
const suffix = date.getHours() > 11 ? 'pm' : 'am';
|
||||||
if (!hour) {
|
if (!hour) {
|
||||||
|
|
|
@ -64,8 +64,9 @@ function checkAndProcessDir(culture) {
|
||||||
function setDocumentDirection(direction) {
|
function setDocumentDirection(direction) {
|
||||||
document.getElementsByTagName('body')[0].setAttribute('dir', direction);
|
document.getElementsByTagName('body')[0].setAttribute('dir', direction);
|
||||||
document.getElementsByTagName('html')[0].setAttribute('dir', direction);
|
document.getElementsByTagName('html')[0].setAttribute('dir', direction);
|
||||||
if (direction === Direction.rtl)
|
if (direction === Direction.rtl) {
|
||||||
import('../styles/rtl.scss');
|
import('../styles/rtl.scss');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIsElementRTL(element) {
|
export function getIsElementRTL(element) {
|
||||||
|
|
|
@ -835,26 +835,24 @@ function updateMenuForPageType(isDashboardPage, isLibraryPage) {
|
||||||
bodyClassList.remove('dashboardDocument');
|
bodyClassList.remove('dashboardDocument');
|
||||||
bodyClassList.remove('hideMainDrawer');
|
bodyClassList.remove('hideMainDrawer');
|
||||||
|
|
||||||
|
if (navDrawerInstance) {
|
||||||
|
navDrawerInstance.setEdgeSwipeEnabled(true);
|
||||||
|
}
|
||||||
|
} else if (isDashboardPage) {
|
||||||
|
bodyClassList.remove('libraryDocument');
|
||||||
|
bodyClassList.add('dashboardDocument');
|
||||||
|
bodyClassList.remove('hideMainDrawer');
|
||||||
|
|
||||||
if (navDrawerInstance) {
|
if (navDrawerInstance) {
|
||||||
navDrawerInstance.setEdgeSwipeEnabled(true);
|
navDrawerInstance.setEdgeSwipeEnabled(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isDashboardPage) {
|
bodyClassList.remove('libraryDocument');
|
||||||
bodyClassList.remove('libraryDocument');
|
bodyClassList.remove('dashboardDocument');
|
||||||
bodyClassList.add('dashboardDocument');
|
bodyClassList.add('hideMainDrawer');
|
||||||
bodyClassList.remove('hideMainDrawer');
|
|
||||||
|
|
||||||
if (navDrawerInstance) {
|
if (navDrawerInstance) {
|
||||||
navDrawerInstance.setEdgeSwipeEnabled(true);
|
navDrawerInstance.setEdgeSwipeEnabled(false);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bodyClassList.remove('libraryDocument');
|
|
||||||
bodyClassList.remove('dashboardDocument');
|
|
||||||
bodyClassList.add('hideMainDrawer');
|
|
||||||
|
|
||||||
if (navDrawerInstance) {
|
|
||||||
navDrawerInstance.setEdgeSwipeEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,12 +54,10 @@ export function toCenter(container, elem, horizontal, skipWhenVisible) {
|
||||||
} else {
|
} else {
|
||||||
container.scrollTo(0, pos.center);
|
container.scrollTo(0, pos.center);
|
||||||
}
|
}
|
||||||
|
} else if (horizontal) {
|
||||||
|
container.scrollLeft = Math.round(pos.center);
|
||||||
} else {
|
} else {
|
||||||
if (horizontal) {
|
container.scrollTop = Math.round(pos.center);
|
||||||
container.scrollLeft = Math.round(pos.center);
|
|
||||||
} else {
|
|
||||||
container.scrollTop = Math.round(pos.center);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,12 +74,10 @@ export function toStart(container, elem, horizontal, skipWhenVisible) {
|
||||||
} else {
|
} else {
|
||||||
container.scrollTo(0, pos.start);
|
container.scrollTo(0, pos.start);
|
||||||
}
|
}
|
||||||
|
} else if (horizontal) {
|
||||||
|
container.scrollLeft = Math.round(pos.start);
|
||||||
} else {
|
} else {
|
||||||
if (horizontal) {
|
container.scrollTop = Math.round(pos.start);
|
||||||
container.scrollLeft = Math.round(pos.start);
|
|
||||||
} else {
|
|
||||||
container.scrollTop = Math.round(pos.start);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1474,5 +1474,9 @@
|
||||||
"AllowSegmentDeletionHelp": "Изтриване на сегментите след изпращане към клиента. Това предотвратява нуждата за съхраняване на целия транскодиран файл на диска. Работи само с включено подтискане. Изключете тази функция ако изпитате проблеми с възпоизвеждането.",
|
"AllowSegmentDeletionHelp": "Изтриване на сегментите след изпращане към клиента. Това предотвратява нуждата за съхраняване на целия транскодиран файл на диска. Работи само с включено подтискане. Изключете тази функция ако изпитате проблеми с възпоизвеждането.",
|
||||||
"LabelThrottleDelaySecondsHelp": "Време в секунди след което транскодера ще бъде подтиснат. Трябва да е достатъчно голям за клиента да поддържа здрав буфер. Работи само ако подтискането е включено.",
|
"LabelThrottleDelaySecondsHelp": "Време в секунди след което транскодера ще бъде подтиснат. Трябва да е достатъчно голям за клиента да поддържа здрав буфер. Работи само ако подтискането е включено.",
|
||||||
"LabelSegmentKeepSeconds": "Време в което да се запазят сегменти",
|
"LabelSegmentKeepSeconds": "Време в което да се запазят сегменти",
|
||||||
"LabelSegmentKeepSecondsHelp": "Време в секунди за което сегментите трябва да се пазят след презаписване. Трябва да е повече от \"Подтискане след\". Работи само ако 'Изтриване на сегменти' е включено."
|
"LabelSegmentKeepSecondsHelp": "Време в секунди за което сегментите трябва да се пазят след презаписване. Трябва да е повече от \"Подтискане след\". Работи само ако 'Изтриване на сегменти' е включено.",
|
||||||
|
"EnableAudioNormalizationHelp": "Нормализацията на звука ще усили сигналът за да поддържа средните честоти на желано ниво (-18dB).",
|
||||||
|
"EnableAudioNormalization": "Нормализация на звука",
|
||||||
|
"Unknown": "Неизвестен",
|
||||||
|
"LabelThrottleDelaySeconds": "Ограничи след"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1763,5 +1763,12 @@
|
||||||
"LabelSegmentKeepSeconds": "Doba ponechání částí",
|
"LabelSegmentKeepSeconds": "Doba ponechání částí",
|
||||||
"LabelSegmentKeepSecondsHelp": "Čas v sekundách, po který budou části překódovaného souboru uloženy. Musí být delší než čas určený v \"Omezit po\". Funguje pouze při zapnuté funkci Odstranění částí.",
|
"LabelSegmentKeepSecondsHelp": "Čas v sekundách, po který budou části překódovaného souboru uloženy. Musí být delší než čas určený v \"Omezit po\". Funguje pouze při zapnuté funkci Odstranění částí.",
|
||||||
"LabelBackdropScreensaverInterval": "Interval šetřiče \"Pozadí\"",
|
"LabelBackdropScreensaverInterval": "Interval šetřiče \"Pozadí\"",
|
||||||
"LabelBackdropScreensaverIntervalHelp": "Čas v sekundách mezi změnou pozadí při použití šetřiče \"Pozadí\"."
|
"LabelBackdropScreensaverIntervalHelp": "Čas v sekundách mezi změnou pozadí při použití šetřiče \"Pozadí\".",
|
||||||
|
"AllowAv1Encoding": "Povolit kódování do formátu AV1",
|
||||||
|
"GridView": "Mřížka",
|
||||||
|
"ListView": "Seznam",
|
||||||
|
"MachineTranslated": "Strojově přeloženo",
|
||||||
|
"ForeignPartsOnly": "Pouze vynucené",
|
||||||
|
"HearingImpairedShort": "Titulky pro neslyšící",
|
||||||
|
"AiTranslated": "Přeložené pomocí UI"
|
||||||
}
|
}
|
||||||
|
|
|
@ -298,6 +298,7 @@
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
"GetThePlugin": "Get the Plugin",
|
"GetThePlugin": "Get the Plugin",
|
||||||
"GoogleCastUnsupported": "Google Cast Unsupported",
|
"GoogleCastUnsupported": "Google Cast Unsupported",
|
||||||
|
"GridView": "Grid View",
|
||||||
"GroupBySeries": "Group by series",
|
"GroupBySeries": "Group by series",
|
||||||
"GroupVersions": "Group versions",
|
"GroupVersions": "Group versions",
|
||||||
"GuestStar": "Guest star",
|
"GuestStar": "Guest star",
|
||||||
|
@ -999,6 +1000,7 @@
|
||||||
"LeaveBlankToNotSetAPassword": "You can leave this field blank to set no password.",
|
"LeaveBlankToNotSetAPassword": "You can leave this field blank to set no password.",
|
||||||
"LibraryAccessHelp": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.",
|
"LibraryAccessHelp": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.",
|
||||||
"List": "List",
|
"List": "List",
|
||||||
|
"ListView": "List View",
|
||||||
"ListPaging": "{0}-{1} of {2}",
|
"ListPaging": "{0}-{1} of {2}",
|
||||||
"Live": "Live",
|
"Live": "Live",
|
||||||
"LiveBroadcasts": "Live broadcasts",
|
"LiveBroadcasts": "Live broadcasts",
|
||||||
|
@ -1717,5 +1719,9 @@
|
||||||
"TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode.",
|
"TonemappingModeHelp": "Select the tone mapping mode. If you experience blown out highlights try switching to the RGB mode.",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"AllowAv1Encoding": "Allow encoding in AV1 format",
|
"AllowAv1Encoding": "Allow encoding in AV1 format",
|
||||||
|
"AiTranslated": "AI Translated",
|
||||||
|
"MachineTranslated": "Machine Translated",
|
||||||
|
"ForeignPartsOnly": "Forced/Foreign parts only",
|
||||||
|
"HearingImpairedShort": "HI/SDH",
|
||||||
"LabelIsHearingImpaired": "For hearing impaired (SDH)"
|
"LabelIsHearingImpaired": "For hearing impaired (SDH)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1232,7 +1232,7 @@
|
||||||
"EnableTonemapping": "Käytä sävykartoitusta",
|
"EnableTonemapping": "Käytä sävykartoitusta",
|
||||||
"EnableBlurHashHelp": "Kuvat, joita ladataan vielä, näytetään yksilöllisellä paikkamerkillä.",
|
"EnableBlurHashHelp": "Kuvat, joita ladataan vielä, näytetään yksilöllisellä paikkamerkillä.",
|
||||||
"EnableBlurHash": "Ota sumennetut paikkamerkit käyttöön kuville",
|
"EnableBlurHash": "Ota sumennetut paikkamerkit käyttöön kuville",
|
||||||
"AllowTonemappingHelp": "Sävykartoitus voi muuttaa videon dynaamisen alueen HDR:stä SDR:ksi säilyttäen samalla kuvan yksityiskohdat ja värit, jotka ovat kohtauksen alkuperäisen ilmeen kannalta erittäin tärkeitä. Toimii tällä hetkellä vain 10bit HDR10, HLG ja Dovi -videoiden kanssa ja edellyttää soveltuvaa OpenCL- tai CUDA-suoritusalustaa.",
|
"AllowTonemappingHelp": "Sävykartoitus voi muuttaa videon dynaamisen alueen HDR:stä SDR:ksi säilyttäen samalla kuvan yksityiskohdat ja värit, jotka ovat kohtauksen alkuperäisen ilmeen kannalta erittäin tärkeitä. Toimii tällä hetkellä vain 10-bit HDR10-, HLG- ja DoVi-videoiden kanssa ja edellyttää soveltuvaa OpenCL- tai CUDA-suoritusalustaa.",
|
||||||
"LabelffmpegPathHelp": "FFmpeg-sovellustiedoston tai -kansion tiedostosijainti.",
|
"LabelffmpegPathHelp": "FFmpeg-sovellustiedoston tai -kansion tiedostosijainti.",
|
||||||
"LabelKodiMetadataEnablePathSubstitutionHelp": "Mahdollistaa kuvien tiedostosijaintien korvauksen palvelimen korvausasetuksien perusteella.",
|
"LabelKodiMetadataEnablePathSubstitutionHelp": "Mahdollistaa kuvien tiedostosijaintien korvauksen palvelimen korvausasetuksien perusteella.",
|
||||||
"ThumbCard": "Pienoiskortti",
|
"ThumbCard": "Pienoiskortti",
|
||||||
|
@ -1761,5 +1761,6 @@
|
||||||
"LabelSegmentKeepSeconds": "Osioiden säilytysaika",
|
"LabelSegmentKeepSeconds": "Osioiden säilytysaika",
|
||||||
"LabelSegmentKeepSecondsHelp": "Aika sekunteina, jonka osiot säilytetään ennen päällekirjoitusta. Oltava \"Rahoita kun on kulunut\" -aikaa suurempi. Toimii vain osioiden poiston ollessa käytössä.",
|
"LabelSegmentKeepSecondsHelp": "Aika sekunteina, jonka osiot säilytetään ennen päällekirjoitusta. Oltava \"Rahoita kun on kulunut\" -aikaa suurempi. Toimii vain osioiden poiston ollessa käytössä.",
|
||||||
"LabelBackdropScreensaverInterval": "Taustanäytönsäästäjän ajoitus",
|
"LabelBackdropScreensaverInterval": "Taustanäytönsäästäjän ajoitus",
|
||||||
"LabelBackdropScreensaverIntervalHelp": "Aika sekuntteina, jonka kuluttua kuva vaihtuu taustanäytönsäästäjää käytettäessä."
|
"LabelBackdropScreensaverIntervalHelp": "Aika sekuntteina, jonka kuluttua kuva vaihtuu taustanäytönsäästäjää käytettäessä.",
|
||||||
|
"AllowAv1Encoding": "Salli enkoodaus AV1-muodossa"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1384,7 +1384,7 @@
|
||||||
"LabelTonemappingRange": "Gamme de mappage tonal",
|
"LabelTonemappingRange": "Gamme de mappage tonal",
|
||||||
"TonemappingAlgorithmHelp": "Le mappage tonal peut être affiné. Si vous n'êtes pas familier avec ces réglages, il est conseillé de choisir les valeurs recommandées. L'algorithme de mappage recommandé est 'BT.2390'.",
|
"TonemappingAlgorithmHelp": "Le mappage tonal peut être affiné. Si vous n'êtes pas familier avec ces réglages, il est conseillé de choisir les valeurs recommandées. L'algorithme de mappage recommandé est 'BT.2390'.",
|
||||||
"LabelTonemappingAlgorithm": "Sélectionner l'algorithme de mappage tonal à utiliser",
|
"LabelTonemappingAlgorithm": "Sélectionner l'algorithme de mappage tonal à utiliser",
|
||||||
"AllowTonemappingHelp": "Le mappage tonal est capable de convertir une gamme dynamique HDR en SDR tout en maintenant les détails et les couleurs d'image si importants au rendu de la scène originale. Pour le moment, ne fonctionne qu'avec les vidéos 10bits HDR10, HLG, DoVi et requiert les environnements d'exécution OpenCL et CUDA correspondant.",
|
"AllowTonemappingHelp": "Le mappage tonal est capable de convertir une gamme dynamique HDR en SDR tout en maintenant les détails et les couleurs d'image si importants au rendu de la scène originale. Pour le moment, ne fonctionne qu'avec les vidéos HDR10 10bits, HLG, DoVi et requiert les environnements d'exécution OpenCL et CUDA correspondant.",
|
||||||
"EnableTonemapping": "Activer le mappage tonal",
|
"EnableTonemapping": "Activer le mappage tonal",
|
||||||
"LabelOpenclDeviceHelp": "Ce dispositif OpenCL est utilisé pour le mappage tonal. La partie à gauche du point est le numéro de plate-forme et la partie à droite est le numéro du dispositif sur la plate-forme. La valeur par défaut est 0,0. Le fichier de l'application FFmpeg contenant l'accélération matérielle OpenCL est nécessaire.",
|
"LabelOpenclDeviceHelp": "Ce dispositif OpenCL est utilisé pour le mappage tonal. La partie à gauche du point est le numéro de plate-forme et la partie à droite est le numéro du dispositif sur la plate-forme. La valeur par défaut est 0,0. Le fichier de l'application FFmpeg contenant l'accélération matérielle OpenCL est nécessaire.",
|
||||||
"LabelOpenclDevice": "Dispositif OpenCL",
|
"LabelOpenclDevice": "Dispositif OpenCL",
|
||||||
|
@ -1748,7 +1748,7 @@
|
||||||
"LabelLevel": "Niveau",
|
"LabelLevel": "Niveau",
|
||||||
"LabelMediaDetails": "Détails du média",
|
"LabelMediaDetails": "Détails du média",
|
||||||
"LabelSystem": "Système",
|
"LabelSystem": "Système",
|
||||||
"LogLevel.Trace": "Trace",
|
"LogLevel.Trace": "Suivi",
|
||||||
"LogLevel.Debug": "Debug",
|
"LogLevel.Debug": "Debug",
|
||||||
"LogLevel.Information": "Information",
|
"LogLevel.Information": "Information",
|
||||||
"LogLevel.Warning": "Avertissement",
|
"LogLevel.Warning": "Avertissement",
|
||||||
|
@ -1763,5 +1763,6 @@
|
||||||
"LabelSegmentKeepSeconds": "Durée de conservation des segments",
|
"LabelSegmentKeepSeconds": "Durée de conservation des segments",
|
||||||
"LabelSegmentKeepSecondsHelp": "Durée en secondes de conservation des segments avant écrasement. La valeur doit être supérieure au délai d'ajustement. Ne fonctionne que si la suppression des segments est activée.",
|
"LabelSegmentKeepSecondsHelp": "Durée en secondes de conservation des segments avant écrasement. La valeur doit être supérieure au délai d'ajustement. Ne fonctionne que si la suppression des segments est activée.",
|
||||||
"LabelBackdropScreensaverIntervalHelp": "Le temps en secondes entre différents fonds d'écran lors de l'utilisation de l'économiseur d'écran à fonds d'écran.",
|
"LabelBackdropScreensaverIntervalHelp": "Le temps en secondes entre différents fonds d'écran lors de l'utilisation de l'économiseur d'écran à fonds d'écran.",
|
||||||
"LabelBackdropScreensaverInterval": "Intervalle de l'économiseur d'écran à fonds d'écran"
|
"LabelBackdropScreensaverInterval": "Intervalle de l'économiseur d'écran à fonds d'écran",
|
||||||
|
"AllowAv1Encoding": "Autoriser l'encodage au format AV1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@
|
||||||
"EnableThemeSongsHelp": "Speel titelmuziek af tijdens het bladeren door de bibliotheek.",
|
"EnableThemeSongsHelp": "Speel titelmuziek af tijdens het bladeren door de bibliotheek.",
|
||||||
"EnableThemeVideosHelp": "Speel titelfilms af op de achtergrond tijdens het bladeren door de bibliotheek.",
|
"EnableThemeVideosHelp": "Speel titelfilms af op de achtergrond tijdens het bladeren door de bibliotheek.",
|
||||||
"Ended": "Gestopt",
|
"Ended": "Gestopt",
|
||||||
"EndsAtValue": "Eindigt om {0}",
|
"EndsAtValue": "Afgelopen om {0}",
|
||||||
"Episodes": "Afleveringen",
|
"Episodes": "Afleveringen",
|
||||||
"ErrorAddingListingsToSchedulesDirect": "Er ging iets mis bij het toevoegen van de lineup aan uw Schedules Direct account. Schedules Direct staat maar een beperkt aantal lineups per account toe. Het kan nodig zijn dat u zich aan moet melden op de Schedules Direct-website en andere lineups moet verwijderen voordat u verder kunt.",
|
"ErrorAddingListingsToSchedulesDirect": "Er ging iets mis bij het toevoegen van de lineup aan uw Schedules Direct account. Schedules Direct staat maar een beperkt aantal lineups per account toe. Het kan nodig zijn dat u zich aan moet melden op de Schedules Direct-website en andere lineups moet verwijderen voordat u verder kunt.",
|
||||||
"ErrorAddingMediaPathToVirtualFolder": "Er ging iets mis bij het toevoegen van het mediapad. Controleer of het pad klopt en of Jellyfin toegang heeft tot de locatie.",
|
"ErrorAddingMediaPathToVirtualFolder": "Er ging iets mis bij het toevoegen van het mediapad. Controleer of het pad klopt en of Jellyfin toegang heeft tot de locatie.",
|
||||||
|
@ -1762,5 +1762,6 @@
|
||||||
"LabelThrottleDelaySecondsHelp": "Tijd in seconden waarna de transcoder wordt afgeknepen. Deze tijd moet voldoende lang zijn zodat de cliënt een gezonde buffer in stand kan houden. Werkt alleen als afknijpen is ingeschakeld.",
|
"LabelThrottleDelaySecondsHelp": "Tijd in seconden waarna de transcoder wordt afgeknepen. Deze tijd moet voldoende lang zijn zodat de cliënt een gezonde buffer in stand kan houden. Werkt alleen als afknijpen is ingeschakeld.",
|
||||||
"LabelSegmentKeepSeconds": "Bewaartijd segmenten",
|
"LabelSegmentKeepSeconds": "Bewaartijd segmenten",
|
||||||
"LabelBackdropScreensaverIntervalHelp": "Het aantal seconden dat een achtergrondafbeelding wordt getoond als onderdeel van de schermbeveiliging.",
|
"LabelBackdropScreensaverIntervalHelp": "Het aantal seconden dat een achtergrondafbeelding wordt getoond als onderdeel van de schermbeveiliging.",
|
||||||
"LabelBackdropScreensaverInterval": "Interval schermbeveiliging"
|
"LabelBackdropScreensaverInterval": "Interval schermbeveiliging",
|
||||||
|
"AllowAv1Encoding": "Coderen in AV1-formaat toestaan"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1760,5 +1760,6 @@
|
||||||
"LabelSegmentKeepSecondsHelp": "Час у секундах, протягом якого сегменти мають зберігатися перед перезаписом. Має бути більшим за \"Обмежити після\". Працює тільки якщо увімкнено видалення сегментів.",
|
"LabelSegmentKeepSecondsHelp": "Час у секундах, протягом якого сегменти мають зберігатися перед перезаписом. Має бути більшим за \"Обмежити після\". Працює тільки якщо увімкнено видалення сегментів.",
|
||||||
"LabelSegmentKeepSeconds": "Час збереження сегментів",
|
"LabelSegmentKeepSeconds": "Час збереження сегментів",
|
||||||
"LabelBackdropScreensaverIntervalHelp": "Час у секундах між різними фонами при використанні фонової заставки.",
|
"LabelBackdropScreensaverIntervalHelp": "Час у секундах між різними фонами при використанні фонової заставки.",
|
||||||
"LabelBackdropScreensaverInterval": "Інтервал між фоновими заставками"
|
"LabelBackdropScreensaverInterval": "Інтервал між фоновими заставками",
|
||||||
|
"AllowAv1Encoding": "Дозволити кодування у форматі AV1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { VideoType } from '@jellyfin/sdk/lib/generated-client/models/video-
|
||||||
import type { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
import type { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
|
||||||
import type { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status';
|
import type { SeriesStatus } from '@jellyfin/sdk/lib/generated-client/models/series-status';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
export type ParentId = string | null | undefined;
|
export type ParentId = string | null | undefined;
|
||||||
|
|
||||||
|
@ -23,12 +24,18 @@ interface Filters {
|
||||||
Years?: number[];
|
Years?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ViewMode {
|
||||||
|
GridView = 'grid',
|
||||||
|
ListView = 'list',
|
||||||
|
}
|
||||||
|
|
||||||
export interface LibraryViewSettings {
|
export interface LibraryViewSettings {
|
||||||
SortBy: ItemSortBy;
|
SortBy: ItemSortBy;
|
||||||
SortOrder: SortOrder;
|
SortOrder: SortOrder;
|
||||||
StartIndex: number;
|
StartIndex: number;
|
||||||
CardLayout: boolean;
|
CardLayout: boolean;
|
||||||
ImageType: string;
|
ImageType: ImageType;
|
||||||
|
ViewMode: ViewMode;
|
||||||
ShowTitle: boolean;
|
ShowTitle: boolean;
|
||||||
ShowYear?: boolean;
|
ShowYear?: boolean;
|
||||||
Filters?: Filters;
|
Filters?: Filters;
|
||||||
|
|
52
src/utils/jellyfin-apiclient/backdropImage.ts
Normal file
52
src/utils/jellyfin-apiclient/backdropImage.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||||
|
import type { ApiClient } from 'jellyfin-apiclient';
|
||||||
|
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||||
|
import { randomInt } from '../number';
|
||||||
|
|
||||||
|
export interface ScaleImageOptions {
|
||||||
|
maxWidth?: number;
|
||||||
|
width?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
height?: number;
|
||||||
|
fillWidth?: number;
|
||||||
|
fillHeight?: number;
|
||||||
|
quality?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the url of the first or a random backdrop image of an item.
|
||||||
|
* If the item has no backdrop image, the url of the first or a random backdrop image of the parent item is returned.
|
||||||
|
* Falls back to the primary image (cover) of the item, if neither the item nor it's parent have at least one backdrop image.
|
||||||
|
* Returns undefined if no usable image was found.
|
||||||
|
* @param apiClient The ApiClient to generate the url.
|
||||||
|
* @param item The item for which the backdrop image is requested.
|
||||||
|
* @param options Optional; allows to scale the backdrop image.
|
||||||
|
* @param random If set to true and the item has more than one backdrop, a random image is returned.
|
||||||
|
* @returns The url of the first or a random backdrop image of the provided item.
|
||||||
|
*/
|
||||||
|
export const getItemBackdropImageUrl = (apiClient: ApiClient, item: BaseItemDto, options: ScaleImageOptions = {}, random = false): string | undefined => {
|
||||||
|
if (item.Id && item.BackdropImageTags?.length) {
|
||||||
|
const backdropImgIndex = random ? randomInt(0, item.BackdropImageTags.length - 1) : 0;
|
||||||
|
return apiClient.getScaledImageUrl(item.Id, {
|
||||||
|
type: ImageType.Backdrop,
|
||||||
|
index: backdropImgIndex,
|
||||||
|
tag: item.BackdropImageTags[backdropImgIndex],
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
} else if (item.ParentBackdropItemId && item.ParentBackdropImageTags?.length) {
|
||||||
|
const backdropImgIndex = random ? randomInt(0, item.ParentBackdropImageTags.length - 1) : 0;
|
||||||
|
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
|
||||||
|
type: ImageType.Backdrop,
|
||||||
|
index: backdropImgIndex,
|
||||||
|
tag: item.ParentBackdropImageTags[backdropImgIndex],
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
} else if (item.Id && item.ImageTags?.Primary) {
|
||||||
|
return apiClient.getScaledImageUrl(item.Id, {
|
||||||
|
type: ImageType.Primary,
|
||||||
|
tag: item.ImageTags.Primary,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
10
src/utils/mediaSource.ts
Normal file
10
src/utils/mediaSource.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the media source is an HLS stream.
|
||||||
|
* @param mediaSource The media source.
|
||||||
|
* @returns _true_ if the media source is an HLS stream, _false_ otherwise.
|
||||||
|
*/
|
||||||
|
export function isHls(mediaSource: MediaSourceInfo|null|undefined): boolean {
|
||||||
|
return (mediaSource?.TranscodingSubProtocol || mediaSource?.Container) === 'hls';
|
||||||
|
}
|
28
webpack.analyze.js
Normal file
28
webpack.analyze.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
|
||||||
|
const prod = require('./webpack.prod');
|
||||||
|
|
||||||
|
const smp = new SpeedMeasurePlugin();
|
||||||
|
|
||||||
|
const config = merge(prod, {
|
||||||
|
plugins: [
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
excludeAssets: /-json\..*\.chunk\.js$/
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchPlugin = (name) => config.plugins.findIndex((e) => e.constructor.name === name);
|
||||||
|
|
||||||
|
// NOTE: We need to re-add the mini css plugin to workaround this issue
|
||||||
|
// https://github.com/stephencookdev/speed-measure-webpack-plugin/issues/167
|
||||||
|
const miniCssPluginIndex = searchPlugin('MiniCssExtractPlugin');
|
||||||
|
const miniCssPlugin = config.plugins[miniCssPluginIndex];
|
||||||
|
|
||||||
|
const exportedConfig = smp.wrap(config);
|
||||||
|
|
||||||
|
exportedConfig.plugins[miniCssPluginIndex] = miniCssPlugin;
|
||||||
|
|
||||||
|
module.exports = exportedConfig;
|
|
@ -1,6 +1,7 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const { DefinePlugin } = require('webpack');
|
const { DefinePlugin } = require('webpack');
|
||||||
|
@ -100,6 +101,11 @@ const config = {
|
||||||
to: path.resolve(__dirname, './dist')
|
to: path.resolve(__dirname, './dist')
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
typescript: {
|
||||||
|
configFile: path.resolve(__dirname, 'tsconfig.json')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
|
@ -110,6 +116,8 @@ const config = {
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
runtimeChunk: 'single',
|
runtimeChunk: 'single',
|
||||||
|
removeAvailableModules: false,
|
||||||
|
removeEmptyChunks: false,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
chunks: 'all',
|
chunks: 'all',
|
||||||
maxInitialRequests: Infinity,
|
maxInitialRequests: Infinity,
|
||||||
|
@ -216,14 +224,22 @@ const config = {
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: [
|
use: [
|
||||||
'worker-loader',
|
'worker-loader',
|
||||||
'ts-loader'
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx)$/,
|
test: /\.(ts|tsx)$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: [{
|
use: [{
|
||||||
loader: 'ts-loader'
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
/* modules that Babel breaks when transforming to ESM */
|
/* modules that Babel breaks when transforming to ESM */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue