Skip to content

Commit 0340029

Browse files
committed
feat: add dropdown menu to display all buttons
1 parent 26ea813 commit 0340029

5 files changed

Lines changed: 115 additions & 6 deletions

File tree

src/entrypoints/play-all-button.content/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export default defineContentScript({
1414
function main() {
1515
setHooks();
1616

17+
ytxEventEmitter.on(YTX_EVENTS.CHANNEL_ENTER, async (channel) => {
18+
await renderDropdown(channel);
19+
});
1720
[
1821
YTX_EVENTS.CATEGORY_ENTER,
1922
YTX_EVENTS.SORT_CHANGED,
@@ -62,3 +65,50 @@ async function maybeRenderButton(channel: Channel) {
6265
YoutubeDOM.sortButtonHolder!.appendChild(playAllButton);
6366
}
6467
}
68+
69+
async function renderDropdown(channel: Channel) {
70+
if (document.querySelector("yt-flexible-actions-view-model .play-all-btns")) {
71+
return;
72+
}
73+
74+
const container = document.createElement("div");
75+
container.className = "play-all-btns";
76+
77+
const button = document.createElement("button");
78+
button.textContent = "Play All Buttons ▼";
79+
80+
const menu = document.createElement("div");
81+
menu.className = "hidden";
82+
83+
for (const categoryKind of YoutubeDOM.categories) {
84+
for (const sortKind of YoutubeDOM.sorts) {
85+
const href = await channel.getPlaylistPath(categoryKind, sortKind);
86+
if (href) {
87+
const a = document.createElement("a");
88+
a.href = href;
89+
a.textContent = `${categoryKind} (${sortKind})`;
90+
menu.appendChild(a);
91+
}
92+
}
93+
}
94+
95+
container.append(button, menu);
96+
97+
document
98+
.querySelector("yt-flexible-actions-view-model")
99+
?.appendChild(container);
100+
101+
let isOpen = false;
102+
button.addEventListener("click", (e) => {
103+
e.stopPropagation();
104+
isOpen = !isOpen;
105+
menu.classList.toggle("hidden", !isOpen);
106+
});
107+
menu.addEventListener("click", (e) => {
108+
e.stopPropagation();
109+
});
110+
document.addEventListener("click", () => {
111+
isOpen = false;
112+
menu.classList.add("hidden");
113+
});
114+
}

src/entrypoints/play-all-button.content/play-all-button.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,54 @@
2626
.play-all-btn:focus {
2727
background-color: #9b33ff;
2828
}
29+
30+
.play-all-btns {
31+
position: relative;
32+
33+
button {
34+
background-color: #8000ff;
35+
color: #f1f1f1;
36+
37+
height: 36px;
38+
width: 100%;
39+
40+
border-radius: 18px;
41+
padding: 0 16px;
42+
43+
font-family: "Roboto", Arial, sans-serif !important;
44+
font-size: 1.4rem;
45+
font-weight: 500;
46+
47+
cursor: pointer;
48+
49+
border-style: none;
50+
}
51+
button:hover {
52+
background-color: #9b33ff;
53+
}
54+
55+
& > div {
56+
position: absolute;
57+
left: 0;
58+
margin-top: 8px;
59+
background-color: #282828;
60+
border-radius: 8px;
61+
min-width: 160px;
62+
z-index: 1000;
63+
overflow: hidden;
64+
65+
a {
66+
display: block;
67+
padding: 12px 16px;
68+
color: #f1f1f1;
69+
text-decoration: none;
70+
font-size: 1.4rem;
71+
}
72+
a:hover {
73+
background-color: #323232;
74+
}
75+
}
76+
& > div.hidden {
77+
display: none;
78+
}
79+
}

src/entrypoints/play-all-button.content/youtube-api.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export async function resolvePlaylistPath(
1717
): Promise<string> {
1818
if (sortKind === "Oldest") {
1919
const videoId = await getOldestItemId(channelId, categoryKind);
20+
if (videoId === null) {
21+
return "";
22+
}
2023
return videoId ? `/watch?v=${videoId}&list=UL01234567890` : "";
2124
} else {
2225
return `${resolveFilteredPlaylistUrl(channelId, categoryKind, sortKind)}&playnext=1`;
@@ -26,12 +29,16 @@ export async function resolvePlaylistPath(
2629
async function getOldestItemId(
2730
channelId: string,
2831
categoryKind: CategoryKind,
29-
): Promise<string> {
32+
): Promise<string | null> {
3033
const playlistUrl = `${resolveFilteredPlaylistUrl(channelId, categoryKind, "Latest")}`;
3134

32-
const videoCount = (
33-
await fetchYtInitialData(`${playlistUrl}&hl=en&persist_hl=1`)
34-
).header.playlistHeaderRenderer.stats[0].runs[0].text;
35+
const playlistHeader = (await fetchYtInitialData(playlistUrl)).header;
36+
if (playlistHeader === undefined) {
37+
return null;
38+
}
39+
40+
const videoCount =
41+
playlistHeader.playlistHeaderRenderer.stats[0].runs[0].text;
3542

3643
const oldestVideoId = (
3744
await fetchYtInitialData(`${playlistUrl}&index=${videoCount}&playnext=1`)

tests/e2e/button.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ searchNavigationModes.forEach((searchNavigationMode) => {
4040
await ytSearchPage.search(channel, searchNavigationMode);
4141
await ytSearchPage.navigateToChannel(channel, channelNavigationMode);
4242

43+
await page.locator(`.play-all-btns`).waitFor({ timeout: 3000 });
4344
const ytChannelPage = new YtChannelPage(channel, page, eventWatcher);
4445
for (const category of YoutubeDOM.categories) {
4546
await ytChannelPage.navigateToCategory(

tests/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export class YtChannelPage {
201201
if (this.page.url().includes("shorts")) {
202202
return await this.page
203203
.locator(
204-
"ytd-browse[role='main'] [href*='/shorts'],ytm-browse [href*='/shorts']",
204+
"ytd-browse[role='main'] #primary #contents [href*='/shorts'],ytm-browse .modern-tabs [href*='/shorts']",
205205
)
206206
.evaluateAll((links, n) => {
207207
const videoIds = links.map(
@@ -212,7 +212,7 @@ export class YtChannelPage {
212212
} else {
213213
return await this.page
214214
.locator(
215-
"ytd-browse[role='main'] [href*='/watch'],ytm-browse [href*='/watch']",
215+
"ytd-browse[role='main'] #primary #contents [href*='/watch'],ytm-browse .modern-tabs [href*='/watch']",
216216
)
217217
.evaluateAll((links, n) => {
218218
const videoIds = links.map(

0 commit comments

Comments
 (0)