mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-01-15 16:33:35 -03:00
Merge pull request #7082 from viown/react-livetv
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer sectionTitleContainer-cards">
|
||||
<h2 class="sectionTitle sectionTitle-cards">
|
||||
<span>${HeaderTunerDevices}</span>
|
||||
</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddDevice submit sectionTitleButton" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="devicesList itemsContainer vertical-wrap" data-hovermenu="false" data-multiselect="false" style="margin-top: .5em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="readOnlyContent">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer">
|
||||
<h2 class="sectionTitle">${HeaderGuideProviders}</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddProvider submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="providerList">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="button" class="raised btnRefresh block button-cancel">
|
||||
<span>${ButtonRefreshGuideData}</span>
|
||||
</button>
|
||||
<progress max="100" min="0" style="width: 100%;" class="refreshGuideProgress"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 += '<div type="button" class="' + cssClass + '" data-id="' + device.Id + '">';
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="' + padderClass + '"></div>';
|
||||
html += '<div class="cardContent searchImage">';
|
||||
html += `<div class="cardImageContainer coveredImage ${getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr" aria-hidden="true"></span></div>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions autoSize" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '<div class="cardText">' + (device.FriendlyName || getTunerName(device.Type)) + '</div>';
|
||||
html += '<div class="cardText cardText-secondary">';
|
||||
html += device.Url || ' ';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
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 += '<div class="paperList">';
|
||||
|
||||
for (let i = 0, length = providers.length; i < length; i++) {
|
||||
const provider = providers[i];
|
||||
html += '<div class="listItem">';
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true"></span>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += '<a is="emby-linkbutton" style="display:block;padding:0;margin:0;text-align:left;" class="clearLink" href="' + getProviderConfigurationUrl(provider.Type) + '&id=' + provider.Id + '">';
|
||||
html += '<h3 class="listItemBodyText">';
|
||||
html += getProviderName(provider.Type);
|
||||
html += '</h3>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += provider.Path || provider.ListingsId || '';
|
||||
html += '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnOptions" data-id="' + provider.Id + '"><span class="material-icons listItemAside more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
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')
|
||||
});
|
||||
});
|
||||
22
src/apps/dashboard/features/livetv/api/useDeleteProvider.ts
Normal file
22
src/apps/dashboard/features/livetv/api/useDeleteProvider.ts
Normal file
@@ -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' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
22
src/apps/dashboard/features/livetv/api/useDeleteTuner.ts
Normal file
22
src/apps/dashboard/features/livetv/api/useDeleteTuner.ts
Normal file
@@ -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' ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
138
src/apps/dashboard/features/livetv/components/Provider.tsx
Normal file
138
src/apps/dashboard/features/livetv/components/Provider.tsx
Normal file
@@ -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<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLButtonElement | null>(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 (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
open={isDeleteProviderDialogOpen}
|
||||
title={globalize.translate('HeaderDeleteProvider')}
|
||||
text={globalize.translate('MessageConfirmDeleteGuideProvider')}
|
||||
onCancel={onDeleteProviderDialogCancel}
|
||||
onConfirm={onConfirmDelete}
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
confirmButtonColor='error'
|
||||
/>
|
||||
<ListItem
|
||||
disablePadding key={provider.Id}
|
||||
secondaryAction={
|
||||
<IconButton ref={actionsRef} onClick={showContextMenu}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemLink to={getProviderConfigurationUrl(provider.Type || '') + '&id=' + provider.Id}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<DvrIcon sx={{ color: '#fff' }} />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={getProviderName(provider.Type)}
|
||||
secondary={provider.Path || provider.ListingsId}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'h3'
|
||||
},
|
||||
secondary: {
|
||||
variant: 'body1'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={showChannelMapper}>
|
||||
<ListItemIcon>
|
||||
<LocationSearchingIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('MapChannels')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showDeleteDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Provider;
|
||||
@@ -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<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(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 (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
open={isConfirmDeleteDialogOpen}
|
||||
title={globalize.translate('HeaderDeleteDevice')}
|
||||
text={globalize.translate('MessageConfirmDeleteTunerDevice')}
|
||||
onCancel={onDeleteDialogClose}
|
||||
onConfirm={onDelete}
|
||||
confirmButtonColor='error'
|
||||
confirmButtonText={globalize.translate('Delete')}
|
||||
/>
|
||||
|
||||
<BaseCard
|
||||
title={tunerHost.FriendlyName || getTunerName(tunerHost.Type) || ''}
|
||||
text={tunerHost.Url || ''}
|
||||
icon={<DvrIcon sx={{ fontSize: 70 }} />}
|
||||
width={340}
|
||||
action={true}
|
||||
actionRef={actionRef}
|
||||
onActionClick={onActionClick}
|
||||
onClick={navigateToEditPage}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={navigateToEditPage}>
|
||||
<ListItemIcon>
|
||||
<EditIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={showDeleteDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{globalize.translate('Delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TunerDeviceCard;
|
||||
@@ -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;
|
||||
12
src/apps/dashboard/features/livetv/utils/getProviderName.ts
Normal file
12
src/apps/dashboard/features/livetv/utils/getProviderName.ts
Normal file
@@ -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;
|
||||
16
src/apps/dashboard/features/livetv/utils/getTunerName.ts
Normal file
16
src/apps/dashboard/features/livetv/utils/getTunerName.ts
Normal file
@@ -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;
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
166
src/apps/dashboard/routes/livetv/index.tsx
Normal file
166
src/apps/dashboard/routes/livetv/index.tsx
Normal file
@@ -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<LiveTvOptions>(CONFIG_KEY);
|
||||
const {
|
||||
data: tasks,
|
||||
isPending: isTasksPending,
|
||||
isError: isTasksError
|
||||
} = useLiveTasks({ isHidden: false });
|
||||
const providerButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [ anchorEl, setAnchorEl ] = useState<HTMLButtonElement | null>(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 <Loading />;
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='liveTvStatusPage'
|
||||
title={globalize.translate('LiveTV')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Form>
|
||||
{(isConfigError || isTasksError) ? (
|
||||
<Alert severity='error'>{globalize.translate('HeaderError')}</Alert>
|
||||
) : (
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h2'>{globalize.translate('HeaderTunerDevices')}</Typography>
|
||||
|
||||
<Button
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
startIcon={<AddIcon />}
|
||||
component={Link}
|
||||
to='/dashboard/livetv/tuner'
|
||||
>
|
||||
{globalize.translate('ButtonAddTunerDevice')}
|
||||
</Button>
|
||||
|
||||
<Stack direction='row' spacing={2}>
|
||||
{ config.TunerHosts?.map(tunerHost => (
|
||||
<TunerDeviceCard
|
||||
key={tunerHost.Id}
|
||||
tunerHost={tunerHost}
|
||||
/>
|
||||
)) }
|
||||
</Stack>
|
||||
|
||||
<Typography variant='h2'>{globalize.translate('HeaderGuideProviders')}</Typography>
|
||||
|
||||
<Stack direction='row' spacing={1.5}>
|
||||
<Button
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
startIcon={<AddIcon />}
|
||||
onClick={showProviderMenu}
|
||||
ref={providerButtonRef}
|
||||
>
|
||||
{globalize.translate('ButtonAddProvider')}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
startIcon={<RefreshIcon />}
|
||||
variant='outlined'
|
||||
onClick={refreshGuideData}
|
||||
loading={refreshGuideTask && refreshGuideTask.State === TaskState.Running}
|
||||
loadingPosition='start'
|
||||
>
|
||||
{globalize.translate('ButtonRefreshGuideData')}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{(refreshGuideTask && refreshGuideTask.State === TaskState.Running) && (
|
||||
<TaskProgress task={refreshGuideTask} />
|
||||
)}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={isMenuOpen}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem onClick={navigateToSchedulesDirect}>
|
||||
<ListItemText>Schedules Direct</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={navigateToXMLTV}>
|
||||
<ListItemText>XMLTV</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{(config.ListingProviders && config.ListingProviders?.length > 0) && (
|
||||
<List sx={{ backgroundColor: 'background.paper' }}>
|
||||
{config.ListingProviders?.map(provider => (
|
||||
<Provider
|
||||
key={provider.Id}
|
||||
provider={provider}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Form>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'LiveTvPage';
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user