diff --git a/apps/channels/api_urls.py b/apps/channels/api_urls.py
index 9dc9de37..fc02d7e5 100644
--- a/apps/channels/api_urls.py
+++ b/apps/channels/api_urls.py
@@ -10,6 +10,7 @@ from .api_views import (
ChannelProfileViewSet,
UpdateChannelMembershipAPIView,
BulkUpdateChannelMembershipAPIView,
+ RecordingViewSet,
)
app_name = 'channels' # for DRF routing
@@ -20,6 +21,7 @@ router.register(r'groups', ChannelGroupViewSet, basename='channel-group')
router.register(r'channels', ChannelViewSet, basename='channel')
router.register(r'logos', LogoViewSet, basename='logo')
router.register(r'profiles', ChannelProfileViewSet, basename='profile')
+router.register(r'recordings', RecordingViewSet, basename='recording')
urlpatterns = [
# Bulk delete is a single APIView, not a ViewSet
diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index 9c391b77..b3d97fee 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -10,8 +10,8 @@ from django.shortcuts import get_object_or_404
from django.db import transaction
import os, json, requests
-from .models import Stream, Channel, ChannelGroup, Logo, ChannelProfile, ChannelProfileMembership
-from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer, LogoSerializer, ChannelProfileMembershipSerializer, BulkChannelProfileMembershipSerializer, ChannelProfileSerializer
+from .models import Stream, Channel, ChannelGroup, Logo, ChannelProfile, ChannelProfileMembership, Recording
+from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer, LogoSerializer, ChannelProfileMembershipSerializer, BulkChannelProfileMembershipSerializer, ChannelProfileSerializer, RecordingSerializer
from .tasks import match_epg_channels
import django_filters
from django_filters.rest_framework import DjangoFilterBackend
@@ -565,3 +565,8 @@ class BulkUpdateChannelMembershipAPIView(APIView):
return Response({"status": "success"}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+class RecordingViewSet(viewsets.ModelViewSet):
+ queryset = Recording.objects.all()
+ serializer_class = RecordingSerializer
+ permission_classes = [IsAuthenticated]
diff --git a/apps/channels/migrations/0014_recording.py b/apps/channels/migrations/0014_recording.py
new file mode 100644
index 00000000..44fa681c
--- /dev/null
+++ b/apps/channels/migrations/0014_recording.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.1.6 on 2025-04-05 22:25
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dispatcharr_channels', '0013_alter_logo_url'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Recording',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('start_time', models.DateTimeField()),
+ ('end_time', models.DateTimeField()),
+ ('task_id', models.CharField(blank=True, max_length=255, null=True)),
+ ('channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recordings', to='dispatcharr_channels.channel')),
+ ],
+ ),
+ ]
diff --git a/apps/channels/models.py b/apps/channels/models.py
index 8a8ddcec..abcecb77 100644
--- a/apps/channels/models.py
+++ b/apps/channels/models.py
@@ -404,3 +404,12 @@ class Logo(models.Model):
def __str__(self):
return self.name
+
+class Recording(models.Model):
+ channel = models.ForeignKey("Channel", on_delete=models.CASCADE, related_name="recordings")
+ start_time = models.DateTimeField()
+ end_time = models.DateTimeField()
+ task_id = models.CharField(max_length=255, null=True, blank=True)
+
+ def __str__(self):
+ return f"{self.channel.name} - {self.start_time} to {self.end_time}"
diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py
index f1d89b41..3ee336a7 100644
--- a/apps/channels/serializers.py
+++ b/apps/channels/serializers.py
@@ -1,5 +1,5 @@
from rest_framework import serializers
-from .models import Stream, Channel, ChannelGroup, ChannelStream, ChannelGroupM3UAccount, Logo, ChannelProfile, ChannelProfileMembership
+from .models import Stream, Channel, ChannelGroup, ChannelStream, ChannelGroupM3UAccount, Logo, ChannelProfile, ChannelProfileMembership, Recording
from apps.epg.serializers import EPGDataSerializer
from core.models import StreamProfile
from apps.epg.models import EPGData
@@ -232,3 +232,10 @@ class ChannelGroupM3UAccountSerializer(serializers.ModelSerializer):
# Optionally, if you only need the id of the ChannelGroup, you can customize it like this:
# channel_group = serializers.PrimaryKeyRelatedField(queryset=ChannelGroup.objects.all())
+
+
+class RecordingSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Recording
+ fields = '__all__'
+ read_only_fields = ['task_id']
diff --git a/apps/channels/signals.py b/apps/channels/signals.py
index 7fcb452e..076f9876 100644
--- a/apps/channels/signals.py
+++ b/apps/channels/signals.py
@@ -1,11 +1,14 @@
# apps/channels/signals.py
-from django.db.models.signals import m2m_changed, pre_save, post_save
+from django.db.models.signals import m2m_changed, pre_save, post_save, post_delete
from django.dispatch import receiver
-from .models import Channel, Stream, ChannelProfile, ChannelProfileMembership
+from django.utils.timezone import now
+from celery.result import AsyncResult
+from .models import Channel, Stream, ChannelProfile, ChannelProfileMembership, Recording
from apps.m3u.models import M3UAccount
from apps.epg.tasks import parse_programs_for_tvg_id
-import logging
+import logging, requests, time
+from .tasks import run_recording
logger = logging.getLogger(__name__)
@@ -62,3 +65,42 @@ def create_profile_memberships(sender, instance, created, **kwargs):
ChannelProfileMembership(channel_profile=instance, channel=channel)
for channel in channels
])
+
+def schedule_recording_task(instance):
+ eta = instance.start_time
+ task = run_recording.apply_async(
+ args=[instance.channel_id, str(instance.start_time), str(instance.end_time)],
+ eta=eta
+ )
+ return task.id
+
+def revoke_task(task_id):
+ if task_id:
+ AsyncResult(task_id).revoke()
+
+@receiver(pre_save, sender=Recording)
+def revoke_old_task_on_update(sender, instance, **kwargs):
+ if not instance.pk:
+ return # New instance
+ try:
+ old = Recording.objects.get(pk=instance.pk)
+ if old.task_id and (
+ old.start_time != instance.start_time or
+ old.end_time != instance.end_time or
+ old.channel_id != instance.channel_id
+ ):
+ revoke_task(old.task_id)
+ instance.task_id = None
+ except Recording.DoesNotExist:
+ pass
+
+@receiver(post_save, sender=Recording)
+def schedule_task_on_save(sender, instance, created, **kwargs):
+ if not instance.task_id and instance.start_time > now():
+ task_id = schedule_recording_task(instance)
+ instance.task_id = task_id
+ instance.save(update_fields=['task_id'])
+
+@receiver(post_delete, sender=Recording)
+def revoke_task_on_delete(sender, instance, **kwargs):
+ revoke_task(instance.task_id)
diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py
index 7acf5831..9ac4d475 100644
--- a/apps/channels/tasks.py
+++ b/apps/channels/tasks.py
@@ -2,12 +2,16 @@
import logging
import os
import re
+import requests
+import time
+from datetime import datetime
from celery import shared_task
from rapidfuzz import fuzz
from sentence_transformers import util
from django.conf import settings
from django.db import transaction
+from django.utils.text import slugify
from apps.channels.models import Channel
from apps.epg.models import EPGData, EPGSource
@@ -217,3 +221,33 @@ def match_epg_channels():
)
return f"Done. Matched {total_matched} channel(s)."
+
+@shared_task
+def run_recording(channel_id, start_time_str, end_time_str):
+ channel = Channel.objects.get(id=channel_id)
+
+ start_time = datetime.fromisoformat(start_time_str)
+ end_time = datetime.fromisoformat(end_time_str)
+
+ duration_seconds = int((end_time - start_time).total_seconds())
+ filename = f'{slugify(channel.name)}-{start_time.strftime("%Y-%m-%d_%H-%M-%S")}.mp4'
+
+ logger.info(f"Starting recording for channel {channel.name}")
+ with requests.get(f"http://localhost:5656/proxy/ts/stream/{channel.uuid}", headers={
+ 'User-Agent': 'Dispatcharr-DVR',
+ }, stream=True) as response:
+ # Raise an exception for bad responses (4xx, 5xx)
+ response.raise_for_status()
+
+ # Open the file in write-binary mode
+ with open(f"/data/recordings/{filename}", 'wb') as file:
+ start_time = time.time() # Start the timer
+ for chunk in response.iter_content(chunk_size=8192): # 8KB chunks
+ if time.time() - start_time > duration_seconds:
+ print(f"Timeout reached: {duration_seconds} seconds")
+ break
+ # Write the chunk to the file
+ file.write(chunk)
+
+ # After the loop, the file and response are closed automatically.
+ logger.info(f"Finished recording for channel {channel.name}")
diff --git a/docker/init/03-init-dispatcharr.sh b/docker/init/03-init-dispatcharr.sh
index d374ad69..0525d26f 100644
--- a/docker/init/03-init-dispatcharr.sh
+++ b/docker/init/03-init-dispatcharr.sh
@@ -1,6 +1,7 @@
#!/bin/bash
mkdir -p /data/logos
+mkdir -p /data/recordings
mkdir -p /app/logo_cache
mkdir -p /app/media
@@ -14,7 +15,8 @@ if [ "$(id -u)" = "0" ]; then
chown $PUID:$PGID /app/uwsgi.sock
chown -R $PUID:$PGID /app
- chown -R $PUID:$PGID /data/logos
+ # Needs to own ALL of /data except db, we handle that below
+ chown -R $PUID:$PGID /data
# Permissions
chown -R postgres:postgres /data/db
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f1abbe02..4f1856f1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,6 +12,7 @@
"@mantine/core": "^7.17.2",
"@mantine/dates": "^7.17.2",
"@mantine/dropzone": "^7.17.2",
+ "@mantine/form": "^7.17.3",
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
@@ -1148,6 +1149,19 @@
"react-dom": "^18.x || ^19.x"
}
},
+ "node_modules/@mantine/form": {
+ "version": "7.17.3",
+ "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.17.3.tgz",
+ "integrity": "sha512-ktERldD8f9lrjjz6wIbwMnNbAZq8XEWPx4K5WuFyjXaK0PI8D+gsXIGKMtA5rVrAUFHCWCdbK3yLgtjJNki8ew==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "klona": "^2.0.6"
+ },
+ "peerDependencies": {
+ "react": "^18.x || ^19.x"
+ }
+ },
"node_modules/@mantine/hooks": {
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.2.tgz",
@@ -2795,7 +2809,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@@ -3296,6 +3309,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 27a7b049..5677558b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,7 @@
"@mantine/core": "^7.17.2",
"@mantine/dates": "^7.17.2",
"@mantine/dropzone": "^7.17.2",
+ "@mantine/form": "^7.17.3",
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 2386efb5..4fa2b9a9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -23,6 +23,7 @@ import '@mantine/core/styles.css'; // Ensure Mantine global styles load
import '@mantine/notifications/styles.css';
import 'mantine-react-table/styles.css';
import '@mantine/dropzone/styles.css';
+import '@mantine/dates/styles.css';
import './index.css';
import mantineTheme from './mantineTheme';
import API from './api';
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 259ac289..2d40959f 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -1034,4 +1034,19 @@ export default class API {
.getState()
.updateProfileChannels(channelIds, profileId, enabled);
}
+
+ static async createRecording(values) {
+ const response = await fetch(`${host}/api/channels/recordings/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await API.getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(values),
+ });
+
+ const retval = await response.json();
+
+ return retval;
+ }
}
diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx
index 4742a89c..ca2f2330 100644
--- a/frontend/src/components/forms/Channel.jsx
+++ b/frontend/src/components/forms/Channel.jsx
@@ -140,7 +140,6 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
logo_id: `${channel.logo?.id}`,
});
- console.log(channel);
setChannelStreams(channel.streams);
} else {
formik.resetForm();
diff --git a/frontend/src/components/forms/M3UProfiles.jsx b/frontend/src/components/forms/M3UProfiles.jsx
index f5da6e58..609e2251 100644
--- a/frontend/src/components/forms/M3UProfiles.jsx
+++ b/frontend/src/components/forms/M3UProfiles.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
import API from '../../api';
import M3UProfile from './M3UProfile';
import usePlaylistsStore from '../../store/playlists';
@@ -11,13 +11,25 @@ import {
Box,
ActionIcon,
Text,
+ NumberInput,
+ useMantineTheme,
+ Center,
+ Group,
+ Switch,
} from '@mantine/core';
import { SquareMinus, SquarePen } from 'lucide-react';
const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
- const profiles = usePlaylistsStore((state) => state.profiles[playlist.id]);
+ const theme = useMantineTheme();
+
+ const { profiles: allProfiles } = usePlaylistsStore();
const [profileEditorOpen, setProfileEditorOpen] = useState(false);
const [profile, setProfile] = useState(null);
+ const [profiles, setProfiles] = useState([]);
+
+ useEffect(() => {
+ setProfiles(allProfiles[playlist.id]);
+ }, [allProfiles]);
const editProfile = (profile = null) => {
if (profile) {
@@ -38,6 +50,13 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
});
};
+ const modifyMaxStreams = async (value, item) => {
+ await API.updateM3UProfile(playlist.id, {
+ ...item,
+ max_streams: value,
+ });
+ };
+
const closeEditor = () => {
setProfile(null);
setProfileEditorOpen(false);
@@ -62,27 +81,51 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
// }}
>
- Max Streams: {item.max_streams}
- toggleActive(item)}
- color="primary"
- />
- editProfile(item)}
- color="yellow.5"
- variant="transparent"
- >
-
-
- deleteProfile(item.id)}
- color="red.9"
- variant="transparent"
- >
-
-
+
+ {item.name}
+ toggleActive(item)}
+ disabled={item.is_default}
+ style={{ paddingTop: 6 }}
+ />
+
+
+
+ modifyMaxStreams(value, item)}
+ style={{ flex: 1 }}
+ />
+
+ {!item.is_default && (
+
+ editProfile(item)}
+ >
+
+
+
+ deleteProfile(item.id)}
+ size="small"
+ variant="transparent"
+ >
+
+
+
+ )}
+
))}
diff --git a/frontend/src/components/forms/Recording.jsx b/frontend/src/components/forms/Recording.jsx
new file mode 100644
index 00000000..db19e4a1
--- /dev/null
+++ b/frontend/src/components/forms/Recording.jsx
@@ -0,0 +1,122 @@
+// Modal.js
+import React, { useState, useEffect } from 'react';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import API from '../../api';
+import useEPGsStore from '../../store/epgs';
+import {
+ LoadingOverlay,
+ TextInput,
+ Button,
+ Checkbox,
+ Modal,
+ Flex,
+ NativeSelect,
+ NumberInput,
+ Space,
+ Select,
+ Alert,
+} from '@mantine/core';
+import useChannelsStore from '../../store/channels';
+import { DateTimePicker } from '@mantine/dates';
+import { CircleAlert } from 'lucide-react';
+import { isNotEmpty, useForm } from '@mantine/form';
+
+const DVR = ({ recording = null, channel = null, isOpen, onClose }) => {
+ const { channels } = useChannelsStore();
+
+ let startTime = new Date();
+ startTime.setMinutes(Math.ceil(startTime.getMinutes() / 30) * 30);
+ startTime.setSeconds(0);
+ startTime.setMilliseconds(0);
+
+ let endTime = new Date();
+ endTime.setMinutes(Math.ceil(endTime.getMinutes() / 30) * 30);
+ endTime.setSeconds(0);
+ endTime.setMilliseconds(0);
+ endTime.setHours(endTime.getHours() + 1);
+
+ const form = useForm({
+ mode: 'uncontrolled',
+ initialValues: {
+ channel_id: recording
+ ? recording.channel_id
+ : channel
+ ? `${channel.id}`
+ : '',
+ start_time: recording ? recording.start_time : startTime,
+ end_time: recording ? recording.end_time : endTime,
+ },
+
+ validate: {
+ channel_id: isNotEmpty('Select a channel'),
+ start_time: isNotEmpty('Select a start time'),
+ end_time: isNotEmpty('Select an end time'),
+ },
+ });
+
+ const onSubmit = async () => {
+ const { channel_id, ...values } = form.getValues();
+ await API.createRecording({
+ ...values,
+ channel: channel_id,
+ });
+ onClose();
+ };
+
+ if (!isOpen) {
+ return <>>;
+ }
+
+ return (
+
+ }
+ style={{ paddingBottom: 5 }}
+ >
+ Recordings may fail if active streams or overlapping recordings use up
+ all available streams
+
+
+
+
+ );
+};
+
+export default DVR;
diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx
index dc9f7b38..949e9173 100644
--- a/frontend/src/components/tables/ChannelsTable.jsx
+++ b/frontend/src/components/tables/ChannelsTable.jsx
@@ -4,6 +4,7 @@ import useChannelsStore from '../../store/channels';
import { notifications } from '@mantine/notifications';
import API from '../../api';
import ChannelForm from '../forms/Channel';
+import RecordingForm from '../forms/Recording';
import { TableHelper } from '../../helpers';
import utils from '../../utils';
import logo from '../../images/logo.png';
@@ -23,6 +24,8 @@ import {
Copy,
CircleCheck,
ScanEye,
+ EllipsisVertical,
+ CircleEllipsis,
} from 'lucide-react';
import ghostImage from '../../images/ghost.svg';
import {
@@ -42,6 +45,7 @@ import {
Center,
Container,
Switch,
+ Menu,
} from '@mantine/core';
const ChannelStreams = ({ channel, isExpanded }) => {
@@ -250,8 +254,13 @@ const ChannelsTable = ({ }) => {
channelsPageSelection,
} = useChannelsStore();
+ const {
+ environment: { env_mode },
+ } = useSettingsStore();
+
const [channel, setChannel] = useState(null);
const [channelModalOpen, setChannelModalOpen] = useState(false);
+ const [recordingModalOpen, setRecordingModalOpen] = useState(false);
const [rowSelection, setRowSelection] = useState([]);
const [channelGroupOptions, setChannelGroupOptions] = useState([]);
const [selectedProfile, setSelectedProfile] = useState(
@@ -259,6 +268,8 @@ const ChannelsTable = ({ }) => {
);
const [channelsEnabledHeaderSwitch, setChannelsEnabledHeaderSwitch] =
useState(false);
+ const [moreActionsAnchorEl, setMoreActionsAnchorEl] = useState(null);
+ const [actionsOpenRow, setActionsOpenRow] = useState(null);
const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase);
const [epgUrl, setEPGUrl] = useState(epgUrlBase);
@@ -322,30 +333,22 @@ const ChannelsTable = ({ }) => {
id: 'enabled',
Header: () => {
if (Object.values(rowSelection).length == 0) {
- return (
-
-
-
- );
+ return ;
}
return (
-
- {
- console.log(channelsPageSelection);
- toggleChannelEnabled(
- channelsPageSelection.map((row) => row.id),
- !channelsEnabledHeaderSwitch
- );
- }}
- disabled={selectedProfileId == '0'}
- />
-
+ {
+ console.log(channelsPageSelection);
+ toggleChannelEnabled(
+ channelsPageSelection.map((row) => row.id),
+ !channelsEnabledHeaderSwitch
+ );
+ }}
+ disabled={selectedProfileId == '0'}
+ />
);
},
enableSorting: false,
@@ -357,25 +360,32 @@ const ChannelsTable = ({ }) => {
return selectedProfileChannels.find((channel) => row.id == channel.id)
.enabled;
},
- size: 20,
mantineTableHeadCellProps: {
- // align: 'center',
+ align: 'right',
style: {
backgroundColor: '#3F3F46',
- minWidth: '20px',
- width: '50px !important',
- justifyContent: 'center',
- // paddingLeft: 8,
- paddingRight: 0,
+ width: '40px',
+ minWidth: '40px',
+ maxWidth: '40px',
+ // // minWidth: '20px',
+ // // width: '50px !important',
+ // // justifyContent: 'center',
+ padding: 0,
+ // // paddingLeft: 8,
+ // // paddingRight: 0,
},
},
mantineTableBodyCellProps: {
- // align: 'center',
+ align: 'right',
style: {
- minWidth: '20px',
- justifyContent: 'center',
- paddingLeft: 0,
- paddingRight: 0,
+ width: '40px',
+ minWidth: '40px',
+ maxWidth: '40px',
+ // // minWidth: '20px',
+ // // justifyContent: 'center',
+ // // paddingLeft: 0,
+ // // paddingRight: 0,
+ padding: 0,
},
},
Cell: ({ row, cell }) => (
@@ -391,25 +401,27 @@ const ChannelsTable = ({ }) => {
},
{
header: '#',
- size: 30,
+ size: 50,
+ maxSize: 50,
accessorKey: 'channel_number',
mantineTableHeadCellProps: {
- style: {
- backgroundColor: '#3F3F46',
- minWidth: '20px',
- justifyContent: 'center',
- paddingLeft: 15,
- paddingRight: 0,
- },
+ align: 'right',
+ // // style: {
+ // // backgroundColor: '#3F3F46',
+ // // // minWidth: '20px',
+ // // // justifyContent: 'center',
+ // // // paddingLeft: 15,
+ // // paddingRight: 0,
+ // // },
},
mantineTableBodyCellProps: {
- align: 'center',
- style: {
- minWidth: '20px',
- justifyContent: 'center',
- paddingLeft: 0,
- paddingRight: 0,
- },
+ align: 'right',
+ // // style: {
+ // // minWidth: '20px',
+ // // // justifyContent: 'center',
+ // // paddingLeft: 0,
+ // // paddingRight: 0,
+ // // },
},
},
{
@@ -481,6 +493,9 @@ const ChannelsTable = ({ }) => {
size: 55,
mantineTableBodyCellProps: {
align: 'center',
+ style: {
+ maxWidth: '55px',
+ },
},
Cell: ({ cell }) => (
{
await API.deleteChannel(id);
};
+ const createRecording = (channel) => {
+ setChannel(channel);
+ setRecordingModalOpen(true);
+ };
+
function handleWatchStream(channelNumber) {
let vidUrl = `/proxy/ts/stream/${channelNumber}`;
if (env_mode == 'dev') {
@@ -602,6 +622,11 @@ const ChannelsTable = ({ }) => {
setChannelModalOpen(false);
};
+ const closeRecordingForm = () => {
+ // setChannel(null);
+ setRecordingModalOpen(false);
+ };
+
useEffect(() => {
if (typeof window !== 'undefined') {
setIsLoading(false);
@@ -699,6 +724,11 @@ const ChannelsTable = ({ }) => {
);
};
+ const handleMoreActionsClick = (event, rowId) => {
+ setMoreActionsAnchorEl(event.currentTarget);
+ setActionsOpenRow(rowId);
+ };
+
const table = useMantineReactTable({
...TableHelper.defaultProperties,
columns,
@@ -734,27 +764,30 @@ const ChannelsTable = ({ }) => {
enableExpandAll: false,
displayColumnDefOptions: {
'mrt-row-select': {
- // size: 20,
+ size: 10,
+ maxSize: 10,
mantineTableHeadCellProps: {
- // align: 'center',
+ align: 'right',
style: {
- paddingLeft: 7,
- width: '30px',
- minWidth: '30px',
+ paddding: 0,
+ // paddingLeft: 7,
+ width: '20px',
+ minWidth: '20px',
backgroundColor: '#3F3F46',
},
},
mantineTableBodyCellProps: {
- align: 'center',
+ align: 'right',
style: {
- // paddingLeft: 10,
- width: '30px',
- minWidth: '30px',
+ paddingLeft: 0,
+ width: '20px',
+ minWidth: '20px',
},
},
},
'mrt-row-expand': {
size: 20,
+ maxSize: 20,
header: '',
mantineTableHeadCellProps: {
style: {
@@ -762,6 +795,7 @@ const ChannelsTable = ({ }) => {
paddingLeft: 2,
width: '20px',
minWidth: '20px',
+ maxWidth: '20px',
backgroundColor: '#3F3F46',
},
},
@@ -771,14 +805,19 @@ const ChannelsTable = ({ }) => {
paddingLeft: 2,
width: '20px',
minWidth: '20px',
+ maxWidth: '20px',
},
},
},
'mrt-row-actions': {
- size: 60,
+ size: 85,
+ maxWidth: 85,
mantineTableHeadCellProps: {
+ align: 'center',
style: {
- paddingLeft: 10,
+ minWidth: '85px',
+ maxWidth: '85px',
+ paddingRight: 40,
fontWeight: 'normal',
color: 'rgb(207,207,207)',
backgroundColor: '#3F3F46',
@@ -786,6 +825,9 @@ const ChannelsTable = ({ }) => {
},
mantineTableBodyCellProps: {
style: {
+ minWidth: '85px',
+ maxWidth: '85px',
+ paddingLeft: 0,
paddingRight: 10,
},
},
@@ -810,7 +852,7 @@ const ChannelsTable = ({ }) => {
{
@@ -823,7 +865,7 @@ const ChannelsTable = ({ }) => {
deleteChannel(row.original.id)}
@@ -834,7 +876,7 @@ const ChannelsTable = ({ }) => {
handleWatchStream(row.original.uuid)}
@@ -842,6 +884,41 @@ const ChannelsTable = ({ }) => {
+
+ {env_mode == 'dev' && (
+
+ )}
),
@@ -1170,6 +1247,12 @@ const ChannelsTable = ({ }) => {
isOpen={channelModalOpen}
onClose={closeChannelForm}
/>
+
+
);
};
diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx
index 1f7ac452..f566cd69 100644
--- a/frontend/src/pages/Guide.jsx
+++ b/frontend/src/pages/Guide.jsx
@@ -312,7 +312,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
}}
>
