diff --git a/web/src/hooks/useTableSettings.ts b/web/src/hooks/useTableSettings.ts index f50e0796..cf6fa2aa 100644 --- a/web/src/hooks/useTableSettings.ts +++ b/web/src/hooks/useTableSettings.ts @@ -69,6 +69,7 @@ export const useTableSettings = ( if (isFunction(updater)) { sortState[1]((prev) => { const next = updater(prev); + console.log(next); setTableSortState(tableName, next); return next; }); @@ -111,9 +112,12 @@ export const useStoreBackedTableSettings = ( state: { columnVisibility: tableState.colVisibilityState.current, pagination: tableState.paginationState.current, + sorting: tableState.sortState.current, }, initialState: { pagination: tableState.paginationState.current, + columnVisibility: tableState.colVisibilityState.current, + sorting: tableState.sortState.current, }, onColumnVisibilityChange: (updater) => { tableState.colVisibilityState.setter(updater); diff --git a/web/src/pages/settings/MediaSourceSettingsPage.tsx b/web/src/pages/settings/MediaSourceSettingsPage.tsx index dc1b7ee5..7f40b0fc 100644 --- a/web/src/pages/settings/MediaSourceSettingsPage.tsx +++ b/web/src/pages/settings/MediaSourceSettingsPage.tsx @@ -4,7 +4,6 @@ import { useMediaSources } from '@/hooks/settingsHooks.ts'; import { Delete, Edit, Refresh, VideoLibrary } from '@mui/icons-material'; import { Box, - Button, Divider, IconButton, Link, @@ -12,22 +11,15 @@ import { Tooltip, Typography, } from '@mui/material'; -import { - useMutation, - useQueryClient, - useSuspenseQuery, -} from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { MediaSourceSettings } from '@tunarr/types'; -import type { GlobalMediaSourceSettings } from '@tunarr/types/schemas'; import { capitalize } from 'lodash-es'; import type { MRT_ColumnDef } from 'material-react-table'; import { MaterialReactTable, useMaterialReactTable, } from 'material-react-table'; -import { useSnackbar } from 'notistack'; -import { useCallback, useMemo, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useMemo, useState } from 'react'; import { DeleteConfirmationDialog } from '../../components/DeleteConfirmationDialog.tsx'; import { EditMediaSourceLibrariesDialog } from '../../components/settings/media_source/EditMediaSourceLibrariesDialog.tsx'; import { EmbyServerEditDialog } from '../../components/settings/media_source/EmbyServerEditDialog.tsx'; @@ -35,12 +27,9 @@ import { JellyfinServerEditDialog } from '../../components/settings/media_source import { LocalMediaEditDialog } from '../../components/settings/media_source/LocalMediaEditDialog.tsx'; import { MediaSourceHealthyTableCell } from '../../components/settings/media_source/MediaSourceHealthyTableCell.tsx'; import { PlexServerEditDialog } from '../../components/settings/media_source/PlexServerEditDialog.tsx'; -import { NumericFormControllerText } from '../../components/util/TypedController.tsx'; import { deleteApiMediaSourcesByIdMutation, - getApiSettingsMediaSourceOptions, postApiMediaSourcesByIdLibrariesRefreshMutation, - putApiSettingsMediaSourceMutation, } from '../../generated/@tanstack/react-query.gen.ts'; import { invalidateTaggedQueries } from '../../helpers/queryUtil.ts'; import { useStoreBackedTableSettings } from '../../hooks/useTableSettings.ts'; @@ -48,9 +37,6 @@ import type { Nullable } from '../../types/util.ts'; export default function MediaSourceSettingsPage() { const { data: servers } = useMediaSources(); - const { data: mediaSourceSettings } = useSuspenseQuery( - getApiSettingsMediaSourceOptions(), - ); const tableState = useStoreBackedTableSettings('MediaSourceSettings'); const [editingMediaSource, setEditingMediaSource] = @@ -191,48 +177,6 @@ export default function MediaSourceSettingsPage() { positionActionsColumn: 'last', }); - const snackbar = useSnackbar(); - - const settingsForm = useForm({ - defaultValues: mediaSourceSettings, - }); - - const updateMediaSourceSettingsMut = useMutation({ - ...putApiSettingsMediaSourceMutation(), - onSuccess: (returned) => { - settingsForm.reset(returned); - snackbar.enqueueSnackbar({ - variant: 'success', - message: 'Successfully updated Media Source settings.', - }); - }, - onError: (err) => { - console.error(err); - snackbar.enqueueSnackbar({ - variant: 'error', - message: - 'Failed to update Media Source settings. Please check server and browser logs for details.', - }); - }, - }); - - const onSubmit = useCallback( - (data: GlobalMediaSourceSettings) => { - updateMediaSourceSettingsMut.mutate({ - body: data, - }); - }, - [updateMediaSourceSettingsMut], - ); - - const onError = useCallback(() => { - snackbar.enqueueSnackbar({ - variant: 'error', - message: - 'There was an error submitting the request to update Media Source settings. Please check the form and try again', - }); - }, [snackbar]); - return ( <> } gap={2}> @@ -266,33 +210,6 @@ export default function MediaSourceSettingsPage() { - - - Scanner Settings - - - - - - {editingMediaSource?.type === 'plex' && ( { + const { data: mediaSourceSettings } = useSuspenseQuery( + getApiSettingsMediaSourceOptions(), + ); + + const settingsForm = useForm({ + defaultValues: mediaSourceSettings, + }); + + const snackbar = useSnackbar(); + + const updateMediaSourceSettingsMut = useMutation({ + ...putApiSettingsMediaSourceMutation(), + onSuccess: (returned) => { + settingsForm.reset(returned); + snackbar.enqueueSnackbar({ + variant: 'success', + message: 'Successfully updated Media Source settings.', + }); + }, + onError: (err) => { + console.error(err); + snackbar.enqueueSnackbar({ + variant: 'error', + message: + 'Failed to update Media Source settings. Please check server and browser logs for details.', + }); + }, + }); + + const onSubmit = useCallback( + (data: GlobalMediaSourceSettings) => { + updateMediaSourceSettingsMut.mutate({ + body: data, + }); + }, + [updateMediaSourceSettingsMut], + ); + + const onError = useCallback(() => { + snackbar.enqueueSnackbar({ + variant: 'error', + message: + 'There was an error submitting the request to update Media Source settings. Please check the form and try again', + }); + }, [snackbar]); + + return ( + + + + + + + + + + + ); +}; diff --git a/web/src/pages/settings/SettingsLayout.tsx b/web/src/pages/settings/SettingsLayout.tsx index a62b3450..ee703cec 100644 --- a/web/src/pages/settings/SettingsLayout.tsx +++ b/web/src/pages/settings/SettingsLayout.tsx @@ -37,9 +37,9 @@ export function SettingsLayout({ currentTab = '/general' }: Props) { to="/settings/ffmpeg" /> diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 4c09d540..d6fcd8e5 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -24,6 +24,7 @@ import { Route as SystemLogsRouteImport } from './routes/system/logs'; import { Route as SystemDebugRouteImport } from './routes/system/debug'; import { Route as SettingsXmltvRouteImport } from './routes/settings/xmltv'; import { Route as SettingsSourcesRouteImport } from './routes/settings/sources'; +import { Route as SettingsScannerRouteImport } from './routes/settings/scanner'; import { Route as SettingsHdhrRouteImport } from './routes/settings/hdhr'; import { Route as SettingsGeneralRouteImport } from './routes/settings/general'; import { Route as SettingsFfmpegRouteImport } from './routes/settings/ffmpeg'; @@ -135,6 +136,11 @@ const SettingsSourcesRoute = SettingsSourcesRouteImport.update({ path: '/sources', getParentRoute: () => SettingsRoute, } as any); +const SettingsScannerRoute = SettingsScannerRouteImport.update({ + id: '/scanner', + path: '/scanner', + getParentRoute: () => SettingsRoute, +} as any); const SettingsHdhrRoute = SettingsHdhrRouteImport.update({ id: '/hdhr', path: '/hdhr', @@ -346,6 +352,7 @@ export interface FileRoutesByFullPath { '/settings/ffmpeg': typeof SettingsFfmpegRoute; '/settings/general': typeof SettingsGeneralRoute; '/settings/hdhr': typeof SettingsHdhrRoute; + '/settings/scanner': typeof SettingsScannerRoute; '/settings/sources': typeof SettingsSourcesRoute; '/settings/xmltv': typeof SettingsXmltvRoute; '/system/debug': typeof SystemDebugRoute; @@ -396,6 +403,7 @@ export interface FileRoutesByTo { '/settings/ffmpeg': typeof SettingsFfmpegRoute; '/settings/general': typeof SettingsGeneralRoute; '/settings/hdhr': typeof SettingsHdhrRoute; + '/settings/scanner': typeof SettingsScannerRoute; '/settings/sources': typeof SettingsSourcesRoute; '/settings/xmltv': typeof SettingsXmltvRoute; '/system/debug': typeof SystemDebugRoute; @@ -447,6 +455,7 @@ export interface FileRoutesById { '/settings/ffmpeg': typeof SettingsFfmpegRoute; '/settings/general': typeof SettingsGeneralRoute; '/settings/hdhr': typeof SettingsHdhrRoute; + '/settings/scanner': typeof SettingsScannerRoute; '/settings/sources': typeof SettingsSourcesRoute; '/settings/xmltv': typeof SettingsXmltvRoute; '/system/debug': typeof SystemDebugRoute; @@ -501,6 +510,7 @@ export interface FileRouteTypes { | '/settings/ffmpeg' | '/settings/general' | '/settings/hdhr' + | '/settings/scanner' | '/settings/sources' | '/settings/xmltv' | '/system/debug' @@ -551,6 +561,7 @@ export interface FileRouteTypes { | '/settings/ffmpeg' | '/settings/general' | '/settings/hdhr' + | '/settings/scanner' | '/settings/sources' | '/settings/xmltv' | '/system/debug' @@ -601,6 +612,7 @@ export interface FileRouteTypes { | '/settings/ffmpeg' | '/settings/general' | '/settings/hdhr' + | '/settings/scanner' | '/settings/sources' | '/settings/xmltv' | '/system/debug' @@ -773,6 +785,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsSourcesRouteImport; parentRoute: typeof SettingsRoute; }; + '/settings/scanner': { + id: '/settings/scanner'; + path: '/scanner'; + fullPath: '/settings/scanner'; + preLoaderRoute: typeof SettingsScannerRouteImport; + parentRoute: typeof SettingsRoute; + }; '/settings/hdhr': { id: '/settings/hdhr'; path: '/hdhr'; @@ -1025,6 +1044,7 @@ interface SettingsRouteChildren { SettingsFfmpegRoute: typeof SettingsFfmpegRoute; SettingsGeneralRoute: typeof SettingsGeneralRoute; SettingsHdhrRoute: typeof SettingsHdhrRoute; + SettingsScannerRoute: typeof SettingsScannerRoute; SettingsSourcesRoute: typeof SettingsSourcesRoute; SettingsXmltvRoute: typeof SettingsXmltvRoute; SettingsFfmpegConfigIdRoute: typeof SettingsFfmpegConfigIdRoute; @@ -1035,6 +1055,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsFfmpegRoute: SettingsFfmpegRoute, SettingsGeneralRoute: SettingsGeneralRoute, SettingsHdhrRoute: SettingsHdhrRoute, + SettingsScannerRoute: SettingsScannerRoute, SettingsSourcesRoute: SettingsSourcesRoute, SettingsXmltvRoute: SettingsXmltvRoute, SettingsFfmpegConfigIdRoute: SettingsFfmpegConfigIdRoute, diff --git a/web/src/routes/settings/scanner.tsx b/web/src/routes/settings/scanner.tsx new file mode 100644 index 00000000..5e542545 --- /dev/null +++ b/web/src/routes/settings/scanner.tsx @@ -0,0 +1,16 @@ +import { + getApiMediaSourcesOptions, + getApiSettingsMediaSourceOptions, +} from '@/generated/@tanstack/react-query.gen.ts'; +import { ScannerSettingsPage } from '@/pages/settings/ScannerSettingsPage.tsx'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/settings/scanner')({ + loader: ({ context }) => { + return Promise.all([ + context.queryClient.ensureQueryData(getApiMediaSourcesOptions()), + context.queryClient.ensureQueryData(getApiSettingsMediaSourceOptions()), + ]); + }, + component: ScannerSettingsPage, +}); diff --git a/web/src/store/index.ts b/web/src/store/index.ts index df27f9bb..aaea151e 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -1,3 +1,4 @@ +import type { Maybe } from '@/types/util.ts'; import { get, isNil, isObject, isUndefined, merge } from 'lodash-es'; import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; @@ -16,9 +17,11 @@ import { type ProgrammingListingsState, createProgrammingListingsState, } from './programmingSelector/store.ts'; +import type { SettingsStateInternal } from './settings/store.ts'; import { type PersistedSettingsState, type SettingsState, + SettingsStateInternalSchema, createSettingsSlice, } from './settings/store.ts'; import type { ThemeEditorStateInner } from './themeEditor/store.ts'; @@ -75,20 +78,22 @@ const useStore = create()( 'settings', ) as unknown; - // let parsedSettings: Maybe; - // if (persistedSettings) { - // const result = - // SettingsStateInternalSchema.safeParse(persistedSettings); - // if (result.error) { - // console.error( - // 'Could not hydrate persisted settings', - // result.error, - // ); - // } else { - // parsedSettings = result.data; - // } - // } - // console.log(parsedSettings); + let parsedSettings: Maybe; + if (persistedSettings) { + const result = SettingsStateInternalSchema.safeParse( + persistedSettings, + { reportInput: true }, + ); + if (result.error) { + console.error( + 'Could not hydrate persisted settings', + result.error, + ); + } else { + parsedSettings = result.data; + // TODO: provide way to convert to the next version + } + } // Migrate to new setting. if ( @@ -113,7 +118,7 @@ const useStore = create()( settings: merge( {}, currentState.settings ?? {}, - isObject(persistedSettings) ? persistedSettings : {}, + parsedSettings ?? {}, ), }; }, diff --git a/web/src/store/settings/store.ts b/web/src/store/settings/store.ts index df5a4462..6d2d9da9 100644 --- a/web/src/store/settings/store.ts +++ b/web/src/store/settings/store.ts @@ -1,4 +1,3 @@ -import type { PaginationState } from '@tanstack/react-table'; import type { TupleToUnion } from '@tunarr/types'; import type { DeepPartial } from 'ts-essentials'; import { z } from 'zod'; @@ -8,27 +7,24 @@ import type { StateCreator } from 'zustand'; export const SupportedLocales = ['en', 'en-gb'] as const; export type SupportedLocales = TupleToUnion; -export interface TableSettings { - pagination: PaginationState; - columnModel: Record; -} - -export const CurrentSettingsSchemaVersion = 1; +const CurrentSettingsSchemaVersion = 1; const PaginationStateSchema = z.object({ pageIndex: z.int(), pageSize: z.int(), }); -export const TableSettingsSchema = z.object({ - pagination: PaginationStateSchema, - columnModel: z.record(z.string(), z.boolean()), - sortState: z.array( - z.object({ - desc: z.boolean(), - id: z.string(), - }), - ), +const TableSettingsSchema = z.object({ + pagination: PaginationStateSchema.optional(), + columnModel: z.record(z.string(), z.boolean()).optional(), + sortState: z + .array( + z.object({ + desc: z.boolean(), + id: z.string(), + }), + ) + .optional(), }); export const SettingsStateInternalSchema = z.object({