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 (