From 19387072296db68fb03d1bb973e6e53629fa7989 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:06:11 +0300 Subject: [PATCH] Migrate DVR page to React --- .../drawer/sections/LiveTvDrawerSection.tsx | 2 +- .../dashboard/controllers/livetvsettings.html | 120 ----- .../dashboard/controllers/livetvsettings.js | 133 ------ src/apps/dashboard/routes/_asyncRoutes.ts | 1 + src/apps/dashboard/routes/_legacyRoutes.ts | 7 - .../dashboard/routes/livetv/recordings.tsx | 414 ++++++++++++++++++ src/strings/en-us.json | 1 + 7 files changed, 417 insertions(+), 261 deletions(-) delete mode 100644 src/apps/dashboard/controllers/livetvsettings.html delete mode 100644 src/apps/dashboard/controllers/livetvsettings.js create mode 100644 src/apps/dashboard/routes/livetv/recordings.tsx diff --git a/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx index bee9cc7b96..bde1c91b51 100644 --- a/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx +++ b/src/apps/dashboard/components/drawer/sections/LiveTvDrawerSection.tsx @@ -29,7 +29,7 @@ const LiveTvDrawerSection = () => { - + diff --git a/src/apps/dashboard/controllers/livetvsettings.html b/src/apps/dashboard/controllers/livetvsettings.html deleted file mode 100644 index 78e70fa8f4..0000000000 --- a/src/apps/dashboard/controllers/livetvsettings.html +++ /dev/null @@ -1,120 +0,0 @@ -
-
-
-
-
-

${HeaderDVR}

-
-
- -
-
- -
${LabelNumberOfGuideDaysHelp}
-
-
-
-
- -
- -
-
${LabelRecordingPathHelp}
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-

${HeaderDefaultRecordingSettings}

-
-
-
- -
-
- ${MinutesBefore} -
-
-
-
-
-
- -
-
- ${MinutesAfter} -
-
-
-
-
-

${HeaderRecordingPostProcessing}

-
-
-
- -
- -
-
-
- -
${LabelPostProcessorArgumentsHelp}
-
-
-
-

${HeaderRecordingMetadataSaving}

- -
- -
${SaveRecordingNFOHelp}
-
- -
- -
${SaveRecordingImagesHelp}
-
-
-
-
- -
-
-
-
-
diff --git a/src/apps/dashboard/controllers/livetvsettings.js b/src/apps/dashboard/controllers/livetvsettings.js deleted file mode 100644 index 0fd30ee5bb..0000000000 --- a/src/apps/dashboard/controllers/livetvsettings.js +++ /dev/null @@ -1,133 +0,0 @@ -import 'jquery'; - -import loading from 'components/loading/loading'; -import globalize from 'lib/globalize'; -import 'elements/emby-button/emby-button'; -import Dashboard from 'utils/dashboard'; -import alert from 'components/alert'; - -function loadPage(page, config) { - page.querySelector('.liveTvSettingsForm').classList.remove('hide'); - page.querySelector('.noLiveTvServices')?.classList.add('hide'); - page.querySelector('#selectGuideDays').value = config.GuideDays || ''; - page.querySelector('#txtPrePaddingMinutes').value = config.PrePaddingSeconds / 60; - page.querySelector('#txtPostPaddingMinutes').value = config.PostPaddingSeconds / 60; - page.querySelector('#txtRecordingPath').value = config.RecordingPath || ''; - page.querySelector('#txtMovieRecordingPath').value = config.MovieRecordingPath || ''; - page.querySelector('#txtSeriesRecordingPath').value = config.SeriesRecordingPath || ''; - page.querySelector('#txtPostProcessor').value = config.RecordingPostProcessor || ''; - page.querySelector('#txtPostProcessorArguments').value = config.RecordingPostProcessorArguments || ''; - page.querySelector('#chkSaveRecordingNFO').checked = config.SaveRecordingNFO; - page.querySelector('#chkSaveRecordingImages').checked = config.SaveRecordingImages; - loading.hide(); -} - -function onSubmit() { - loading.show(); - const form = this; - ApiClient.getNamedConfiguration('livetv').then(function (config) { - config.GuideDays = form.querySelector('#selectGuideDays').value || null; - const recordingPath = form.querySelector('#txtRecordingPath').value || null; - const movieRecordingPath = form.querySelector('#txtMovieRecordingPath').value || null; - const seriesRecordingPath = form.querySelector('#txtSeriesRecordingPath').value || null; - const recordingPathChanged = recordingPath != config.RecordingPath || movieRecordingPath != config.MovieRecordingPath || seriesRecordingPath != config.SeriesRecordingPath; - config.RecordingPath = recordingPath; - config.MovieRecordingPath = movieRecordingPath; - config.SeriesRecordingPath = seriesRecordingPath; - config.RecordingEncodingFormat = 'mkv'; - config.PrePaddingSeconds = 60 * form.querySelector('#txtPrePaddingMinutes').value; - config.PostPaddingSeconds = 60 * form.querySelector('#txtPostPaddingMinutes').value; - config.RecordingPostProcessor = form.querySelector('#txtPostProcessor').value; - config.RecordingPostProcessorArguments = form.querySelector('#txtPostProcessorArguments').value; - config.SaveRecordingNFO = form.querySelector('#chkSaveRecordingNFO').checked; - config.SaveRecordingImages = form.querySelector('#chkSaveRecordingImages').checked; - ApiClient.updateNamedConfiguration('livetv', config).then(function () { - Dashboard.processServerConfigurationUpdateResult(); - showSaveMessage(recordingPathChanged); - }); - }); - return false; -} - -function showSaveMessage(recordingPathChanged) { - let msg = ''; - - if (recordingPathChanged) { - msg += globalize.translate('MessageChangeRecordingPath'); - } - - if (msg) { - alert(msg); - } -} - -$(document).on('pageinit', '#liveTvSettingsPage', function () { - const page = this; - $('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit); - $('#btnSelectRecordingPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - callback: function (path) { - if (path) { - page.querySelector('#txtRecordingPath').value = path; - } - - picker.close(); - }, - validateWriteable: true - }); - }); - }); - $('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - callback: function (path) { - if (path) { - page.querySelector('#txtMovieRecordingPath').value = path; - } - - picker.close(); - }, - validateWriteable: true - }); - }); - }); - $('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - callback: function (path) { - if (path) { - page.querySelector('#txtSeriesRecordingPath').value = path; - } - - picker.close(); - }, - validateWriteable: true - }); - }); - }); - $('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () { - import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => { - const picker = new DirectoryBrowser(); - picker.show({ - includeFiles: true, - callback: function (path) { - if (path) { - page.querySelector('#txtPostProcessor').value = path; - } - - picker.close(); - } - }); - }); - }); -}).on('pageshow', '#liveTvSettingsPage', function () { - loading.show(); - const page = this; - ApiClient.getNamedConfiguration('livetv').then(function (config) { - loadPage(page, config); - }); -}); diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 268e900801..e1e7416068 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -11,6 +11,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/recordings', type: AppType.Dashboard }, { path: 'logs', type: AppType.Dashboard }, { path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard }, { path: 'playback/resume', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index 00ca6f057d..06bfb1a149 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -44,13 +44,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'livetvguideprovider', view: 'livetvguideprovider.html' } - }, { - path: 'recordings', - pageProps: { - appType: AppType.Dashboard, - controller: 'livetvsettings', - view: 'livetvsettings.html' - } }, { path: 'livetv', pageProps: { diff --git a/src/apps/dashboard/routes/livetv/recordings.tsx b/src/apps/dashboard/routes/livetv/recordings.tsx new file mode 100644 index 0000000000..3307c1c54b --- /dev/null +++ b/src/apps/dashboard/routes/livetv/recordings.tsx @@ -0,0 +1,414 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import Box from '@mui/material/Box'; +import MenuItem from '@mui/material/MenuItem'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Page from 'components/Page'; +import { QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration'; +import globalize from 'lib/globalize'; +import { ActionFunctionArgs, Form, useActionData, useNavigation, useSubmit } from 'react-router-dom'; +import type { LiveTvOptions } from '@jellyfin/sdk/lib/generated-client/models/live-tv-options'; +import Loading from 'components/loading/LoadingComponent'; +import Alert from '@mui/material/Alert'; +import InputAdornment from '@mui/material/InputAdornment'; +import IconButton from '@mui/material/IconButton'; +import SearchIcon from '@mui/icons-material/Search'; +import DirectoryBrowser from 'components/directorybrowser/directorybrowser'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import FormHelperText from '@mui/material/FormHelperText'; +import Button from '@mui/material/Button'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import { queryClient } from 'utils/query/queryClient'; +import { ActionData } from 'types/actionData'; + +const CONFIG_KEY = 'livetv'; + +export const action = async ({ request }: ActionFunctionArgs) => { + const api = ServerConnections.getCurrentApi(); + if (!api) throw new Error('No Api instance available'); + + const data = await request.json() as LiveTvOptions; + + await getConfigurationApi(api) + .updateNamedConfiguration({ key: CONFIG_KEY, body: data }); + + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY, CONFIG_KEY ] + }); + + return { + isSaved: true + }; +}; + +export const Component = () => { + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const { data: initialConfig, isPending, isError } = useNamedConfiguration(CONFIG_KEY); + const [ config, setConfig ] = useState(null); + const [ prePaddingMinutes, setPrePaddingMinutes ] = useState(''); + const [ postPaddingMinutes, setPostPaddingMinutes ] = useState(''); + const isSubmitting = navigation.state === 'submitting'; + const submit = useSubmit(); + + useEffect(() => { + if (initialConfig && config == null) { + setConfig(initialConfig); + if (initialConfig.PrePaddingSeconds) { + setPrePaddingMinutes((initialConfig.PrePaddingSeconds / 60).toString()); + } + if (initialConfig.PostPaddingSeconds) { + setPostPaddingMinutes((initialConfig.PostPaddingSeconds / 60).toString()); + } + } + }, [ initialConfig, config ]); + + const onPrePaddingMinutesChange = useCallback((e: React.ChangeEvent) => { + setPrePaddingMinutes(e.target.value); + setConfig({ + ...config, + PrePaddingSeconds: parseInt(e.target.value, 10) * 60 + }); + }, [ config ]); + + const onPostPaddingMinutesChange = useCallback((e: React.ChangeEvent) => { + setPostPaddingMinutes(e.target.value); + setConfig({ + ...config, + PostPaddingSeconds: parseInt(e.target.value, 10) * 60 + }); + }, [ config ]); + + const onChange = useCallback((e: React.ChangeEvent) => { + setConfig({ + ...config, + [e.target.name]: e.target.value + }); + }, [ config ]); + + const onCheckboxChange = useCallback((e: React.ChangeEvent) => { + setConfig({ + ...config, + [e.target.name]: e.target.checked + }); + }, [ config ]); + + const showRecordingPathPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + if (path) { + setConfig({ + ...config, + RecordingPath: path + }); + } + + picker.close(); + }, + validateWriteable: true + }); + }, [ config ]); + + const showMovieRecordingPathPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + if (path) { + setConfig({ + ...config, + MovieRecordingPath: path + }); + } + + picker.close(); + }, + validateWriteable: true + }); + }, [ config ]); + + const showSeriesRecordingPathPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + if (path) { + setConfig({ + ...config, + SeriesRecordingPath: path + }); + } + + picker.close(); + }, + validateWriteable: true + }); + }, [ config ]); + + const showPostProcessorPicker = useCallback(() => { + const picker = new DirectoryBrowser(); + + picker.show({ + callback: (path: string) => { + if (path) { + setConfig({ + ...config, + RecordingPostProcessor: path + }); + } + + picker.close(); + }, + validateWriteable: true + }); + }, [ config ]); + + const onSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + if (config) { + submit( + JSON.stringify(config), + { method: 'post', encType: 'application/json' } + ); + } + }, [ config, submit ]); + + if (isPending || !config) { + return ; + } + + return ( + + + {isError ? ( + {globalize.translate('LiveTVPageLoadError')} + ) : ( +
+ + {globalize.translate('HeaderDVR')} + + {(!isSubmitting && actionData?.isSaved) && ( + + {globalize.translate('SettingsSaved')} + + )} + + + {globalize.translate('Auto')} + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + + + + + + ) + }, + + inputLabel: { + shrink: !!config.RecordingPath + } + }} + /> + + + + + + + ) + }, + + inputLabel: { + shrink: !!config.MovieRecordingPath + } + }} + /> + + + + + + + ) + }, + + inputLabel: { + shrink: !!config.SeriesRecordingPath + } + }} + /> + + {globalize.translate('HeaderDefaultRecordingSettings')} + + + {globalize.translate('MinutesBefore')} + + ) + } + }} + /> + + + {globalize.translate('MinutesAfter')} + + ) + } + }} + /> + + {globalize.translate('HeaderRecordingPostProcessing')} + + + + + + + ) + }, + + inputLabel: { + shrink: !!config.RecordingPostProcessor + } + }} + /> + + + + {globalize.translate('HeaderRecordingMetadataSaving')} + + + + } + label={globalize.translate('SaveRecordingNFO')} + /> + {globalize.translate('SaveRecordingNFOHelp')} + + + + + } + label={globalize.translate('SaveRecordingImages')} + /> + {globalize.translate('SaveRecordingImagesHelp')} + + + + +
+ )} +
+
+ ); +}; + +Component.displayName = 'LiveTvRecordingsPage'; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 6909988d75..6f8a7339c9 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1052,6 +1052,7 @@ "Live": "Live", "LiveBroadcasts": "Live broadcasts", "LiveTV": "Live TV", + "LiveTVPageLoadError": "Failed to load recordings page", "Localization": "Localization", "LogLevel.Trace": "Trace", "LogLevel.Debug": "Debug",