mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-21 09:09:22 +00:00
Merge branch 'dev' into proxy-stats
This commit is contained in:
commit
616b8640f8
16 changed files with 520 additions and 158 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
81
frontend/package-lock.json
generated
81
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export default {
|
|||
mantinePaperProps: {
|
||||
style: {
|
||||
'--mrt-selected-row-background-color': '#163632',
|
||||
'--mrt-base-background-color': '#27272A',
|
||||
},
|
||||
},
|
||||
mantineSelectAllCheckboxProps: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue