Merge branch 'master' into issue5486

This commit is contained in:
kevgrig
2025-07-10 21:40:51 -05:00
committed by GitHub
334 changed files with 14930 additions and 10715 deletions
+1 -14
View File
@@ -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' />
</>
);
+104
View File
@@ -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;
+26 -25
View File
@@ -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 />;
}
+1 -1
View File
@@ -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
View File
@@ -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>
);
+18
View File
@@ -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
)
);
-161
View File
@@ -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);
+3 -3
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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;
+5 -5
View File
@@ -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);
}
+2 -1
View File
@@ -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;
-74
View File
@@ -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;
+85
View File
@@ -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);
}
-109
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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 -1
View File
@@ -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');
+1 -1
View File
@@ -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';
+6
View File
@@ -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';
+3 -2
View File
@@ -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');
+3 -2
View File
@@ -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({
+3 -2
View File
@@ -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) {
+21 -16
View File
@@ -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) {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 () {
+3 -2
View File
@@ -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';
+22 -9
View File
@@ -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) {
+6 -11
View File
@@ -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');
+9 -15
View File
@@ -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';
+1 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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). */
+1 -1
View File
@@ -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';
+7 -5
View File
@@ -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';
+1 -1
View File
@@ -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');
+28 -6
View File
@@ -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'
+2 -1
View File
@@ -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,