Skip to content

Commit cb5936b

Browse files
authored
feat: add sniffer playlist removal (#528)
1 parent 196aa76 commit cb5936b

5 files changed

Lines changed: 148 additions & 0 deletions

File tree

src/popup/src/modules/Sniffer/SnifferController.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface ReturnType {
1212
currentPlaylistId: string | undefined;
1313
filter: string;
1414
clearPlaylists: () => void;
15+
removePlaylist: (playlistId: string) => void;
1516
setFilter: (filter: string) => void;
1617
setCurrentPlaylistId: (playlistId?: string) => void;
1718
copyPlaylistsToClipboard: () => void;
@@ -67,6 +68,14 @@ const useSnifferController = (): ReturnType => {
6768
dispatch(playlistsSlice.actions.clearPlaylists());
6869
}
6970

71+
function removePlaylist(playlistId: string) {
72+
dispatch(playlistsSlice.actions.removePlaylist({ playlistID: playlistId }));
73+
setExpandedPlaylists((prev) => prev.filter((id) => id !== playlistId));
74+
if (currentPlaylistId === playlistId) {
75+
setCurrentPlaylistId(undefined);
76+
}
77+
}
78+
7079
function addDirectPlaylist() {
7180
if (!directURI) return;
7281
dispatch(
@@ -105,6 +114,7 @@ const useSnifferController = (): ReturnType => {
105114
return {
106115
filter,
107116
clearPlaylists,
117+
removePlaylist,
108118
setFilter,
109119
setCurrentPlaylistId,
110120
playlists,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// @vitest-environment jsdom
2+
import React from "react";
3+
import { afterEach, describe, expect, it } from "vitest";
4+
import { configureStore } from "@reduxjs/toolkit";
5+
import type { Playlist } from "@hls-downloader/core/lib/entities";
6+
import { rootReducer } from "@hls-downloader/core/lib/store/root-reducer";
7+
import { Provider } from "react-redux";
8+
import { act } from "react";
9+
import { createRoot, Root } from "react-dom/client";
10+
import SnifferModule from "./SnifferModule";
11+
12+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
13+
14+
const roots: Array<{ root: Root; container: HTMLDivElement }> = [];
15+
16+
const createPlaylist = (
17+
id: string,
18+
uri: string,
19+
createdAt: number,
20+
pageTitle: string
21+
): Playlist =>
22+
({
23+
id,
24+
uri,
25+
createdAt,
26+
pageTitle,
27+
initiator: "hls.js",
28+
} as Playlist);
29+
30+
afterEach(() => {
31+
for (const { root, container } of roots.splice(0)) {
32+
act(() => {
33+
root.unmount();
34+
});
35+
container.remove();
36+
}
37+
});
38+
39+
function renderSniffer() {
40+
const store = configureStore({
41+
reducer: rootReducer,
42+
preloadedState: {
43+
playlists: {
44+
playlists: {
45+
first: createPlaylist(
46+
"first",
47+
"https://example.com/first.m3u8",
48+
2,
49+
"First Video"
50+
),
51+
second: createPlaylist(
52+
"second",
53+
"https://example.com/second.m3u8",
54+
1,
55+
"Second Video"
56+
),
57+
},
58+
playlistsStatus: {
59+
first: { status: "ready" },
60+
second: { status: "ready" },
61+
},
62+
},
63+
} as any,
64+
});
65+
66+
const container = document.createElement("div");
67+
document.body.appendChild(container);
68+
const root = createRoot(container);
69+
roots.push({ root, container });
70+
71+
act(() => {
72+
root.render(
73+
<Provider store={store}>
74+
<SnifferModule />
75+
</Provider>
76+
);
77+
});
78+
79+
return { container, store };
80+
}
81+
82+
function click(element: Element) {
83+
act(() => {
84+
element.dispatchEvent(new MouseEvent("click", { bubbles: true }));
85+
});
86+
}
87+
88+
describe("SnifferModule", () => {
89+
it("removes one sniffed playlist without clearing the list", () => {
90+
const { container, store } = renderSniffer();
91+
92+
expect(container.textContent).toContain("First Video");
93+
expect(container.textContent).toContain("Second Video");
94+
95+
const toggles = container.querySelectorAll(
96+
'button[aria-label="Toggle playlist details"]'
97+
);
98+
click(toggles[0]);
99+
100+
const removeButtons = Array.from(
101+
container.querySelectorAll("button")
102+
).filter((button) => button.textContent?.includes("Remove"));
103+
click(removeButtons[0]);
104+
105+
expect(container.textContent).not.toContain("First Video");
106+
expect(container.textContent).toContain("Second Video");
107+
expect(store.getState().playlists.playlists.first).toBeUndefined();
108+
expect(store.getState().playlists.playlists.second?.uri).toBe(
109+
"https://example.com/second.m3u8"
110+
);
111+
});
112+
});

src/popup/src/modules/Sniffer/SnifferModule.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const SnifferModule = () => {
1111
playlists,
1212
currentPlaylistId,
1313
copyPlaylistsToClipboard,
14+
removePlaylist,
1415
directURI,
1516
setDirectURI,
1617
addDirectPlaylist,
@@ -22,6 +23,7 @@ const SnifferModule = () => {
2223
<SnifferView
2324
filter={filter}
2425
clearPlaylists={clearPlaylists}
26+
removePlaylist={removePlaylist}
2527
copyPlaylistsToClipboard={copyPlaylistsToClipboard}
2628
setFilter={setFilter}
2729
setCurrentPlaylistId={setCurrentPlaylistId}

src/popup/src/modules/Sniffer/SnifferView.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const Empty: Story = {
5252
currentPlaylistId={undefined}
5353
filter=""
5454
clearPlaylists={() => {}}
55+
removePlaylist={() => {}}
5556
setFilter={() => {}}
5657
setCurrentPlaylistId={() => {}}
5758
directURI=""
@@ -71,6 +72,7 @@ export const WithItems: Story = {
7172
currentPlaylistId={undefined}
7273
filter=""
7374
clearPlaylists={() => {}}
75+
removePlaylist={() => {}}
7476
setFilter={() => {}}
7577
setCurrentPlaylistId={() => {}}
7678
directURI=""
@@ -90,6 +92,7 @@ export const Selected: Story = {
9092
currentPlaylistId="1"
9193
filter=""
9294
clearPlaylists={() => {}}
95+
removePlaylist={() => {}}
9396
setFilter={() => {}}
9497
setCurrentPlaylistId={() => {}}
9598
directURI=""
@@ -109,6 +112,7 @@ export const LongTitles: Story = {
109112
currentPlaylistId={undefined}
110113
filter=""
111114
clearPlaylists={() => {}}
115+
removePlaylist={() => {}}
112116
setFilter={() => {}}
113117
setCurrentPlaylistId={() => {}}
114118
directURI=""
@@ -128,6 +132,7 @@ export const WithManualInput: Story = {
128132
currentPlaylistId={undefined}
129133
filter=""
130134
clearPlaylists={() => {}}
135+
removePlaylist={() => {}}
131136
setFilter={() => {}}
132137
setCurrentPlaylistId={() => {}}
133138
directURI="https://example.com/manual.m3u8"
@@ -164,6 +169,11 @@ export const TransitionDemo: Story = {
164169
clearPlaylists={() => {
165170
setCurrentPlaylistId(undefined);
166171
}}
172+
removePlaylist={(playlistId) => {
173+
setExpandedPlaylists((prev) =>
174+
prev.filter((id) => id !== playlistId)
175+
);
176+
}}
167177
setFilter={setFilter}
168178
setCurrentPlaylistId={setCurrentPlaylistId}
169179
directURI={directURI}

src/popup/src/modules/Sniffer/SnifferView.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Copy,
1414
Radio,
1515
ArrowRight,
16+
Trash2,
1617
} from "lucide-react";
1718
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
1819
import gsap from "gsap";
@@ -23,6 +24,7 @@ interface Props {
2324
currentPlaylistId: string | undefined;
2425
filter: string;
2526
clearPlaylists: () => void;
27+
removePlaylist: (playlistId: string) => void;
2628
copyPlaylistsToClipboard: () => void;
2729
setFilter: (filter: string) => void;
2830
setCurrentPlaylistId: (playlistId?: string) => void;
@@ -35,6 +37,7 @@ interface Props {
3537

3638
const SnifferView = ({
3739
clearPlaylists,
40+
removePlaylist,
3841
copyPlaylistsToClipboard,
3942
setFilter,
4043
filter,
@@ -199,6 +202,7 @@ const SnifferView = ({
199202
expanded={expandedPlaylists.includes(item.id)}
200203
onToggle={() => toggleExpandedPlaylist(item.id)}
201204
onOpen={() => setCurrentPlaylistId(item.id)}
205+
onRemove={() => removePlaylist(item.id)}
202206
/>
203207
))}
204208
</ScrollArea>
@@ -215,11 +219,13 @@ const PlaylistRow = ({
215219
expanded,
216220
onToggle,
217221
onOpen,
222+
onRemove,
218223
}: {
219224
playlist: Playlist;
220225
expanded: boolean;
221226
onToggle: () => void;
222227
onOpen: () => void;
228+
onRemove: () => void;
223229
}) => {
224230
const detailsRef = useRef<HTMLDivElement | null>(null);
225231

@@ -309,6 +315,14 @@ const PlaylistRow = ({
309315
>
310316
Copy URL <Copy className="h-3.5 w-3.5" />
311317
</Button>
318+
<Button
319+
size="sm"
320+
variant="ghost"
321+
className="gap-1"
322+
onClick={onRemove}
323+
>
324+
Remove <Trash2 className="h-3.5 w-3.5" />
325+
</Button>
312326
</div>
313327
</div>
314328
</div>

0 commit comments

Comments
 (0)