diff --git a/docker/build-dev.sh b/docker/build-dev.sh old mode 100644 new mode 100755 index 13c7f964..0b723671 --- a/docker/build-dev.sh +++ b/docker/build-dev.sh @@ -1,3 +1,3 @@ #!/bin/bash -docker build -t dispatcharr/dispatcharr:dev .. +docker build -t dispatcharr/dispatcharr:dev -f Dockerfile .. diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index f9035512..214c47da 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -3,13 +3,13 @@ services: # build: # context: .. # dockerfile: docker/Dockerfile.dev - image: dispatcharr/dispatcharr + image: dispatcharr/dispatcharr:dev container_name: dispatcharr_dev ports: - "5656:5656" - 9191:9191 volumes: - - /home/ghost/code/Dispatcharr:/app + - ../:/app environment: - DISPATCHARR_ENV=dev - DB_ENGINE=sqlite diff --git a/frontend/package.json b/frontend/package.json index 7a599852..38affdfe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,7 +30,7 @@ "zustand": "^5.0.3" }, "scripts": { - "start": "react-scripts start", + "start": "PORT=9191 react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", @@ -54,5 +54,5 @@ "last 1 safari version" ] }, - "proxy": "http://host.docker.internal:5656" + "proxy": "http://localhost:5656" } diff --git a/frontend/src/App.js b/frontend/src/App.js index 914e13ac..257e209a 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -28,6 +28,7 @@ import Settings from './pages/Settings'; import StreamProfiles from './pages/StreamProfiles'; import useAuthStore from './store/auth'; import logo from './images/logo.png'; +import Alert from './components/Alert'; // NEW: import the floating PiP component import FloatingVideo from './components/FloatingVideo'; @@ -110,8 +111,8 @@ const App = () => { flexDirection: 'column', ml: `${open ? drawerWidth : miniDrawerWidth}px`, transition: 'width 0.3s, margin-left 0.3s', - // height: '100vh', backgroundColor: '#495057', + height: '100%', }} > { + {/* Global Snackbar for system-wide messages */} + + {/* Always-available floating video; remains visible across page changes */} diff --git a/frontend/src/components/Alert.js b/frontend/src/components/Alert.js new file mode 100644 index 00000000..8f625364 --- /dev/null +++ b/frontend/src/components/Alert.js @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; +import { Snackbar, Alert, Button } from '@mui/material'; +import useAlertStore from '../store/alerts'; + +const AlertPopup = () => { + const { open, message, severity, hideAlert } = useAlertStore(); + + const handleClose = () => { + hideAlert(); + }; + + return ( + + + {message} + + + ); +}; + +export default AlertPopup; diff --git a/frontend/src/components/forms/UserAgent.js b/frontend/src/components/forms/UserAgent.js index 6f5f80c4..db13429f 100644 --- a/frontend/src/components/forms/UserAgent.js +++ b/frontend/src/components/forms/UserAgent.js @@ -1,5 +1,5 @@ // Modal.js -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; import { TextField, Button, @@ -9,22 +9,23 @@ import { DialogTitle, DialogContent, DialogActions, -} from "@mui/material"; -import { useFormik } from "formik"; -import * as Yup from "yup"; -import API from "../../api"; +} from '@mui/material'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import API from '../../api'; +import useSettingsStore from '../../store/settings'; const UserAgent = ({ userAgent = null, isOpen, onClose }) => { const formik = useFormik({ initialValues: { - user_agent_name: "", - user_agent: "", - description: "", + user_agent_name: '', + user_agent: '', + description: '', is_active: true, }, validationSchema: Yup.object({ - user_agent_name: Yup.string().required("Name is required"), - user_agent: Yup.string().required("User-Agent is required"), + user_agent_name: Yup.string().required('Name is required'), + user_agent: Yup.string().required('User-Agent is required'), }), onSubmit: async (values, { setSubmitting, resetForm }) => { if (userAgent?.id) { @@ -60,8 +61,8 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { User-Agent @@ -132,7 +133,7 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { color="primary" disabled={formik.isSubmitting} > - {formik.isSubmitting ? : "Submit"} + {formik.isSubmitting ? : 'Submit'} diff --git a/frontend/src/components/tables/StreamProfilesTable.js b/frontend/src/components/tables/StreamProfilesTable.js index b23257a8..c22d6c4b 100644 --- a/frontend/src/components/tables/StreamProfilesTable.js +++ b/frontend/src/components/tables/StreamProfilesTable.js @@ -30,6 +30,8 @@ import useEPGsStore from '../../store/epgs'; import StreamProfileForm from '../forms/StreamProfile'; import useStreamProfilesStore from '../../store/streamProfiles'; import { TableHelper } from '../../helpers'; +import useSettingsStore from '../../store/settings'; +import useAlertStore from '../../store/alerts'; const StreamProfiles = () => { const [profile, setProfile] = useState(null); @@ -40,6 +42,8 @@ const StreamProfiles = () => { const [activeFilterValue, setActiveFilterValue] = useState('all'); const streamProfiles = useStreamProfilesStore((state) => state.profiles); + const { settings } = useSettingsStore(); + const { showAlert } = useAlertStore(); const columns = useMemo( //column definitions... @@ -112,8 +116,13 @@ const StreamProfiles = () => { setProfileModalOpen(true); }; - const deleteStreamProfile = async (ids) => { - await API.deleteStreamProfile(ids); + const deleteStreamProfile = async (id) => { + if (id == settings['default-stream-profile'].value) { + showAlert('Cannot delete default stream-profile', 'error'); + return; + } + + await API.deleteStreamProfile(id); }; const closeStreamProfileForm = () => { diff --git a/frontend/src/components/tables/UserAgentsTable.js b/frontend/src/components/tables/UserAgentsTable.js index 7271aff8..a8ecb81d 100644 --- a/frontend/src/components/tables/UserAgentsTable.js +++ b/frontend/src/components/tables/UserAgentsTable.js @@ -26,6 +26,8 @@ import { import useUserAgentsStore from '../../store/userAgents'; import UserAgentForm from '../forms/UserAgent'; import { TableHelper } from '../../helpers'; +import useSettingsStore from '../../store/settings'; +import useAlertStore from '../../store/alerts'; const UserAgentsTable = () => { const [userAgent, setUserAgent] = useState(null); @@ -34,6 +36,8 @@ const UserAgentsTable = () => { const [activeFilterValue, setActiveFilterValue] = useState('all'); const userAgents = useUserAgentsStore((state) => state.userAgents); + const { settings } = useSettingsStore(); + const { showAlert } = useAlertStore(); const columns = useMemo( //column definitions... @@ -108,8 +112,18 @@ const UserAgentsTable = () => { const deleteUserAgent = async (ids) => { if (Array.isArray(ids)) { + if (ids.includes(settings['default-user-agent'].value)) { + showAlert('Cannot delete default user-agent', 'error'); + return; + } + await API.deleteUserAgents(ids); } else { + if (ids == settings['default-user-agent'].value) { + showAlert('Cannot delete default user-agent', 'error'); + return; + } + await API.deleteUserAgent(ids); } }; diff --git a/frontend/src/pages/Guide.js b/frontend/src/pages/Guide.js index 63edb1e7..e17b92fe 100644 --- a/frontend/src/pages/Guide.js +++ b/frontend/src/pages/Guide.js @@ -19,6 +19,7 @@ import API from '../api'; import useChannelsStore from '../store/channels'; import logo from '../images/logo.png'; import useVideoStore from '../store/useVideoStore'; // NEW import +import useAlertStore from '../store/alerts'; /** Layout constants */ const CHANNEL_WIDTH = 120; // Width of the channel/logo column @@ -44,6 +45,7 @@ export default function TVChannelGuide({ startDate, endDate }) { const [now, setNow] = useState(dayjs()); const [selectedProgram, setSelectedProgram] = useState(null); const [loading, setLoading] = useState(true); + const { showAlert } = useAlertStore(); const guideRef = useRef(null); @@ -51,6 +53,8 @@ export default function TVChannelGuide({ startDate, endDate }) { useEffect(() => { if (!channels || channels.length === 0) { console.warn('No channels provided or empty channels array'); + showAlert('No channels available', 'error'); + setLoading(false); return; } diff --git a/frontend/src/store/alerts.js b/frontend/src/store/alerts.js new file mode 100644 index 00000000..b51076a3 --- /dev/null +++ b/frontend/src/store/alerts.js @@ -0,0 +1,24 @@ +// frontend/src/store/useAlertStore.js +import { create } from 'zustand'; + +/** + * Global store to track whether a floating video is visible and which URL is playing. + */ +const useAlertStore = create((set) => ({ + open: false, + message: '', + severity: 'info', + + showAlert: (message, severity = 'info') => + set({ + open: true, + message, + severity, + }), + + hideAlert: () => { + set({ open: false }); + }, +})); + +export default useAlertStore; diff --git a/uwsgi.dev.ini b/uwsgi.dev.ini index 9ad83bd2..8b3415ab 100644 --- a/uwsgi.dev.ini +++ b/uwsgi.dev.ini @@ -17,4 +17,4 @@ max-fd = 10000 attach-daemon = celery -A dispatcharr worker -l info attach-daemon = redis-server -attach-daemon = npm run start +attach-daemon = cd /app/frontend && npm run start