mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 00:53:35 -04:00
fix: split media source page from scanner settings
remove duplication of media source table
This commit is contained in:
@@ -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 = <Data extends MRT_RowData>(
|
||||
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);
|
||||
|
||||
@@ -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<GlobalMediaSourceSettings>({
|
||||
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 (
|
||||
<>
|
||||
<Stack divider={<Divider />} gap={2}>
|
||||
@@ -266,33 +210,6 @@ export default function MediaSourceSettingsPage() {
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', mb: 1 }}></Box>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={settingsForm.handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Typography variant="h5">Scanner Settings</Typography>
|
||||
<NumericFormControllerText
|
||||
control={settingsForm.control}
|
||||
name="rescanIntervalHours"
|
||||
prettyFieldName="Rescan Interval (hours)"
|
||||
TextFieldProps={{
|
||||
label: 'Rescan Interval (hours)',
|
||||
helperText:
|
||||
'How frequently libraries should be scanned (starting from midnight).',
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={!settingsForm.formState.isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
{editingMediaSource?.type === 'plex' && (
|
||||
<PlexServerEditDialog
|
||||
|
||||
90
web/src/pages/settings/ScannerSettingsPage.tsx
Normal file
90
web/src/pages/settings/ScannerSettingsPage.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NumericFormControllerText } from '@/components/util/TypedController.tsx';
|
||||
import {
|
||||
getApiSettingsMediaSourceOptions,
|
||||
putApiSettingsMediaSourceMutation,
|
||||
} from '@/generated/@tanstack/react-query.gen.ts';
|
||||
import { Box, Button, Stack } from '@mui/material';
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import type { GlobalMediaSourceSettings } from '@tunarr/types/schemas';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export const ScannerSettingsPage = () => {
|
||||
const { data: mediaSourceSettings } = useSuspenseQuery(
|
||||
getApiSettingsMediaSourceOptions(),
|
||||
);
|
||||
|
||||
const settingsForm = useForm<GlobalMediaSourceSettings>({
|
||||
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 (
|
||||
<Stack>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={settingsForm.handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<NumericFormControllerText
|
||||
control={settingsForm.control}
|
||||
name="rescanIntervalHours"
|
||||
prettyFieldName="Rescan Interval (hours)"
|
||||
TextFieldProps={{
|
||||
label: 'Rescan Interval (hours)',
|
||||
helperText:
|
||||
'How frequently libraries should be scanned (starting from midnight).',
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={!settingsForm.formState.isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -37,9 +37,9 @@ export function SettingsLayout({ currentTab = '/general' }: Props) {
|
||||
to="/settings/ffmpeg"
|
||||
/>
|
||||
<RouterTabLink
|
||||
label="Sources"
|
||||
value="/sources"
|
||||
to="/settings/sources"
|
||||
label="Scanner"
|
||||
value="/scanner"
|
||||
to="/settings/scanner"
|
||||
/>
|
||||
<RouterTabLink label="HDHR" value="/hdhr" to="/settings/hdhr" />
|
||||
</Tabs>
|
||||
|
||||
@@ -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,
|
||||
|
||||
16
web/src/routes/settings/scanner.tsx
Normal file
16
web/src/routes/settings/scanner.tsx
Normal file
@@ -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,
|
||||
});
|
||||
@@ -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<State>()(
|
||||
'settings',
|
||||
) as unknown;
|
||||
|
||||
// let parsedSettings: Maybe<SettingsStateInternal>;
|
||||
// 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<SettingsStateInternal>;
|
||||
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<State>()(
|
||||
settings: merge(
|
||||
{},
|
||||
currentState.settings ?? {},
|
||||
isObject(persistedSettings) ? persistedSettings : {},
|
||||
parsedSettings ?? {},
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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<typeof SupportedLocales>;
|
||||
|
||||
export interface TableSettings {
|
||||
pagination: PaginationState;
|
||||
columnModel: Record<string, boolean>;
|
||||
}
|
||||
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user