diff --git a/src/apps/dashboard/controllers/livetvstatus.html b/src/apps/dashboard/controllers/livetvstatus.html deleted file mode 100644 index f1ac392ee0..0000000000 --- a/src/apps/dashboard/controllers/livetvstatus.html +++ /dev/null @@ -1,40 +0,0 @@ -
-
-
-
-
-
-

- ${HeaderTunerDevices} -

- -
-
-
-
- -
-
-
-

${HeaderGuideProviders}

- -
- -
-
- -
- - -
-
-
-
-
-
diff --git a/src/apps/dashboard/controllers/livetvstatus.js b/src/apps/dashboard/controllers/livetvstatus.js deleted file mode 100644 index 742d02a901..0000000000 --- a/src/apps/dashboard/controllers/livetvstatus.js +++ /dev/null @@ -1,338 +0,0 @@ -import 'jquery'; - -import globalize from 'lib/globalize'; -import taskButton from 'scripts/taskbutton'; -import dom from 'utils/dom'; -import layoutManager from 'components/layoutManager'; -import loading from 'components/loading/loading'; -import browser from 'scripts/browser'; -import 'components/listview/listview.scss'; -import 'styles/flexstyles.scss'; -import 'elements/emby-itemscontainer/emby-itemscontainer'; -import 'components/cardbuilder/card.scss'; -import 'material-design-icons-iconfont'; -import 'elements/emby-button/emby-button'; -import Dashboard from 'utils/dashboard'; -import confirm from 'components/confirm/confirm'; -import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils'; - -const enableFocusTransform = !browser.slow && !browser.edge; - -function getDeviceHtml(device) { - const padderClass = 'cardPadder-backdrop'; - let cssClass = 'card scalableCard backdropCard backdropCard-scalable'; - const cardBoxCssClass = 'cardBox visualCardBox'; - let html = ''; - - // TODO move card creation code to Card component - - if (layoutManager.tv) { - cssClass += ' show-focus'; - - if (enableFocusTransform) { - cssClass += ' show-animation'; - } - } - - html += '
'; - html += '
'; - html += '
'; - html += '
'; - html += '
'; - html += `
`; - html += '
'; - html += '
'; - html += '
'; - html += ''; - html += '
' + (device.FriendlyName || getTunerName(device.Type)) + '
'; - html += '
'; - html += device.Url || ' '; - html += '
'; - html += '
'; - html += '
'; - html += '
'; - return html; -} - -function renderDevices(page, devices) { - page.querySelector('.devicesList').innerHTML = devices.map(getDeviceHtml).join(''); -} - -function deleteDevice(page, id) { - const message = globalize.translate('MessageConfirmDeleteTunerDevice'); - - confirm(message, globalize.translate('HeaderDeleteDevice')).then(function () { - loading.show(); - ApiClient.ajax({ - type: 'DELETE', - url: ApiClient.getUrl('LiveTv/TunerHosts', { - Id: id - }) - }).then(function () { - reload(page); - }); - }); -} - -function reload(page) { - loading.show(); - ApiClient.getNamedConfiguration('livetv').then(function (config) { - renderDevices(page, config.TunerHosts); - renderProviders(page, config.ListingProviders); - }); - loading.hide(); -} - -function submitAddDeviceForm(page) { - page.querySelector('.dlgAddDevice').close(); - loading.show(); - ApiClient.ajax({ - type: 'POST', - url: ApiClient.getUrl('LiveTv/TunerHosts'), - data: JSON.stringify({ - Type: page.querySelector('#selectTunerDeviceType').value, - Url: page.querySelector('#txtDevicePath').value - }), - contentType: 'application/json' - }).then(function () { - reload(page); - }, function () { - Dashboard.alert({ - message: globalize.translate('ErrorAddingTunerDevice') - }); - }); -} - -function renderProviders(page, providers) { - let html = ''; - - if (providers.length) { - html += '
'; - - for (let i = 0, length = providers.length; i < length; i++) { - const provider = providers[i]; - html += '
'; - html += ''; - html += ''; - html += ''; - html += '
'; - } - - html += '
'; - } - - const elem = page.querySelector('.providerList'); - elem.innerHTML = html; - if (elem.querySelector('.btnOptions')) { - const btnOptionElements = elem.querySelectorAll('.btnOptions'); - btnOptionElements.forEach(function (btn) { - btn.addEventListener('click', function () { - const id = this.getAttribute('data-id'); - showProviderOptions(page, id, btn); - }); - }); - } -} - -function showProviderOptions(page, providerId, button) { - const items = []; - items.push({ - name: globalize.translate('Delete'), - id: 'delete' - }); - items.push({ - name: globalize.translate('MapChannels'), - id: 'map' - }); - - import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => { - actionsheet.show({ - items: items, - positionTo: button - }).then(function (id) { - switch (id) { - case 'delete': - deleteProvider(page, providerId); - break; - - case 'map': - mapChannels(page, providerId); - } - }); - }); -} - -function mapChannels(page, providerId) { - import('components/channelMapper/channelMapper').then(({ default: ChannelMapper }) => { - new ChannelMapper({ - serverId: ApiClient.serverInfo().Id, - providerId: providerId - }).show(); - }); -} - -function deleteProvider(page, id) { - const message = globalize.translate('MessageConfirmDeleteGuideProvider'); - - confirm(message, globalize.translate('HeaderDeleteProvider')).then(function () { - loading.show(); - ApiClient.ajax({ - type: 'DELETE', - url: ApiClient.getUrl('LiveTv/ListingProviders', { - Id: id - }) - }).then(function () { - reload(page); - }, function () { - reload(page); - }); - }); -} - -function getTunerName(providerId) { - switch (providerId.toLowerCase()) { - case 'm3u': - return 'M3U'; - case 'hdhomerun': - return 'HDHomeRun'; - case 'hauppauge': - return 'Hauppauge'; - case 'satip': - return 'DVB'; - default: - return 'Unknown'; - } -} - -function getProviderName(providerId) { - switch (providerId.toLowerCase()) { - case 'schedulesdirect': - return 'Schedules Direct'; - case 'xmltv': - return 'XMLTV'; - default: - return 'Unknown'; - } -} - -function getProviderConfigurationUrl(providerId) { - switch (providerId.toLowerCase()) { - case 'xmltv': - return '#/dashboard/livetv/guide?type=xmltv'; - case 'schedulesdirect': - return '#/dashboard/livetv/guide?type=schedulesdirect'; - } -} - -function addProvider(button) { - const menuItems = []; - menuItems.push({ - name: 'Schedules Direct', - id: 'SchedulesDirect' - }); - menuItems.push({ - name: 'XMLTV', - id: 'xmltv' - }); - - import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => { - actionsheet.show({ - items: menuItems, - positionTo: button, - callback: function (id) { - Dashboard.navigate(getProviderConfigurationUrl(id)); - } - }); - }); -} - -function addDevice() { - Dashboard.navigate('dashboard/livetv/tuner'); -} - -function showDeviceMenu(button, tunerDeviceId) { - const items = []; - items.push({ - name: globalize.translate('Delete'), - id: 'delete' - }); - items.push({ - name: globalize.translate('Edit'), - id: 'edit' - }); - - import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => { - actionsheet.show({ - items: items, - positionTo: button - }).then(function (id) { - switch (id) { - case 'delete': - deleteDevice(dom.parentWithClass(button, 'page'), tunerDeviceId); - break; - - case 'edit': - Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId); - } - }); - }); -} - -function onDevicesListClick(e) { - const card = dom.parentWithClass(e.target, 'card'); - - if (card) { - const id = card.getAttribute('data-id'); - const btnCardOptions = dom.parentWithClass(e.target, 'btnCardOptions'); - - if (btnCardOptions) { - showDeviceMenu(btnCardOptions, id); - } else { - Dashboard.navigate('dashboard/livetv/tuner?id=' + id); - } - } -} - -$(document).on('pageinit', '#liveTvStatusPage', function () { - const page = this; - page.querySelector('.btnAddDevice').addEventListener('click', function () { - addDevice(); - }); - if (page.querySelector('.formAddDevice')) { - // NOTE: unused? - page.querySelector('.formAddDevice').addEventListener('submit', function (e) { - e.preventDefault(); - submitAddDeviceForm(page); - }); - } - page.querySelector('.btnAddProvider').addEventListener('click', function () { - addProvider(this); - }); - page.querySelector('.devicesList').addEventListener('click', onDevicesListClick); -}).on('pageshow', '#liveTvStatusPage', function () { - const page = this; - reload(page); - taskButton({ - mode: 'on', - progressElem: page.querySelector('.refreshGuideProgress'), - taskKey: 'RefreshGuide', - button: page.querySelector('.btnRefresh') - }); -}).on('pagehide', '#liveTvStatusPage', function () { - const page = this; - taskButton({ - mode: 'off', - progressElem: page.querySelector('.refreshGuideProgress'), - taskKey: 'RefreshGuide', - button: page.querySelector('.btnRefresh') - }); -}); diff --git a/src/apps/dashboard/features/livetv/api/useDeleteProvider.ts b/src/apps/dashboard/features/livetv/api/useDeleteProvider.ts new file mode 100644 index 0000000000..826dae5c98 --- /dev/null +++ b/src/apps/dashboard/features/livetv/api/useDeleteProvider.ts @@ -0,0 +1,22 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useApi } from 'hooks/useApi'; +import { queryClient } from 'utils/query/queryClient'; +import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; +import { LiveTvApiDeleteListingProviderRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api'; + +export const useDeleteProvider = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: LiveTvApiDeleteListingProviderRequest) => ( + getLiveTvApi(api!) + .deleteListingProvider(params) + ), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [ 'NamedConfiguration', 'livetv' ] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/livetv/api/useDeleteTuner.ts b/src/apps/dashboard/features/livetv/api/useDeleteTuner.ts new file mode 100644 index 0000000000..ce22194548 --- /dev/null +++ b/src/apps/dashboard/features/livetv/api/useDeleteTuner.ts @@ -0,0 +1,22 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useApi } from 'hooks/useApi'; +import { queryClient } from 'utils/query/queryClient'; +import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api'; +import { LiveTvApiDeleteTunerHostRequest } from '@jellyfin/sdk/lib/generated-client/api/live-tv-api'; + +export const useDeleteTuner = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: LiveTvApiDeleteTunerHostRequest) => ( + getLiveTvApi(api!) + .deleteTunerHost(params) + ), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [ 'NamedConfiguration', 'livetv' ] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/livetv/components/Provider.tsx b/src/apps/dashboard/features/livetv/components/Provider.tsx new file mode 100644 index 0000000000..7159347423 --- /dev/null +++ b/src/apps/dashboard/features/livetv/components/Provider.tsx @@ -0,0 +1,138 @@ +import React, { useCallback, useRef, useState } from 'react'; +import type { ListingsProviderInfo } from '@jellyfin/sdk/lib/generated-client/models/listings-provider-info'; +import Avatar from '@mui/material/Avatar'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemLink from 'components/ListItemLink'; +import DvrIcon from '@mui/icons-material/Dvr'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import getProviderConfigurationUrl from '../utils/getProviderConfigurationUrl'; +import ListItemText from '@mui/material/ListItemText'; +import getProviderName from '../utils/getProviderName'; +import IconButton from '@mui/material/IconButton'; +import ConfirmDialog from 'components/ConfirmDialog'; +import globalize from 'lib/globalize'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ChannelMapper from 'components/channelMapper/channelMapper'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { useDeleteProvider } from '../api/useDeleteProvider'; + +interface ProviderProps { + provider: ListingsProviderInfo +} + +const Provider = ({ provider }: ProviderProps) => { + const [ isDeleteProviderDialogOpen, setIsDeleteProviderDialogOpen ] = useState(false); + const actionsRef = useRef(null); + const [ anchorEl, setAnchorEl ] = useState(null); + const [ isMenuOpen, setIsMenuOpen ] = useState(false); + const deleteProvider = useDeleteProvider(); + + const showChannelMapper = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + void new ChannelMapper({ + serverId: ServerConnections.currentApiClient()?.serverId(), + providerId: provider.Id + }).show(); + }, [ provider ]); + + const showContextMenu = useCallback(() => { + setAnchorEl(actionsRef.current); + setIsMenuOpen(true); + }, []); + + const showDeleteDialog = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + setIsDeleteProviderDialogOpen(true); + }, []); + + const onDeleteProviderDialogCancel = useCallback(() => { + setIsDeleteProviderDialogOpen(false); + }, []); + + const onMenuClose = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + }, []); + + const onConfirmDelete = useCallback(() => { + if (provider.Id) { + deleteProvider.mutate({ + id: provider.Id + }, { + onSettled: () => { + setIsDeleteProviderDialogOpen(false); + } + }); + } + }, [ deleteProvider, provider ]); + + return ( + <> + + + + + } + > + + + + + + + + + + + + + + {globalize.translate('MapChannels')} + + + + + + {globalize.translate('Delete')} + + + + ); +}; + +export default Provider; diff --git a/src/apps/dashboard/features/livetv/components/TunerDeviceCard.tsx b/src/apps/dashboard/features/livetv/components/TunerDeviceCard.tsx new file mode 100644 index 0000000000..cd253f2b8a --- /dev/null +++ b/src/apps/dashboard/features/livetv/components/TunerDeviceCard.tsx @@ -0,0 +1,110 @@ +import React, { useCallback, useRef, useState } from 'react'; +import type { TunerHostInfo } from '@jellyfin/sdk/lib/generated-client/models/tuner-host-info'; +import BaseCard from 'apps/dashboard/components/BaseCard'; +import DvrIcon from '@mui/icons-material/Dvr'; +import getTunerName from '../utils/getTunerName'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import ListItemText from '@mui/material/ListItemText'; +import globalize from 'lib/globalize'; +import { useNavigate } from 'react-router-dom'; +import ConfirmDialog from 'components/ConfirmDialog'; +import { useDeleteTuner } from '../api/useDeleteTuner'; + +interface TunerDeviceCardProps { + tunerHost: TunerHostInfo; +} + +const TunerDeviceCard = ({ tunerHost }: TunerDeviceCardProps) => { + const navigate = useNavigate(); + const actionRef = useRef(null); + const [ anchorEl, setAnchorEl ] = useState(null); + const [ isMenuOpen, setIsMenuOpen ] = useState(false); + const [ isConfirmDeleteDialogOpen, setIsConfirmDeleteDialogOpen ] = useState(false); + const deleteTuner = useDeleteTuner(); + + const navigateToEditPage = useCallback(() => { + navigate(`/dashboard/livetv/tuner?id=${tunerHost.Id}`); + }, [ navigate, tunerHost ]); + + const onDelete = useCallback(() => { + if (tunerHost.Id) { + deleteTuner.mutate({ + id: tunerHost.Id + }, { + onSettled: () => { + setIsConfirmDeleteDialogOpen(false); + } + }); + } + }, [ deleteTuner, tunerHost ]); + + const showDeleteDialog = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + setIsConfirmDeleteDialogOpen(true); + }, []); + + const onDeleteDialogClose = useCallback(() => { + setIsConfirmDeleteDialogOpen(false); + }, []); + + const onActionClick = useCallback(() => { + setAnchorEl(actionRef.current); + setIsMenuOpen(true); + }, []); + + const onMenuClose = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + }, []); + + return ( + <> + + + } + width={340} + action={true} + actionRef={actionRef} + onActionClick={onActionClick} + onClick={navigateToEditPage} + /> + + + + + + + {globalize.translate('Edit')} + + + + + + {globalize.translate('Delete')} + + + + ); +}; + +export default TunerDeviceCard; diff --git a/src/apps/dashboard/features/livetv/utils/getProviderConfigurationUrl.ts b/src/apps/dashboard/features/livetv/utils/getProviderConfigurationUrl.ts new file mode 100644 index 0000000000..4991801460 --- /dev/null +++ b/src/apps/dashboard/features/livetv/utils/getProviderConfigurationUrl.ts @@ -0,0 +1,10 @@ +const getProviderConfigurationUrl = (providerId: string) => { + switch (providerId?.toLowerCase()) { + case 'xmltv': + return '/dashboard/livetv/guide?type=xmltv'; + case 'schedulesdirect': + return '/dashboard/livetv/guide?type=schedulesdirect'; + } +}; + +export default getProviderConfigurationUrl; diff --git a/src/apps/dashboard/features/livetv/utils/getProviderName.ts b/src/apps/dashboard/features/livetv/utils/getProviderName.ts new file mode 100644 index 0000000000..d3ce3bc18a --- /dev/null +++ b/src/apps/dashboard/features/livetv/utils/getProviderName.ts @@ -0,0 +1,12 @@ +const getProviderName = (providerId: string | null | undefined) => { + switch (providerId?.toLowerCase()) { + case 'schedulesdirect': + return 'Schedules Direct'; + case 'xmltv': + return 'XMLTV'; + default: + return 'Unknown'; + } +}; + +export default getProviderName; diff --git a/src/apps/dashboard/features/livetv/utils/getTunerName.ts b/src/apps/dashboard/features/livetv/utils/getTunerName.ts new file mode 100644 index 0000000000..ea43ce870b --- /dev/null +++ b/src/apps/dashboard/features/livetv/utils/getTunerName.ts @@ -0,0 +1,16 @@ +const getTunerName = (providerId: string | null | undefined) => { + switch (providerId?.toLowerCase()) { + case 'm3u': + return 'M3U'; + case 'hdhomerun': + return 'HDHomeRun'; + case 'hauppauge': + return 'Hauppauge'; + case 'satip': + return 'DVB'; + default: + return 'Unknown'; + } +}; + +export default getTunerName; diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index ded4d2b530..80ddf24c5b 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -13,6 +13,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'libraries/display', type: AppType.Dashboard }, { path: 'libraries/metadata', type: AppType.Dashboard }, { path: 'libraries/nfo', type: AppType.Dashboard }, + { path: 'livetv', type: AppType.Dashboard }, { path: 'livetv/recordings', type: AppType.Dashboard }, { path: 'logs', type: AppType.Dashboard }, { path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index 31b96d9347..a7c9726828 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -16,13 +16,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'livetvguideprovider', view: 'livetvguideprovider.html' } - }, { - path: 'livetv', - pageProps: { - appType: AppType.Dashboard, - controller: 'livetvstatus', - view: 'livetvstatus.html' - } }, { path: 'livetv/tuner', pageProps: { diff --git a/src/apps/dashboard/routes/livetv/index.tsx b/src/apps/dashboard/routes/livetv/index.tsx new file mode 100644 index 0000000000..d061b10328 --- /dev/null +++ b/src/apps/dashboard/routes/livetv/index.tsx @@ -0,0 +1,166 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import Box from '@mui/material/Box'; +import Page from 'components/Page'; +import { useNamedConfiguration } from 'hooks/useNamedConfiguration'; +import type { LiveTvOptions } from '@jellyfin/sdk/lib/generated-client/models/live-tv-options'; +import globalize from 'lib/globalize'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Loading from 'components/loading/LoadingComponent'; +import TunerDeviceCard from 'apps/dashboard/features/livetv/components/TunerDeviceCard'; +import useLiveTasks from 'apps/dashboard/features/tasks/hooks/useLiveTasks'; +import Button from '@mui/material/Button'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import AddIcon from '@mui/icons-material/Add'; +import { Form, Link, useNavigate } from 'react-router-dom'; +import { useStartTask } from 'apps/dashboard/features/tasks/api/useStartTask'; +import { TaskState } from '@jellyfin/sdk/lib/generated-client/models/task-state'; +import TaskProgress from 'apps/dashboard/features/tasks/components/TaskProgress'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import Alert from '@mui/material/Alert'; +import List from '@mui/material/List'; +import Provider from 'apps/dashboard/features/livetv/components/Provider'; + +const CONFIG_KEY = 'livetv'; + +export const Component = () => { + const navigate = useNavigate(); + const { + data: config, + isPending: isConfigPending, + isError: isConfigError + } = useNamedConfiguration(CONFIG_KEY); + const { + data: tasks, + isPending: isTasksPending, + isError: isTasksError + } = useLiveTasks({ isHidden: false }); + const providerButtonRef = useRef(null); + const [ anchorEl, setAnchorEl ] = useState(null); + const [ isMenuOpen, setIsMenuOpen ] = useState(false); + const startTask = useStartTask(); + + const navigateToSchedulesDirect = useCallback(() => { + navigate('/dashboard/livetv/guide?type=schedulesdirect'); + }, [ navigate ]); + + const navigateToXMLTV = useCallback(() => { + navigate('/dashboard/livetv/guide?type=xmltv'); + }, [ navigate ]); + + const showProviderMenu = useCallback(() => { + setAnchorEl(providerButtonRef.current); + setIsMenuOpen(true); + }, []); + + const onMenuClose = useCallback(() => { + setAnchorEl(null); + setIsMenuOpen(false); + }, []); + + const refreshGuideTask = useMemo(() => ( + tasks?.find((value) => value.Key === 'RefreshGuide') + ), [ tasks ]); + + const refreshGuideData = useCallback(() => { + if (refreshGuideTask?.Id) { + startTask.mutate({ + taskId: refreshGuideTask.Id + }); + } + }, [ startTask, refreshGuideTask ]); + + if (isConfigPending || isTasksPending) return ; + + return ( + + +
+ {(isConfigError || isTasksError) ? ( + {globalize.translate('HeaderError')} + ) : ( + + {globalize.translate('HeaderTunerDevices')} + + + + + { config.TunerHosts?.map(tunerHost => ( + + )) } + + + {globalize.translate('HeaderGuideProviders')} + + + + + + + {(refreshGuideTask && refreshGuideTask.State === TaskState.Running) && ( + + )} + + + Schedules Direct + + + XMLTV + + + + {(config.ListingProviders && config.ListingProviders?.length > 0) && ( + + {config.ListingProviders?.map(provider => ( + + ))} + + )} + + )} +
+
+
+ ); +}; + +Component.displayName = 'LiveTvPage'; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index fbebca80fe..28fbf0b55f 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -92,8 +92,10 @@ "ButtonActivate": "Activate", "ButtonAddImage": "Add Image", "ButtonAddMediaLibrary": "Add Media Library", + "ButtonAddProvider": "Add Provider", "ButtonAddScheduledTaskTrigger": "Add Trigger", "ButtonAddServer": "Add Server", + "ButtonAddTunerDevice": "Add Tuner Device", "ButtonAddUser": "Add User", "ButtonArrowLeft": "Left", "ButtonArrowRight": "Right",