diff --git a/core/api_views.py b/core/api_views.py
index 26dfaadd..917afe12 100644
--- a/core/api_views.py
+++ b/core/api_views.py
@@ -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)
-
-
-
diff --git a/frontend/src/App.js b/frontend/src/App.js
index 2c654d63..914e13ac 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -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 = () => {
} />
} />
} />
- }
- />
+ } />
} />
+ } />
>
) : (
} />
diff --git a/frontend/src/api.js b/frontend/src/api.js
index fdd68e63..f840d1ab 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -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;
+ }
}
diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js
index e07bdd4d..ca84d536 100644
--- a/frontend/src/components/Sidebar.js
+++ b/frontend/src/components/Sidebar.js
@@ -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: , route: '/guide' },
+ { text: 'Settings', icon: , route: '/settings' },
];
const Sidebar = ({ open }) => {
diff --git a/frontend/src/pages/Settings.js b/frontend/src/pages/Settings.js
new file mode 100644
index 00000000..9f4da8d2
--- /dev/null
+++ b/frontend/src/pages/Settings.js
@@ -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 (
+
+
+
+ Settings
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js
index 43421cca..0b9f2f8a 100644
--- a/frontend/src/store/auth.js
+++ b/frontend/src/store/auth.js
@@ -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(),
]);
},
diff --git a/frontend/src/store/settings.js b/frontend/src/store/settings.js
new file mode 100644
index 00000000..9c13be2f
--- /dev/null
+++ b/frontend/src/store/settings.js
@@ -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;