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 + +
+ + + Default User-Agent + + + + + + Default Stream Profile + + + + + + + + +
+
+
+ ); +}; + +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;