core settings update capabilities

This commit is contained in:
kappa118 2025-03-01 11:19:15 -05:00
parent 19cac3ccad
commit c42f2d048a
7 changed files with 246 additions and 53 deletions

View file

@ -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)

View file

@ -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 />} />

View file

@ -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;
}
}

View file

@ -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 }) => {

View 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;

View file

@ -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(),
]);
},

View 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;