diff --git a/frontend/src/components/tables/CustomTable/index.jsx b/frontend/src/components/tables/CustomTable/index.jsx
index d523d757..883e01d7 100644
--- a/frontend/src/components/tables/CustomTable/index.jsx
+++ b/frontend/src/components/tables/CustomTable/index.jsx
@@ -21,6 +21,7 @@ const useTable = ({
state = [],
columnSizing,
setColumnSizing,
+ onColumnVisibilityChange,
...options
}) => {
const [selectedTableIds, setSelectedTableIds] = useState([]);
@@ -98,12 +99,13 @@ const useTable = ({
},
...options,
state: {
- ...options.state,
+ ...state,
selectedTableIds,
...(columnSizing && { columnSizing }),
},
onStateChange: options.onStateChange,
...(setColumnSizing && { onColumnSizingChange: setColumnSizing }),
+ ...(onColumnVisibilityChange && { onColumnVisibilityChange }),
getCoreRowModel: options.getCoreRowModel ?? getCoreRowModel(),
enableColumnResizing: true,
columnResizeMode: 'onChange',
diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx
index 3ac06ac1..e8d99ad7 100644
--- a/frontend/src/components/tables/StreamsTable.jsx
+++ b/frontend/src/components/tables/StreamsTable.jsx
@@ -23,6 +23,9 @@ import {
Filter,
Square,
SquareCheck,
+ Eye,
+ EyeOff,
+ RotateCcw,
} from 'lucide-react';
import {
TextInput,
@@ -242,6 +245,64 @@ const StreamsTable = ({ onReady }) => {
'streams-table-column-sizing',
{}
);
+
+ // Column visibility - persisted to localStorage
+ // Default visible: name, group, m3u
+ // Default hidden: tvg_id, source_info
+ const DEFAULT_COLUMN_VISIBILITY = {
+ actions: true,
+ select: true,
+ name: true,
+ group: true,
+ m3u: true,
+ tvg_id: false,
+ source_info: false,
+ };
+
+ const [storedColumnVisibility, setStoredColumnVisibility] = useLocalStorage(
+ 'streams-table-column-visibility',
+ null // Use null as default to detect fresh install
+ );
+
+ // Merge defaults with stored values, ensuring all columns have values
+ // - Fresh install (null): use defaults
+ // - Existing users: merge settings with defaults for any new columns
+ const columnVisibility = useMemo(() => {
+ if (!storedColumnVisibility || typeof storedColumnVisibility !== 'object') {
+ return DEFAULT_COLUMN_VISIBILITY;
+ }
+ // Merge: start with defaults, overlay stored values only for keys that exist in defaults
+ const merged = { ...DEFAULT_COLUMN_VISIBILITY };
+ for (const key of Object.keys(DEFAULT_COLUMN_VISIBILITY)) {
+ if (key in storedColumnVisibility && typeof storedColumnVisibility[key] === 'boolean') {
+ merged[key] = storedColumnVisibility[key];
+ }
+ }
+ return merged;
+ }, [storedColumnVisibility]);
+
+ const setColumnVisibility = (newValue) => {
+ if (typeof newValue === 'function') {
+ setStoredColumnVisibility((prev) => {
+ const prevMerged = prev && typeof prev === 'object' ? { ...DEFAULT_COLUMN_VISIBILITY, ...prev } : DEFAULT_COLUMN_VISIBILITY;
+ return newValue(prevMerged);
+ });
+ } else {
+ setStoredColumnVisibility(newValue);
+ }
+ };
+
+ const toggleColumnVisibility = (columnId) => {
+ setColumnVisibility((prev) => ({
+ ...prev,
+ [columnId]: !prev[columnId],
+ }));
+ };
+
+ const resetColumnVisibility = () => {
+ setStoredColumnVisibility(DEFAULT_COLUMN_VISIBILITY);
+ };
+
const debouncedFilters = useDebounce(filters, 500, () => {
// Reset to first page whenever filters change to avoid "Invalid page" errors
setPagination({
@@ -272,6 +333,7 @@ const StreamsTable = ({ onReady }) => {
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
const env_mode = useSettingsStore((s) => s.environment.env_mode);
const showVideo = useVideoStore((s) => s.showVideo);
+ const videoIsVisible = useVideoStore((s) => s.isVisible);
const data = useStreamsTableStore((s) => s.streams);
const pageCount = useStreamsTableStore((s) => s.pageCount);
@@ -389,6 +451,61 @@ const StreamsTable = ({ onReady }) => {
),
},
+ {
+ header: 'Source Info',
+ id: 'source_info',
+ accessorKey: 'stream_stats',
+ size: columnSizing.source_info || 120,
+ cell: ({ getValue }) => {
+ const stats = getValue();
+ if (!stats) return