Add constants for app features

This commit is contained in:
Bill Thornton
2025-04-30 17:41:36 -04:00
parent bb810a183a
commit fdcf1b06c3
34 changed files with 211 additions and 129 deletions

View File

@@ -11,6 +11,7 @@ import Typography from '@mui/material/Typography';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
import { AppFeature } from 'constants/appFeature';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { useThemes } from 'hooks/useThemes'; import { useThemes } from 'hooks/useThemes';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -32,7 +33,7 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant='h2'>{globalize.translate('Display')}</Typography> <Typography variant='h2'>{globalize.translate('Display')}</Typography>
{ appHost.supports('displaymode') && ( { appHost.supports(AppFeature.DisplayMode) && (
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel id='display-settings-layout-label'>{globalize.translate('LabelDisplayMode')}</InputLabel> <InputLabel id='display-settings-layout-label'>{globalize.translate('LabelDisplayMode')}</InputLabel>
<Select <Select
@@ -124,7 +125,7 @@ export function DisplayPreferences({ onChange, values }: Readonly<DisplayPrefere
</FormControl> </FormControl>
) } ) }
{ screensavers.length > 0 && appHost.supports('screensaver') && ( { screensavers.length > 0 && appHost.supports(AppFeature.Screensaver) && (
<Fragment> <Fragment>
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel id='display-settings-screensaver-label'>{globalize.translate('LabelScreensaver')}</InputLabel> <InputLabel id='display-settings-screensaver-label'>{globalize.translate('LabelScreensaver')}</InputLabel>

View File

@@ -10,8 +10,9 @@ import React from 'react';
import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from 'apps/experimental/features/preferences/constants/locales'; import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from 'apps/experimental/features/preferences/constants/locales';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
import datetime from 'scripts/datetime'; import { AppFeature } from 'constants/appFeature';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import datetime from 'scripts/datetime';
import type { DisplaySettingsValues } from '../types/displaySettingsValues'; import type { DisplaySettingsValues } from '../types/displaySettingsValues';
@@ -21,14 +22,14 @@ interface LocalizationPreferencesProps {
} }
export function LocalizationPreferences({ onChange, values }: Readonly<LocalizationPreferencesProps>) { export function LocalizationPreferences({ onChange, values }: Readonly<LocalizationPreferencesProps>) {
if (!appHost.supports('displaylanguage') && !datetime.supportsLocalization()) { if (!appHost.supports(AppFeature.DisplayLanguage) && !datetime.supportsLocalization()) {
return null; return null;
} }
return ( return (
<Stack spacing={3}> <Stack spacing={3}>
<Typography variant='h2'>{globalize.translate('Localization')}</Typography> <Typography variant='h2'>{globalize.translate('Localization')}</Typography>
{ appHost.supports('displaylanguage') && ( { appHost.supports(AppFeature.DisplayLanguage) && (
<FormControl fullWidth> <FormControl fullWidth>
<InputLabel id='display-settings-language-label'>{globalize.translate('LabelDisplayLanguage')}</InputLabel> <InputLabel id='display-settings-language-label'>{globalize.translate('LabelDisplayLanguage')}</InputLabel>
<Select <Select
@@ -46,7 +47,7 @@ export function LocalizationPreferences({ onChange, values }: Readonly<Localizat
</Select> </Select>
<FormHelperText component={Stack} id='display-settings-language-description'> <FormHelperText component={Stack} id='display-settings-language-description'>
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span> <span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
{ appHost.supports('externallinks') && ( { appHost.supports(AppFeature.ExternalLinks) && (
<Link <Link
href='https://github.com/jellyfin/jellyfin' href='https://github.com/jellyfin/jellyfin'
rel='noopener noreferrer' rel='noopener noreferrer'

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
import { AppFeature } from 'constants/appFeature';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import themeManager from 'scripts/themeManager'; import themeManager from 'scripts/themeManager';
import { currentSettings, UserSettings } from 'scripts/settings/userSettings'; import { currentSettings, UserSettings } from 'scripts/settings/userSettings';
@@ -120,7 +121,7 @@ async function saveDisplaySettings({
}: SaveDisplaySettingsParams) { }: SaveDisplaySettingsParams) {
const user = await api.getUser(userId); const user = await api.getUser(userId);
if (appHost.supports('displaylanguage')) { if (appHost.supports(AppFeature.DisplayLanguage)) {
userSettings.language(normalizeValue(newDisplaySettings.language)); userSettings.language(normalizeValue(newDisplaySettings.language));
} }
userSettings.customCss(normalizeValue(newDisplaySettings.customCss)); userSettings.customCss(normalizeValue(newDisplaySettings.customCss));

View File

@@ -6,6 +6,7 @@ import { appHost } from 'components/apphost';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page'; import Page from 'components/Page';
import { AppFeature } from 'constants/appFeature';
import LinkButton from 'elements/emby-button/LinkButton'; import LinkButton from 'elements/emby-button/LinkButton';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { useQuickConnectEnabled } from 'hooks/useQuickConnect'; import { useQuickConnectEnabled } from 'hooks/useQuickConnect';
@@ -185,7 +186,7 @@ const UserSettingsPage: FC = () => {
</div> </div>
</LinkButton> </LinkButton>
{appHost.supports('clientsettings') && ( {appHost.supports(AppFeature.ClientSettings) && (
<LinkButton <LinkButton
onClick={shell.openClientSettings} onClick={shell.openClientSettings}
className='clientSettings listItem-border' className='clientSettings listItem-border'
@@ -290,7 +291,7 @@ const UserSettingsPage: FC = () => {
{globalize.translate('HeaderUser')} {globalize.translate('HeaderUser')}
</h2> </h2>
{appHost.supports('multiserver') && ( {appHost.supports(AppFeature.MultiServer) && (
<LinkButton <LinkButton
onClick={Dashboard.selectServer} onClick={Dashboard.selectServer}
className='selectServer listItem-border' className='selectServer listItem-border'
@@ -330,7 +331,7 @@ const UserSettingsPage: FC = () => {
</div> </div>
</LinkButton> </LinkButton>
{appHost.supports('exitmenu') && ( {appHost.supports(AppFeature.ExitMenu) && (
<LinkButton <LinkButton
onClick={appHost.exit} onClick={appHost.exit}
className='exitApp listItem-border' className='exitApp listItem-border'

View File

@@ -12,6 +12,7 @@ import UserPasswordForm from '../../../../components/dashboard/users/UserPasswor
import loading from '../../../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast'; import toast from '../../../../components/toast/toast';
import Page from '../../../../components/Page'; import Page from '../../../../components/Page';
import { AppFeature } from 'constants/appFeature';
const UserProfile: FunctionComponent = () => { const UserProfile: FunctionComponent = () => {
const [ searchParams ] = useSearchParams(); const [ searchParams ] = useSearchParams();
@@ -61,7 +62,7 @@ const UserProfile: FunctionComponent = () => {
if (user.PrimaryImageTag) { if (user.PrimaryImageTag) {
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide'); (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
} else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { } else if (appHost.supports(AppFeature.FileInput) && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
} }

View File

@@ -2,6 +2,7 @@ import React, { FC, useEffect, useState } from 'react';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
import Page from 'components/Page'; import Page from 'components/Page';
import { AppFeature } from 'constants/appFeature';
import LinkButton from 'elements/emby-button/LinkButton'; import LinkButton from 'elements/emby-button/LinkButton';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { ConnectionState } from 'lib/jellyfin-apiclient'; import { ConnectionState } from 'lib/jellyfin-apiclient';
@@ -50,7 +51,7 @@ const ConnectionErrorPage: FC<ConnectionErrorPageProps> = ({
{message && ( {message && (
<p>{message}</p> <p>{message}</p>
)} )}
{appHost.supports('multiserver') && ( {appHost.supports(AppFeature.MultiServer) && (
<LinkButton <LinkButton
className='raised' className='raised'
href='/selectserver' href='/selectserver'

View File

@@ -5,6 +5,7 @@ import * as htmlMediaHelper from '../components/htmlMediaHelper';
import * as webSettings from '../scripts/settings/webSettings'; import * as webSettings from '../scripts/settings/webSettings';
import globalize from '../lib/globalize'; import globalize from '../lib/globalize';
import profileBuilder from '../scripts/browserDeviceProfile'; import profileBuilder from '../scripts/browserDeviceProfile';
import { AppFeature } from 'constants/appFeature';
const appName = 'Jellyfin Web'; const appName = 'Jellyfin Web';
@@ -169,14 +170,6 @@ function getDeviceName() {
return deviceName; return deviceName;
} }
function supportsVoiceInput() {
if (!browser.tv) {
return window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.oSpeechRecognition || window.msSpeechRecognition;
}
return false;
}
function supportsFullscreen() { function supportsFullscreen() {
if (browser.tv) { if (browser.tv) {
return false; return false;
@@ -235,77 +228,65 @@ const supportedFeatures = function () {
const features = []; const features = [];
if (navigator.share) { if (navigator.share) {
features.push('sharing'); features.push(AppFeature.Sharing);
} }
if (!browser.edgeUwp && !browser.tv && !browser.xboxOne && !browser.ps4) { 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) { if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
features.push('exit'); features.push(AppFeature.Exit);
} else {
features.push('plugins');
} }
if (!browser.operaTv && !browser.tizen && !browser.orsay && !browser.web0s && !browser.ps4) { if (!browser.operaTv && !browser.tizen && !browser.orsay && !browser.web0s && !browser.ps4) {
features.push('externallinks'); features.push(AppFeature.ExternalLinks);
features.push('externalpremium');
}
if (!browser.operaTv) {
features.push('externallinkdisplay');
}
if (supportsVoiceInput()) {
features.push('voiceinput');
} }
if (supportsHtmlMediaAutoplay()) { if (supportsHtmlMediaAutoplay()) {
features.push('htmlaudioautoplay'); features.push(AppFeature.HtmlAudioAutoplay);
features.push('htmlvideoautoplay'); features.push(AppFeature.HtmlVideoAutoplay);
} }
if (supportsFullscreen()) { if (supportsFullscreen()) {
features.push('fullscreenchange'); features.push(AppFeature.Fullscreen);
} }
if (browser.tv || browser.xboxOne || browser.ps4 || browser.mobile || browser.ipad) { 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) { 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) { if (!browser.operaTv && !browser.tizen && !browser.orsay && !browser.web0s && !browser.edgeUwp) {
features.push('remotevideo'); features.push(AppFeature.RemoteVideo);
} }
features.push('displaylanguage'); features.push(AppFeature.DisplayLanguage);
features.push('otherapppromotions'); features.push(AppFeature.DisplayMode);
features.push('displaymode'); features.push(AppFeature.TargetBlank);
features.push('targetblank'); features.push(AppFeature.Screensaver);
features.push('screensaver');
webSettings.getMultiServer().then(enabled => { 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())) { if (!browser.orsay && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) {
features.push('subtitleappearancesettings'); features.push(AppFeature.SubtitleAppearance);
} }
if (!browser.orsay) { if (!browser.orsay) {
features.push('subtitleburnsettings'); features.push(AppFeature.SubtitleBurnIn);
} }
if (!browser.tv && !browser.ps4 && !browser.xboxOne) { if (!browser.tv && !browser.ps4 && !browser.xboxOne) {
features.push('fileinput'); features.push(AppFeature.FileInput);
} }
if (browser.chrome || browser.edgeChromium) { if (browser.chrome || browser.edgeChromium) {
features.push('chromecast'); features.push(AppFeature.Chromecast);
} }
return features; return features;

View File

@@ -1,4 +1,6 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { AppFeature } from 'constants/appFeature';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import { pluginManager } from '../pluginManager'; import { pluginManager } from '../pluginManager';
@@ -68,19 +70,19 @@ function showOrHideMissingEpisodesField(context) {
} }
function loadForm(context, user, userSettings) { function loadForm(context, user, userSettings) {
if (appHost.supports('displaylanguage')) { if (appHost.supports(AppFeature.DisplayLanguage)) {
context.querySelector('.languageSection').classList.remove('hide'); context.querySelector('.languageSection').classList.remove('hide');
} else { } else {
context.querySelector('.languageSection').classList.add('hide'); context.querySelector('.languageSection').classList.add('hide');
} }
if (appHost.supports('displaymode')) { if (appHost.supports(AppFeature.DisplayMode)) {
context.querySelector('.fldDisplayMode').classList.remove('hide'); context.querySelector('.fldDisplayMode').classList.remove('hide');
} else { } else {
context.querySelector('.fldDisplayMode').classList.add('hide'); context.querySelector('.fldDisplayMode').classList.add('hide');
} }
if (appHost.supports('externallinks')) { if (appHost.supports(AppFeature.ExternalLinks)) {
context.querySelector('.learnHowToContributeContainer').classList.remove('hide'); context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
} else { } else {
context.querySelector('.learnHowToContributeContainer').classList.add('hide'); context.querySelector('.learnHowToContributeContainer').classList.add('hide');
@@ -88,7 +90,7 @@ function loadForm(context, user, userSettings) {
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator); 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('.selectScreensaverContainer').classList.remove('hide');
context.querySelector('.txtBackdropScreensaverIntervalContainer').classList.remove('hide'); context.querySelector('.txtBackdropScreensaverIntervalContainer').classList.remove('hide');
context.querySelector('.txtScreensaverTimeContainer').classList.remove('hide'); context.querySelector('.txtScreensaverTimeContainer').classList.remove('hide');
@@ -143,7 +145,7 @@ function loadForm(context, user, userSettings) {
function saveUser(context, user, userSettingsInstance, apiClient) { function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked; user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
if (appHost.supports('displaylanguage')) { if (appHost.supports(AppFeature.DisplayLanguage)) {
userSettingsInstance.language(context.querySelector('#selectLanguage').value); userSettingsInstance.language(context.querySelector('#selectLanguage').value);
} }

View File

@@ -1,3 +1,4 @@
import { AppFeature } from 'constants/appFeature';
import dom from '../../scripts/dom'; import dom from '../../scripts/dom';
import loading from '../loading/loading'; import loading from '../loading/loading';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
@@ -205,7 +206,7 @@ function getRemoteImageHtml(image, imageType) {
html += '<div class="cardPadder-' + shape + '"></div>'; html += '<div class="cardPadder-' + shape + '"></div>';
html += '<div class="cardContent">'; 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>'; html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
} else { } 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>'; 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>';

View File

@@ -1,3 +1,4 @@
import { AppFeature } from 'constants/appFeature';
import dialogHelper from '../dialogHelper/dialogHelper'; import dialogHelper from '../dialogHelper/dialogHelper';
import loading from '../loading/loading'; import loading from '../loading/loading';
import dom from '../../scripts/dom'; import dom from '../../scripts/dom';
@@ -339,7 +340,7 @@ function showActionSheet(context, imageCard) {
function initEditor(context, options) { function initEditor(context, options) {
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu'); 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++) { for (let i = 0, length = uploadButtons.length; i < length; i++) {
if (isFileInputSupported) { if (isFileInputSupported) {
uploadButtons[i].classList.remove('hide'); uploadButtons[i].classList.remove('hide');

View File

@@ -11,6 +11,7 @@ import { playbackManager } from './playback/playbackmanager';
import toast from './toast/toast'; import toast from './toast/toast';
import * as userSettings from '../scripts/settings/userSettings'; import * as userSettings from '../scripts/settings/userSettings';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { AppFeature } from 'constants/appFeature';
function getDeleteLabel(type) { function getDeleteLabel(type) {
switch (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? // CanDownload should probably be updated to return true for these items?
if (user.Policy.EnableContentDownloading && (item.Type === 'Season' || item.Type == 'Series')) { if (user.Policy.EnableContentDownloading && (item.Type === 'Season' || item.Type == 'Series')) {
commands.push({ commands.push({

View File

@@ -6,6 +6,7 @@ import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
import { appHost } from './apphost'; import { appHost } from './apphost';
import { AppFeature } from 'constants/appFeature';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { toApi } from 'utils/jellyfin-apiclient/compat'; import { toApi } from 'utils/jellyfin-apiclient/compat';
@@ -238,7 +239,7 @@ export function canShare (item, user) {
if (isLocalItem(item)) { if (isLocalItem(item)) {
return false; return false;
} }
return user.Policy.EnablePublicSharing && appHost.supports('sharing'); return user.Policy.EnablePublicSharing && appHost.supports(AppFeature.Sharing);
} }
export function enableDateAddedDisplay (item) { export function enableDateAddedDisplay (item) {

View File

@@ -1,3 +1,4 @@
import { AppFeature } from 'constants/appFeature';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
import loading from '../loading/loading'; import loading from '../loading/loading';
@@ -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 // Disabled because there is no callback for this item
} }

View File

@@ -1,6 +1,7 @@
import { getImageUrl } from 'apps/stable/features/playback/utils/image'; import { getImageUrl } from 'apps/stable/features/playback/utils/image';
import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText';
import { appRouter, isLyricsPage } from 'components/router/appRouter'; import { appRouter, isLyricsPage } from 'components/router/appRouter';
import { AppFeature } from 'constants/appFeature';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import datetime from '../../scripts/datetime'; import datetime from '../../scripts/datetime';
@@ -244,7 +245,7 @@ function bindEvents(elem) {
toggleRepeatButtonIcon = toggleRepeatButton.querySelector('.material-icons'); toggleRepeatButtonIcon = toggleRepeatButton.querySelector('.material-icons');
volumeSliderContainer.classList.toggle('hide', appHost.supports('physicalvolumecontrol')); volumeSliderContainer.classList.toggle('hide', appHost.supports(AppFeature.PhysicalVolumeControl));
volumeSlider.addEventListener('input', (e) => { volumeSlider.addEventListener('input', (e) => {
if (currentPlayer) { if (currentPlayer) {
@@ -441,7 +442,7 @@ function updatePlayerVolumeState(isMuted, volumeLevel) {
showVolumeSlider = false; showVolumeSlider = false;
} }
if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) { if (currentPlayer.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl)) {
showMuteButton = false; showMuteButton = false;
showVolumeSlider = false; showVolumeSlider = false;
} }

View File

@@ -24,6 +24,7 @@ import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdrop
import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent';
import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager'; import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager';
import { bindMediaSessionSubscriber } from 'apps/stable/features/playback/utils/mediaSessionSubscriber'; import { bindMediaSessionSubscriber } from 'apps/stable/features/playback/utils/mediaSessionSubscriber';
import { AppFeature } from 'constants/appFeature';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { MediaError } from 'types/mediaError'; import { MediaError } from 'types/mediaError';
import { getMediaError } from 'utils/mediaError'; import { getMediaError } from 'utils/mediaError';
@@ -41,7 +42,7 @@ function enableLocalPlaylistManagement(player) {
} }
function supportsPhysicalVolumeControl(player) { function supportsPhysicalVolumeControl(player) {
return player.isLocalPlayer && appHost.supports('physicalvolumecontrol'); return player.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl);
} }
function bindToFullscreenChange(player) { function bindToFullscreenChange(player) {
@@ -317,7 +318,7 @@ function getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiCl
PlaySessionId: startingPlaySession, PlaySessionId: startingPlaySession,
StartTimeTicks: startPosition || 0, StartTimeTicks: startPosition || 0,
EnableRedirection: true, EnableRedirection: true,
EnableRemoteMedia: appHost.supports('remoteaudio'), EnableRemoteMedia: appHost.supports(AppFeature.RemoteAudio),
EnableAudioVbrEncoding: transcodingProfile.EnableAudioVbrEncoding EnableAudioVbrEncoding: transcodingProfile.EnableAudioVbrEncoding
}); });
} }
@@ -598,7 +599,7 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
const isFolderRip = mediaSource.VideoType === 'BluRay' || mediaSource.VideoType === 'Dvd' || mediaSource.VideoType === 'HdDvd'; const isFolderRip = mediaSource.VideoType === 'BluRay' || mediaSource.VideoType === 'Dvd' || mediaSource.VideoType === 'HdDvd';
if (mediaSource.SupportsDirectPlay || isFolderRip) { if (mediaSource.SupportsDirectPlay || isFolderRip) {
if (mediaSource.IsRemote && !appHost.supports('remotevideo')) { if (mediaSource.IsRemote && !appHost.supports(AppFeature.RemoteVideo)) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@@ -3689,7 +3690,7 @@ export class PlaybackManager {
return streamInfo ? streamInfo.playbackStartTimeTicks : null; return streamInfo ? streamInfo.playbackStartTimeTicks : null;
}; };
if (appHost.supports('remotecontrol')) { if (appHost.supports(AppFeature.RemoteControl)) {
import('../../scripts/serverNotifications').then(({ default: serverNotifications }) => { import('../../scripts/serverNotifications').then(({ default: serverNotifications }) => {
Events.on(serverNotifications, 'ServerShuttingDown', self.setDefaultPlayerActive.bind(self)); Events.on(serverNotifications, 'ServerShuttingDown', self.setDefaultPlayerActive.bind(self));
Events.on(serverNotifications, 'ServerRestarting', self.setDefaultPlayerActive.bind(self)); Events.on(serverNotifications, 'ServerRestarting', self.setDefaultPlayerActive.bind(self));
@@ -4060,7 +4061,7 @@ export class PlaybackManager {
'PlayTrailers' 'PlayTrailers'
]; ];
if (appHost.supports('fullscreenchange')) { if (appHost.supports(AppFeature.Fullscreen)) {
list.push('ToggleFullscreen'); list.push('ToggleFullscreen');
} }

View File

@@ -1,3 +1,4 @@
import { AppFeature } from 'constants/appFeature';
import Events from '../../utils/events.ts'; import Events from '../../utils/events.ts';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import loading from '../loading/loading'; 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 // 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 // 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; menuOptions.enableHistory = false;
} }

View File

@@ -3,6 +3,7 @@ import escapeHTML from 'escape-html';
import { MediaSegmentAction } from 'apps/stable/features/playback/constants/mediaSegmentAction'; import { MediaSegmentAction } from 'apps/stable/features/playback/constants/mediaSegmentAction';
import { getId, getMediaSegmentAction } from 'apps/stable/features/playback/utils/mediaSegmentSettings'; import { getId, getMediaSegmentAction } from 'apps/stable/features/playback/utils/mediaSegmentSettings';
import { AppFeature } from 'constants/appFeature';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import appSettings from '../../scripts/settings/appSettings'; import appSettings from '../../scripts/settings/appSettings';
@@ -147,7 +148,7 @@ function showHideQualityFields(context, user, apiClient) {
context.querySelector('.videoQualitySection').classList.add('hide'); context.querySelector('.videoQualitySection').classList.add('hide');
} }
if (appHost.supports('multiserver')) { if (appHost.supports(AppFeature.MultiServer)) {
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide'); context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
context.querySelector('.fldVideoInternetQuality').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; 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'); context.querySelector('.fldExternalPlayer').classList.remove('hide');
} else { } else {
context.querySelector('.fldExternalPlayer').classList.add('hide'); 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)) { if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
context.querySelector('.qualitySections').classList.remove('hide'); 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'); context.querySelector('.fldChromecastQuality').classList.remove('hide');
} else { } else {
context.querySelector('.fldChromecastQuality').classList.add('hide'); context.querySelector('.fldChromecastQuality').classList.add('hide');

View File

@@ -2,6 +2,7 @@ import escapeHtml from 'escape-html';
import { getImageUrl } from 'apps/stable/features/playback/utils/image'; import { getImageUrl } from 'apps/stable/features/playback/utils/image';
import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText';
import { AppFeature } from 'constants/appFeature';
import datetime from '../../scripts/datetime'; import datetime from '../../scripts/datetime';
import { clearBackdrop, setBackdrops } from '../backdrop/backdrop'; import { clearBackdrop, setBackdrops } from '../backdrop/backdrop';
@@ -371,7 +372,7 @@ export default function () {
showVolumeSlider = false; showVolumeSlider = false;
} }
if (currentPlayer.isLocalPlayer && appHost.supports('physicalvolumecontrol')) { if (currentPlayer.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl)) {
showMuteButton = false; showMuteButton = false;
showVolumeSlider = false; showVolumeSlider = false;
} }

View File

@@ -2,6 +2,7 @@
* Image viewer component * Image viewer component
* @module components/slideshow/slideshow * @module components/slideshow/slideshow
*/ */
import { AppFeature } from 'constants/appFeature';
import dialogHelper from '../dialogHelper/dialogHelper'; import dialogHelper from '../dialogHelper/dialogHelper';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import inputManager from '../../scripts/inputManager'; import inputManager from '../../scripts/inputManager';
@@ -172,10 +173,10 @@ export default function (options) {
if (actionButtonsOnTop) { if (actionButtonsOnTop) {
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true); 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); html += getIcon('file_download', 'btnDownload slideshowButton', true);
} }
if (appHost.supports('sharing')) { if (appHost.supports(AppFeature.Sharing)) {
html += getIcon('share', 'btnShare slideshowButton', true); html += getIcon('share', 'btnShare slideshowButton', true);
} }
if (screenfull.isEnabled) { if (screenfull.isEnabled) {
@@ -190,10 +191,10 @@ export default function (options) {
html += '<div class="slideshowBottomBar hide">'; html += '<div class="slideshowBottomBar hide">';
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true, true); 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); html += getIcon('file_download', 'btnDownload slideshowButton', true);
} }
if (appHost.supports('sharing')) { if (appHost.supports(AppFeature.Sharing)) {
html += getIcon('share', 'btnShare slideshowButton', true); html += getIcon('share', 'btnShare slideshowButton', true);
} }
if (screenfull.isEnabled) { if (screenfull.isEnabled) {

View File

@@ -1,4 +1,6 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { AppFeature } from 'constants/appFeature';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
import dialogHelper from '../dialogHelper/dialogHelper'; import dialogHelper from '../dialogHelper/dialogHelper';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
@@ -440,7 +442,7 @@ function showEditorInternal(itemId, serverId) {
} }
// Don't allow redirection to other websites from the TV layout // 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(); dlg.querySelector('.btnHelp').remove();
} }

View File

@@ -1,3 +1,4 @@
import { AppFeature } from 'constants/appFeature';
import globalize from '../../lib/globalize'; import globalize from '../../lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import { appHost } from '../apphost'; import { appHost } from '../apphost';
@@ -40,7 +41,7 @@ function getSubtitleAppearanceObject(context) {
function loadForm(context, user, userSettings, appearanceSettings, apiClient) { function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
apiClient.getCultures().then(function (allCultures) { 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'); context.querySelector('.fldBurnIn').classList.remove('hide');
} }
@@ -208,7 +209,7 @@ function embed(options, self) {
options.element.querySelector('.btnSave').classList.remove('hide'); options.element.querySelector('.btnSave').classList.remove('hide');
} }
if (appHost.supports('subtitleappearancesettings')) { if (appHost.supports(AppFeature.SubtitleAppearance)) {
options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide'); options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide');
self._fullPreview = options.element.querySelector('.subtitleappearance-fullpreview'); self._fullPreview = options.element.querySelector('.subtitleappearance-fullpreview');

View File

@@ -16,6 +16,7 @@ import React, { FC, useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
import { AppFeature } from 'constants/appFeature';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { useQuickConnectEnabled } from 'hooks/useQuickConnect'; import { useQuickConnectEnabled } from 'hooks/useQuickConnect';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -97,7 +98,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
{appHost.supports('clientsettings') && ([ {appHost.supports(AppFeature.ClientSettings) && ([
<Divider key='client-settings-divider' />, <Divider key='client-settings-divider' />,
<MenuItem <MenuItem
key='client-settings-button' key='client-settings-button'
@@ -156,7 +157,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
</MenuItem> </MenuItem>
)} )}
{appHost.supports('multiserver') && ( {appHost.supports(AppFeature.MultiServer) && (
<MenuItem <MenuItem
onClick={onSelectServerClick} onClick={onSelectServerClick}
> >
@@ -180,7 +181,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
{appHost.supports('exitmenu') && ([ {appHost.supports(AppFeature.ExitMenu) && ([
<Divider key='exit-menu-divider' />, <Divider key='exit-menu-divider' />,
<MenuItem <MenuItem
key='exit-menu-button' key='exit-menu-button'

View File

@@ -0,0 +1,57 @@
/** App feature flags */
export enum AppFeature {
/** The app supports changing the URL hash when the cast menu opens */
CastMenuHashChange = 'castmenuhashchange',
/** The app supports Chromecast */
Chromecast = 'chromecast',
/** The app supports showing client settings via a menu entry */
ClientSettings = 'clientsettings',
/** The app supports configuring the display language */
DisplayLanguage = 'displaylanguage',
/** The app supports configuring the display mode (TV, Desktop, etc.) */
DisplayMode = 'displaymode',
/** The app can exit via back navigation */
Exit = 'exit',
/** The app can be exited via a menu entry */
ExitMenu = 'exitmenu',
/** The app can open external URLs */
ExternalLinks = 'externallinks',
/** The app supports enabling external players */
ExternalPlayerIntent = 'externalplayerintent',
/** The app supports file downloads */
FileDownload = 'filedownload',
/** The app supports file input elements */
FileInput = 'fileinput',
/** The app supports enabling fullscreen media playback */
Fullscreen = 'fullscreenchange',
/** The app supports autoplay on the audio element */
HtmlAudioAutoplay = 'htmlaudioautoplay',
/** The app supports autoplay on the video element */
HtmlVideoAutoplay = 'htmlvideoautoplay',
/** The app supports switching servers */
MultiServer = 'multiserver',
/** The app supports playback of BluRay folders */
NativeBluRayPlayback = 'nativeblurayplayback',
/** The app supports playback of DVD folders */
NativeDvdPlayback = 'nativedvdplayback',
/** The app supports playback of ISO files */
NativeIsoPlayback = 'nativeisoplayback',
/** The app supports physical volume buttons */
PhysicalVolumeControl = 'physicalvolumecontrol',
/** The app supports playing remote audio */
RemoteAudio = 'remoteaudio',
/** The app supports the remote control (casting) feature */
RemoteControl = 'remotecontrol',
/** The app supports playing remote video */
RemoteVideo = 'remotevideo',
/** The app supports displaying a screensaver */
Screensaver = 'screensaver',
/** The app supports sharing content */
Sharing = 'sharing',
/** The app supports configuring subtitle appearance */
SubtitleAppearance = 'subtitleappearancesettings',
/** The app supports configuring subtitle burn-in */
SubtitleBurnIn = 'subtitleburnsettings',
/** The app can open URLs in a blank page. */
TargetBlank = 'targetblank'
}

View File

@@ -21,6 +21,7 @@ import loading from 'components/loading/loading';
import { playbackManager } from 'components/playback/playbackmanager'; import { playbackManager } from 'components/playback/playbackmanager';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import itemShortcuts from 'components/shortcuts'; import itemShortcuts from 'components/shortcuts';
import { AppFeature } from 'constants/appFeature';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import browser from 'scripts/browser'; import browser from 'scripts/browser';
@@ -636,7 +637,7 @@ function reloadFromItem(instance, page, params, item, user) {
if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) { if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) {
let location = item.ProductionLocations[0]; let location = item.ProductionLocations[0];
if (!layoutManager.tv && appHost.supports('externallinks')) { if (!layoutManager.tv && appHost.supports(AppFeature.ExternalLinks)) {
location = `<a is="emby-linkbutton" class="button-link textlink" target="_blank" href="https://www.openstreetmap.org/search?query=${encodeURIComponent(location)}">${escapeHtml(location)}</a>`; location = `<a is="emby-linkbutton" class="button-link textlink" target="_blank" href="https://www.openstreetmap.org/search?query=${encodeURIComponent(location)}">${escapeHtml(location)}</a>`;
} else { } else {
location = escapeHtml(location); location = escapeHtml(location);
@@ -650,7 +651,7 @@ function reloadFromItem(instance, page, params, item, user) {
setPeopleHeader(page, item); setPeopleHeader(page, item);
loading.hide(); loading.hide();
if (item.Type === 'Book' && item.CanDownload && appHost.supports('filedownload')) { if (item.Type === 'Book' && item.CanDownload && appHost.supports(AppFeature.FileDownload)) {
hideAll(page, 'btnDownload', true); hideAll(page, 'btnDownload', true);
} }
@@ -1083,7 +1084,7 @@ function renderDetails(page, item, apiClient, context) {
renderLyricsContainer(page, item, apiClient); renderLyricsContainer(page, item, apiClient);
// Don't allow redirection to other websites from the TV layout // Don't allow redirection to other websites from the TV layout
if (!layoutManager.tv && appHost.supports('externallinks')) { if (!layoutManager.tv && appHost.supports(AppFeature.ExternalLinks)) {
renderLinks(page, item); renderLinks(page, item);
} }

View File

@@ -1,4 +1,10 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent';
import { AppFeature } from 'constants/appFeature';
import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time';
import { EventType } from 'types/eventType';
import { playbackManager } from '../../../components/playback/playbackmanager'; import { playbackManager } from '../../../components/playback/playbackmanager';
import browser from '../../../scripts/browser'; import browser from '../../../scripts/browser';
import dom from '../../../scripts/dom'; import dom from '../../../scripts/dom';
@@ -27,9 +33,6 @@ import LibraryMenu from '../../../scripts/libraryMenu';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop';
import { pluginManager } from '../../../components/pluginManager'; import { pluginManager } from '../../../components/pluginManager';
import { PluginType } from '../../../types/plugin.ts'; import { PluginType } from '../../../types/plugin.ts';
import { EventType } from 'types/eventType';
import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time';
import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent';
function getOpenedDialog() { function getOpenedDialog() {
return document.querySelector('.dialogContainer .dialog.opened'); return document.querySelector('.dialogContainer .dialog.opened');
@@ -872,7 +875,7 @@ export default function (view) {
showVolumeSlider = false; showVolumeSlider = false;
} }
if (player.isLocalPlayer && appHost.supports('physicalvolumecontrol')) { if (player.isLocalPlayer && appHost.supports(AppFeature.PhysicalVolumeControl)) {
showMuteButton = false; showMuteButton = false;
showVolumeSlider = false; showVolumeSlider = false;
} }

View File

@@ -1,5 +1,9 @@
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import markdownIt from 'markdown-it'; import markdownIt from 'markdown-it';
import { AppFeature } from 'constants/appFeature';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import { appHost } from '../../../components/apphost'; import { appHost } from '../../../components/apphost';
import appSettings from '../../../scripts/settings/appSettings'; import appSettings from '../../../scripts/settings/appSettings';
import dom from '../../../scripts/dom'; import dom from '../../../scripts/dom';
@@ -15,7 +19,6 @@ import toast from '../../../components/toast/toast';
import dialogHelper from '../../../components/dialogHelper/dialogHelper'; import dialogHelper from '../../../components/dialogHelper/dialogHelper';
import baseAlert from '../../../components/alert'; import baseAlert from '../../../components/alert';
import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils'; import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import './login.scss'; import './login.scss';
@@ -263,7 +266,7 @@ export default function (view, params) {
loading.show(); loading.show();
libraryMenu.setTransparentMenu(true); libraryMenu.setTransparentMenu(true);
if (!appHost.supports('multiserver')) { if (!appHost.supports(AppFeature.MultiServer)) {
view.querySelector('.btnSelectServer').classList.add('hide'); view.querySelector('.btnSelectServer').classList.add('hide');
} }

View File

@@ -1,9 +1,12 @@
import React, { AnchorHTMLAttributes, DetailedHTMLProps, MouseEvent, useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import layoutManager from '../../components/layoutManager'; import React, { AnchorHTMLAttributes, DetailedHTMLProps, MouseEvent, useCallback } from 'react';
import shell from '../../scripts/shell';
import { appRouter } from '../../components/router/appRouter'; import { appHost } from 'components/apphost';
import { appHost } from '../../components/apphost'; import layoutManager from 'components/layoutManager';
import { appRouter } from 'components/router/appRouter';
import { AppFeature } from 'constants/appFeature';
import shell from 'scripts/shell';
import './emby-button.scss'; import './emby-button.scss';
interface LinkButtonProps extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, interface LinkButtonProps extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -28,7 +31,7 @@ const LinkButton: React.FC<LinkButtonProps> = ({
const url = href || ''; const url = href || '';
if (url !== '#') { if (url !== '#') {
if (target) { if (target) {
if (!appHost.supports('targetblank')) { if (!appHost.supports(AppFeature.TargetBlank)) {
e.preventDefault(); e.preventDefault();
shell.openUrl(url); shell.openUrl(url);
} }
@@ -45,7 +48,7 @@ const LinkButton: React.FC<LinkButtonProps> = ({
onClick?.(e); onClick?.(e);
}, [ href, target, onClick ]); }, [ href, target, onClick ]);
if (isAutoHideEnabled === true && !appHost.supports('externallinks')) { if (isAutoHideEnabled === true && !appHost.supports(AppFeature.ExternalLinks)) {
return null; return null;
} }

View File

@@ -1,9 +1,12 @@
import 'webcomponents.js/webcomponents-lite'; import 'webcomponents.js/webcomponents-lite';
import { removeEventListener, addEventListener } from '../../scripts/dom';
import layoutManager from '../../components/layoutManager'; import { appHost } from 'components/apphost';
import shell from '../../scripts/shell'; import layoutManager from 'components/layoutManager';
import { appRouter } from '../../components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import { appHost } from '../../components/apphost'; import { AppFeature } from 'constants/appFeature';
import { removeEventListener, addEventListener } from 'scripts/dom';
import shell from 'scripts/shell';
import './emby-button.scss'; import './emby-button.scss';
const EmbyButtonPrototype = Object.create(HTMLButtonElement.prototype); const EmbyButtonPrototype = Object.create(HTMLButtonElement.prototype);
@@ -13,7 +16,7 @@ function onAnchorClick(e) {
const href = this.getAttribute('href') || ''; const href = this.getAttribute('href') || '';
if (href !== '#') { if (href !== '#') {
if (this.getAttribute('target')) { if (this.getAttribute('target')) {
if (!appHost.supports('targetblank')) { if (!appHost.supports(AppFeature.TargetBlank)) {
e.preventDefault(); e.preventDefault();
shell.openUrl(href); shell.openUrl(href);
} }
@@ -46,7 +49,7 @@ EmbyButtonPrototype.attachedCallback = function () {
addEventListener(this, 'click', onAnchorClick, {}); addEventListener(this, 'click', onAnchorClick, {});
if (this.getAttribute('data-autohide') === 'true') { if (this.getAttribute('data-autohide') === 'true') {
if (appHost.supports('externallinks')) { if (appHost.supports(AppFeature.ExternalLinks)) {
this.classList.remove('hide'); this.classList.remove('hide');
} else { } else {
this.classList.add('hide'); this.classList.add('hide');

View File

@@ -12,6 +12,7 @@ import autoFocuser from './components/autoFocuser';
import loading from 'components/loading/loading'; import loading from 'components/loading/loading';
import { pluginManager } from './components/pluginManager'; import { pluginManager } from './components/pluginManager';
import { appRouter } from './components/router/appRouter'; import { appRouter } from './components/router/appRouter';
import { AppFeature } from 'constants/appFeature';
import globalize from './lib/globalize'; import globalize from './lib/globalize';
import { loadCoreDictionary } from 'lib/globalize/loader'; import { loadCoreDictionary } from 'lib/globalize/loader';
import { initialize as initializeAutoCast } from 'scripts/autocast'; import { initialize as initializeAutoCast } from 'scripts/autocast';
@@ -136,7 +137,7 @@ async function loadPlugins() {
console.dir(pluginManager); console.dir(pluginManager);
let list = await getPlugins(); let list = await getPlugins();
if (!appHost.supports('remotecontrol')) { if (!appHost.supports(AppFeature.RemoteControl)) {
// Disable remote player plugins if not supported // Disable remote player plugins if not supported
list = list.filter(plugin => !plugin.startsWith('sessionPlayer') list = list.filter(plugin => !plugin.startsWith('sessionPlayer')
&& !plugin.startsWith('chromecastPlayer')); && !plugin.startsWith('chromecastPlayer'));
@@ -165,12 +166,12 @@ function loadPlatformFeatures() {
import('./components/nowPlayingBar/nowPlayingBar'); import('./components/nowPlayingBar/nowPlayingBar');
} }
if (appHost.supports('remotecontrol')) { if (appHost.supports(AppFeature.RemoteControl)) {
import('./components/playback/playerSelectionMenu'); import('./components/playback/playerSelectionMenu');
import('./components/playback/remotecontrolautoplay'); import('./components/playback/remotecontrolautoplay');
} }
if (!appHost.supports('physicalvolumecontrol') || browser.touch) { if (!appHost.supports(AppFeature.PhysicalVolumeControl) || browser.touch) {
import('./components/playback/volumeosd'); import('./components/playback/volumeosd');
} }

View File

@@ -1,3 +1,6 @@
import { AppFeature } from 'constants/appFeature';
import { MediaError } from 'types/mediaError';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import { appHost } from '../../components/apphost'; import { appHost } from '../../components/apphost';
import * as htmlMediaHelper from '../../components/htmlMediaHelper'; import * as htmlMediaHelper from '../../components/htmlMediaHelper';
@@ -5,7 +8,6 @@ import profileBuilder from '../../scripts/browserDeviceProfile';
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings'; import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
import { PluginType } from '../../types/plugin.ts'; import { PluginType } from '../../types/plugin.ts';
import Events from '../../utils/events.ts'; import Events from '../../utils/events.ts';
import { MediaError } from 'types/mediaError';
function getDefaultProfile() { function getDefaultProfile() {
return profileBuilder({}); return profileBuilder({});
@@ -278,7 +280,7 @@ class HtmlAudioPlayer {
} }
// TODO: Move volume control to PlaybackManager. Player should just be a wrapper that translates commands into API calls. // TODO: Move volume control to PlaybackManager. Player should just be a wrapper that translates commands into API calls.
if (!appHost.supports('physicalvolumecontrol')) { if (!appHost.supports(AppFeature.PhysicalVolumeControl)) {
elem.volume = htmlMediaHelper.getSavedVolume(); elem.volume = htmlMediaHelper.getSavedVolume();
} }

View File

@@ -1,4 +1,10 @@
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import debounce from 'lodash-es/debounce';
import Screenfull from 'screenfull';
import { AppFeature } from 'constants/appFeature';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import { MediaError } from 'types/mediaError';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import appSettings from '../../scripts/settings/appSettings'; import appSettings from '../../scripts/settings/appSettings';
@@ -27,9 +33,7 @@ import {
getBufferedRanges getBufferedRanges
} from '../../components/htmlMediaHelper'; } from '../../components/htmlMediaHelper';
import itemHelper from '../../components/itemHelper'; import itemHelper from '../../components/itemHelper';
import Screenfull from 'screenfull';
import globalize from '../../lib/globalize'; import globalize from '../../lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import profileBuilder, { canPlaySecondaryAudio } from '../../scripts/browserDeviceProfile'; import profileBuilder, { canPlaySecondaryAudio } from '../../scripts/browserDeviceProfile';
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings'; import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop';
@@ -37,8 +41,6 @@ import { PluginType } from '../../types/plugin.ts';
import Events from '../../utils/events.ts'; import Events from '../../utils/events.ts';
import { includesAny } from '../../utils/container.ts'; import { includesAny } from '../../utils/container.ts';
import { isHls } from '../../utils/mediaSource.ts'; import { isHls } from '../../utils/mediaSource.ts';
import debounce from 'lodash-es/debounce';
import { MediaError } from 'types/mediaError';
/** /**
* Returns resolved URL. * Returns resolved URL.
@@ -1667,7 +1669,7 @@ export class HtmlVideoPlayer {
const cssClass = 'htmlvideoplayer'; const cssClass = 'htmlvideoplayer';
// Can't autoplay in these browsers so we need to use the full controls, at least until playback starts // Can't autoplay in these browsers so we need to use the full controls, at least until playback starts
if (!appHost.supports('htmlvideoautoplay')) { if (!appHost.supports(AppFeature.HtmlVideoAutoplay)) {
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" controls="controls" webkit-playsinline playsinline>'; html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" controls="controls" webkit-playsinline playsinline>';
} else if (browser.web0s) { } else if (browser.web0s) {
// in webOS, setting preload auto allows resuming videos // in webOS, setting preload auto allows resuming videos
@@ -1683,7 +1685,7 @@ export class HtmlVideoPlayer {
const videoElement = playerDlg.querySelector('video'); const videoElement = playerDlg.querySelector('video');
// TODO: Move volume control to PlaybackManager. Player should just be a wrapper that translates commands into API calls. // TODO: Move volume control to PlaybackManager. Player should just be a wrapper that translates commands into API calls.
if (!appHost.supports('physicalvolumecontrol')) { if (!appHost.supports(AppFeature.PhysicalVolumeControl)) {
videoElement.volume = getSavedVolume(); videoElement.volume = getSavedVolume();
} }

View File

@@ -1,4 +1,5 @@
import { appHost } from '../../../components/apphost'; import { appHost } from 'components/apphost';
import { AppFeature } from 'constants/appFeature';
/** /**
* Creates an audio element that plays a silent sound. * Creates an audio element that plays a silent sound.
@@ -35,7 +36,7 @@ class PlaybackPermissionManager {
* @returns {Promise} Promise that resolves succesfully if playback permission is allowed. * @returns {Promise} Promise that resolves succesfully if playback permission is allowed.
*/ */
check () { check () {
if (appHost.supports('htmlaudioautoplay')) { if (appHost.supports(AppFeature.HtmlAudioAutoplay)) {
return Promise.resolve(true); return Promise.resolve(true);
} }

View File

@@ -1,8 +1,9 @@
import { playbackManager } from '../components/playback/playbackmanager'; import { appHost } from 'components/apphost';
import focusManager from '../components/focusManager'; import focusManager from 'components/focusManager';
import { appRouter } from '../components/router/appRouter'; import { playbackManager } from 'components/playback/playbackmanager';
import dom from './dom'; import { appRouter } from 'components/router/appRouter';
import { appHost } from '../components/apphost'; import { AppFeature } from 'constants/appFeature';
import dom from 'scripts/dom';
let lastInputTime = new Date().getTime(); let lastInputTime = new Date().getTime();
@@ -111,7 +112,7 @@ export function handleCommand(commandName, options) {
'back': () => { 'back': () => {
if (appRouter.canGoBack()) { if (appRouter.canGoBack()) {
appRouter.back(); appRouter.back();
} else if (appHost.supports('exit')) { } else if (appHost.supports(AppFeature.Exit)) {
appHost.exit(); appHost.exit();
} }
}, },

View File

@@ -4,7 +4,10 @@ import Headroom from 'headroom.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ApiClient } from 'jellyfin-apiclient'; import { ApiClient } from 'jellyfin-apiclient';
import { AppFeature } from 'constants/appFeature';
import { getUserViewsQuery } from 'hooks/useUserViews'; import { getUserViewsQuery } from 'hooks/useUserViews';
import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import { EventType } from 'types/eventType'; import { EventType } from 'types/eventType';
import { toApi } from 'utils/jellyfin-apiclient/compat'; import { toApi } from 'utils/jellyfin-apiclient/compat';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
@@ -19,8 +22,6 @@ import { playbackManager } from '../components/playback/playbackmanager';
import { pluginManager } from '../components/pluginManager'; import { pluginManager } from '../components/pluginManager';
import groupSelectionMenu from '../plugins/syncPlay/ui/groupSelectionMenu'; import groupSelectionMenu from '../plugins/syncPlay/ui/groupSelectionMenu';
import browser from './browser'; import browser from './browser';
import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import imageHelper from '../utils/image'; import imageHelper from '../utils/image';
import { getMenuLinks } from '../scripts/settings/webSettings'; import { getMenuLinks } from '../scripts/settings/webSettings';
import Dashboard, { pageClassOn } from '../utils/dashboard'; import Dashboard, { pageClassOn } from '../utils/dashboard';
@@ -348,14 +349,14 @@ function refreshLibraryInfoInDrawer(user) {
html += globalize.translate('HeaderUser'); html += globalize.translate('HeaderUser');
html += '</h3>'; html += '</h3>';
if (appHost.supports('multiserver')) { if (appHost.supports(AppFeature.MultiServer)) {
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnSelectServer" data-itemid="selectserver" href="#"><span class="material-icons navMenuOptionIcon storage" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('SelectServer')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnSelectServer" data-itemid="selectserver" href="#"><span class="material-icons navMenuOptionIcon storage" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('SelectServer')}</span></a>`;
} }
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnSettings" data-itemid="settings" href="#"><span class="material-icons navMenuOptionIcon settings" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('Settings')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnSettings" data-itemid="settings" href="#"><span class="material-icons navMenuOptionIcon settings" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('Settings')}</span></a>`;
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnLogout" data-itemid="logout" href="#"><span class="material-icons navMenuOptionIcon exit_to_app" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('ButtonSignOut')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder btnLogout" data-itemid="logout" href="#"><span class="material-icons navMenuOptionIcon exit_to_app" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('ButtonSignOut')}</span></a>`;
if (appHost.supports('exitmenu')) { if (appHost.supports(AppFeature.ExitMenu)) {
html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder exitApp" data-itemid="exitapp" href="#"><span class="material-icons navMenuOptionIcon close" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('ButtonExitApp')}</span></a>`; html += `<a is="emby-linkbutton" class="navMenuOption lnkMediaFolder exitApp" data-itemid="exitapp" href="#"><span class="material-icons navMenuOptionIcon close" aria-hidden="true"></span><span class="navMenuOptionText">${globalize.translate('ButtonExitApp')}</span></a>`;
} }