Merge branch 'master' into issue5486
This commit is contained in:
@@ -1,15 +1,5 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import layoutManager from './layoutManager';
|
||||
import { DRAWER_WIDTH } from './ResponsiveDrawer';
|
||||
|
||||
const styles = layoutManager.experimental ? {
|
||||
left: {
|
||||
md: DRAWER_WIDTH
|
||||
}
|
||||
} : {};
|
||||
|
||||
const Backdrop = () => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
@@ -18,10 +8,7 @@ const Backdrop = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className='backdropContainer'
|
||||
sx={styles}
|
||||
/>
|
||||
<div className='backdropContainer' />
|
||||
<div className='backgroundContainer' />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import Page from 'components/Page';
|
||||
import toast from 'components/toast/toast';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import LinkButton from 'elements/emby-button/LinkButton';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ConnectionState, ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
interface ConnectionErrorPageProps {
|
||||
state: ConnectionState
|
||||
}
|
||||
|
||||
const ConnectionErrorPage: FC<ConnectionErrorPageProps> = ({
|
||||
state
|
||||
}) => {
|
||||
const [ title, setTitle ] = useState<string>();
|
||||
const [ htmlMessage, setHtmlMessage ] = useState<string>();
|
||||
const [ message, setMessage ] = useState<string>();
|
||||
const [ isConnectDisabled, setIsConnectDisabled ] = useState(false);
|
||||
|
||||
const onForceConnect = useCallback(async () => {
|
||||
setIsConnectDisabled(true);
|
||||
|
||||
try {
|
||||
const server = ServerConnections.getLastUsedServer();
|
||||
await ServerConnections.updateSavedServerId(server);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error('[ConnectionErrorPage] Failed to force connect to server', err);
|
||||
toast(globalize.translate('HeaderConnectionFailure'));
|
||||
setIsConnectDisabled(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
switch (state) {
|
||||
case ConnectionState.ServerMismatch:
|
||||
setTitle(globalize.translate('HeaderServerMismatch'));
|
||||
setHtmlMessage(undefined);
|
||||
setMessage(globalize.translate('MessageServerMismatch'));
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
setTitle(globalize.translate('HeaderUpdateRequired'));
|
||||
setHtmlMessage(globalize.translate(
|
||||
'ServerUpdateNeeded',
|
||||
'<a href="https://jellyfin.org/downloads/server/">jellyfin.org/downloads/server</a>'
|
||||
));
|
||||
setMessage(undefined);
|
||||
return;
|
||||
case ConnectionState.Unavailable:
|
||||
setTitle(globalize.translate('HeaderServerUnavailable'));
|
||||
setHtmlMessage(undefined);
|
||||
setMessage(globalize.translate('MessageUnableToConnectToServer'));
|
||||
}
|
||||
}, [ state ]);
|
||||
|
||||
if (!title) return;
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='connectionErrorPage'
|
||||
className='mainAnimatedPage standalonePage'
|
||||
isBackButtonEnabled={false}
|
||||
>
|
||||
<div className='padded-left padded-right'>
|
||||
<h1>{title}</h1>
|
||||
{htmlMessage && (
|
||||
<p
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlMessage) }}
|
||||
style={{ maxWidth: '80ch' }}
|
||||
/>
|
||||
)}
|
||||
{message && (
|
||||
<p style={{ maxWidth: '80ch' }}>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{appHost.supports(AppFeature.MultiServer) && (
|
||||
<LinkButton
|
||||
className='raised'
|
||||
href='/selectserver'
|
||||
>
|
||||
{globalize.translate('ButtonChangeServer')}
|
||||
</LinkButton>
|
||||
)}
|
||||
|
||||
{state === ConnectionState.ServerMismatch && (
|
||||
<LinkButton
|
||||
onClick={onForceConnect}
|
||||
style={ isConnectDisabled ? { pointerEvents: 'none' } : undefined }
|
||||
>
|
||||
{globalize.translate('ConnectAnyway')}
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionErrorPage;
|
||||
@@ -2,12 +2,10 @@ import React, { FunctionComponent, useCallback, useEffect, useState } from 'reac
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { ApiClient, ConnectResponse } from 'jellyfin-apiclient';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState';
|
||||
import { ConnectionState, ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import alert from './alert';
|
||||
import ConnectionErrorPage from './ConnectionErrorPage';
|
||||
import Loading from './loading/LoadingComponent';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
||||
enum AccessLevel {
|
||||
/** Requires a user with administrator access */
|
||||
@@ -33,6 +31,12 @@ type ConnectionRequiredProps = {
|
||||
level?: AccessLevelValue
|
||||
};
|
||||
|
||||
const ERROR_STATES = [
|
||||
ConnectionState.ServerMismatch,
|
||||
ConnectionState.ServerUpdateNeeded,
|
||||
ConnectionState.Unavailable
|
||||
];
|
||||
|
||||
const fetchPublicSystemInfo = async (apiClient: ApiClient) => {
|
||||
const infoResponse = await fetch(
|
||||
`${apiClient.serverAddress()}/System/Info/Public`,
|
||||
@@ -57,8 +61,16 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [ errorState, setErrorState ] = useState<ConnectionState>();
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
|
||||
const navigateIfNotThere = useCallback((route: BounceRoutes) => {
|
||||
// If we try to navigate to the current route, just set isLoading = false
|
||||
if (location.pathname === route) setIsLoading(false);
|
||||
// Otherwise navigate to the route
|
||||
else navigate(route);
|
||||
}, [ location.pathname, navigate ]);
|
||||
|
||||
const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
@@ -78,31 +90,14 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
if (location.pathname === BounceRoutes.SelectServer) {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
}
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigateIfNotThere(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, navigate]);
|
||||
}, [ navigateIfNotThere, location.pathname, navigate ]);
|
||||
|
||||
const handleWizard = useCallback(async (firstConnection: ConnectResponse | null) => {
|
||||
const apiClient = firstConnection?.ApiClient || ServerConnections.currentApiClient();
|
||||
@@ -195,7 +190,9 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
console.debug('[ConnectionRequired] connection state', firstConnection?.State);
|
||||
ServerConnections.firstConnection = true;
|
||||
|
||||
if (level === AccessLevel.Wizard) {
|
||||
if (ERROR_STATES.includes(firstConnection?.State)) {
|
||||
setErrorState(firstConnection.State);
|
||||
} else if (level === AccessLevel.Wizard) {
|
||||
handleWizard(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] could not validate wizard status', err);
|
||||
@@ -218,6 +215,10 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
});
|
||||
}, [handleIncompleteWizard, handleWizard, level, validateUserAccess]);
|
||||
|
||||
if (errorState) {
|
||||
return <ConnectionErrorPage state={errorState} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const ElevationScroll = ({ children, elevate = false }: { children: ReactElement
|
||||
const isElevated = elevate || trigger;
|
||||
|
||||
return React.cloneElement(children, {
|
||||
color: isElevated ? 'primary' : 'transparent',
|
||||
color: isElevated ? 'default' : 'transparent',
|
||||
elevation: isElevated ? 4 : 0
|
||||
});
|
||||
};
|
||||
|
||||
+31
-30
@@ -2,9 +2,10 @@ import type { SvgIconComponent } from '@mui/icons-material';
|
||||
import ImageNotSupported from '@mui/icons-material/ImageNotSupported';
|
||||
import Box from '@mui/material/Box/Box';
|
||||
import Paper from '@mui/material/Paper/Paper';
|
||||
import Skeleton from '@mui/material/Skeleton/Skeleton';
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
import { LoadingSkeleton } from './LoadingSkeleton';
|
||||
|
||||
interface ImageProps {
|
||||
isLoading: boolean
|
||||
alt?: string
|
||||
@@ -30,36 +31,36 @@ const Image: FC<ImageProps> = ({
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{isLoading && (
|
||||
<Skeleton
|
||||
variant='rectangular'
|
||||
width='100%'
|
||||
height='100%'
|
||||
/>
|
||||
)}
|
||||
{url ? (
|
||||
<img
|
||||
src={url}
|
||||
alt={alt}
|
||||
width='100%'
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<FallbackIcon
|
||||
sx={{
|
||||
height: '25%',
|
||||
width: 'auto'
|
||||
}}
|
||||
<LoadingSkeleton
|
||||
isLoading={isLoading}
|
||||
variant='rectangular'
|
||||
width='100%'
|
||||
height='100%'
|
||||
>
|
||||
{url ? (
|
||||
<img
|
||||
src={url}
|
||||
alt={alt}
|
||||
width='100%'
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<FallbackIcon
|
||||
sx={{
|
||||
height: '25%',
|
||||
width: 'auto'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</LoadingSkeleton>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import Skeleton, { type SkeletonProps } from '@mui/material/Skeleton/Skeleton';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
interface LoadingSkeletonProps extends SkeletonProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const LoadingSkeleton: FC<LoadingSkeletonProps> = ({
|
||||
children,
|
||||
isLoading,
|
||||
...props
|
||||
}) => (
|
||||
isLoading ? (
|
||||
<Skeleton {...props} />
|
||||
) : (
|
||||
children
|
||||
)
|
||||
);
|
||||
@@ -1,161 +0,0 @@
|
||||
// NOTE: This is used for jsdoc return type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { MINIMUM_VERSION } from '@jellyfin/sdk/lib/versions';
|
||||
import { ConnectionManager, Credentials, ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import Events from '../utils/events.ts';
|
||||
import { setUserInfo } from '../scripts/settings/userSettings';
|
||||
import appSettings from '../scripts/settings/appSettings';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
|
||||
const normalizeImageOptions = options => {
|
||||
if (!options.quality && (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight)) {
|
||||
options.quality = 90;
|
||||
}
|
||||
};
|
||||
|
||||
const getMaxBandwidth = () => {
|
||||
if (navigator.connection) {
|
||||
let max = navigator.connection.downlinkMax;
|
||||
if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
|
||||
max /= 8;
|
||||
max *= 1000000;
|
||||
max *= 0.7;
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
class ServerConnections extends ConnectionManager {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.localApiClient = null;
|
||||
this.firstConnection = null;
|
||||
|
||||
// Set the apiclient minimum version to match the SDK
|
||||
this._minServerVersion = MINIMUM_VERSION;
|
||||
|
||||
Events.on(this, 'localusersignedout', (_e, logoutInfo) => {
|
||||
setUserInfo(null, null);
|
||||
// Ensure the updated credentials are persisted to storage
|
||||
credentialProvider.credentials(credentialProvider.credentials());
|
||||
|
||||
if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') {
|
||||
window.NativeShell.onLocalUserSignedOut(logoutInfo);
|
||||
}
|
||||
});
|
||||
|
||||
Events.on(this, 'apiclientcreated', (_e, apiClient) => {
|
||||
apiClient.getMaxBandwidth = getMaxBandwidth;
|
||||
apiClient.normalizeImageOptions = normalizeImageOptions;
|
||||
});
|
||||
}
|
||||
|
||||
initApiClient(server) {
|
||||
console.debug('creating ApiClient singleton');
|
||||
|
||||
const apiClient = new ApiClient(
|
||||
server,
|
||||
appHost.appName(),
|
||||
appHost.appVersion(),
|
||||
appHost.deviceName(),
|
||||
appHost.deviceId()
|
||||
);
|
||||
|
||||
apiClient.enableAutomaticNetworking = false;
|
||||
apiClient.manualAddressOnly = true;
|
||||
|
||||
this.addApiClient(apiClient);
|
||||
|
||||
this.setLocalApiClient(apiClient);
|
||||
|
||||
console.debug('loaded ApiClient singleton');
|
||||
}
|
||||
|
||||
connect(options) {
|
||||
return super.connect({
|
||||
enableAutoLogin: appSettings.enableAutoLogin(),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
setLocalApiClient(apiClient) {
|
||||
if (apiClient) {
|
||||
this.localApiClient = apiClient;
|
||||
window.ApiClient = apiClient;
|
||||
}
|
||||
}
|
||||
|
||||
getLocalApiClient() {
|
||||
return this.localApiClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ApiClient that is currently connected.
|
||||
* @returns {ApiClient|undefined} apiClient
|
||||
*/
|
||||
currentApiClient() {
|
||||
let apiClient = this.getLocalApiClient();
|
||||
|
||||
if (!apiClient) {
|
||||
const server = this.getLastUsedServer();
|
||||
|
||||
if (server) {
|
||||
apiClient = this.getApiClient(server.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Api that is currently connected.
|
||||
* @returns {Api|undefined} The current Api instance.
|
||||
*/
|
||||
getCurrentApi() {
|
||||
const apiClient = this.currentApiClient();
|
||||
if (!apiClient) return;
|
||||
|
||||
return toApi(apiClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ApiClient that is currently connected or throws if not defined.
|
||||
* @async
|
||||
* @returns {Promise<ApiClient>} The current ApiClient instance.
|
||||
*/
|
||||
async getCurrentApiClientAsync() {
|
||||
const apiClient = this.currentApiClient();
|
||||
if (!apiClient) throw new Error('[ServerConnection] No current ApiClient instance');
|
||||
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
onLocalUserSignedIn(user) {
|
||||
const apiClient = this.getApiClient(user.ServerId);
|
||||
this.setLocalApiClient(apiClient);
|
||||
return setUserInfo(user.Id, apiClient).then(() => {
|
||||
if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedIn === 'function') {
|
||||
return window.NativeShell.onLocalUserSignedIn(user, apiClient.accessToken());
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const credentialProvider = new Credentials();
|
||||
|
||||
const capabilities = Dashboard.capabilities(appHost);
|
||||
|
||||
export default new ServerConnections(
|
||||
credentialProvider,
|
||||
appHost.appName(),
|
||||
appHost.appVersion(),
|
||||
appHost.deviceName(),
|
||||
appHost.deviceId(),
|
||||
capabilities);
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FunctionComponent, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ServerConnections from './ServerConnections';
|
||||
import viewManager from './viewManager/viewManager';
|
||||
import globalize from '../lib/globalize';
|
||||
import type { RestoreViewFailResponse } from '../types/viewManager';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import type { RestoreViewFailResponse } from 'types/viewManager';
|
||||
|
||||
interface ServerContentPageProps {
|
||||
view: string
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
import Events from '../utils/events.ts';
|
||||
import globalize from '../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../scripts/dom';
|
||||
import { formatRelative } from 'date-fns';
|
||||
import serverNotifications from '../scripts/serverNotifications';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import './listview/listview.scss';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import alert from './alert';
|
||||
import { getLocale } from '../utils/dateFnsLocale.ts';
|
||||
import { toBoolean } from '../utils/string.ts';
|
||||
|
||||
+20
-39
@@ -5,6 +5,7 @@ import * as htmlMediaHelper from '../components/htmlMediaHelper';
|
||||
import * as webSettings from '../scripts/settings/webSettings';
|
||||
import globalize from '../lib/globalize';
|
||||
import profileBuilder from '../scripts/browserDeviceProfile';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
|
||||
const appName = 'Jellyfin Web';
|
||||
|
||||
@@ -169,14 +170,6 @@ function getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
function supportsVoiceInput() {
|
||||
if (!browser.tv) {
|
||||
return window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.oSpeechRecognition || window.msSpeechRecognition;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function supportsFullscreen() {
|
||||
if (browser.tv) {
|
||||
return false;
|
||||
@@ -235,77 +228,65 @@ const supportedFeatures = function () {
|
||||
const features = [];
|
||||
|
||||
if (navigator.share) {
|
||||
features.push('sharing');
|
||||
features.push(AppFeature.Sharing);
|
||||
}
|
||||
|
||||
if (!browser.edgeUwp && !browser.tv && !browser.xboxOne && !browser.ps4) {
|
||||
features.push('filedownload');
|
||||
features.push(AppFeature.FileDownload);
|
||||
}
|
||||
|
||||
if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
|
||||
features.push('exit');
|
||||
} else {
|
||||
features.push('plugins');
|
||||
features.push(AppFeature.Exit);
|
||||
}
|
||||
|
||||
if (!browser.operaTv && !browser.tizen && !browser.orsay && !browser.web0s && !browser.ps4) {
|
||||
features.push('externallinks');
|
||||
features.push('externalpremium');
|
||||
}
|
||||
|
||||
if (!browser.operaTv) {
|
||||
features.push('externallinkdisplay');
|
||||
}
|
||||
|
||||
if (supportsVoiceInput()) {
|
||||
features.push('voiceinput');
|
||||
features.push(AppFeature.ExternalLinks);
|
||||
}
|
||||
|
||||
if (supportsHtmlMediaAutoplay()) {
|
||||
features.push('htmlaudioautoplay');
|
||||
features.push('htmlvideoautoplay');
|
||||
features.push(AppFeature.HtmlAudioAutoplay);
|
||||
features.push(AppFeature.HtmlVideoAutoplay);
|
||||
}
|
||||
|
||||
if (supportsFullscreen()) {
|
||||
features.push('fullscreenchange');
|
||||
features.push(AppFeature.Fullscreen);
|
||||
}
|
||||
|
||||
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile || browser.ipad) {
|
||||
features.push('physicalvolumecontrol');
|
||||
features.push(AppFeature.PhysicalVolumeControl);
|
||||
}
|
||||
|
||||
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
|
||||
features.push('remotecontrol');
|
||||
features.push(AppFeature.RemoteControl);
|
||||
}
|
||||
|
||||
if (!browser.operaTv && !browser.tizen && !browser.orsay && !browser.web0s && !browser.edgeUwp) {
|
||||
features.push('remotevideo');
|
||||
features.push(AppFeature.RemoteVideo);
|
||||
}
|
||||
|
||||
features.push('displaylanguage');
|
||||
features.push('otherapppromotions');
|
||||
features.push('displaymode');
|
||||
features.push('targetblank');
|
||||
features.push('screensaver');
|
||||
features.push(AppFeature.DisplayLanguage);
|
||||
features.push(AppFeature.DisplayMode);
|
||||
features.push(AppFeature.TargetBlank);
|
||||
features.push(AppFeature.Screensaver);
|
||||
|
||||
webSettings.getMultiServer().then(enabled => {
|
||||
if (enabled) features.push('multiserver');
|
||||
if (enabled) features.push(AppFeature.MultiServer);
|
||||
});
|
||||
|
||||
if (!browser.orsay && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) {
|
||||
features.push('subtitleappearancesettings');
|
||||
features.push(AppFeature.SubtitleAppearance);
|
||||
}
|
||||
|
||||
if (!browser.orsay) {
|
||||
features.push('subtitleburnsettings');
|
||||
features.push(AppFeature.SubtitleBurnIn);
|
||||
}
|
||||
|
||||
if (!browser.tv && !browser.ps4 && !browser.xboxOne) {
|
||||
features.push('fileinput');
|
||||
features.push(AppFeature.FileInput);
|
||||
}
|
||||
|
||||
if (browser.chrome || browser.edgeChromium) {
|
||||
features.push('chromecast');
|
||||
features.push(AppFeature.Chromecast);
|
||||
}
|
||||
|
||||
return features;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import browser from '../../scripts/browser';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import dom from '../../scripts/dom';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
import './backdrop.scss';
|
||||
|
||||
@@ -213,19 +213,19 @@ function enabled() {
|
||||
let rotationInterval;
|
||||
let currentRotatingImages = [];
|
||||
let currentRotationIndex = -1;
|
||||
export function setBackdrops(items, imageOptions, enableImageRotation, isEnabled = false) {
|
||||
export function setBackdrops(items, imageOptions, isEnabled = false) {
|
||||
if (isEnabled || enabled()) {
|
||||
const images = getImageUrls(items, imageOptions);
|
||||
|
||||
if (images.length) {
|
||||
startRotation(images, enableImageRotation);
|
||||
setBackdropImages(images);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startRotation(images, enableImageRotation) {
|
||||
export function setBackdropImages(images) {
|
||||
if (isEqual(images, currentRotatingImages)) {
|
||||
return;
|
||||
}
|
||||
@@ -235,7 +235,7 @@ function startRotation(images, enableImageRotation) {
|
||||
currentRotatingImages = images;
|
||||
currentRotationIndex = -1;
|
||||
|
||||
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
|
||||
if (images.length > 1 && enableRotation()) {
|
||||
rotationInterval = setInterval(onRotationInterval, 24000);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import browser from 'scripts/browser';
|
||||
import datetime from 'scripts/datetime';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
import { getItemTypeIcon, getLibraryIcon } from 'utils/image';
|
||||
|
||||
@@ -22,7 +23,6 @@ import itemHelper from '../itemHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import itemShortcuts from '../shortcuts';
|
||||
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
@@ -1333,6 +1333,7 @@ function updateUserData(card, userData) {
|
||||
innerCardFooter.appendChild(itemProgressBar);
|
||||
}
|
||||
|
||||
card.setAttribute('data-positionticks', userData.PlaybackPositionTicks);
|
||||
itemProgressBar.innerHTML = progressHtml;
|
||||
} else {
|
||||
itemProgressBar = card.querySelector('.itemProgressBar');
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import layoutManager from '../layoutManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import dom from '../../scripts/dom';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import actionsheet from '../actionSheet/actionSheet';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
@@ -10,7 +11,6 @@ import '../../elements/emby-button/emby-button';
|
||||
import '../listview/listview.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
export default class ChannelMapper {
|
||||
constructor(options) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import loading from '../loading/loading';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
@@ -13,7 +14,6 @@ import '../../elements/emby-select/emby-select';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
let currentServerId;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import browser from '../../scripts/browser';
|
||||
import dialog from '../dialog/dialog';
|
||||
import globalize from '../../lib/globalize';
|
||||
|
||||
function useNativeConfirm() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
&& !(browser.tizenVersion && (browser.tizenVersion < 3 || browser.tizenVersion >= 8))
|
||||
&& browser.tv
|
||||
&& window.confirm;
|
||||
}
|
||||
|
||||
async function nativeConfirm(options) {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
title: '',
|
||||
text: options
|
||||
};
|
||||
}
|
||||
|
||||
const text = (options.text || '').replaceAll('<br/>', '\n');
|
||||
await appRouter.ready();
|
||||
const result = window.confirm(text);
|
||||
|
||||
if (result) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
async function customConfirm(text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
|
||||
items.push({
|
||||
name: options.cancelText || globalize.translate('ButtonCancel'),
|
||||
id: 'cancel',
|
||||
type: 'cancel'
|
||||
});
|
||||
|
||||
items.push({
|
||||
name: options.confirmText || globalize.translate('ButtonOk'),
|
||||
id: 'ok',
|
||||
type: options.primary === 'delete' ? 'delete' : 'submit'
|
||||
});
|
||||
|
||||
options.buttons = items;
|
||||
|
||||
await appRouter.ready();
|
||||
|
||||
return dialog.show(options).then(result => {
|
||||
if (result === 'ok') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
|
||||
const confirm = useNativeConfirm() ? nativeConfirm : customConfirm;
|
||||
|
||||
export default confirm;
|
||||
@@ -0,0 +1,85 @@
|
||||
import dialog from 'components/dialog/dialog';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'lib/globalize';
|
||||
import browser from 'scripts/browser';
|
||||
|
||||
interface OptionItem {
|
||||
id: string,
|
||||
name: string,
|
||||
type: 'cancel' | 'delete' | 'submit'
|
||||
}
|
||||
|
||||
interface ConfirmOptions {
|
||||
title?: string,
|
||||
text: string
|
||||
cancelText?: string,
|
||||
confirmText?: string,
|
||||
primary?: string
|
||||
buttons?: OptionItem[]
|
||||
}
|
||||
|
||||
function shouldUseNativeConfirm() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
&& !(browser.tizenVersion && (browser.tizenVersion < 3 || browser.tizenVersion >= 8))
|
||||
&& browser.tv
|
||||
&& !!window.confirm;
|
||||
}
|
||||
|
||||
async function nativeConfirm(options: string | ConfirmOptions) {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
text: options
|
||||
} as ConfirmOptions;
|
||||
}
|
||||
|
||||
const text = (options.text || '').replace(/<br\/>/g, '\n');
|
||||
await appRouter.ready();
|
||||
const result = window.confirm(text);
|
||||
|
||||
if (result) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(new Error('Confirm dialog rejected'));
|
||||
}
|
||||
}
|
||||
|
||||
async function customConfirm(options: string | ConfirmOptions, title: string = '') {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
title,
|
||||
text: options
|
||||
};
|
||||
}
|
||||
|
||||
const items: OptionItem[] = [];
|
||||
|
||||
items.push({
|
||||
name: options.cancelText || globalize.translate('ButtonCancel'),
|
||||
id: 'cancel',
|
||||
type: 'cancel'
|
||||
});
|
||||
|
||||
items.push({
|
||||
name: options.confirmText || globalize.translate('ButtonOk'),
|
||||
id: 'ok',
|
||||
type: options.primary === 'delete' ? 'delete' : 'submit'
|
||||
});
|
||||
|
||||
options.buttons = items;
|
||||
|
||||
await appRouter.ready();
|
||||
|
||||
return dialog.show(options).then(result => {
|
||||
if (result === 'ok') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Confirm dialog rejected'));
|
||||
});
|
||||
}
|
||||
|
||||
const confirm = shouldUseNativeConfirm() ? nativeConfirm : customConfirm;
|
||||
|
||||
export default confirm;
|
||||
@@ -3,6 +3,7 @@ import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
import inputManager from '../../scripts/inputManager';
|
||||
import { toBoolean } from '../../utils/string.ts';
|
||||
import { hide } from '../loading/loading.ts';
|
||||
import dom from '../../scripts/dom';
|
||||
|
||||
import { history } from 'RootAppRouter';
|
||||
@@ -99,6 +100,8 @@ function DialogHashHandler(dlg, hash, resolve) {
|
||||
}
|
||||
|
||||
removeBackdrop(dlg);
|
||||
hide();
|
||||
|
||||
dlg.classList.remove('opened');
|
||||
|
||||
if (removeScrollLockOnClose) {
|
||||
|
||||
@@ -212,7 +212,9 @@ function initEditor(content, options, fileOptions) {
|
||||
let networkSharePath = this.querySelector('#txtNetworkPath');
|
||||
networkSharePath = networkSharePath ? networkSharePath.value : null;
|
||||
const path = this.querySelector('#txtDirectoryPickerPath').value;
|
||||
validatePath(path, options.validateWriteable, ApiClient).then(options.callback(path, networkSharePath));
|
||||
validatePath(path, options.validateWriteable, ApiClient).then(
|
||||
options.callback(path, networkSharePath)
|
||||
).catch(() => { /* no-op */ });
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
@@ -6,6 +8,7 @@ import { appHost } from '../apphost';
|
||||
import focusManager from '../focusManager';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import loading from '../loading/loading';
|
||||
import skinManager from '../../scripts/themeManager';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
@@ -14,7 +17,6 @@ import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-textarea/emby-textarea';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './displaySettings.template.html';
|
||||
|
||||
@@ -68,19 +70,19 @@ function showOrHideMissingEpisodesField(context) {
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings) {
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
if (appHost.supports(AppFeature.DisplayLanguage)) {
|
||||
context.querySelector('.languageSection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.languageSection').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('displaymode')) {
|
||||
if (appHost.supports(AppFeature.DisplayMode)) {
|
||||
context.querySelector('.fldDisplayMode').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDisplayMode').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('externallinks')) {
|
||||
if (appHost.supports(AppFeature.ExternalLinks)) {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
|
||||
@@ -88,7 +90,7 @@ function loadForm(context, user, userSettings) {
|
||||
|
||||
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
|
||||
|
||||
if (appHost.supports('screensaver')) {
|
||||
if (appHost.supports(AppFeature.Screensaver)) {
|
||||
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
|
||||
context.querySelector('.txtBackdropScreensaverIntervalContainer').classList.remove('hide');
|
||||
context.querySelector('.txtScreensaverTimeContainer').classList.remove('hide');
|
||||
@@ -143,7 +145,7 @@ function loadForm(context, user, userSettings) {
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
|
||||
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
if (appHost.supports(AppFeature.DisplayLanguage)) {
|
||||
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
export function getFetchPromise(request) {
|
||||
const headers = request.headers || {};
|
||||
|
||||
if (request.dataType === 'json') {
|
||||
headers.accept = 'application/json';
|
||||
}
|
||||
|
||||
const fetchRequest = {
|
||||
headers: headers,
|
||||
method: request.type,
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
let contentType = request.contentType;
|
||||
|
||||
if (request.data) {
|
||||
if (typeof request.data === 'string') {
|
||||
fetchRequest.body = request.data;
|
||||
} else {
|
||||
fetchRequest.body = paramsToString(request.data);
|
||||
|
||||
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
let url = request.url;
|
||||
|
||||
if (request.query) {
|
||||
const paramString = paramsToString(request.query);
|
||||
if (paramString) {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.timeout) {
|
||||
return fetch(url, fetchRequest);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(url, fetchRequest, request.timeout);
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, options, timeoutMs) {
|
||||
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = setTimeout(reject, timeoutMs);
|
||||
|
||||
options = options || {};
|
||||
options.credentials = 'same-origin';
|
||||
|
||||
fetch(url, options).then(function (response) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
|
||||
|
||||
resolve(response);
|
||||
}, function (error) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param params {Record<string, string | number | boolean>}
|
||||
* @returns {string} Query string
|
||||
*/
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
// eslint-disable-next-line sonarjs/different-types-comparison
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function ajax(request) {
|
||||
if (!request) {
|
||||
throw new Error('Request cannot be null');
|
||||
}
|
||||
|
||||
request.headers = request.headers || {};
|
||||
|
||||
console.debug(`requesting url: ${request.url}`);
|
||||
|
||||
return getFetchPromise(request).then(function (response) {
|
||||
console.debug(`response status: ${response.status}, url: ${request.url}`);
|
||||
if (response.status < 400) {
|
||||
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
|
||||
return response.json();
|
||||
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
|
||||
return response.text();
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(`request failed to url: ${request.url}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import dom from '../../scripts/dom';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import union from 'lodash-es/union';
|
||||
import Events from '../../utils/events.ts';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-collapse/emby-collapse';
|
||||
import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './filterdialog.template.html';
|
||||
import { stopMultiSelect } from '../../components/multiSelect/multiSelect';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import inputManager from '../../scripts/inputManager';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
@@ -14,7 +15,6 @@ import '../../elements/emby-select/emby-select';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './filtermenu.template.html';
|
||||
|
||||
function onSubmit(e) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../scripts/dom';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
||||
function onGroupedCardClick(e, card) {
|
||||
const itemId = card.getAttribute('data-id');
|
||||
|
||||
@@ -2,6 +2,7 @@ import escapeHtml from 'escape-html';
|
||||
import inputManager from '../../scripts/inputManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import Events from '../../utils/events.ts';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import serverNotifications from '../../scripts/serverNotifications';
|
||||
@@ -25,7 +26,6 @@ import '../../elements/emby-tabs/emby-tabs';
|
||||
import '../../elements/emby-scroller/emby-scroller';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import 'webcomponents.js/webcomponents-lite';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './tvguide.template.html';
|
||||
|
||||
function showViewSettings(instance) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import { getUserViewsQuery } from 'hooks/useUserViews';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
@@ -15,7 +16,6 @@ import dom from '../../scripts/dom';
|
||||
import '../listview/listview.scss';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './homeScreenSettings.template.html';
|
||||
import { LibraryTab } from '../../types/libraryTab.ts';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { ApiClient } from 'jellyfin-apiclient';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { ApiClient } from 'jellyfin-apiclient';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape } from 'utils/card';
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collec
|
||||
import escapeHtml from 'escape-html';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import globalize from 'lib/globalize';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
|
||||
|
||||
import type { SectionContainerElement, SectionOptions } from './section';
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base
|
||||
import type { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import type { UserSettings } from 'scripts/settings/userSettings';
|
||||
import { getBackdropShape, getPortraitShape } from 'utils/card';
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ export function enableHlsJsPlayer(runTimeTicks, mediaType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Native HLS support in WebOS only plays stereo sound. hls.js works better, but works only on WebOS 4 or newer.
|
||||
// Using hls.js also seems to fix fast forward issues that native HLS has.
|
||||
if (browser.web0sVersion >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The native players on these devices support seeking live streams, no need to use hls.js here
|
||||
if (browser.tizen || browser.web0s) {
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import dom from '../../scripts/dom';
|
||||
import loading from '../loading/loading';
|
||||
import { appHost } from '../apphost';
|
||||
@@ -7,12 +8,12 @@ import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../formdialog.scss';
|
||||
import '../cardbuilder/card.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './imageDownloader.template.html';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
@@ -205,7 +206,7 @@ function getRemoteImageHtml(image, imageType) {
|
||||
html += '<div class="cardPadder-' + shape + '"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
|
||||
if (layoutManager.tv || !appHost.supports('externallinks')) {
|
||||
if (layoutManager.tv || !appHost.supports(AppFeature.ExternalLinks)) {
|
||||
html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
|
||||
} else {
|
||||
html += '<a is="emby-linkbutton" target="_blank" href="' + image.Url + '" class="button-link cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain"></a>';
|
||||
|
||||
@@ -10,11 +10,12 @@ import loading from '../loading/loading';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../formdialog.scss';
|
||||
import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './imageUploader.template.html';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import layoutManager from '../layoutManager';
|
||||
import focusManager from '../focusManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import browser from '../../scripts/browser';
|
||||
@@ -13,7 +15,6 @@ import '../formdialog.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import './imageeditor.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import confirm from '../confirm/confirm';
|
||||
import template from './imageeditor.template.html';
|
||||
@@ -339,7 +340,7 @@ function showActionSheet(context, imageCard) {
|
||||
|
||||
function initEditor(context, options) {
|
||||
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu');
|
||||
const isFileInputSupported = appHost.supports('fileinput');
|
||||
const isFileInputSupported = appHost.supports(AppFeature.FileInput);
|
||||
for (let i = 0, length = uploadButtons.length; i < length; i++) {
|
||||
if (isFileInputSupported) {
|
||||
uploadButtons[i].classList.remove('hide');
|
||||
|
||||
@@ -2,15 +2,16 @@ import browser from '../scripts/browser';
|
||||
import { copy } from '../scripts/clipboard';
|
||||
import dom from '../scripts/dom';
|
||||
import globalize from '../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import actionsheet from './actionSheet/actionSheet';
|
||||
import { appHost } from './apphost';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import itemHelper, { canEditPlaylist } from './itemHelper';
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import toast from './toast/toast';
|
||||
import * as userSettings from '../scripts/settings/userSettings';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
|
||||
function getDeleteLabel(type) {
|
||||
switch (type) {
|
||||
@@ -169,7 +170,7 @@ export async function getCommands(options) {
|
||||
});
|
||||
}
|
||||
|
||||
if (appHost.supports('filedownload')) {
|
||||
if (appHost.supports(AppFeature.FileDownload)) {
|
||||
// CanDownload should probably be updated to return true for these items?
|
||||
if (user.Policy.EnableContentDownloading && (item.Type === 'Season' || item.Type == 'Series')) {
|
||||
commands.push({
|
||||
|
||||
@@ -6,8 +6,9 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'
|
||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import globalize from 'lib/globalize';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
|
||||
export function getDisplayName(item, options = {}) {
|
||||
@@ -238,7 +239,7 @@ export function canShare (item, user) {
|
||||
if (isLocalItem(item)) {
|
||||
return false;
|
||||
}
|
||||
return user.Policy.EnablePublicSharing && appHost.supports('sharing');
|
||||
return user.Policy.EnablePublicSharing && appHost.supports(AppFeature.Sharing);
|
||||
}
|
||||
|
||||
export function enableDateAddedDisplay (item) {
|
||||
|
||||
@@ -5,22 +5,27 @@
|
||||
*/
|
||||
|
||||
import escapeHtml from 'escape-html';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import toast from '../toast/toast';
|
||||
import { copy } from '../../scripts/clipboard';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../lib/globalize';
|
||||
import itemHelper from '../../components/itemHelper';
|
||||
import loading from '../loading/loading';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../listview/listview.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../formdialog.scss';
|
||||
|
||||
import dialogHelper from 'components/dialogHelper/dialogHelper';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import loading from 'components/loading/loading';
|
||||
import toast from 'components/toast/toast';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { copy } from 'scripts/clipboard';
|
||||
import dom from 'scripts/dom';
|
||||
import { getReadableSize } from 'utils/file';
|
||||
|
||||
import 'components/formdialog.scss';
|
||||
import 'components/listview/listview.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
import 'styles/flexstyles.scss';
|
||||
|
||||
import template from './itemMediaInfo.template.html';
|
||||
|
||||
// Do not add extra spaces between tags - they will be copied into the result
|
||||
@@ -68,7 +73,7 @@ function getMediaSourceHtml(user, item, version) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}<br/>`;
|
||||
}
|
||||
if (version.Size) {
|
||||
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
const size = getReadableSize(version.Size);
|
||||
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
|
||||
}
|
||||
version.MediaStreams.sort(itemHelper.sortTracks);
|
||||
|
||||
@@ -8,6 +8,7 @@ import escapeHtml from 'escape-html';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import focusManager from '../focusManager';
|
||||
@@ -18,7 +19,6 @@ import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../formdialog.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../cardbuilder/card.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './itemidentifier.template.html';
|
||||
import datetime from '../../scripts/datetime';
|
||||
|
||||
@@ -7,10 +7,10 @@ export class LazyLoader {
|
||||
createObserver() {
|
||||
const callback = this.options.callback;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const newObserver = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
callback(entry);
|
||||
callback(entry, observer);
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -18,7 +18,7 @@ export class LazyLoader {
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
this.observer = observer;
|
||||
this.observer = newObserver;
|
||||
}
|
||||
|
||||
addElements(elements) {
|
||||
|
||||
@@ -10,12 +10,12 @@ import mediaInfo from '../mediainfo/mediainfo';
|
||||
import indicators from '../indicators/indicators';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import './listview.scss';
|
||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-playstatebutton/emby-playstatebutton';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
|
||||
import markdownIt from 'markdown-it';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import loading from '../loading/loading';
|
||||
import focusManager from '../focusManager';
|
||||
import dom from '../../scripts/dom';
|
||||
@@ -16,7 +17,6 @@ import 'material-design-icons-iconfont';
|
||||
import './lyricseditor.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './lyricseditor.template.html';
|
||||
import templatePreview from './lyricspreview.template.html';
|
||||
|
||||
@@ -3,12 +3,12 @@ import escapeHtml from 'escape-html';
|
||||
import { getLyricsApi } from '@jellyfin/sdk/lib/utils/api/lyrics-api';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import dom from '../../scripts/dom';
|
||||
import loading from '../../components/loading/loading';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import template from './lyricsuploader.template.html';
|
||||
import toast from '../toast/toast';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
|
||||
@@ -6,6 +6,8 @@ import datetime from '../../scripts/datetime';
|
||||
import loading from '../loading/loading';
|
||||
import focusManager from '../focusManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
@@ -17,7 +19,6 @@ import '../formdialog.scss';
|
||||
import '../../styles/clearbutton.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import template from './metadataEditor.template.html';
|
||||
|
||||
@@ -67,16 +67,15 @@ function show(person) {
|
||||
if (type === PersonKind.Unknown) {
|
||||
continue;
|
||||
}
|
||||
selectPersonTypeOptions += `<option value="${type}">\${${type}}</option>`;
|
||||
const selected = person.Type === type ? 'selected' : '';
|
||||
selectPersonTypeOptions += `<option value="${type}" ${selected}>\${${type}}</option>`;
|
||||
}
|
||||
dlg.querySelector('.selectPersonType').innerHTML = globalize.translateHtml(selectPersonTypeOptions);
|
||||
|
||||
dlg.querySelector('.selectPersonType').addEventListener('change', function () {
|
||||
if (this.value === 'Actor') {
|
||||
dlg.querySelector('.fldRole').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.fldRole').classList.add('hide');
|
||||
}
|
||||
dlg.querySelector('.fldRole').classList.toggle(
|
||||
'hide',
|
||||
![ PersonKind.Actor, PersonKind.GuestStar ].includes(this.value));
|
||||
});
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import browser from '../../scripts/browser';
|
||||
import { appHost } from '../apphost';
|
||||
import loading from '../loading/loading';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../../scripts/dom';
|
||||
import './multiSelect.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import confirm from '../confirm/confirm';
|
||||
import itemHelper from '../itemHelper';
|
||||
@@ -198,7 +199,7 @@ function showMenuForSelectedItems(e) {
|
||||
});
|
||||
}
|
||||
|
||||
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
|
||||
if (user.Policy.EnableContentDownloading && appHost.supports(AppFeature.FileDownload)) {
|
||||
// Disabled because there is no callback for this item
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import serverNotifications from '../../scripts/serverNotifications';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import Events from '../../utils/events.ts';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||
import ServerConnections from '../../components/ServerConnections';
|
||||
|
||||
import NotificationIcon from './notificationicon.png';
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getImageUrl } from 'apps/stable/features/playback/utils/image';
|
||||
import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText';
|
||||
import { appRouter, isLyricsPage } from 'components/router/appRouter';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import datetime from '../../scripts/datetime';
|
||||
import Events from '../../utils/events.ts';
|
||||
@@ -14,7 +16,6 @@ import globalize from 'lib/globalize';
|
||||
import itemContextMenu from '../itemContextMenu';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import appFooter from '../appFooter/appFooter';
|
||||
import itemShortcuts from '../shortcuts';
|
||||
import './nowPlayingBar.scss';
|
||||
@@ -244,7 +245,7 @@ function bindEvents(elem) {
|
||||
|
||||
toggleRepeatButtonIcon = toggleRepeatButton.querySelector('.material-icons');
|
||||
|
||||
volumeSliderContainer.classList.toggle('hide', appHost.supports('physicalvolumecontrol'));
|
||||
volumeSliderContainer.classList.toggle('hide', appHost.supports(AppFeature.PhysicalVolumeControl));
|
||||
|
||||
volumeSlider.addEventListener('input', (e) => {
|
||||
if (currentPlayer) {
|
||||
@@ -441,7 +442,7 @@ function updatePlayerVolumeState(isMuted, volumeLevel) {
|
||||
showVolumeSlider = false;
|
||||
}
|
||||
|
||||
if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) {
|
||||
if (currentPlayer.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl)) {
|
||||
showMuteButton = false;
|
||||
showVolumeSlider = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import { getItemQuery } from 'hooks/useItem';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
|
||||
import { PlaybackErrorCode } from '@jellyfin/sdk/lib/generated-client/models/playback-error-code.js';
|
||||
import { getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api/media-info-api';
|
||||
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
||||
@@ -14,7 +15,6 @@ import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import globalize from '../../lib/globalize';
|
||||
import loading from '../loading/loading';
|
||||
import { appHost } from '../apphost';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import { includesAny } from '../../utils/container.ts';
|
||||
@@ -24,10 +24,11 @@ import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdrop
|
||||
import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent';
|
||||
import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager';
|
||||
import { bindMediaSessionSubscriber } from 'apps/stable/features/playback/utils/mediaSessionSubscriber';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { MediaError } from 'types/mediaError';
|
||||
import { getMediaError } from 'utils/mediaError';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js';
|
||||
import { bindSkipSegment } from './skipsegment.ts';
|
||||
|
||||
const UNLIMITED_ITEMS = -1;
|
||||
@@ -41,7 +42,7 @@ function enableLocalPlaylistManagement(player) {
|
||||
}
|
||||
|
||||
function supportsPhysicalVolumeControl(player) {
|
||||
return player.isLocalPlayer && appHost.supports('physicalvolumecontrol');
|
||||
return player.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl);
|
||||
}
|
||||
|
||||
function bindToFullscreenChange(player) {
|
||||
@@ -317,7 +318,7 @@ function getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiCl
|
||||
PlaySessionId: startingPlaySession,
|
||||
StartTimeTicks: startPosition || 0,
|
||||
EnableRedirection: true,
|
||||
EnableRemoteMedia: appHost.supports('remoteaudio'),
|
||||
EnableRemoteMedia: appHost.supports(AppFeature.RemoteAudio),
|
||||
EnableAudioVbrEncoding: transcodingProfile.EnableAudioVbrEncoding
|
||||
});
|
||||
}
|
||||
@@ -598,7 +599,7 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
|
||||
const isFolderRip = mediaSource.VideoType === 'BluRay' || mediaSource.VideoType === 'Dvd' || mediaSource.VideoType === 'HdDvd';
|
||||
|
||||
if (mediaSource.SupportsDirectPlay || isFolderRip) {
|
||||
if (mediaSource.IsRemote && !appHost.supports('remotevideo')) {
|
||||
if (mediaSource.IsRemote && !appHost.supports(AppFeature.RemoteVideo)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
@@ -1842,7 +1843,7 @@ export class PlaybackManager {
|
||||
ArtistIds: firstItem.Id,
|
||||
Filters: 'IsNotFolder',
|
||||
Recursive: true,
|
||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||
SortBy: options.shuffle ? 'Random' : 'Album,ParentIndexNumber,IndexNumber,SortName',
|
||||
MediaTypes: 'Audio'
|
||||
}, queryOptions));
|
||||
case 'PhotoAlbum':
|
||||
@@ -1873,6 +1874,14 @@ export class PlaybackManager {
|
||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||
MediaTypes: 'Video'
|
||||
}, queryOptions));
|
||||
case 'Studio':
|
||||
return getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||
StudioIds: firstItem.Id,
|
||||
Filters: 'IsNotFolder',
|
||||
Recursive: true,
|
||||
SortBy: options.shuffle ? 'Random' : 'SortName',
|
||||
MediaTypes: 'Video'
|
||||
}, queryOptions));
|
||||
case 'Series':
|
||||
case 'Season':
|
||||
return getSeriesOrSeasonPlaybackPromise(firstItem, options, items);
|
||||
@@ -1923,7 +1932,11 @@ export class PlaybackManager {
|
||||
if (options.shuffle) {
|
||||
sortBy = 'Random';
|
||||
} else if (firstItem.Type !== 'BoxSet') {
|
||||
sortBy = 'SortName';
|
||||
if (firstItem.CollectionType === 'music' || firstItem.MediaType === 'Audio') {
|
||||
sortBy = 'Album,ParentIndexNumber,IndexNumber,SortName';
|
||||
} else {
|
||||
sortBy = 'SortName';
|
||||
}
|
||||
}
|
||||
|
||||
return getItemsForPlayback(serverId, mergePlaybackQueries({
|
||||
@@ -3698,7 +3711,7 @@ export class PlaybackManager {
|
||||
return streamInfo ? streamInfo.playbackStartTimeTicks : null;
|
||||
};
|
||||
|
||||
if (appHost.supports('remotecontrol')) {
|
||||
if (appHost.supports(AppFeature.RemoteControl)) {
|
||||
import('../../scripts/serverNotifications').then(({ default: serverNotifications }) => {
|
||||
Events.on(serverNotifications, 'ServerShuttingDown', self.setDefaultPlayerActive.bind(self));
|
||||
Events.on(serverNotifications, 'ServerRestarting', self.setDefaultPlayerActive.bind(self));
|
||||
@@ -4069,7 +4082,7 @@ export class PlaybackManager {
|
||||
'PlayTrailers'
|
||||
];
|
||||
|
||||
if (appHost.supports('fullscreenchange')) {
|
||||
if (appHost.supports(AppFeature.Fullscreen)) {
|
||||
list.push('ToggleFullscreen');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import Events from '../../utils/events.ts';
|
||||
import browser from '../../scripts/browser';
|
||||
import loading from '../loading/loading';
|
||||
@@ -96,7 +97,7 @@ export function show(button) {
|
||||
|
||||
// Unfortunately we can't allow the url to change or chromecast will throw a security error
|
||||
// Might be able to solve this in the future by moving the dialogs to hashbangs
|
||||
if (!(!browser.chrome && !browser.edgeChromium || appHost.supports('castmenuhashchange'))) {
|
||||
if (!(!browser.chrome && !browser.edgeChromium || appHost.supports(AppFeature.CastMenuHashChange))) {
|
||||
menuOptions.enableHistory = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import actionsheet from '../actionSheet/actionSheet';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import qualityoptions from '../qualityOptions';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
function showQualityMenu(player, btn) {
|
||||
const videoStream = playbackManager.currentMediaSource(player).MediaStreams.filter(function (stream) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 18%;
|
||||
bottom: 8rem;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
}
|
||||
@@ -11,26 +11,21 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-right: 16%;
|
||||
margin-right: 6rem;
|
||||
z-index: 10000;
|
||||
padding: 12px 20px;
|
||||
color: black;
|
||||
background-color: #303030;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
border-radius: 0.2em;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
transition: opacity 200ms ease-out;
|
||||
gap: 3px;
|
||||
box-shadow: 7px 6px 15px -14px rgba(0, 0, 0, 0.65);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-height: 500px) {
|
||||
.skip-button {
|
||||
bottom: 27%;
|
||||
}
|
||||
}
|
||||
|
||||
.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import escapeHTML from 'escape-html';
|
||||
|
||||
import { MediaSegmentAction } from 'apps/stable/features/playback/constants/mediaSegmentAction';
|
||||
import { getId, getMediaSegmentAction } from 'apps/stable/features/playback/utils/mediaSegmentSettings';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import appSettings from '../../scripts/settings/appSettings';
|
||||
import { appHost } from '../apphost';
|
||||
@@ -12,7 +14,6 @@ import qualityoptions from '../qualityOptions';
|
||||
import globalize from '../../lib/globalize';
|
||||
import loading from '../loading/loading';
|
||||
import Events from '../../utils/events.ts';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './playbackSettings.template.html';
|
||||
|
||||
@@ -147,7 +148,7 @@ function showHideQualityFields(context, user, apiClient) {
|
||||
context.querySelector('.videoQualitySection').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('multiserver')) {
|
||||
if (appHost.supports(AppFeature.MultiServer)) {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
|
||||
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
|
||||
|
||||
@@ -204,7 +205,7 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
|
||||
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
|
||||
});
|
||||
|
||||
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
|
||||
if (appHost.supports(AppFeature.ExternalPlayerIntent) && userId === loggedInUserId) {
|
||||
context.querySelector('.fldExternalPlayer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldExternalPlayer').classList.add('hide');
|
||||
@@ -213,7 +214,7 @@ function loadForm(context, user, userSettings, systemInfo, apiClient) {
|
||||
if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
|
||||
context.querySelector('.qualitySections').classList.remove('hide');
|
||||
|
||||
if (appHost.supports('chromecast') && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
if (appHost.supports(AppFeature.Chromecast) && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.fldChromecastQuality').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldChromecastQuality').classList.add('hide');
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import globalize from '../../lib/globalize';
|
||||
import Events from '../../utils/events.ts';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { PluginType } from 'types/plugin';
|
||||
import Events from 'utils/events';
|
||||
import { getReadableSize } from 'utils/file';
|
||||
|
||||
import layoutManager from '../layoutManager';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import playMethodHelper from '../playback/playmethodhelper';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
|
||||
import 'elements/emby-button/paper-icon-button-light';
|
||||
|
||||
import './playerstats.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
function init(instance) {
|
||||
const parent = document.createElement('div');
|
||||
@@ -210,16 +214,6 @@ function getDisplayTranscodeFps(session, player) {
|
||||
return `${transcodeFramerate} fps (${(transcodeFramerate / originalFramerate).toFixed(2)}x)`;
|
||||
}
|
||||
|
||||
function getReadableSize(size) {
|
||||
if (size >= 1073741824) {
|
||||
return parseFloat((size / 1073741824).toFixed(1)) + ' GiB';
|
||||
} else if (size >= 1048576) {
|
||||
return parseFloat((size / 1048576).toFixed(1)) + ' MiB';
|
||||
} else {
|
||||
return Math.floor(size / 1024) + ' KiB';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaSourceStats(session, player) {
|
||||
const sessionStats = [];
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import escapeHtml from 'escape-html';
|
||||
import toast from 'components/toast/toast';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
|
||||
import { PluginType } from 'types/plugin';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
@@ -19,7 +20,6 @@ import layoutManager from '../layoutManager';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-input/emby-input';
|
||||
|
||||
@@ -8,8 +8,8 @@ import { appRouter } from './router/appRouter';
|
||||
import * as inputManager from '../scripts/inputManager';
|
||||
import toast from '../components/toast/toast';
|
||||
import confirm from '../components/confirm/confirm';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import * as dashboard from '../utils/dashboard';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
|
||||
// TODO: replace with each plugin version
|
||||
const cacheParam = new Date().getTime();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../../scripts/dom';
|
||||
import recordingHelper from './recordinghelper';
|
||||
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import './recordingfields.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
function onRecordingButtonClick() {
|
||||
const item = this.item;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import layoutManager from '../layoutManager';
|
||||
import mediaInfo from '../mediainfo/mediainfo';
|
||||
import loading from '../loading/loading';
|
||||
@@ -8,6 +9,7 @@ import datetime from '../../scripts/datetime';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import RecordingFields from './recordingfields';
|
||||
import Events from '../../utils/events.ts';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
@@ -16,7 +18,6 @@ import '../../elements/emby-input/emby-input';
|
||||
import '../formdialog.scss';
|
||||
import './recordingcreator.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import template from './recordingcreator.template.html';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import layoutManager from '../layoutManager';
|
||||
import loading from '../loading/loading';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
|
||||
import '../../styles/scrollstyles.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-collapse/emby-collapse';
|
||||
@@ -13,7 +15,6 @@ import '../formdialog.scss';
|
||||
import './recordingcreator.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './recordingeditor.template.html';
|
||||
|
||||
let currentDialog;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import Events from '../../utils/events.ts';
|
||||
import serverNotifications from '../../scripts/serverNotifications';
|
||||
import loading from '../loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import recordingHelper from './recordinghelper';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import './recordingfields.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './recordingfields.template.html';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import globalize from '../../lib/globalize';
|
||||
import globalize from 'lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import loading from '../loading/loading';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import confirm from '../confirm/confirm';
|
||||
import dialog from '../dialog/dialog';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import layoutManager from '../layoutManager';
|
||||
import loading from '../loading/loading';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import datetime from '../../scripts/datetime';
|
||||
|
||||
import '../../styles/scrollstyles.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
@@ -14,7 +16,6 @@ import '../formdialog.scss';
|
||||
import './recordingcreator.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './seriesrecordingeditor.template.html';
|
||||
|
||||
let currentDialog;
|
||||
|
||||
@@ -3,6 +3,8 @@ import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
@@ -10,7 +12,6 @@ import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
function getEditorHtml() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import escapeHtml from 'escape-html';
|
||||
|
||||
import { getImageUrl } from 'apps/stable/features/playback/utils/image';
|
||||
import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
|
||||
import datetime from '../../scripts/datetime';
|
||||
import { clearBackdrop, setBackdrops } from '../backdrop/backdrop';
|
||||
@@ -11,9 +12,11 @@ import { playbackManager } from '../playback/playbackmanager';
|
||||
import Events from '../../utils/events.ts';
|
||||
import { appHost } from '../apphost';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import layoutManager from '../layoutManager';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import itemContextMenu from '../itemContextMenu';
|
||||
|
||||
import '../cardbuilder/card.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
@@ -21,7 +24,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import './remotecontrol.scss';
|
||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-slider/emby-slider';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
|
||||
@@ -370,7 +372,7 @@ export default function () {
|
||||
showVolumeSlider = false;
|
||||
}
|
||||
|
||||
if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) {
|
||||
if (currentPlayer.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl)) {
|
||||
showMuteButton = false;
|
||||
showVolumeSlider = false;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { setBackdropTransparency } from '../backdrop/backdrop';
|
||||
import globalize from '../../lib/globalize';
|
||||
import itemHelper from '../itemHelper';
|
||||
import loading from '../loading/loading';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { getItemQuery } from 'hooks/useItem';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
/** Pages of "no return" (when "Go back" should behave differently, probably quitting the application). */
|
||||
|
||||
@@ -8,9 +8,9 @@ import { playbackManager } from './playback/playbackmanager';
|
||||
import inputManager from '../scripts/inputManager';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import globalize from '../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../scripts/dom';
|
||||
import recordingHelper from './recordingcreator/recordinghelper';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import toast from './toast/toast';
|
||||
import * as userSettings from '../scripts/settings/userSettings';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
* Image viewer component
|
||||
* @module components/slideshow/slideshow
|
||||
*/
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import inputManager from '../../scripts/inputManager';
|
||||
import layoutManager from '../layoutManager';
|
||||
import focusManager from '../focusManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import { appHost } from '../apphost';
|
||||
import dom from '../../scripts/dom';
|
||||
|
||||
import './style.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import screenfull from 'screenfull';
|
||||
import { randomInt } from '../../utils/number.ts';
|
||||
|
||||
@@ -171,10 +173,10 @@ export default function (options) {
|
||||
if (actionButtonsOnTop) {
|
||||
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true);
|
||||
|
||||
if (appHost.supports('filedownload') && slideshowOptions.user?.Policy.EnableContentDownloading) {
|
||||
if (appHost.supports(AppFeature.FileDownload) && slideshowOptions.user?.Policy.EnableContentDownloading) {
|
||||
html += getIcon('file_download', 'btnDownload slideshowButton', true);
|
||||
}
|
||||
if (appHost.supports('sharing')) {
|
||||
if (appHost.supports(AppFeature.Sharing)) {
|
||||
html += getIcon('share', 'btnShare slideshowButton', true);
|
||||
}
|
||||
if (screenfull.isEnabled) {
|
||||
@@ -189,10 +191,10 @@ export default function (options) {
|
||||
html += '<div class="slideshowBottomBar hide">';
|
||||
|
||||
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true, true);
|
||||
if (appHost.supports('filedownload') && slideshowOptions?.user.Policy.EnableContentDownloading) {
|
||||
if (appHost.supports(AppFeature.FileDownload) && slideshowOptions?.user.Policy.EnableContentDownloading) {
|
||||
html += getIcon('file_download', 'btnDownload slideshowButton', true);
|
||||
}
|
||||
if (appHost.supports('sharing')) {
|
||||
if (appHost.supports(AppFeature.Sharing)) {
|
||||
html += getIcon('share', 'btnShare slideshowButton', true);
|
||||
}
|
||||
if (screenfull.isEnabled) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { appHost } from '../apphost';
|
||||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import loading from '../loading/loading';
|
||||
import focusManager from '../focusManager';
|
||||
import dom from '../../scripts/dom';
|
||||
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../listview/listview.scss';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
@@ -15,7 +19,6 @@ import 'material-design-icons-iconfont';
|
||||
import './subtitleeditor.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import confirm from '../confirm/confirm';
|
||||
import template from './subtitleeditor.template.html';
|
||||
@@ -439,7 +442,7 @@ function showEditorInternal(itemId, serverId) {
|
||||
}
|
||||
|
||||
// Don't allow redirection to other websites from the TV layout
|
||||
if (layoutManager.tv || !appHost.supports('externallinks')) {
|
||||
if (layoutManager.tv || !appHost.supports(AppFeature.ExternalLinks)) {
|
||||
dlg.querySelector('.btnHelp').remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,14 @@ function getTextStyles(settings, preview) {
|
||||
if (!preview) {
|
||||
const pos = parseInt(settings.verticalPosition, 10);
|
||||
const lineHeight = 1.35; // FIXME: It is better to read this value from element
|
||||
const line = Math.abs(pos * lineHeight);
|
||||
if (pos < 0) {
|
||||
list.push({ name: 'min-height', value: `${line}em` });
|
||||
const margin = Math.abs(pos + 1) * lineHeight;
|
||||
list.push({ name: 'margin-bottom', value: `${margin}em` });
|
||||
list.push({ name: 'margin-top', value: '' });
|
||||
} else {
|
||||
list.push({ name: 'min-height', value: '' });
|
||||
list.push({ name: 'margin-top', value: `${line}em` });
|
||||
const margin = pos * lineHeight;
|
||||
list.push({ name: 'margin-bottom', value: '' });
|
||||
list.push({ name: 'margin-top', value: `${margin}em` });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { appHost } from '../apphost';
|
||||
import appSettings from '../../scripts/settings/appSettings';
|
||||
import focusManager from '../focusManager';
|
||||
@@ -8,6 +10,7 @@ import subtitleAppearanceHelper from './subtitleappearancehelper';
|
||||
import settingsHelper from '../settingshelper';
|
||||
import dom from '../../scripts/dom';
|
||||
import Events from '../../utils/events.ts';
|
||||
|
||||
import '../listview/listview.scss';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-slider/emby-slider';
|
||||
@@ -15,7 +18,6 @@ import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import './subtitlesettings.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './subtitlesettings.template.html';
|
||||
|
||||
@@ -39,7 +41,7 @@ function getSubtitleAppearanceObject(context) {
|
||||
|
||||
function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
|
||||
apiClient.getCultures().then(function (allCultures) {
|
||||
if (appHost.supports('subtitleburnsettings') && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
if (appHost.supports(AppFeature.SubtitleBurnIn) && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.fldBurnIn').classList.remove('hide');
|
||||
}
|
||||
|
||||
@@ -207,7 +209,7 @@ function embed(options, self) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('subtitleappearancesettings')) {
|
||||
if (appHost.supports(AppFeature.SubtitleAppearance)) {
|
||||
options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide');
|
||||
|
||||
self._fullPreview = options.element.querySelector('.subtitleappearance-fullpreview');
|
||||
|
||||
@@ -3,14 +3,15 @@ import escapeHtml from 'escape-html';
|
||||
import { getSubtitleApi } from '@jellyfin/sdk/lib/utils/api/subtitle-api';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import dom from '../../scripts/dom';
|
||||
import loading from '../../components/loading/loading';
|
||||
import scrollHelper from '../../scripts/scrollHelper';
|
||||
import layoutManager from '../layoutManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import template from './subtitleuploader.template.html';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-select/emby-select';
|
||||
import '../formdialog.scss';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
||||
|
||||
import { getItemQuery } from 'hooks/useItem';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
|
||||
import { ItemKind } from 'types/base/models/item-kind';
|
||||
import Events from 'utils/events.ts';
|
||||
@@ -10,7 +11,6 @@ import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
||||
let currentOwnerId;
|
||||
let currentThemeIds = [];
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import './toast.scss';
|
||||
|
||||
let toastContainer;
|
||||
interface Toast {
|
||||
text: string
|
||||
}
|
||||
|
||||
let toastContainer: HTMLDivElement;
|
||||
|
||||
function getToastContainer() {
|
||||
if (!toastContainer) {
|
||||
@@ -12,24 +16,24 @@ function getToastContainer() {
|
||||
return toastContainer;
|
||||
}
|
||||
|
||||
function remove(elem) {
|
||||
function remove(elem: HTMLElement) {
|
||||
setTimeout(function () {
|
||||
elem.parentNode.removeChild(elem);
|
||||
elem.parentNode?.removeChild(elem);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function animateRemove(elem) {
|
||||
function animateRemove(elem: HTMLElement) {
|
||||
setTimeout(function () {
|
||||
elem.classList.add('toastHide');
|
||||
remove(elem);
|
||||
}, 3300);
|
||||
}
|
||||
|
||||
export default function (options) {
|
||||
export default function (options: string | Toast) {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
text: options
|
||||
};
|
||||
} as Toast;
|
||||
}
|
||||
|
||||
const elem = document.createElement('div');
|
||||
@@ -2,6 +2,7 @@ import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import AppSettingsAlt from '@mui/icons-material/AppSettingsAlt';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import Download from '@mui/icons-material/Download';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import Logout from '@mui/icons-material/Logout';
|
||||
import PhonelinkLock from '@mui/icons-material/PhonelinkLock';
|
||||
@@ -16,6 +17,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import { AppFeature } from 'constants/appFeature';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import { useQuickConnectEnabled } from 'hooks/useQuickConnect';
|
||||
import globalize from 'lib/globalize';
|
||||
@@ -36,6 +38,11 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
const { user } = useApi();
|
||||
const { data: isQuickConnectEnabled } = useQuickConnectEnabled();
|
||||
|
||||
const onDownloadManagerClick = useCallback(() => {
|
||||
shell.openDownloadManager();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onClientSettingsClick = useCallback(() => {
|
||||
shell.openClientSettings();
|
||||
onMenuClose();
|
||||
@@ -97,10 +104,25 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('clientsettings') && ([
|
||||
<Divider key='client-settings-divider' />,
|
||||
{(appHost.supports(AppFeature.DownloadManagement) || appHost.supports(AppFeature.ClientSettings)) && (
|
||||
<Divider />
|
||||
)}
|
||||
|
||||
{appHost.supports(AppFeature.DownloadManagement) && (
|
||||
<MenuItem
|
||||
onClick={onDownloadManagerClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Download />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('DownloadManager')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{appHost.supports(AppFeature.ClientSettings) && (
|
||||
<MenuItem
|
||||
key='client-settings-button'
|
||||
onClick={onClientSettingsClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
@@ -110,7 +132,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
{globalize.translate('ClientSettings')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
])}
|
||||
)}
|
||||
|
||||
{/* ADMIN LINKS */}
|
||||
{user?.Policy?.IsAdministrator && ([
|
||||
@@ -156,7 +178,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{appHost.supports('multiserver') && (
|
||||
{appHost.supports(AppFeature.MultiServer) && (
|
||||
<MenuItem
|
||||
onClick={onSelectServerClick}
|
||||
>
|
||||
@@ -180,7 +202,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('exitmenu') && ([
|
||||
{appHost.supports(AppFeature.ExitMenu) && ([
|
||||
<Divider key='exit-menu-divider' />,
|
||||
<MenuItem
|
||||
key='exit-menu-button'
|
||||
|
||||
@@ -5,8 +5,9 @@ import mediaInfo from '../mediainfo/mediainfo';
|
||||
import layoutManager from '../layoutManager';
|
||||
import focusManager from '../focusManager';
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import itemHelper from '../itemHelper';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
import './upnextdialog.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../styles/flexstyles.scss';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import dom from '../../scripts/dom';
|
||||
import itemHelper from '../itemHelper';
|
||||
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import 'material-design-icons-iconfont';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import './userdatabuttons.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
const userDataMethods = {
|
||||
markPlayed: markPlayed,
|
||||
|
||||
Reference in New Issue
Block a user