feat: implement channel management features (add, update, delete)
This commit is contained in:
@@ -14,7 +14,6 @@ import ToastContainer from './components/notifications/ToastContainer';
|
||||
function App() {
|
||||
const [channels, setChannels] = useState<Channel[]>([]);
|
||||
const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [syncEnabled, setSyncEnabled] = useState(() => {
|
||||
@@ -22,9 +21,9 @@ function App() {
|
||||
return savedValue !== null ? JSON.parse(savedValue) : true;
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [editChannel, setEditChannel] = useState<Channel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
apiService
|
||||
.request<Channel[]>('/channels/', 'GET')
|
||||
.then((data) => setChannels(data))
|
||||
@@ -35,7 +34,6 @@ function App() {
|
||||
.then((data) => setSelectedChannel(data))
|
||||
.catch((error) => console.error('Error loading current channel:', error));
|
||||
|
||||
|
||||
console.log('Subscribing to events');
|
||||
const channelAddedListener = (channel: Channel) => {
|
||||
setChannels((prevChannels) => [...prevChannels, channel]);
|
||||
@@ -45,24 +43,63 @@ function App() {
|
||||
setSelectedChannel(nextChannel);
|
||||
};
|
||||
|
||||
const channelUpdatedListener = (updatedChannel: Channel) => {
|
||||
setChannels((prevChannels) =>
|
||||
prevChannels.map((channel) =>
|
||||
channel.id === updatedChannel.id ? updatedChannel : channel
|
||||
)
|
||||
);
|
||||
|
||||
setSelectedChannel((selectedChannel: Channel) => {
|
||||
|
||||
if(selectedChannel?.id === updatedChannel.id) {
|
||||
|
||||
if((selectedChannel?.url != updatedChannel.url || JSON.stringify(selectedChannel?.headers) != JSON.stringify(updatedChannel.headers)) && selectedChannel?.restream == updatedChannel.restream){
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return updatedChannel;
|
||||
}
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
const channelDeletedListener = (deletedChannel: number) => {
|
||||
setChannels((prevChannels) =>
|
||||
prevChannels.filter((channel) => channel.id !== deletedChannel)
|
||||
);
|
||||
};
|
||||
|
||||
socketService.subscribeToEvent('channel-added', channelAddedListener);
|
||||
socketService.subscribeToEvent('channel-selected', channelSelectedListener);
|
||||
socketService.subscribeToEvent('channel-updated', channelUpdatedListener);
|
||||
socketService.subscribeToEvent('channel-deleted', channelDeletedListener);
|
||||
|
||||
socketService.connect();
|
||||
|
||||
return () => {
|
||||
socketService.unsubscribeFromEvent('channel-added', channelAddedListener);
|
||||
socketService.unsubscribeFromEvent('channel-selected', channelSelectedListener);
|
||||
socketService.unsubscribeFromEvent('channel-updated', channelUpdatedListener);
|
||||
socketService.unsubscribeFromEvent('channel-deleted', channelDeletedListener);
|
||||
socketService.disconnect();
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const filteredChannels = channels.filter(channel =>
|
||||
const filteredChannels = channels.filter((channel) =>
|
||||
channel.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleEditChannel = (channel: Channel) => {
|
||||
setEditChannel(channel);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
@@ -84,8 +121,11 @@ function App() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Users className="w-6 h-6 text-blue-500" />
|
||||
<button onClick={() => setIsSettingsOpen(true)} className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
|
||||
<Settings className="w-6 h-6 text-blue-500" />
|
||||
<button
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
<Settings className="w-6 h-6 text-blue-500" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -110,10 +150,11 @@ function App() {
|
||||
channels={filteredChannels}
|
||||
selectedChannel={selectedChannel}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onEditChannel={handleEditChannel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VideoPlayer channel={selectedChannel} syncEnabled={syncEnabled}/>
|
||||
<VideoPlayer channel={selectedChannel} syncEnabled={syncEnabled} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-4">
|
||||
@@ -124,7 +165,11 @@ function App() {
|
||||
|
||||
<AddChannelModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false)
|
||||
setEditChannel(null);
|
||||
}}
|
||||
channel={editChannel}
|
||||
/>
|
||||
|
||||
<SettingsModal
|
||||
@@ -143,4 +188,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
||||
@@ -6,9 +6,10 @@ interface ChannelListProps {
|
||||
channels: Channel[];
|
||||
selectedChannel: Channel | null;
|
||||
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
onEditChannel: (channel: Channel) => void;
|
||||
}
|
||||
|
||||
function ChannelList({ channels, selectedChannel, setSearchQuery }: ChannelListProps) {
|
||||
function ChannelList({ channels, selectedChannel, setSearchQuery, onEditChannel }: ChannelListProps) {
|
||||
|
||||
const onSelectChannel = (channel: Channel) => {
|
||||
setSearchQuery('');
|
||||
@@ -16,12 +17,18 @@ function ChannelList({ channels, selectedChannel, setSearchQuery }: ChannelListP
|
||||
socketService.setCurrentChannel(channel.id);
|
||||
};
|
||||
|
||||
const onRightClickChannel = (event: React.MouseEvent, channel: Channel) => {
|
||||
event.preventDefault();
|
||||
onEditChannel(channel);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex space-x-3 hover:overflow-x-auto overflow-hidden pb-2 px-1 pt-1 scroll-container">
|
||||
{channels.map((channel) => (
|
||||
<button
|
||||
key={channel.id}
|
||||
onClick={() => onSelectChannel(channel)}
|
||||
onContextMenu={(event) => onRightClickChannel(event, channel)}
|
||||
className={`group relative p-2 rounded-lg transition-all ${
|
||||
selectedChannel?.id === channel.id
|
||||
? 'bg-blue-500 bg-opacity-20 ring-2 ring-blue-500'
|
||||
|
||||
@@ -159,7 +159,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
|
||||
title: 'Stream Error',
|
||||
message: is403 && !channel.restream
|
||||
? 'Access denied. Try with restream option for this channel.'
|
||||
: 'The stream is not working. Check the source.',
|
||||
: `The stream is not working. Check the source. ${data.response?.text}`,
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
@@ -173,7 +173,7 @@ function VideoPlayer({ channel, syncEnabled }: VideoPlayerProps) {
|
||||
hlsRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [channel?.url, syncEnabled]);
|
||||
}, [channel?.url, channel?.restream, channel?.headers, syncEnabled]);
|
||||
|
||||
const handleVideoClick = (event: React.MouseEvent<HTMLVideoElement>) => {
|
||||
if (videoRef.current?.muted) {
|
||||
|
||||
@@ -1,36 +1,75 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Trash2, X } from 'lucide-react';
|
||||
import socketService from '../../services/SocketService';
|
||||
import { CustomHeader } from '../../types';
|
||||
import { CustomHeader, Channel } from '../../types';
|
||||
import CustomHeaderInput from './CustomHeaderInput';
|
||||
|
||||
interface AddChannelModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
channel?: Channel | null;
|
||||
}
|
||||
|
||||
function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
|
||||
function AddChannelModal({ isOpen, onClose, channel }: AddChannelModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [avatar, setAvatar] = useState('');
|
||||
const [restream, setRestream] = useState(false);
|
||||
const [headers, setHeaders] = useState<CustomHeader[]>([]);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (channel) {
|
||||
setName(channel.name);
|
||||
setUrl(channel.url);
|
||||
setAvatar(channel.avatar);
|
||||
setRestream(channel.restream);
|
||||
setHeaders(channel.headers);
|
||||
setIsEditMode(true);
|
||||
} else {
|
||||
setName('');
|
||||
setUrl('');
|
||||
setAvatar('');
|
||||
setRestream(false);
|
||||
setHeaders([]);
|
||||
setIsEditMode(false);
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !url.trim()) return;
|
||||
|
||||
socketService.addChannel(
|
||||
name.trim(),
|
||||
url.trim(),
|
||||
avatar.trim() || 'https://via.placeholder.com/64',
|
||||
restream,
|
||||
JSON.stringify(headers)
|
||||
);
|
||||
if (isEditMode && channel) {
|
||||
handleUpdate(channel.id);
|
||||
} else {
|
||||
socketService.addChannel(
|
||||
name.trim(),
|
||||
url.trim(),
|
||||
avatar.trim() || 'https://via.placeholder.com/64',
|
||||
restream,
|
||||
JSON.stringify(headers)
|
||||
);
|
||||
}
|
||||
|
||||
setName('');
|
||||
setUrl('');
|
||||
setAvatar('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (channel) {
|
||||
socketService.deleteChannel(channel.id);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (id: number) => {
|
||||
socketService.updateChannel(id, {
|
||||
name: name.trim(),
|
||||
url: url.trim(),
|
||||
avatar: avatar.trim() || 'https://via.placeholder.com/64',
|
||||
restream,
|
||||
headers: headers,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -54,7 +93,7 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
|
||||
<div className="bg-gray-800 rounded-lg w-full max-w-md">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||
<h2 className="text-xl font-semibold">Add New Channel</h2>
|
||||
<h2 className="text-xl font-semibold">{isEditMode ? 'Edit Channel' : 'Add New Channel'}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-700 rounded-full transition-colors"
|
||||
@@ -171,6 +210,15 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
{isEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 bg-red-600 rounded-lg hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -182,7 +230,7 @@ function AddChannelModal({ isOpen, onClose }: AddChannelModalProps) {
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Add Channel
|
||||
{isEditMode ? 'Update Channel' : 'Add Channel'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -81,6 +81,20 @@ class SocketService {
|
||||
|
||||
this.socket.emit('set-current-channel', id);
|
||||
}
|
||||
|
||||
// Channel löschen
|
||||
deleteChannel(id: number) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('delete-channel', id);
|
||||
}
|
||||
|
||||
// Channel aktualisieren
|
||||
updateChannel(id: number, updatedAttributes: any) {
|
||||
if (!this.socket) throw new Error('Socket is not connected.');
|
||||
|
||||
this.socket.emit('update-channel', { id, updatedAttributes });
|
||||
}
|
||||
}
|
||||
|
||||
const socketService = new SocketService();
|
||||
|
||||
Reference in New Issue
Block a user