diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx
index f4ddb7b..17e98d1 100644
--- a/frontend/src/components/ChannelDiscovery.tsx
+++ b/frontend/src/components/ChannelDiscovery.tsx
@@ -4,6 +4,7 @@ import { UserPlus } from "lucide-react";
import { api, type DiscoveredChannel } from "../lib/api";
import { accountKey, LS } from "../lib/storage";
import { formatCountOrDash } from "../lib/format";
+import { subsColumn } from "./channelColumns";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import Tooltip from "./Tooltip";
@@ -78,17 +79,7 @@ export default function ChannelDiscovery({
),
},
- {
- key: "subs",
- header: t("channels.cols.subs"),
- align: "right",
- nowrap: true,
- sortable: true,
- sortValue: (c) => c.subscriber_count ?? -1,
- render: (c) => (
- {formatCountOrDash(c.subscriber_count)}
- ),
- },
+ subsColumn(t),
{
key: "videos",
header: t("channels.discovery.cols.totalVideos"),
diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx
index 4b08b08..85b7f93 100644
--- a/frontend/src/components/Channels.tsx
+++ b/frontend/src/components/Channels.tsx
@@ -20,7 +20,8 @@ import { api, type ManagedChannel, type Tag } from "../lib/api";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { accountKey, LS } from "../lib/storage";
import { useDismiss } from "../lib/useDismiss";
-import { formatCountOrDash, formatEta, formatTotalHours, relativeTime } from "../lib/format";
+import { formatEta, formatTotalHours, relativeTime } from "../lib/format";
+import { subsColumn } from "./channelColumns";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
import DataTable, { type Column } from "./DataTable";
@@ -274,17 +275,7 @@ export default function Channels({
sortValue: (c) => c.stored_videos,
render: (c) => {c.stored_videos.toLocaleString()},
},
- {
- key: "subs",
- header: t("channels.cols.subs"),
- align: "right",
- nowrap: true,
- sortable: true,
- sortValue: (c) => c.subscriber_count ?? -1,
- render: (c) => (
- {formatCountOrDash(c.subscriber_count)}
- ),
- },
+ subsColumn(t),
{
key: "lastUpload",
header: t("channels.cols.lastUpload"),
diff --git a/frontend/src/components/CollapsedFilterRail.tsx b/frontend/src/components/CollapsedFilterRail.tsx
new file mode 100644
index 0000000..41124a6
--- /dev/null
+++ b/frontend/src/components/CollapsedFilterRail.tsx
@@ -0,0 +1,39 @@
+import { ChevronRight, SlidersHorizontal } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+// The thin 46px rail shown when a filter sidebar is collapsed (both the feed Sidebar and the Plex
+// PlexSidebar rendered byte-identical copies): an expand control + a filter icon carrying the
+// active-filter count so you can tell filters are on without opening the panel.
+export default function CollapsedFilterRail({
+ activeCount,
+ onToggleCollapse,
+}: {
+ activeCount: number;
+ onToggleCollapse: () => void;
+}) {
+ const { t } = useTranslation();
+ return (
+
+ );
+}
diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx
index 65ee75c..7358318 100644
--- a/frontend/src/components/PlexSidebar.tsx
+++ b/frontend/src/components/PlexSidebar.tsx
@@ -1,7 +1,8 @@
import { useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { ChevronRight, Film, Layers, ListMusic, Plus, SlidersHorizontal, Tv2, X } from "lucide-react";
+import { Film, Layers, ListMusic, Plus, Tv2, X } from "lucide-react";
+import CollapsedFilterRail from "./CollapsedFilterRail";
import { api, EMPTY_PLEX_FILTERS, plexFilterCount, type PlexFilters } from "../lib/api";
import { useDebounced } from "../lib/useDebounced";
@@ -115,30 +116,7 @@ export default function PlexSidebar({
)?.key;
if (collapsed) {
- return (
-
- );
+ return ;
}
return (
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 1aeaaf2..6368c22 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -5,7 +5,6 @@ import {
Check,
ChevronDown,
ChevronLeft,
- ChevronRight,
Eye,
EyeOff,
GripVertical,
@@ -13,7 +12,6 @@ import {
Pencil,
RotateCcw,
Share2,
- SlidersHorizontal,
User,
X,
} from "lucide-react";
@@ -44,6 +42,7 @@ import { useDebounced } from "../lib/useDebounced";
import { Switch } from "./ui/form";
import TagManager from "./TagManager";
import SavedViewsWidget from "./SavedViewsWidget";
+import CollapsedFilterRail from "./CollapsedFilterRail";
// Filter ids; display labels resolved at render time via t("sidebar.sort.") etc.
const SORT_IDS = [
@@ -477,30 +476,7 @@ export default function Sidebar({
// Collapsed: a thin rail (mirroring the nav) — an expand control plus a filter icon that
// carries the active-filter count so you can tell filters are on without opening it.
if (collapsed) {
- return (
-
- );
+ return ;
}
return (
diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx
index 726bb38..32a76d6 100644
--- a/frontend/src/components/VideoCard.tsx
+++ b/frontend/src/components/VideoCard.tsx
@@ -285,6 +285,23 @@ function VideoCard({
{video.title}
);
+ // Title + channel button + meta — identical in both layouts (only their wrapper / Actions placement
+ // differs). Safe to reuse the same element: exactly one of the two branches below renders.
+ const textBlock = (
+ <>
+ {title}
+
+ {meta}
+ >
+ );
if (view === "list") {
return (
@@ -295,19 +312,7 @@ function VideoCard({
)}
>
-
- {title}
-
-
{meta}
-
+ {textBlock}
);
@@ -328,17 +333,7 @@ function VideoCard({
className="w-9 h-9 rounded-full shrink-0 mt-0.5"
/>
- {title}
-
-
{meta}
+ {textBlock}
diff --git a/frontend/src/components/channelColumns.tsx b/frontend/src/components/channelColumns.tsx
new file mode 100644
index 0000000..3de4670
--- /dev/null
+++ b/frontend/src/components/channelColumns.tsx
@@ -0,0 +1,18 @@
+import type { TFunction } from "i18next";
+import { formatCountOrDash } from "../lib/format";
+import type { Column } from "./DataTable";
+
+/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical
+ * in the Channels manager and the ChannelDiscovery table, which show subscriber counts on different
+ * channel row shapes. */
+export function subsColumn(t: TFunction): Column {
+ return {
+ key: "subs",
+ header: t("channels.cols.subs"),
+ align: "right",
+ nowrap: true,
+ sortable: true,
+ sortValue: (c) => c.subscriber_count ?? -1,
+ render: (c) => {formatCountOrDash(c.subscriber_count)},
+ };
+}