Skip to content

Commit

Permalink
Merge pull request #49 from antebrl/42-playlist-group-selection
Browse files Browse the repository at this point in the history
42 playlist group selection
  • Loading branch information
antebrl authored Jan 6, 2025
2 parents 460ef35 + 2677924 commit 2752747
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 39 deletions.
3 changes: 2 additions & 1 deletion backend/models/Channel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Channel {
static nextId = 0;
constructor(name, url, avatar, mode, headers = [], group = null, playlist = null) {
constructor(name, url, avatar, mode, headers = [], group = null, playlist = null, playlistName = null) {
this.id = Channel.nextId++;
this.name = name;
this.url = url;
Expand All @@ -10,6 +10,7 @@ class Channel {
this.headers = headers;
this.group = group;
this.playlist = playlist;
this.playlistName = playlistName;
}

restream() {
Expand Down
10 changes: 5 additions & 5 deletions backend/services/ChannelService.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,26 @@ class ChannelService {
return this.channels;
}

getFilteredChannels({ playlist, group }) {
getFilteredChannels({ playlistName, group }) {
let filtered = this.channels;
if (playlist) {
filtered = filtered.filter(ch => ch.playlist && ch.playlist == playlist);
if (playlistName) {
filtered = filtered.filter(ch => ch.playlistName && ch.playlistName.toLowerCase() == playlistName.toLowerCase());
}
if (group) {
filtered = filtered.filter(ch => ch.group && ch.group.toLowerCase() === group.toLowerCase());
}
return filtered;
}

addChannel({ name, url, avatar, mode, headersJson, group = false, playlist = false }) {
addChannel({ name, url, avatar, mode, headersJson, group = null, playlist = null, playlistName = null }) {
const existing = this.channels.find(channel => channel.url === url);

if (existing) {
throw new Error('Channel already exists');
}

const headers = JSON.parse(headersJson);
const newChannel = new Channel(name, url, avatar, mode, headers, group, playlist);
const newChannel = new Channel(name, url, avatar, mode, headers, group, playlist, playlistName);
this.channels.push(newChannel);

return newChannel;
Expand Down
7 changes: 4 additions & 3 deletions backend/services/PlaylistService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const ChannelService = require('./ChannelService');

class PlaylistService {

async addPlaylist(playlistUrl, mode, headersJson) {
async addPlaylist(playlistUrl, playlistName, mode, headersJson) {

const response = await fetch(playlistUrl);
const content = await response.text();
Expand All @@ -21,7 +21,8 @@ class PlaylistService {
mode: mode,
headersJson: headersJson,
group: channel.group.title,
playlist: playlistUrl
playlist: playlistUrl,
playlistName: playlistName
});
} catch (error) {
console.error(error);
Expand All @@ -42,7 +43,7 @@ class PlaylistService {
for(let channel of channels) {
channel = await ChannelService.updateChannel(channel.id, updatedAttributes);
}

return channels;
}

Expand Down
10 changes: 6 additions & 4 deletions backend/socket/PlaylistSocketHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ const Channel = require('../models/Channel');

module.exports = (io, socket) => {

socket.on('add-playlist', async ({ playlist, mode, headersJson }) => {
socket.on('add-playlist', async ({ playlist, playlistName, mode, headersJson }) => {
try {
const channels = await PlaylistService.addPlaylist(playlist, mode, headersJson);
const channels = await PlaylistService.addPlaylist(playlist, playlistName, mode, headersJson);

if (channels) {
channels.forEach(channel => {
io.emit('channel-added', channel);
Expand All @@ -21,7 +22,8 @@ module.exports = (io, socket) => {

socket.on('update-playlist', async ({ playlist, updatedAttributes }) => {
try {
const channels = PlaylistService.updatePlaylist(playlist, updatedAttributes);
const channels = await PlaylistService.updatePlaylist(playlist, updatedAttributes);

channels.forEach(channel => {
io.emit('channel-updated', channel.toFrontendJson());
});
Expand All @@ -34,7 +36,7 @@ module.exports = (io, socket) => {

socket.on('delete-playlist', async (playlist) => {
try {
const channels = PlaylistService.deletePlaylist(playlist);
const channels = await PlaylistService.deletePlaylist(playlist);
channels.forEach(channel => {
io.emit('channel-deleted', channel.id);
});
Expand Down
162 changes: 145 additions & 17 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Search, Plus, Settings, Users, Radio, Tv2 } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { Search, Plus, Settings, Users, Radio, Tv2, ChevronDown } from 'lucide-react';
import VideoPlayer from './components/VideoPlayer';
import ChannelList from './components/ChannelList';
import Chat from './components/chat/Chat';
Expand Down Expand Up @@ -28,6 +28,45 @@ function App() {
const [sessionProvider, setSessionProvider] = useState<SessionHandler | null>(null);
const [sessionQuery, setSessionQuery] = useState<string | undefined>(undefined);


const [selectedPlaylist, setSelectedPlaylist] = useState<string>('All Channels');
const [selectedGroup, setSelectedGroup] = useState<string>('Category');
const [isPlaylistDropdownOpen, setIsPlaylistDropdownOpen] = useState(false);
const [isGroupDropdownOpen, setIsGroupDropdownOpen] = useState(false);

// Get unique playlists from channels
const playlists = useMemo(() => {
const uniquePlaylists = new Set(channels.map(channel => channel.playlistName).filter(playlistName => playlistName !== null));
return ['All Channels', ...Array.from(uniquePlaylists)];
}, [channels]);

const filteredChannels = useMemo(() => {
//Filter by playlist
let filteredByPlaylist = selectedPlaylist === 'All Channels' ? channels : channels.filter(channel =>
channel.playlistName === selectedPlaylist
);

//Filter by group
filteredByPlaylist = selectedGroup === 'Category' ? filteredByPlaylist : filteredByPlaylist.filter(channel =>
channel.group === selectedGroup
);

//Filter by name search
return filteredByPlaylist.filter(channel =>
channel.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [channels, selectedPlaylist, selectedGroup, searchQuery]);

const groups = useMemo(() => {
let uniqueGroups;
if(selectedPlaylist === 'All Channels') {
uniqueGroups = new Set(channels.map(channel => channel.group).filter(group => group !== null));
} else {
uniqueGroups = new Set(channels.filter(channel => channel.group !== null && channel.playlistName === selectedPlaylist).map(channel => channel.group));
}
return ['Category', ...Array.from(uniqueGroups)];
}, [selectedPlaylist, channels]);

useEffect(() => {
apiService
.request<Channel[]>('/channels/', 'GET')
Expand Down Expand Up @@ -115,9 +154,6 @@ function App() {
};
}, []);

const filteredChannels = channels.filter((channel) =>
channel.name.toLowerCase().includes(searchQuery.toLowerCase())
);

const handleEditChannel = (channel: Channel) => {
setEditChannel(channel);
Expand Down Expand Up @@ -158,12 +194,103 @@ function App() {
<div className="col-span-12 lg:col-span-8 space-y-4">
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<Tv2 className="w-5 h-5 text-blue-500" />
<h2 className="text-xl font-semibold">Live Channels</h2>
<div className="flex items-center space-x-4">
<div className="relative">
<button
onClick={() => {
setIsPlaylistDropdownOpen(!isPlaylistDropdownOpen);
setIsGroupDropdownOpen(false);
}}
className="flex items-center space-x-2 group"
>
<div className="flex items-center space-x-2">
<Tv2 className="w-5 h-5 text-blue-500" />
<h2 className="text-xl font-semibold group-hover:text-blue-400 transition-colors">
{selectedPlaylist}
</h2>
</div>
<ChevronDown className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${isPlaylistDropdownOpen ? 'rotate-180' : ''}`} />
</button>

{isPlaylistDropdownOpen && (
<div className="absolute top-full left-0 mt-1 w-48 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50 overflow-hidden">
<div className="max-h-72 overflow-y-auto scroll-container">
{playlists.map((playlist) => (
<button
key={playlist}
onClick={() => {
setSelectedPlaylist(playlist);
setSelectedGroup('Category');
setIsPlaylistDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors hover:bg-gray-700 ${
selectedPlaylist === playlist ? 'text-blue-400 text-base font-semibold' : 'text-gray-200'
}`}
style={{
whiteSpace: 'normal',
wordWrap: 'break-word',
overflowWrap: 'anywhere',
}}
>
{playlist}
</button>
))}
</div>
</div>
)}
</div>

{/* Group Dropdown */}
<div className="relative">
<button
onClick={() => {
setIsGroupDropdownOpen(!isGroupDropdownOpen);
setIsPlaylistDropdownOpen(false);
}}
className="flex items-center space-x-2 group py-0.5 px-1.5 rounded-lg transition-all bg-white bg-opacity-10"
>
<div className="flex items-center space-x-2">
<h4 className="text-base text-gray-300 group-hover:text-blue-400 transition-colors">
{selectedGroup}
</h4>
</div>
<ChevronDown className={`w-3 h-3 text-gray-400 transition-transform duration-200 ${isGroupDropdownOpen ? 'rotate-180' : ''}`} />
</button>

{isGroupDropdownOpen && (
<div className="absolute top-full left-0 mt-1 w-48 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50 overflow-hidden">
<div className="max-h-72 overflow-y-auto scroll-container">
{groups.map((group) => (
<button
key={group}
onClick={() => {
setSelectedGroup(group);
setIsGroupDropdownOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm transition-colors hover:bg-gray-700 ${
selectedGroup === group ? 'text-blue-400 text-base font-semibold' : 'text-gray-200'
}`}
style={{
whiteSpace: 'normal',
wordWrap: 'break-word',
overflowWrap: 'anywhere',
}}
>
{group === 'Category' ? 'All Categories' : group}
</button>
))}
</div>
</div>
)}
</div>
</div>

<button
onClick={() => setIsModalOpen(true)}
onClick={() => {
setIsModalOpen(true);
setIsGroupDropdownOpen(false);
setIsPlaylistDropdownOpen(false);
}}
className="p-2 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="w-5 h-5" />
Expand All @@ -187,14 +314,15 @@ function App() {
</div>
</div>

<ChannelModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false)
setEditChannel(null);
}}
channel={editChannel}
/>
{isModalOpen && (
<ChannelModal
onClose={() => {
setIsModalOpen(false);
setEditChannel(null);
}}
channel={editChannel}
/>
)}

<SettingsModal
isOpen={isSettingsOpen}
Expand Down
Loading

0 comments on commit 2752747

Please sign in to comment.