Skip to content

Commit 5a2a9b8

Browse files
committedFeb 13, 2025
Initial commit
0 parents  commit 5a2a9b8

16 files changed

+10637
-0
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.wxt
2+
dist
3+
node_modules

‎.vscode/tasks.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"type": "npm",
6+
"script": "dev",
7+
"presentation": {
8+
"reveal": "silent"
9+
},
10+
"runOptions": {
11+
"runOn": "folderOpen"
12+
}
13+
}
14+
]
15+
}

‎eslint.config.mjs

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import path from "node:path";
2+
3+
import { includeIgnoreFile } from "@eslint/compat";
4+
import eslint from "@eslint/js";
5+
import alloy from "eslint-config-alloy/base.js";
6+
import alloyts from "eslint-config-alloy/typescript.js";
7+
import importPlugin from "eslint-plugin-import";
8+
import noAutoFix from "eslint-plugin-no-autofix";
9+
import unusedImports from "eslint-plugin-unused-imports";
10+
import tseslint from "typescript-eslint";
11+
12+
function main() {
13+
const setting = Object.values(SettingConfig).flat();
14+
const main = Object.values(MainConfig).flat();
15+
const linting = Object.values(LintingConfig)
16+
.flat()
17+
.map((config) => unfixable(config));
18+
return tseslint.config(...setting, ...main, ...linting);
19+
}
20+
21+
function unfixable(config) {
22+
if (!config.rules) {
23+
return config;
24+
}
25+
const newRules = Object.fromEntries(
26+
Object.entries(config.rules).flatMap(([key, value]) => [
27+
[key, "off"],
28+
[`noAutoFix/` + key, value],
29+
]),
30+
);
31+
return {
32+
...config,
33+
plugins: {
34+
...config.plugins,
35+
noAutoFix: noAutoFix,
36+
},
37+
rules: newRules,
38+
};
39+
}
40+
41+
class SettingConfig {
42+
static ignore = [includeIgnoreFile(path.resolve(import.meta.dirname, ".gitignore"))];
43+
}
44+
45+
class MainConfig {
46+
static js = [eslint.configs.recommended, { rules: alloy.rules }];
47+
static ts = [
48+
tseslint.configs.strictTypeChecked,
49+
tseslint.configs.stylisticTypeChecked,
50+
{ rules: alloyts.rules },
51+
];
52+
53+
static typecheckRulesSettings = [
54+
// Linting with Type Information https://typescript-eslint.io/getting-started/typed-linting/
55+
{
56+
languageOptions: {
57+
parserOptions: {
58+
projectService: true,
59+
tsconfigRootDir: import.meta.dirname,
60+
},
61+
},
62+
},
63+
// type-checked rules for files outscoped of tsconfig
64+
// cause Error: `Parsing error: was not found by the project service`.
65+
// avoid the error with disabling typechecked rules for the files
66+
{
67+
files: ["**/*.{js,cjs,mjs,jsx}"],
68+
...tseslint.configs.disableTypeChecked,
69+
},
70+
];
71+
static removingTsRulesFromJs = {
72+
files: ["**/*.{js,cjs,mjs,jsx}"],
73+
rules: {
74+
"@typescript-eslint/explicit-member-accessibility": "off",
75+
"@typescript-eslint/consistent-type-imports": "off",
76+
"@typescript-eslint/no-unused-expressions": "off",
77+
},
78+
};
79+
static tuning = {
80+
rules: {
81+
"max-params": ["off", { max: 3 }],
82+
"@typescript-eslint/no-unused-vars": ["off"],
83+
"@typescript-eslint/member-ordering": ["off"],
84+
"@typescript-eslint/dot-notation": ["off"],
85+
},
86+
};
87+
88+
static sortImport = {
89+
plugins: {
90+
importPlugin,
91+
},
92+
rules: {
93+
"importPlugin/consistent-type-specifier-style": ["warn", "prefer-top-level"],
94+
"importPlugin/first": ["warn"],
95+
"importPlugin/newline-after-import": ["warn", { considerComments: true }],
96+
"importPlugin/no-duplicates": ["warn"],
97+
"importPlugin/no-namespace": ["warn"],
98+
"importPlugin/order": [
99+
"warn",
100+
{
101+
"newlines-between": "always",
102+
named: true,
103+
alphabetize: {
104+
order: "asc",
105+
orderImportKind: "desc",
106+
},
107+
groups: ["builtin", "external", ["internal", "parent", "sibling"], "index", "object"],
108+
},
109+
],
110+
},
111+
settings: {
112+
"import/parsers": {
113+
"@typescript-eslint/parser": [".ts", ".tsx"],
114+
},
115+
"import/resolver": {
116+
typescript: {
117+
project: ["**/tsconfig.json"],
118+
},
119+
},
120+
},
121+
};
122+
}
123+
124+
class LintingConfig {
125+
static unusedImports = {
126+
plugins: {
127+
unusedImports,
128+
},
129+
rules: {
130+
"unused-imports/no-unused-imports": ["warn"],
131+
},
132+
};
133+
}
134+
135+
export default main();

‎package-lock.json

+10,285
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "browser-youtube-play-all",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "",
6+
"type": "module",
7+
"scripts": {
8+
"build": "wxt build",
9+
"build:firefox": "wxt build -b firefox",
10+
"compile": "tsc --noEmit",
11+
"dev": "wxt",
12+
"dev:firefox": "wxt -b firefox",
13+
"postinstall": "wxt prepare",
14+
"zip": "wxt zip",
15+
"zip:firefox": "wxt zip -b firefox"
16+
},
17+
"prettier": {
18+
"arrowParens": "always",
19+
"plugins": [
20+
"prettier-plugin-packagejson"
21+
],
22+
"printWidth": 100,
23+
"trailingComma": "all"
24+
},
25+
"devDependencies": {
26+
"@eslint/compat": "^1.2.6",
27+
"@eslint/js": "^9.20.0",
28+
"@types/chrome": "^0.0.304",
29+
"@types/eslint__js": "^8.42.3",
30+
"@typescript-eslint/parser": "^8.24.0",
31+
"eslint": "^9.20.1",
32+
"eslint-config-alloy": "^5.1.2",
33+
"eslint-import-resolver-typescript": "^3.7.0",
34+
"eslint-plugin-import": "^2.31.0",
35+
"eslint-plugin-no-autofix": "^2.1.0",
36+
"eslint-plugin-unused-imports": "^4.1.4",
37+
"prettier": "^3.5.0",
38+
"prettier-plugin-packagejson": "^2.5.8",
39+
"typescript": "^5.7.3",
40+
"typescript-eslint": "^8.24.0",
41+
"wxt": "^0.19.27"
42+
}
43+
}

‎src/entrypoints/background.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { browser } from "wxt/browser";
2+
import { defineBackground } from "wxt/sandbox";
3+
4+
export default defineBackground(() => {
5+
console.log("Hello background!", { id: browser.runtime.id });
6+
7+
// chrome.runtime.onInstalled.addListener(async () => {
8+
// const tabs = await chrome.tabs.query({
9+
// url: ["http://*/*", "https://*/*"],
10+
// });
11+
// tabs
12+
// .filter(tab => tab.id)
13+
// .forEach(tab => {
14+
// chrome.scripting.executeScript({
15+
// files: ["dist/content-script.js"],
16+
// target: { tabId: tab.id! },
17+
// world: "MAIN",
18+
// });
19+
// });
20+
// });
21+
});

‎src/entrypoints/content.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { injectScript } from "wxt/client";
2+
import { defineContentScript } from "wxt/sandbox";
3+
4+
export default defineContentScript({
5+
matches: [
6+
"https://youtube.com/*",
7+
// https://*.youtube.com/*
8+
],
9+
runAt: "document_start",
10+
async main() {
11+
await injectScript("/injection.js", {
12+
keepInDom: true,
13+
});
14+
},
15+
});

‎src/entrypoints/injection.ts

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { defineUnlistedScript } from "wxt/sandbox";
2+
3+
export default defineUnlistedScript(main);
4+
5+
function main() {
6+
alert("Content script is running");
7+
console.log("Content script is running");
8+
9+
const observer = new MutationObserver(addPlayAllButton);
10+
11+
// window.addEventListener("yt-navigate-start", removeButton);
12+
window.addEventListener("yt-navigate-finish", () => {
13+
if (
14+
window.location.pathname.endsWith("/videos") ||
15+
window.location.pathname.endsWith("/shorts") ||
16+
window.location.pathname.endsWith("/streams")
17+
) {
18+
observer.disconnect();
19+
20+
addPlayAllButton();
21+
22+
const element = document.querySelector("ytd-rich-grid-renderer")!;
23+
observer.observe(element, {
24+
attributes: true,
25+
childList: false,
26+
subtree: false,
27+
});
28+
}
29+
});
30+
}
31+
32+
function addPlayAllButton() {
33+
const playListId = getPlayListId();
34+
const buttonLabel = "Play All";
35+
36+
const buttonHolder = document.querySelector("#primary #header #chips")!;
37+
buttonHolder.insertAdjacentHTML(
38+
"beforeend",
39+
`<a class="play-all-btn" href="/playlist?list=${playListId}&playnext=1">${buttonLabel}</a>`,
40+
);
41+
}
42+
43+
function getChannelId() {
44+
return document
45+
.querySelector<HTMLLinkElement>("link[rel='canonical']")!
46+
.href.split("/")
47+
.at(-1)!
48+
.substring(2);
49+
}
50+
51+
function getPlayListId(): string {
52+
const videoKind = window.location.pathname.split("/").at(-1) ?? "";
53+
const sortByPopularButton = document.querySelector("#primary #header #chips>:nth-child(2)");
54+
const isSortedByPopular = sortByPopularButton?.hasAttribute("selected") ?? false;
55+
const playlistPrefix = getPlayListPrefix(videoKind, isSortedByPopular);
56+
57+
const channelId = getChannelId();
58+
59+
return `${playlistPrefix}${channelId}`;
60+
}
61+
62+
function getPlayListPrefix(
63+
kind: "videos" | "shorts" | "streams" | string,
64+
isSortedByPopular: boolean,
65+
): string {
66+
switch (true) {
67+
case kind === "videos" && isSortedByPopular:
68+
return "UULF";
69+
case kind === "videos" && !isSortedByPopular:
70+
return "UULP";
71+
case kind === "shorts" && isSortedByPopular:
72+
return "UUSH";
73+
case kind === "shorts" && !isSortedByPopular:
74+
return "UUPS";
75+
case kind === "streams" && isSortedByPopular:
76+
return "UULV";
77+
case kind === "streams" && !isSortedByPopular:
78+
return "UUPV";
79+
default:
80+
return "UU";
81+
}
82+
}

‎src/public/icon/128.png

3 KB
Loading

‎src/public/icon/16.png

559 Bytes
Loading

‎src/public/icon/32.png

916 Bytes
Loading

‎src/public/icon/48.png

1.3 KB
Loading

‎src/public/icon/96.png

2.31 KB
Loading

‎src/public/wxt.svg

+15
Loading

‎tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./.wxt/tsconfig.json"
3+
}

‎wxt.config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defineConfig } from "wxt";
2+
3+
// See https://wxt.dev/api/config.html
4+
export default defineConfig({
5+
srcDir: "src",
6+
outDir: "dist",
7+
imports: false,
8+
extensionApi: "chrome",
9+
manifest: ({ browser }) => ({
10+
name: "Youtube Play All",
11+
...(browser === "chrome"
12+
? {
13+
host_permissions: [
14+
"https://youtube.com/*",
15+
// https://*.youtube.com/*
16+
],
17+
}
18+
: {}),
19+
}),
20+
});

0 commit comments

Comments
 (0)
Please sign in to comment.