Merge branch 'dev' into proxy-stats

This commit is contained in:
SergeantPanda 2025-03-27 10:02:30 -05:00
commit 616b8640f8
16 changed files with 520 additions and 158 deletions

View file

@ -12,4 +12,4 @@ def refresh_account_on_save(sender, instance, created, **kwargs):
if it is active or newly created.
"""
if created:
refresh_m3u_groups(instance.id)
refresh_single_m3u_account.delay(instance.id)

View file

@ -8,6 +8,7 @@ from celery.result import AsyncResult
from celery import shared_task, current_app, group
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
from .models import M3UAccount
from apps.channels.models import Stream, ChannelGroup, ChannelGroupM3UAccount
from asgiref.sync import async_to_sync
@ -179,7 +180,7 @@ def process_m3u_batch(account_id, batch, group_names, hash_keys):
account = M3UAccount.objects.get(id=account_id)
existing_groups = {group.name: group for group in ChannelGroup.objects.filter(
m3u_account__m3u_account=account, # Filter by the M3UAccount
m3u_account__enabled=True # Filter by the enabled flag in the join table
m3u_account__enabled=True # Filter by the enabled flag in the join table
)}
streams_to_create = []
@ -269,9 +270,29 @@ def process_m3u_batch(account_id, batch, group_names, hash_keys):
return f"Batch processed: {len(streams_to_create)} created, {len(streams_to_update)} updated."
def cleanup_streams(account_id):
account = M3UAccount.objects.get(id=account_id, is_active=True)
existing_groups = ChannelGroup.objects.filter(
m3u_account__m3u_account=account,
m3u_account__enabled=True,
).values_list('id', flat=True)
logger.info(f"Found {len(existing_groups)} active groups")
streams = Stream.objects.filter(m3u_account=account)
streams_to_delete = Stream.objects.filter(
m3u_account=account
).exclude(
channel_group__in=existing_groups # Exclude products having any of the excluded tags
)
# Delete the filtered products
streams_to_delete.delete()
logger.info(f"Cleanup complete")
def refresh_m3u_groups(account_id):
if not acquire_lock('refresh_m3u_account_groups', account_id):
return f"Task already running for account_id={account_id}."
return f"Task already running for account_id={account_id}.", None
# Record start time
start_time = time.time()
@ -345,7 +366,10 @@ def refresh_single_m3u_account(account_id, use_cache=False):
groups = data['groups']
if not extinf_data:
extinf_data, groups = refresh_m3u_groups(account_id)
try:
extinf_data, groups = refresh_m3u_groups(account_id)
except:
return "Failed to update m3u account"
hash_keys = CoreSettings.get_m3u_hash_key().split(",")
@ -371,6 +395,10 @@ def refresh_single_m3u_account(account_id, use_cache=False):
progress = int((completed_batches / total_batches) * 100)
# Send progress update via Channels
# Don't send 100% because we want to clean up after
if progress == 100:
progress = 99
send_progress_update(progress, account_id)
# Optionally remove completed task from the group to prevent processing it again
@ -378,6 +406,10 @@ def refresh_single_m3u_account(account_id, use_cache=False):
else:
logger.debug(f"Task is still running.")
# Run cleanup
cleanup_streams(account_id)
send_progress_update(100, account_id)
end_time = time.time()
# Calculate elapsed time

View file

@ -8,10 +8,11 @@
"name": "vite",
"version": "0.0.0",
"dependencies": {
"@mantine/core": "^7.17.0",
"@mantine/dates": "^7.17.0",
"@mantine/hooks": "^7.17.0",
"@mantine/notifications": "^7.17.1",
"@mantine/core": "^7.17.2",
"@mantine/dates": "^7.17.2",
"@mantine/dropzone": "^7.17.2",
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
"allotment": "^1.20.3",
"axios": "^1.8.2",
@ -1082,9 +1083,9 @@
"license": "Apache-2.0"
},
"node_modules/@mantine/core": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.1.tgz",
"integrity": "sha512-V8O3Ftq4la4I4wNDkTfH4Slkt/pCEU32pTE/DkO46zua0VFxfOAJeLjaol0s11//T+bXx82DtjMsd9APWPuFhA==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.2.tgz",
"integrity": "sha512-R6MYhitJ0JEgrhadd31Nw9FhRaQwDHjXUs5YIlitKH/fTOz9gKSxKjzmNng3bEBQCcbEDOkZj3FRcBgTUh/F0Q==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.28",
@ -1095,56 +1096,71 @@
"type-fest": "^4.27.0"
},
"peerDependencies": {
"@mantine/hooks": "7.17.1",
"@mantine/hooks": "7.17.2",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/dates": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.1.tgz",
"integrity": "sha512-L9MlIDb528RpznUeeW71xS4q3lYGolElz/f7xGRXEu9gHLaNJufbxroTw2N8RC6p/+RN1ZrSXEsjlr2euiofAw==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.2.tgz",
"integrity": "sha512-7bB992j8f+uEi280jab0/8i5yfsN/3oSrMDFwatZ+7XSDUwiP0YFib/FVX0pNSSqdFpbXhUmsZEECX71QtHw+Q==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"@mantine/core": "7.17.1",
"@mantine/hooks": "7.17.1",
"@mantine/core": "7.17.2",
"@mantine/hooks": "7.17.2",
"dayjs": ">=1.0.0",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/dropzone": {
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.17.2.tgz",
"integrity": "sha512-NMQ1SDmnW0sf3GO6p1r/VIcg/xWqlRmfnWCr00/bGRbBEGbyaUwL3LSn+KYBJdY+3/jNGvGa+xflWDvnby5tzw==",
"license": "MIT",
"dependencies": {
"react-dropzone-esm": "15.2.0"
},
"peerDependencies": {
"@mantine/core": "7.17.2",
"@mantine/hooks": "7.17.2",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/hooks": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.1.tgz",
"integrity": "sha512-mkHLrXMPd5xdI5WD7UOLwNEpdh/i6A7HaRDTXvjDE2/S0N8VmAE+BlvdyvWRMi7ODp2zVqJdP8cF1tgUn+Z0fA==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.2.tgz",
"integrity": "sha512-tbErVcGZu0E4dSmE6N0k6Tv1y9R3SQmmQgwqorcc+guEgKMdamc36lucZGlJnSGUmGj+WLUgELkEQ0asdfYBDA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
}
},
"node_modules/@mantine/notifications": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.1.tgz",
"integrity": "sha512-jsCNkkjgtsGYIMbCrzBY0UBckoXyeaSWbEoJdvMlfA+LaeOQrSLxa+ot+1+wPaoZxR+1Q1xOwC1X5bTxHKudBA==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.2.tgz",
"integrity": "sha512-vg0L8cmihz0ODg4WJ9MAyK06WPt/6g67ksIUFxd4F8RfdJbIMLTsNG9yWoSfuhtXenUg717KaA917IWLjDSaqw==",
"license": "MIT",
"dependencies": {
"@mantine/store": "7.17.1",
"@mantine/store": "7.17.2",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@mantine/core": "7.17.1",
"@mantine/hooks": "7.17.1",
"@mantine/core": "7.17.2",
"@mantine/hooks": "7.17.2",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/store": {
"version": "7.17.1",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.1.tgz",
"integrity": "sha512-is1c0FycakMsbTElKGWO59LarjMIk24JUXfjP/QIrB0XqpXreq3u7aN4hoNqr1IftTZSfVBii4W8pVFeWaV55g==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.2.tgz",
"integrity": "sha512-UoMUYQK/z58hMueCkpDIXc49gPgrVO/zcpb0k+B7MFU51EIUiFzHLxLFBmWrgCAM6rzJORqN8JjyCd/PB9j4aw==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
@ -3603,6 +3619,21 @@
"node": ">=6"
}
},
"node_modules/react-dropzone-esm": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.2.0.tgz",
"integrity": "sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",

View file

@ -10,10 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^7.17.0",
"@mantine/dates": "^7.17.0",
"@mantine/hooks": "^7.17.0",
"@mantine/notifications": "^7.17.1",
"@mantine/core": "^7.17.2",
"@mantine/dates": "^7.17.2",
"@mantine/dropzone": "^7.17.2",
"@mantine/hooks": "^7.17.2",
"@mantine/notifications": "^7.17.2",
"@tabler/icons-react": "^3.31.0",
"allotment": "^1.20.3",
"axios": "^1.8.2",

View file

@ -22,6 +22,7 @@ import { Box, AppShell, MantineProvider } from '@mantine/core';
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 './index.css';
import mantineTheme from './mantineTheme';
import API from './api';

View file

@ -19,7 +19,7 @@ export const WebsocketProvider = ({ children }) => {
const { fetchStreams } = useStreamsStore();
const { setChannelStats, fetchChannelGroups } = useChannelsStore();
const { setRefreshProgress } = usePlaylistsStore();
const { fetchPlaylists, setRefreshProgress } = usePlaylistsStore();
const { fetchEPGData } = useEPGsStore();
const ws = useRef(null);
@ -68,6 +68,7 @@ export const WebsocketProvider = ({ children }) => {
fetchStreams();
fetchChannelGroups();
fetchEPGData();
fetchPlaylists();
}
setRefreshProgress(event.data.account, event.data.progress);
}

View file

@ -197,13 +197,33 @@ export default class API {
static async updateChannel(values) {
const { id, ...payload } = values;
let body = null;
if (values.logo_file) {
// Must send FormData for file upload
body = new FormData();
for (const prop in values) {
body.append(prop, values[prop]);
}
} else {
body = { ...values };
delete body.logo_file;
body = JSON.stringify(body);
}
console.log(body);
const response = await fetch(`${host}/api/channels/channels/${id}/`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
...(values.logo_file
? {}
: {
'Content-Type': 'application/json',
}),
},
body: JSON.stringify(payload),
body: body,
});
const retval = await response.json();

View file

@ -3,8 +3,7 @@ import React, { useEffect, useRef } from 'react';
import Draggable from 'react-draggable';
import useVideoStore from '../store/useVideoStore';
import mpegts from 'mpegts.js';
import { ActionIcon, Flex } from '@mantine/core';
import { SquareX } from 'lucide-react';
import { CloseButton, Flex } from '@mantine/core';
export default function FloatingVideo() {
const { isVisible, streamUrl, hideVideo } = useVideoStore();
@ -68,9 +67,7 @@ export default function FloatingVideo() {
>
{/* Simple header row with a close button */}
<Flex justify="flex-end" style={{ padding: 3 }}>
<ActionIcon variant="transparent" onClick={hideVideo}>
<SquareX color="red" size="30" />
</ActionIcon>
<CloseButton onClick={hideVideo} />
</Flex>
{/* The <video> element used by mpegts.js */}

View file

@ -22,11 +22,17 @@ import {
Grid,
Flex,
Select,
Divider,
Stack,
useMantineTheme,
} from '@mantine/core';
import { SquarePlus } from 'lucide-react';
import { ListOrdered, SquarePlus, SquareX } from 'lucide-react';
import useEPGsStore from '../../store/epgs';
import { Dropzone } from '@mantine/dropzone';
const Channel = ({ channel = null, isOpen, onClose }) => {
const theme = useMantineTheme();
const channelGroups = useChannelsStore((state) => state.channelGroups);
const streams = useStreamsStore((state) => state.streams);
const { profiles: streamProfiles } = useStreamProfilesStore();
@ -34,7 +40,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
const { tvgs } = useEPGsStore();
const [logoFile, setLogoFile] = useState(null);
const [logoPreview, setLogoPreview] = useState(logo);
const [logoPreview, setLogoPreview] = useState(null);
const [channelStreams, setChannelStreams] = useState([]);
const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);
@ -50,11 +56,14 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
setChannelStreams(Array.from(streamSet));
};
const handleLogoChange = (e) => {
const file = e.target.files[0];
if (file) {
setLogoFile(file);
setLogoPreview(URL.createObjectURL(file));
const handleLogoChange = (files) => {
if (files.length === 1) {
console.log(files[0]);
setLogoFile(files[0]);
setLogoPreview(URL.createObjectURL(files[0]));
} else {
setLogoFile(null);
setLogoPreview(null);
}
};
@ -77,6 +86,10 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
values.stream_profile_id = null;
}
if (values.stream_profile_id == null) {
delete values.stream_profile_id;
}
if (channel?.id) {
await API.updateChannel({
id: channel.id,
@ -94,7 +107,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
resetForm();
setLogoFile(null);
setLogoPreview(logo);
setLogoPreview(null);
setSubmitting(false);
onClose();
},
@ -250,14 +263,23 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
return <></>;
}
console.log(streamProfiles);
return (
<>
<Modal opened={isOpen} onClose={onClose} size={800} title="Channel">
<Modal
opened={isOpen}
onClose={onClose}
size={1000}
title={
<Group gap="5">
<ListOrdered size="20" />
<Text>Channels</Text>
</Group>
}
styles={{ content: { '--mantine-color-body': '#27272A' } }}
>
<form onSubmit={formik.handleSubmit}>
<Grid gap={2}>
<Grid.Col span={6}>
<Group justify="space-between" align="top">
<Stack gap="5" style={{ flex: 1 }}>
<TextInput
id="name"
name="name"
@ -265,40 +287,43 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
value={formik.values.name}
onChange={formik.handleChange}
error={formik.errors.name ? formik.touched.name : ''}
size="xs"
/>
<Grid>
<Grid.Col span={11}>
<Select
id="channel_group_id"
name="channel_group_id"
label="Channel Group"
value={formik.values.channel_group_id}
onChange={formik.handleChange}
error={
formik.errors.channel_group_id
? formik.touched.channel_group_id
: ''
}
data={Object.values(channelGroups).map((option, index) => ({
value: `${option.id}`,
label: option.name,
}))}
/>
</Grid.Col>
<Grid.Col span={1}>
<Flex gap="sm">
<Select
id="channel_group_id"
name="channel_group_id"
label="Channel Group"
value={formik.values.channel_group_id}
onChange={(value) => {
formik.setFieldValue('channel_group_id', value); // Update Formik's state with the new value
}}
error={
formik.errors.channel_group_id
? formik.touched.channel_group_id
: ''
}
data={Object.values(channelGroups).map((option, index) => ({
value: `${option.id}`,
label: option.name,
}))}
size="xs"
style={{ flex: 1 }}
/>
<Flex align="flex-end">
<ActionIcon
color="green.5"
color={theme.tailwind.green[5]}
onClick={() => setChannelGroupModalOpen(true)}
title="Create new group"
size="small"
variant="light"
style={{ marginTop: '175%' }} // @TODO: I don't like this, figure out better placement
variant="transparent"
style={{ marginBottom: 5 }}
>
<SquarePlus />
<SquarePlus size="20" />
</ActionIcon>
</Grid.Col>
</Grid>
</Flex>
</Flex>
<Select
id="stream_profile_id"
@ -319,6 +344,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
label: option.name,
}))
)}
size="xs"
/>
<TextInput
@ -332,10 +358,81 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
? formik.touched.channel_number
: ''
}
size="xs"
/>
</Grid.Col>
</Stack>
<Grid.Col span={6}>
<Divider size="sm" orientation="vertical" />
<Stack justify="flex-start" style={{ flex: 1 }}>
<TextInput
id="logo_url"
name="logo_url"
label="Logo (URL)"
value={formik.values.logo_url}
onChange={formik.handleChange}
error={formik.errors.logo_url ? formik.touched.logo_url : ''}
size="xs"
/>
<Group>
<Divider size="xs" style={{ flex: 1 }} />
<Text size="xs" c="dimmed">
OR
</Text>
<Divider size="xs" style={{ flex: 1 }} />
</Group>
<Stack>
<Group justify="space-between">
<Text size="sm">Upload Logo</Text>
{logoPreview && (
<ActionIcon
variant="transparent"
color="red.9"
onClick={handleLogoChange}
>
<SquareX />
</ActionIcon>
)}
</Group>
<Dropzone
onDrop={handleLogoChange}
onReject={(files) => console.log('rejected files', files)}
maxSize={5 * 1024 ** 2}
>
<Group
justify="center"
gap="xl"
mih={40}
style={{ pointerEvents: 'none' }}
>
<div>
{logoPreview && (
<Center>
<img
src={logoPreview || logo}
alt="Selected"
style={{ maxWidth: 50, height: 'auto' }}
/>
</Center>
)}
{!logoPreview && (
<Text size="sm" inline>
Drag images here or click to select files
</Text>
)}
</div>
</Group>
</Dropzone>
<Center></Center>
</Stack>
</Stack>
<Divider size="sm" orientation="vertical" />
<Stack gap="5" style={{ flex: 1 }} justify="flex-start">
<TextInput
id="tvg_name"
name="tvg_name"
@ -343,6 +440,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
value={formik.values.tvg_name}
onChange={formik.handleChange}
error={formik.errors.tvg_name ? formik.touched.tvg_name : ''}
size="xs"
/>
<Select
@ -359,6 +457,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
value: tvg.name,
label: tvg.tvg_id,
}))}
size="xs"
/>
<TextInput
@ -368,34 +467,10 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
style={{ marginBottom: 2 }}
value={formik.values.logo_url}
onChange={formik.handleChange}
size="xs"
/>
<Group style={{ paddingTop: 10 }}>
<Text>Logo</Text>
{/* Display selected image */}
<Box>
<img
src={logoPreview}
alt="Selected"
style={{ maxWidth: 50, height: 'auto' }}
/>
</Box>
<input
type="file"
id="logo"
name="logo"
accept="image/*"
onChange={(event) => handleLogoChange(event)}
style={{ display: 'none' }}
/>
<label htmlFor="logo">
<Button variant="contained" component="span" size="small">
Browse...
</Button>
</label>
</Group>
</Grid.Col>
</Grid>
</Stack>
</Group>
{/* <Grid gap={2}>
<Grid.Col span={6}>
@ -412,8 +487,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
<Button
type="submit"
variant="contained"
color="primary"
variant="default"
disabled={formik.isSubmitting}
>
Submit

View file

@ -14,7 +14,6 @@ import {
Flex,
NativeSelect,
FileInput,
Select,
Space,
} from '@mantine/core';
import M3UGroupFilter from './M3UGroupFilter';
@ -30,6 +29,7 @@ const M3U = ({ playlist = null, isOpen, onClose, playlistCreated = false }) => {
const [loadingText, setLoadingText] = useState('');
const handleFileChange = (file) => {
console.log(file);
if (file) {
setFile(file);
}
@ -57,13 +57,16 @@ const M3U = ({ playlist = null, isOpen, onClose, playlistCreated = false }) => {
uploaded_file: file,
});
} else {
setLoadingText('Loading groups...');
newPlaylist = await API.addPlaylist({
...values,
uploaded_file: file,
});
await fetchChannelGroups();
// Don't prompt for group filters, but keeping this here
// in case we want to revive it
newPlaylist = null;
}
resetForm();
@ -204,12 +207,23 @@ const M3U = ({ playlist = null, isOpen, onClose, playlistCreated = false }) => {
</Button>
</>
)}
{!playlist && (
<Button
type="submit"
variant="contained"
color="primary"
disabled={formik.isSubmitting}
size="sm"
>
Save & Select Groups
</Button>
)}
<Button
type="submit"
variant="contained"
color="primary"
disabled={formik.isSubmitting}
size="small"
size="sm"
>
Save
</Button>

View file

@ -1,41 +1,41 @@
.mantine-Stack-root .navlink {
display: flex;
flex-direction: row; /* Ensures horizontal layout */
flex-wrap: nowrap;
align-items: center;
gap: 12px;
padding: 5px 8px !important;
border-radius: 6px;
color: #D4D4D8; /* Default color when not active */
background-color: transparent; /* Default background when not active */
border: 1px solid transparent;
transition: all 0.3s ease;
}
display: flex;
flex-direction: row; /* Ensures horizontal layout */
flex-wrap: nowrap;
align-items: center;
gap: 12px;
padding: 5px 8px !important;
border-radius: 6px;
color: #D4D4D8; /* Default color when not active */
background-color: transparent; /* Default background when not active */
border: 1px solid transparent;
transition: all 0.3s ease;
}
/* Active state styles */
.navlink.navlink-active {
color: #FFFFFF;
background-color: #245043;
border: 1px solid #3BA882;
}
/* Active state styles */
.navlink.navlink-active {
color: #FFFFFF;
background-color: #245043;
border: 1px solid #3BA882;
}
/* Hover effect */
.navlink:hover {
background-color: #2A2F34; /* Gray hover effect when not active */
border: 1px solid #3D3D42;
}
/* Hover effect */
.navlink:hover {
background-color: #2A2F34; /* Gray hover effect when not active */
border: 1px solid #3D3D42;
}
/* Hover effect for active state */
.navlink.navlink-active:hover {
background-color: #3A3A40;
border: 1px solid #3BA882;
}
/* Hover effect for active state */
.navlink.navlink-active:hover {
background-color: #3A3A40;
border: 1px solid #3BA882;
}
/* Collapse condition for justifyContent */
.navlink.navlink-collapsed {
justify-content: center;
}
/* Collapse condition for justifyContent */
.navlink.navlink-collapsed {
justify-content: center;
}
.navlink:not(.navlink-collapsed) {
justify-content: flex-start;
}
.navlink:not(.navlink-collapsed) {
justify-content: flex-start;
}

View file

@ -245,6 +245,8 @@ const ChannelsTable = ({}) => {
handleFilterChange(column.id, e.target.value);
}}
size="xs"
variant="unstyled"
className="table-input-header"
/>
),
Cell: ({ cell }) => (
@ -273,6 +275,8 @@ const ChannelsTable = ({}) => {
handleGroupChange(value);
}}
data={channelGroupOptions}
variant="unstyled"
className="table-input-header"
/>
),
},
@ -490,6 +494,12 @@ const ChannelsTable = ({}) => {
'mrt-row-expand': {
size: 10,
header: '',
mantineTableHeadCellProps: {
padding: 0,
},
mantineTableBodyCellProps: {
padding: 0,
},
},
'mrt-row-actions': {
size: 74,
@ -516,7 +526,7 @@ const ChannelsTable = ({}) => {
<ActionIcon
size="sm"
variant="transparent"
color="yellow.5"
color={theme.tailwind.yellow[3]}
onClick={() => {
editChannel(row.original);
}}
@ -529,7 +539,7 @@ const ChannelsTable = ({}) => {
<ActionIcon
size="sm"
variant="transparent"
color="red.9"
color={theme.tailwind.red[6]}
onClick={() => deleteChannel(row.original.id)}
>
<SquareMinus size="18" />
@ -540,7 +550,7 @@ const ChannelsTable = ({}) => {
<ActionIcon
size="sm"
variant="transparent"
color="green.5"
color={theme.tailwind.green[5]}
onClick={() => handleWatchStream(row.original.uuid)}
>
<CirclePlay size="18" />
@ -701,6 +711,7 @@ const ChannelsTable = ({}) => {
<Paper
style={{
height: 'calc(100vh - 75px)',
backgroundColor: '#27272A',
}}
>
{/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
@ -751,10 +762,10 @@ const ChannelsTable = ({}) => {
size="xs"
onClick={() => editChannel()}
p={5}
color="green"
color={theme.tailwind.green[5]}
style={{
borderWidth: '1px',
borderColor: 'green',
borderColor: theme.tailwind.green[5],
color: 'white',
}}
>

View file

@ -33,6 +33,7 @@ import {
NumberInput,
NativeSelect,
MultiSelect,
useMantineTheme,
} from '@mantine/core';
import {
IconArrowDown,
@ -45,6 +46,8 @@ import {
import { useNavigate } from 'react-router-dom';
const StreamsTable = ({}) => {
const theme = useMantineTheme();
/**
* useState
*/
@ -118,6 +121,11 @@ const StreamsTable = ({}) => {
onClick={(e) => e.stopPropagation()}
onChange={handleFilterChange}
size="xs"
variant="unstyled"
className="table-input-header"
style={{
paddingLeft: 10,
}}
/>
),
Cell: ({ cell }) => (
@ -146,6 +154,8 @@ const StreamsTable = ({}) => {
onClick={handleSelectClick}
onChange={handleGroupChange}
data={groupOptions}
variant="unstyled"
className="table-input-header"
/>
</Box>
),
@ -179,6 +189,8 @@ const StreamsTable = ({}) => {
label: playlist.name,
value: `${playlist.id}`,
}))}
variant="unstyled"
className="table-input-header"
/>
</Box>
),
@ -498,7 +510,7 @@ const StreamsTable = ({}) => {
<Tooltip label="Add to Channel">
<ActionIcon
size="sm"
color="blue.5"
color={theme.tailwind.blue[4]}
variant="transparent"
onClick={() => addStreamToChannel(row.original.id)}
disabled={
@ -516,7 +528,7 @@ const StreamsTable = ({}) => {
<Tooltip label="Create New Channel">
<ActionIcon
size="sm"
color="green.5"
color={theme.tailwind.green[5]}
variant="transparent"
onClick={() => createChannelFromStream(row.original)}
>
@ -563,6 +575,7 @@ const StreamsTable = ({}) => {
displayColumnDefOptions: {
'mrt-row-actions': {
size: 30,
color: 'white',
},
'mrt-row-select': {
size: 20,
@ -618,6 +631,7 @@ const StreamsTable = ({}) => {
<Paper
style={{
height: 'calc(100vh - 75px)',
backgroundColor: '#27272A',
}}
>
{/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */}
@ -655,10 +669,10 @@ const StreamsTable = ({}) => {
size="xs"
onClick={() => editStream()}
p={5}
color="green"
color={theme.tailwind.green[5]}
style={{
borderWidth: '1px',
borderColor: 'green',
borderColor: theme.tailwind.green[5],
color: 'white',
}}
>

View file

@ -17,6 +17,7 @@ export default {
mantinePaperProps: {
style: {
'--mrt-selected-row-background-color': '#163632',
'--mrt-base-background-color': '#27272A',
},
},
mantineSelectAllCheckboxProps: {

View file

@ -39,3 +39,9 @@ code {
table.mrt-table tr.mantine-Table-tr.mantine-Table-tr-detail-panel td.mantine-Table-td-detail-panel {
width: 100% !important;
}
.table-input-header input::placeholder {
color: rgb(207,207,207);
font-weight: normal;
font-size: 14px;
}

View file

@ -1,6 +1,165 @@
import { createTheme, MantineProvider, rem } from '@mantine/core';
const theme = createTheme({
tailwind: {
red: [
'oklch(0.971 0.013 17.38)',
'oklch(0.936 0.032 17.717)',
'oklch(0.885 0.062 18.334)',
'oklch(0.808 0.114 19.571)',
'oklch(0.704 0.191 22.216)',
'oklch(0.637 0.237 25.331)',
'oklch(0.577 0.245 27.325)',
'oklch(0.505 0.213 27.518)',
'oklch(0.444 0.177 26.899)',
'oklch(0.396 0.141 25.723)',
'oklch(0.258 0.092 26.042)',
],
orange: [
'oklch(0.98 0.016 73.684)',
'oklch(0.954 0.038 75.164)',
'oklch(0.901 0.076 70.697)',
'oklch(0.837 0.128 66.29)',
'oklch(0.75 0.183 55.934)',
'oklch(0.705 0.213 47.604)',
'oklch(0.646 0.222 41.116)',
'oklch(0.553 0.195 38.402)',
'oklch(0.47 0.157 37.304)',
'oklch(0.408 0.123 38.172)',
'oklch(0.266 0.079 36.259)',
],
amber: [
'oklch(0.987 0.022 95.277)',
'oklch(0.962 0.059 95.617)',
'oklch(0.924 0.12 95.746)',
'oklch(0.879 0.169 91.605)',
'oklch(0.828 0.189 84.429)',
'oklch(0.769 0.188 70.08)',
'oklch(0.666 0.179 58.318)',
'oklch(0.555 0.163 48.998)',
'oklch(0.473 0.137 46.201)',
'oklch(0.414 0.112 45.904)',
'oklch(0.279 0.077 45.635)',
],
yellow: [
'oklch(0.987 0.026 102.212)',
'oklch(0.973 0.071 103.193)',
'oklch(0.945 0.129 101.54)',
'oklch(0.905 0.182 98.111)',
'oklch(0.852 0.199 91.936)',
'oklch(0.795 0.184 86.047)',
'oklch(0.681 0.162 75.834)',
'oklch(0.554 0.135 66.442)',
'oklch(0.476 0.114 61.907)',
'oklch(0.421 0.095 57.708)',
'oklch(0.286 0.066 53.813)',
],
lime: [
'oklch(0.986 0.031 120.757)',
'oklch(0.967 0.067 122.328)',
'oklch(0.938 0.127 124.321)',
'oklch(0.897 0.196 126.665)',
'oklch(0.841 0.238 128.85)',
'oklch(0.768 0.233 130.85)',
'oklch(0.648 0.2 131.684)',
'oklch(0.532 0.157 131.589)',
'oklch(0.453 0.124 130.933)',
'oklch(0.405 0.101 131.063)',
'oklch(0.274 0.072 132.109)',
],
green: [
'oklch(0.982 0.018 155.826)',
'oklch(0.962 0.044 156.743)',
'oklch(0.925 0.084 155.995)',
'oklch(0.871 0.15 154.449)',
'oklch(0.792 0.209 151.711)',
'oklch(0.723 0.219 149.579)',
'oklch(0.627 0.194 149.214)',
'oklch(0.527 0.154 150.069)',
'oklch(0.448 0.119 151.328)',
'oklch(0.393 0.095 152.535)',
'oklch(0.266 0.065 152.934)',
],
emerald: [
'oklch(0.979 0.021 166.113)',
'oklch(0.95 0.052 163.051)',
'oklch(0.905 0.093 164.15)',
'oklch(0.845 0.143 164.978)',
'oklch(0.765 0.177 163.223)',
'oklch(0.696 0.17 162.48)',
'oklch(0.596 0.145 163.225)',
'oklch(0.508 0.118 165.612)',
'oklch(0.432 0.095 166.913)',
'oklch(0.378 0.077 168.94)',
'oklch(0.262 0.051 172.552)',
],
teal: [
'oklch(0.984 0.014 180.72)',
'oklch(0.953 0.051 180.801)',
'oklch(0.91 0.096 180.426)',
'oklch(0.855 0.138 181.071)',
'oklch(0.777 0.152 181.912)',
'oklch(0.704 0.14 182.503)',
'oklch(0.6 0.118 184.704)',
'oklch(0.511 0.096 186.391)',
'oklch(0.437 0.078 188.216)',
'oklch(0.386 0.063 188.416)',
'oklch(0.277 0.046 192.524)',
],
cyan: [
'oklch(0.984 0.019 200.873)',
'oklch(0.956 0.045 203.388)',
'oklch(0.917 0.08 205.041)',
'oklch(0.865 0.127 207.078)',
'oklch(0.789 0.154 211.53)',
'oklch(0.715 0.143 215.221)',
'oklch(0.609 0.126 221.723)',
'oklch(0.52 0.105 223.128)',
'oklch(0.45 0.085 224.283)',
'oklch(0.398 0.07 227.392)',
'oklch(0.302 0.056 229.695)',
],
sky: [
'oklch(0.977 0.013 236.62)',
'oklch(0.951 0.026 236.824)',
'oklch(0.901 0.058 230.902)',
'oklch(0.828 0.111 230.318)',
'oklch(0.746 0.16 232.661)',
'oklch(0.685 0.169 237.323)',
'oklch(0.588 0.158 241.966)',
'oklch(0.5 0.134 242.749)',
'oklch(0.443 0.11 240.79)',
'oklch(0.391 0.09 240.876)',
'oklch(0.293 0.066 243.157)',
],
blue: [
'oklch(0.97 0.014 254.604)',
'oklch(0.932 0.032 255.585)',
'oklch(0.882 0.059 254.128)',
'oklch(0.809 0.105 251.813)',
'oklch(0.707 0.165 254.624)',
'oklch(0.623 0.214 259.815)',
'oklch(0.546 0.214 264.39)',
'oklch(0.466 0.188 267.053)',
'oklch(0.396 0.156 269.132)',
'oklch(0.342 0.125 270.105)',
'oklch(0.246 0.092 272.445)',
],
indigo: [
'oklch(0.938 0.015 268.693)',
'oklch(0.901 0.036 269.522)',
'oklch(0.843 0.072 267.564)',
'oklch(0.761 0.12 264.179)',
'oklch(0.652 0.159 267.585)',
'oklch(0.573 0.194 271.68)',
'oklch(0.492 0.205 275.687)',
'oklch(0.418 0.179 278.963)',
'oklch(0.357 0.145 282.163)',
'oklch(0.305 0.112 283.928)',
'oklch(0.213 0.078 287.434)',
],
},
palette: {
mode: 'dark',
background: {