Standardized the style to match other tables. Size follows user setting.

This commit is contained in:
SergeantPanda 2025-05-04 20:38:20 -05:00
parent 30e82fb302
commit 323045cef7
3 changed files with 237 additions and 161 deletions

View file

@ -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]);

View file

@ -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 = () => {
<>
<ActionIcon
variant="transparent"
size="sm" // Makes the button smaller
size={iconSize} // Use standardized icon size
color="yellow.5" // Red color for delete actions
onClick={() => editEPG(row.original)}
>
<SquarePen size="18" /> {/* Small icon size */}
<SquarePen size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
</ActionIcon>
<ActionIcon
variant="transparent"
size="sm" // Makes the button smaller
size={iconSize} // Use standardized icon size
color="red.9" // Red color for delete actions
onClick={() => deleteEPG(row.original.id)}
>
<SquareMinus size="18" /> {/* Small icon size */}
<SquareMinus size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
</ActionIcon>
<ActionIcon
variant="transparent"
size="sm" // Makes the button smaller
size={iconSize} // Use standardized icon size
color="blue.5" // Red color for delete actions
onClick={() => refreshEPG(row.original.id)}
disabled={!row.original.is_active}
>
<RefreshCcw size="18" /> {/* Small icon size */}
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} /> {/* Small icon size */}
</ActionIcon>
</>
),
@ -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 (

View file

@ -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 (
<Tooltip label={value} disabled={!value}>
<div
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
}}
>
{value}
</div>
</Tooltip>
);
},
},
{
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 (
<Text
size="sm"
fw={500}
c={getStatusColor(data.status)}
>
{formatStatusText(data.status)}
</Text>
);
},
},
{
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 (
<Tooltip label={data.last_message} multiline width={300}>
<Text c="dimmed" size="xs" lineClamp={2} style={{ color: theme.colors.red[6] }}>
{data.last_message}
</Text>
</Tooltip>
);
}
// Show success message for successful sources
if (data.status === 'success' && data.last_message) {
return (
<Text c="dimmed" size="xs" style={{ color: theme.colors.green[6] }}>
{data.last_message}
</Text>
);
}
// Otherwise return empty cell
return null;
},
},
{
header: 'Active',
accessorKey: 'is_active',
size: 80,
minSize: 60,
sortingFn: 'basic',
mantineTableBodyCellProps: {
align: 'left',
},
Cell: ({ row, cell }) => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Switch
size="xs"
checked={cell.getValue()}
onChange={() => toggleActive(row.original)}
/>
</Box>
),
},
{
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 (
<Tooltip label={value} disabled={!value}>
<div
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
}}
>
{value}
</div>
</Tooltip>
);
},
},
{
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 (
<Text size={tableSize === 'compact' ? 'xs' : 'sm'} c={getStatusColor(value)}>
{formatStatusText(value)}
</Text>
);
},
},
{
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 (
<Tooltip label={value} multiline width={300}>
<Text c="dimmed" size={tableSize === 'compact' ? 'xs' : 'sm'} lineClamp={2} style={{ color: theme.colors.red[6] }}>
{value}
</Text>
</Tooltip>
);
}
// Show success message with green styling for success
if (data.status === 'success') {
return (
<Tooltip label={value} multiline width={300}>
<Text c="dimmed" size={tableSize === 'compact' ? 'xs' : 'sm'} style={{ color: theme.colors.green[6] }}>
{value}
</Text>
</Tooltip>
);
}
// For all other status values, just use dimmed text
return (
<Tooltip label={value} multiline width={300}>
<Text c="dimmed" size={tableSize === 'compact' ? 'xs' : 'sm'} lineClamp={2}>
{value}
</Text>
</Tooltip>
);
},
},
{
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 (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Switch
size="xs"
checked={cell.getValue()}
onChange={() => toggleActive(row.original)}
/>
</Box>
);
},
},
// 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 = () => {
<>
<ActionIcon
variant="transparent"
size="sm"
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
color="yellow.5"
onClick={() => {
editPlaylist(row.original);
}}
>
<SquarePen size="18" />
<SquarePen size={tableSize === 'compact' ? 16 : 18} />
</ActionIcon>
<ActionIcon
variant="transparent"
size="sm"
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
color="red.9"
onClick={() => deletePlaylist(row.original.id)}
>
<SquareMinus size="18" />
<SquareMinus size={tableSize === 'compact' ? 16 : 18} />
</ActionIcon>
<ActionIcon
variant="transparent"
size="sm"
size={tableSize === 'compact' ? 'xs' : tableSize === 'large' ? 'md' : 'sm'} // Use standardized icon size
color="blue.5"
onClick={() => refreshPlaylist(row.original.id)}
disabled={!row.original.is_active}
>
<RefreshCcw size="18" />
<RefreshCcw size={tableSize === 'compact' ? 16 : 18} />
</ActionIcon>
</>
),
@ -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 (