diff --git a/frontend/src/components/M3URefreshNotification.jsx b/frontend/src/components/M3URefreshNotification.jsx
index 2cb6fdd8..5eefeef0 100644
--- a/frontend/src/components/M3URefreshNotification.jsx
+++ b/frontend/src/components/M3URefreshNotification.jsx
@@ -29,6 +29,7 @@ export default function M3URefreshNotification() {
return;
}
+ // Store the updated status first
setNotificationStatus({
...notificationStatus,
[data.account]: data,
@@ -49,11 +50,17 @@ export default function M3URefreshNotification() {
return; // Exit early for any error status
}
+ // Check if we already have an error stored for this account, and if so, don't show further notifications
+ const currentStatus = notificationStatus[data.account];
+ if (currentStatus && currentStatus.status === "error") {
+ // Don't show any other notifications once we've hit an error
+ return;
+ }
+
const taskProgress = data.progress;
// Only show start and completion notifications for normal operation
if (data.progress != 0 && data.progress != 100) {
- console.log('not 0 or 100');
return;
}
@@ -98,6 +105,18 @@ export default function M3URefreshNotification() {
};
useEffect(() => {
+ // Reset notificationStatus when playlists change to prevent stale data
+ if (playlists.length > 0 && Object.keys(notificationStatus).length > 0) {
+ const validIds = playlists.map(p => p.id);
+ const currentIds = Object.keys(notificationStatus).map(Number);
+
+ // If we have notification statuses for playlists that no longer exist, reset the state
+ if (!currentIds.every(id => validIds.includes(id))) {
+ setNotificationStatus({});
+ }
+ }
+
+ // Process all refresh progress updates
Object.values(refreshProgress).map((data) => handleM3UUpdate(data));
}, [playlists, refreshProgress]);
diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx
index 778ae320..cd3773ac 100644
--- a/frontend/src/components/tables/EPGsTable.jsx
+++ b/frontend/src/components/tables/EPGsTable.jsx
@@ -22,7 +22,8 @@ import { notifications } from '@mantine/notifications';
import { IconSquarePlus } from '@tabler/icons-react';
import { RefreshCcw, SquareMinus, SquarePen } from 'lucide-react';
import dayjs from 'dayjs';
-
+import useSettingsStore from '../../store/settings';
+import useLocalStorage from '../../hooks/useLocalStorage';
// Helper function to format status text
const formatStatusText = (status) => {
@@ -51,6 +52,14 @@ const EPGsTable = () => {
const refreshProgress = useEPGsStore((s) => s.refreshProgress);
const theme = useMantineTheme();
+ // Get tableSize directly from localStorage instead of the store
+ const [tableSize] = useLocalStorage('table-size', 'default');
+
+ // Get proper size for action icons to match ChannelsTable
+ const iconSize = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm';
+
+ // Calculate density for Mantine Table
+ const tableDensity = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'xl' : 'md';
const toggleActive = async (epg) => {
try {
@@ -278,11 +287,12 @@ const EPGsTable = () => {
isLoading,
sorting,
rowSelection,
+ density: tableDensity,
},
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
- density: 'compact',
+ density: tableDensity,
},
enableRowActions: true,
positionActionsColumn: 'last',
@@ -296,28 +306,28 @@ const EPGsTable = () => {
<>
editEPG(row.original)}
>
- {/* Small icon size */}
+ {/* Small icon size */}
deleteEPG(row.original.id)}
>
- {/* Small icon size */}
+ {/* Small icon size */}
refreshEPG(row.original.id)}
disabled={!row.original.is_active}
>
- {/* Small icon size */}
+ {/* Small icon size */}
>
),
@@ -327,6 +337,18 @@ const EPGsTable = () => {
overflowX: 'auto', // Ensure horizontal scrolling works
},
},
+ mantineTableProps: {
+ ...TableHelper.defaultProperties.mantineTableProps,
+ className: `table-size-${tableSize}`,
+ },
+ // Add custom cell styles to match CustomTable's sizing
+ mantineTableBodyCellProps: {
+ style: {
+ height: tableSize === 'compact' ? '28px' : tableSize === 'large' ? '48px' : '40px',
+ fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-sm)' : 'var(--mantine-font-size-md)',
+ padding: tableSize === 'compact' ? '2px 8px' : '4px 10px'
+ }
+ },
});
return (
diff --git a/frontend/src/components/tables/M3UsTable.jsx b/frontend/src/components/tables/M3UsTable.jsx
index 9f59f6c3..4a22ff87 100644
--- a/frontend/src/components/tables/M3UsTable.jsx
+++ b/frontend/src/components/tables/M3UsTable.jsx
@@ -16,10 +16,14 @@ import {
Switch,
Progress,
Stack,
+ Badge,
+ Group,
} from '@mantine/core';
import { SquareMinus, SquarePen, RefreshCcw, Check, X } from 'lucide-react';
import { IconSquarePlus } from '@tabler/icons-react'; // Import custom icons
import dayjs from 'dayjs';
+import useSettingsStore from '../../store/settings';
+import useLocalStorage from '../../hooks/useLocalStorage';
// Helper function to format status text
const formatStatusText = (status) => {
@@ -58,6 +62,7 @@ const M3UTable = () => {
const setRefreshProgress = usePlaylistsStore((s) => s.setRefreshProgress);
const theme = useMantineTheme();
+ const [tableSize] = useLocalStorage('table-size', 'default');
const generateStatusString = (data) => {
if (data.progress == 100) {
@@ -232,150 +237,6 @@ const M3UTable = () => {
);
};
- const toggleActive = async (playlist) => {
- try {
- // Send only the is_active field to trigger our special handling
- await API.updatePlaylist({
- id: playlist.id,
- is_active: !playlist.is_active,
- }, true); // Add a new parameter to indicate this is just a toggle
- } catch (error) {
- console.error('Error toggling active state:', error);
- }
- };
-
- const columns = useMemo(
- //column definitions...
- () => [
- {
- header: 'Name',
- accessorKey: 'name',
- size: 150,
- minSize: 100, // Minimum width
- },
- {
- header: 'URL / File',
- accessorKey: 'server_url',
- size: 200,
- minSize: 120,
- Cell: ({ cell, row }) => {
- const value = cell.getValue() || row.original.file_path || '';
- return (
-
-
- {value}
-
-
- );
- },
- },
- {
- header: 'Max Streams',
- accessorKey: 'max_streams',
- size: 120,
- minSize: 80,
- },
- {
- header: 'Status',
- accessorKey: 'status',
- size: 100,
- minSize: 80,
- Cell: ({ row }) => {
- const data = row.original;
-
- // Check if there's an active progress for this M3U
- if (refreshProgress[data.id] && refreshProgress[data.id].progress < 100) {
- return generateStatusString(refreshProgress[data.id]);
- }
-
- // Return simple text display with appropriate color
- return (
-
- {formatStatusText(data.status)}
-
- );
- },
- },
- {
- header: 'Status Message',
- accessorKey: 'last_message',
- size: 250,
- minSize: 150,
- enableSorting: false,
- Cell: ({ row }) => {
- const data = row.original;
-
- // Show error message when status is error
- if (data.status === 'error' && data.last_message) {
- return (
-
-
- {data.last_message}
-
-
- );
- }
-
- // Show success message for successful sources
- if (data.status === 'success' && data.last_message) {
- return (
-
- {data.last_message}
-
- );
- }
-
- // Otherwise return empty cell
- return null;
- },
- },
- {
- header: 'Active',
- accessorKey: 'is_active',
- size: 80,
- minSize: 60,
- sortingFn: 'basic',
- mantineTableBodyCellProps: {
- align: 'left',
- },
- Cell: ({ row, cell }) => (
-
- toggleActive(row.original)}
- />
-
- ),
- },
- {
- header: 'Updated',
- accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'),
- size: 180,
- minSize: 100,
- enableSorting: false,
- },
- ],
- [refreshProgress, theme]
- );
-
- //optionally access the underlying virtualizer instance
- const rowVirtualizerInstanceRef = useRef(null);
-
- const [isLoading, setIsLoading] = useState(true);
- const [sorting, setSorting] = useState([]);
-
const editPlaylist = async (playlist = null) => {
if (playlist) {
setPlaylist(playlist);
@@ -414,6 +275,157 @@ const M3UTable = () => {
setIsLoading(false);
};
+ const toggleActive = async (playlist) => {
+ try {
+ // Send only the is_active field to trigger our special handling
+ await API.updatePlaylist({
+ id: playlist.id,
+ is_active: !playlist.is_active,
+ }, true); // Add a new parameter to indicate this is just a toggle
+ } catch (error) {
+ console.error('Error toggling active state:', error);
+ }
+ };
+
+ const columns = useMemo(
+ () => [
+ {
+ header: 'Name',
+ accessorKey: 'name',
+ size: 150,
+ minSize: 100, // Minimum width
+ },
+ {
+ header: 'Account Type',
+ accessorKey: 'account_type',
+ size: 100,
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ return value === 'XC' ? 'XC' : 'M3U';
+ },
+ },
+ {
+ header: 'URL / File',
+ accessorKey: 'server_url',
+ size: 200,
+ minSize: 120,
+ Cell: ({ cell, row }) => {
+ const value = cell.getValue() || row.original.file_path || '';
+ return (
+
+
+ {value}
+
+
+ );
+ },
+ },
+ {
+ header: 'Max Streams',
+ accessorKey: 'max_streams',
+ size: 100,
+ },
+ {
+ header: 'Status',
+ accessorKey: 'status',
+ size: 100,
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ if (!value) return null;
+
+ // Match EPG table styling with Text component
+ return (
+
+ {formatStatusText(value)}
+
+ );
+ },
+ },
+ {
+ header: 'Status Message',
+ accessorKey: 'last_message',
+ size: 200,
+ Cell: ({ cell, row }) => {
+ const value = cell.getValue();
+ if (!value) return null;
+
+ const data = row.original;
+
+ // Show error message with red styling for errors
+ if (data.status === 'error') {
+ return (
+
+
+ {value}
+
+
+ );
+ }
+
+ // Show success message with green styling for success
+ if (data.status === 'success') {
+ return (
+
+
+ {value}
+
+
+ );
+ }
+
+ // For all other status values, just use dimmed text
+ return (
+
+
+ {value}
+
+
+ );
+ },
+ },
+ {
+ header: 'Updated',
+ accessorKey: 'updated_at',
+ size: 120,
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ return value ? new Date(value).toLocaleString() : 'Never';
+ },
+ },
+ {
+ header: 'Active',
+ accessorKey: 'is_active',
+ size: 80,
+ Cell: ({ cell, row }) => {
+ return (
+
+ toggleActive(row.original)}
+ />
+
+ );
+ },
+ },
+ // Remove the custom Actions column here
+ ],
+ [refreshPlaylist, editPlaylist, deletePlaylist, toggleActive]
+ );
+
+ //optionally access the underlying virtualizer instance
+ const rowVirtualizerInstanceRef = useRef(null);
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [sorting, setSorting] = useState([]);
+
const closeModal = (newPlaylist = null) => {
if (newPlaylist) {
setPlaylistCreated(true);
@@ -447,6 +459,8 @@ const M3UTable = () => {
}
}, [sorting]);
+ const tableDensity = tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'xl' : 'md';
+
const table = useMantineReactTable({
...TableHelper.defaultProperties,
columns,
@@ -461,13 +475,16 @@ const M3UTable = () => {
isLoading,
sorting,
rowSelection,
+ // Use density directly from tableSize
+ density: tableDensity,
},
rowVirtualizerInstanceRef, //optional
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
initialState: {
- density: 'compact',
+ // Use density directly from tableSize
+ density: tableDensity,
},
- enableRowActions: true,
+ enableRowActions: true, // Enable row actions
positionActionsColumn: 'last',
displayColumnDefOptions: {
'mrt-row-actions': {
@@ -479,30 +496,30 @@ const M3UTable = () => {
<>
{
editPlaylist(row.original);
}}
>
-
+
deletePlaylist(row.original.id)}
>
-
+
refreshPlaylist(row.original.id)}
disabled={!row.original.is_active}
>
-
+
>
),
@@ -512,6 +529,24 @@ const M3UTable = () => {
overflowX: 'auto', // Ensure horizontal scrolling works
},
},
+ mantineTableProps: {
+ ...TableHelper.defaultProperties.mantineTableProps,
+ className: `table-size-${tableSize}`,
+ },
+ // Add custom cell styles to match CustomTable's sizing
+ mantineTableBodyCellProps: {
+ style: {
+ height: tableSize === 'compact' ? '28px' : tableSize === 'large' ? '48px' : '40px',
+ fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-xs)' : 'var(--mantine-font-size-sm)',
+ padding: tableSize === 'compact' ? '2px 8px' : '4px 10px'
+ }
+ },
+ // Additional text styling to match ChannelsTable
+ mantineTableBodyProps: {
+ style: {
+ fontSize: tableSize === 'compact' ? 'var(--mantine-font-size-xs)' : 'var(--mantine-font-size-sm)',
+ }
+ },
});
return (