mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-08-03 15:22:36 +00:00
core settings update capabilities
This commit is contained in:
parent
19cac3ccad
commit
c42f2d048a
7 changed files with 246 additions and 53 deletions
|
|
@ -28,27 +28,3 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
"""
|
||||
queryset = CoreSettings.objects.all()
|
||||
serializer_class = CoreSettingsSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
if CoreSettings.objects.exists():
|
||||
return Response(
|
||||
{"detail": "Core settings already exist. Use PUT to update."},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Always return the singleton instance (creating it if needed)
|
||||
settings_instance, created = CoreSettings.objects.get_or_create(pk=1)
|
||||
serializer = self.get_serializer(settings_instance)
|
||||
return Response([serializer.data]) # Return as a list for DRF router compatibility
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
# Retrieve the singleton instance
|
||||
settings_instance = get_object_or_404(CoreSettings, pk=1)
|
||||
serializer = self.get_serializer(settings_instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
import theme from './theme';
|
||||
import EPG from './pages/EPG';
|
||||
import Guide from './pages/Guide';
|
||||
import Settings from './pages/Settings';
|
||||
import StreamProfiles from './pages/StreamProfiles';
|
||||
import useAuthStore from './store/auth';
|
||||
import logo from './images/logo.png';
|
||||
|
|
@ -109,6 +110,7 @@ const App = () => {
|
|||
flexDirection: 'column',
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
// height: '100vh',
|
||||
backgroundColor: '#495057',
|
||||
}}
|
||||
>
|
||||
|
|
@ -126,11 +128,9 @@ const App = () => {
|
|||
<Route path="/channels" element={<Channels />} />
|
||||
<Route path="/m3u" element={<M3U />} />
|
||||
<Route path="/epg" element={<EPG />} />
|
||||
<Route
|
||||
path="/stream-profiles"
|
||||
element={<StreamProfiles />}
|
||||
/>
|
||||
<Route path="/stream-profiles" element={<StreamProfiles />} />
|
||||
<Route path="/guide" element={<Guide />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import usePlaylistsStore from './store/playlists';
|
|||
import useEPGsStore from './store/epgs';
|
||||
import useStreamsStore from './store/streams';
|
||||
import useStreamProfilesStore from './store/streamProfiles';
|
||||
import useSettingsStore from './store/settings';
|
||||
|
||||
// const axios = Axios.create({
|
||||
// withCredentials: true,
|
||||
|
|
@ -189,34 +190,34 @@ export default class API {
|
|||
return retval;
|
||||
}
|
||||
|
||||
static async assignChannelNumbers(channelIds) {
|
||||
// Make the request
|
||||
const response = await fetch(`${host}/api/channels/channels/assign/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ channel_order: channelIds }),
|
||||
});
|
||||
static async assignChannelNumbers(channelIds) {
|
||||
// Make the request
|
||||
const response = await fetch(`${host}/api/channels/channels/assign/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ channel_order: channelIds }),
|
||||
});
|
||||
|
||||
// The backend returns something like { "message": "Channels have been auto-assigned!" }
|
||||
if (!response.ok) {
|
||||
// If you want to handle errors gracefully:
|
||||
const text = await response.text();
|
||||
throw new Error(`Assign channels failed: ${response.status} => ${text}`);
|
||||
// The backend returns something like { "message": "Channels have been auto-assigned!" }
|
||||
if (!response.ok) {
|
||||
// If you want to handle errors gracefully:
|
||||
const text = await response.text();
|
||||
throw new Error(`Assign channels failed: ${response.status} => ${text}`);
|
||||
}
|
||||
|
||||
// Usually it has a { message: "..."} or similar
|
||||
const retval = await response.json();
|
||||
|
||||
// If you want to automatically refresh the channel list in Zustand:
|
||||
await useChannelsStore.getState().fetchChannels();
|
||||
|
||||
// Return the entire JSON result (so the caller can see the "message")
|
||||
return retval;
|
||||
}
|
||||
|
||||
// Usually it has a { message: "..."} or similar
|
||||
const retval = await response.json();
|
||||
|
||||
// If you want to automatically refresh the channel list in Zustand:
|
||||
await useChannelsStore.getState().fetchChannels();
|
||||
|
||||
// Return the entire JSON result (so the caller can see the "message")
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async createChannelFromStream(values) {
|
||||
const response = await fetch(`${host}/api/channels/channels/from-stream/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -705,4 +706,35 @@ static async assignChannelNumbers(channelIds) {
|
|||
const playlist = await API.getPlaylist(accountId);
|
||||
usePlaylistsStore.getState().updateProfiles(playlist.id, playlist.profiles);
|
||||
}
|
||||
|
||||
static async getSettings() {
|
||||
const response = await fetch(`${host}/api/core/settings/`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
},
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
return retval;
|
||||
}
|
||||
|
||||
static async updateSetting(values) {
|
||||
const { id, ...payload } = values;
|
||||
const response = await fetch(`${host}/api/core/settings/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${await getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const retval = await response.json();
|
||||
if (retval.id) {
|
||||
useSettingsStore.getState().updateSetting(retval);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
VideoFile as VideoFileIcon,
|
||||
LiveTv as LiveTvIcon,
|
||||
PlaylistPlay as PlaylistPlayIcon,
|
||||
Settings as SettingsIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
const items = [
|
||||
|
|
@ -25,6 +26,7 @@ const items = [
|
|||
route: '/stream-profiles',
|
||||
},
|
||||
{ text: 'TV Guide', icon: <LiveTvIcon />, route: '/guide' },
|
||||
{ text: 'Settings', icon: <SettingsIcon />, route: '/settings' },
|
||||
];
|
||||
|
||||
const Sidebar = ({ open }) => {
|
||||
|
|
|
|||
149
frontend/src/pages/Settings.js
Normal file
149
frontend/src/pages/Settings.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Grid2,
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
FormControl,
|
||||
Select,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import useSettingsStore from '../store/settings';
|
||||
import useUserAgentsStore from '../store/userAgents';
|
||||
import useStreamProfilesStore from '../store/streamProfiles';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import API from '../api';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { settings } = useSettingsStore();
|
||||
const { userAgents } = useUserAgentsStore();
|
||||
const { profiles: streamProfiles } = useStreamProfilesStore();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
'default-user-agent': '',
|
||||
'default-stream-profile': '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
'default-user-agent': Yup.string().required('User-Agent is required'),
|
||||
'default-stream-profile': Yup.string().required(
|
||||
'Stream Profile is required'
|
||||
),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
const changedSettings = {};
|
||||
for (const setting in values) {
|
||||
if (values[setting] != settings[setting].value) {
|
||||
changedSettings[setting] = values[setting];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(changedSettings);
|
||||
for (const updated in changedSettings) {
|
||||
await API.updateSetting({
|
||||
...settings[updated],
|
||||
value: values[updated],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues(
|
||||
Object.values(settings).reduce((acc, setting) => {
|
||||
acc[setting.key] = parseInt(setting.value) || setting.value;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}, [settings, streamProfiles, userAgents]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Box mt={4}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Settings
|
||||
</Typography>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid2 container spacing={3}>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="user-agent-label">Default User-Agent</InputLabel>
|
||||
<Select
|
||||
labelId="user-agent-label"
|
||||
id={settings['default-user-agent'].id}
|
||||
name={settings['default-user-agent'].key}
|
||||
label={settings['default-user-agent'].name}
|
||||
value={formik.values['default-user-agent']}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched['default-user-agent'] &&
|
||||
Boolean(formik.errors['default-user-agent'])
|
||||
}
|
||||
helperText={
|
||||
formik.touched['default-user-agent'] &&
|
||||
formik.errors['default-user-agent']
|
||||
}
|
||||
variant="standard"
|
||||
>
|
||||
{userAgents.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.user_agent_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel id="stream-profile-label">
|
||||
Default Stream Profile
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="stream-profile-label"
|
||||
id={settings['default-stream-profile'].id}
|
||||
name={settings['default-stream-profile'].key}
|
||||
label={settings['default-stream-profile'].name}
|
||||
value={formik.values['default-stream-profile']}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={
|
||||
formik.touched['default-stream-profile'] &&
|
||||
Boolean(formik.errors['default-stream-profile'])
|
||||
}
|
||||
helperText={
|
||||
formik.touched['default-stream-profile'] &&
|
||||
formik.errors['default-stream-profile']
|
||||
}
|
||||
variant="standard"
|
||||
>
|
||||
{streamProfiles.map((option, index) => (
|
||||
<MenuItem key={index} value={option.id}>
|
||||
{option.profile_name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid2>
|
||||
|
||||
<Box mt={4} display="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
size="small"
|
||||
>
|
||||
{formik.isSubmitting ? <CircularProgress size={24} /> : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
|
|
@ -6,6 +6,7 @@ import useUserAgentsStore from './userAgents';
|
|||
import usePlaylistsStore from './playlists';
|
||||
import useEPGsStore from './epgs';
|
||||
import useStreamProfilesStore from './streamProfiles';
|
||||
import useSettingsStore from './settings';
|
||||
|
||||
const decodeToken = (token) => {
|
||||
if (!token) return null;
|
||||
|
|
@ -37,6 +38,7 @@ const useAuthStore = create((set, get) => ({
|
|||
usePlaylistsStore.getState().fetchPlaylists(),
|
||||
useEPGsStore.getState().fetchEPGs(),
|
||||
useStreamProfilesStore.getState().fetchProfiles(),
|
||||
useSettingsStore.getState().fetchSettings(),
|
||||
]);
|
||||
},
|
||||
|
||||
|
|
|
|||
32
frontend/src/store/settings.js
Normal file
32
frontend/src/store/settings.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api';
|
||||
|
||||
const useSettingsStore = create((set) => ({
|
||||
settings: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchSettings: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const settings = await api.getSettings();
|
||||
set({
|
||||
settings: settings.reduce((acc, setting) => {
|
||||
acc[setting.key] = setting;
|
||||
return acc;
|
||||
}, {}),
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
set({ error: 'Failed to load settings.', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateSetting: (setting) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, [setting.key]: setting },
|
||||
})),
|
||||
}));
|
||||
|
||||
export default useSettingsStore;
|
||||
Loading…
Add table
Add a link
Reference in a new issue