Merge pull request #12 from bakkot/lint

Lint
This commit is contained in:
Simon 2022-12-03 12:12:29 +07:00 committed by GitHub
commit 9fadbd5c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2515 additions and 546 deletions

21
.eslintrc.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
module.exports = {
extends: ['eslint:recommended', 'eslint-config-prettier'],
parserOptions: {
ecmaVersion: 2020,
},
env: {
browser: true,
},
globals: {
browser: 'readonly',
chrome: 'readonly',
},
rules: {
strict: ['error', 'global'],
'no-unused-vars': ['error', { vars: 'local' }],
eqeqeq: ['error', 'always', { null: 'ignore' }],
curly: ['error', 'multi-line'],
'no-var': 'error',
},
};

16
.github/workflows/lint_js.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: lint_js
on: [pull_request, push]
jobs:
check:
name: lint_js
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm ci
- run: npm run lint
- run: npm run format -- --check

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ extension/manifest.json
# release builds # release builds
release/* release/*
# JavaScript stuff
node_modules

View File

@ -78,3 +78,6 @@ Join us on [Discord](https://www.tubearchivist.com/discord) and help us improve
- [ ] Implement download button for videos on playlist - [ ] Implement download button for videos on playlist
- [ ] Error handling for connection errors - [ ] Error handling for connection errors
- [X] Dynamically inject buttons with mutation observer - [X] Dynamically inject buttons with mutation observer
## Making changes to the JavaScript
The JavaScript does not require any build step; you just edit the files directly. However, there is config for eslint and prettier (a linter and formatter respectively); their use is recommended but not required. To use them, install `node`, run `npm i` from the root directory of this repository to install dependencies, then run `npm run lint` and `npm run format` to run eslint and prettier respectively.

View File

@ -2,226 +2,204 @@
extension background script listening for events extension background script listening for events
*/ */
console.log("running background.js"); 'use strict';
console.log('running background.js');
let browserType = getBrowser(); let browserType = getBrowser();
// boilerplate to dedect browser type api // boilerplate to dedect browser type api
function getBrowser() { function getBrowser() {
if (typeof chrome !== "undefined") { if (typeof chrome !== 'undefined') {
if (typeof browser !== "undefined") { if (typeof browser !== 'undefined') {
return browser; return browser;
} else {
return chrome;
}
} else { } else {
console.log("failed to detect browser"); return chrome;
throw "browser detection error" }
}; } else {
console.log('failed to detect browser');
throw 'browser detection error';
}
} }
// send get request to API backend // send get request to API backend
async function sendGet(path) { async function sendGet(path) {
let access = await getAccess();
const url = `${access.url}:${access.port}/${path}`;
console.log('GET: ' + url);
let access = await getAccess(); const rawResponse = await fetch(url, {
const url = `${access.url}:${access.port}/${path}`; method: 'GET',
console.log("GET: " + url); headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Token ' + access.apiKey,
mode: 'no-cors',
},
});
const rawResponse = await fetch(url, { const content = await rawResponse.json();
method: "GET", return content;
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Token " + access.apiKey,
"mode": "no-cors"
}
});
const content = await rawResponse.json();
return content;
} }
// send post/put request to API backend // send post/put request to API backend
async function sendData(path, payload, method) { async function sendData(path, payload, method) {
let access = await getAccess();
const url = `${access.url}:${access.port}/${path}`;
console.log(`${method}: ${url}`);
console.log(`${method}: ${JSON.stringify(payload)}`);
let access = await getAccess(); try {
const url = `${access.url}:${access.port}/${path}`; const rawResponse = await fetch(url, {
console.log(`${method}: ${url}`); method: method,
console.log(`${method}: ${JSON.stringify(payload)}`); headers: {
Accept: 'application/json',
try { 'Content-Type': 'application/json',
const rawResponse = await fetch(url, { Authorization: 'Token ' + access.apiKey,
method: method, mode: 'no-cors',
headers: { },
"Accept": "application/json", body: JSON.stringify(payload),
"Content-Type": "application/json", });
"Authorization": "Token " + access.apiKey, const content = await rawResponse.json();
"mode": "no-cors" return content;
}, } catch (e) {
body: JSON.stringify(payload) console.error(e);
}); return null;
const content = await rawResponse.json(); }
return content;
} catch (e) {
console.error(e);
return null
}
} }
// read access details from storage.local // read access details from storage.local
async function getAccess() { async function getAccess() {
let storage = await browserType.storage.local.get('access');
var storage = await browserType.storage.local.get("access"); return storage.access;
return storage.access
} }
// check if cookie is valid // check if cookie is valid
async function getCookieState() { async function getCookieState() {
const path = 'api/cookie/';
let response = await sendGet(path);
console.log('cookie state: ' + JSON.stringify(response));
const path = "api/cookie/"; return response;
let response = await sendGet(path)
console.log("cookie state: " + JSON.stringify(response));
return response
} }
// send ping to server, return response // send ping to server, return response
async function verifyConnection() { async function verifyConnection() {
const path = 'api/ping/';
let response = await sendGet(path);
console.log('verify connection: ' + JSON.stringify(response));
const path = "api/ping/"; return response;
let response = await sendGet(path)
console.log("verify connection: " + JSON.stringify(response));
return response
} }
// send youtube link from injected buttons // send youtube link from injected buttons
async function youtubeLink(youtubeMessage) { async function youtubeLink(youtubeMessage) {
let path;
let path; let payload;
let payload;
if (youtubeMessage.action === "download") {
path = "api/download/";
payload = {
"data": [
{
"youtube_id": youtubeMessage.url,
"status": "pending",
}
]
}
} else if (youtubeMessage.action === "subscribe") {
path = "api/channel/";
payload = {
"data": [
{
"channel_id": youtubeMessage.url,
"channel_subscribed": true,
}
]
}
}
let response = await sendData(path, payload, "POST"); if (youtubeMessage.action === 'download') {
return response path = 'api/download/';
payload = {
data: [
{
youtube_id: youtubeMessage.url,
status: 'pending',
},
],
};
} else if (youtubeMessage.action === 'subscribe') {
path = 'api/channel/';
payload = {
data: [
{
channel_id: youtubeMessage.url,
channel_subscribed: true,
},
],
};
}
let response = await sendData(path, payload, 'POST');
return response;
} }
async function cookieStr(cookieLines) { async function cookieStr(cookieLines) {
const path = 'api/cookie/';
let payload = {
cookie: cookieLines.join('\n'),
};
let response = await sendData(path, payload, 'PUT');
const path = "api/cookie/"; return response;
let payload = {
"cookie": cookieLines.join("\n")
}
let response = await sendData(path, payload, "PUT");
return response
} }
function buildCookieLine(cookie) { function buildCookieLine(cookie) {
return [ return [
cookie.domain, cookie.domain,
"TRUE", 'TRUE',
cookie.path, cookie.path,
cookie.httpOnly.toString().toUpperCase(), cookie.httpOnly.toString().toUpperCase(),
Math.trunc(cookie.expirationDate) || 0, Math.trunc(cookie.expirationDate) || 0,
cookie.name, cookie.name,
cookie.value, cookie.value,
].join("\t"); ].join('\t');
} }
async function sendCookies() { async function sendCookies() {
console.log("function sendCookies"); console.log('function sendCookies');
let cookieStores = await browserType.cookies.getAllCookieStores(); let cookieStores = await browserType.cookies.getAllCookieStores();
var cookieLines = [ let cookieLines = [
"# Netscape HTTP Cookie File", '# Netscape HTTP Cookie File',
"# https://curl.haxx.se/rfc/cookie_spec.html", '# https://curl.haxx.se/rfc/cookie_spec.html',
"# This is a generated file! Do not edit.\n" '# This is a generated file! Do not edit.\n',
]; ];
for (let i = 0; i < cookieStores.length; i++) { for (let i = 0; i < cookieStores.length; i++) {
const cookieStore = cookieStores[i]; const cookieStore = cookieStores[i];
var allCookiesStore = await browserType.cookies.getAll({ let allCookiesStore = await browserType.cookies.getAll({
domain: ".youtube.com", domain: '.youtube.com',
storeId: cookieStore["id"] storeId: cookieStore['id'],
}); });
for (let j = 0; j < allCookiesStore.length; j++) { for (let j = 0; j < allCookiesStore.length; j++) {
const cookie = allCookiesStore[j]; const cookie = allCookiesStore[j];
cookieLines.push(buildCookieLine(cookie)); cookieLines.push(buildCookieLine(cookie));
}
} }
}
let response = cookieStr(cookieLines); let response = cookieStr(cookieLines);
return response
return response;
} }
// process and return message if needed // process and return message if needed
function handleMessage(request, sender, sendResponse) { function handleMessage(request, sender, sendResponse) {
console.log("message background.js listener: " + JSON.stringify(request)); console.log('message background.js listener: ' + JSON.stringify(request));
if (request.verify === true) { if (request.verify === true) {
let response = verifyConnection(); let response = verifyConnection();
response.then(message => { response.then(message => {
sendResponse(message); sendResponse(message);
}) });
} else if (request.youtube) { } else if (request.youtube) {
let response = youtubeLink(request.youtube); let response = youtubeLink(request.youtube);
response.then(message => { response.then(message => {
sendResponse(message) sendResponse(message);
}) });
} else if (request.cookieState) { } else if (request.cookieState) {
let response = getCookieState(); let response = getCookieState();
response.then(message => { response.then(message => {
sendResponse(message) sendResponse(message);
}) });
} else if (request.sendCookie) { } else if (request.sendCookie) {
console.log("backgound: " + JSON.stringify(request)); console.log('backgound: ' + JSON.stringify(request));
let response = sendCookies(); let response = sendCookies();
response.then(message => { response.then(message => {
sendResponse(message) sendResponse(message);
}) });
} }
return true; return true;
} }
browserType.runtime.onMessage.addListener(handleMessage); browserType.runtime.onMessage.addListener(handleMessage);

View File

@ -2,190 +2,172 @@
Loaded into popup index.html Loaded into popup index.html
*/ */
'use strict';
let browserType = getBrowser(); let browserType = getBrowser();
// boilerplate to dedect browser type api // boilerplate to dedect browser type api
function getBrowser() { function getBrowser() {
if (typeof chrome !== "undefined") { if (typeof chrome !== 'undefined') {
if (typeof browser !== "undefined") { if (typeof browser !== 'undefined') {
return browser; return browser;
} else {
return chrome;
}
} else { } else {
console.log("failed to dedect browser"); return chrome;
throw "browser detection error" }
}; } else {
console.log('failed to dedect browser');
throw 'browser detection error';
}
} }
// store access details // store access details
document.getElementById("save-login").addEventListener("click", function () { document.getElementById('save-login').addEventListener('click', function () {
let url = document.getElementById("full-url").value; let url = document.getElementById('full-url').value;
if (!url.includes('://')) { if (!url.includes('://')) {
url = 'http://' + url; url = 'http://' + url;
} }
let parsed = new URL(url); let parsed = new URL(url);
let toStore = { let toStore = {
"access": { access: {
"url": `${parsed.protocol}//${parsed.hostname}`, url: `${parsed.protocol}//${parsed.hostname}`,
"port": parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), port: parsed.port || (parsed.protocol === 'https:' ? '443' : '80'),
"apiKey": document.getElementById("api-key").value apiKey: document.getElementById('api-key').value,
} },
}; };
browserType.storage.local.set(toStore, function() { browserType.storage.local.set(toStore, function () {
console.log("Stored connection details: " + JSON.stringify(toStore)); console.log('Stored connection details: ' + JSON.stringify(toStore));
pingBackend(); pingBackend();
}); });
}) });
// verify connection status // verify connection status
document.getElementById("status-icon").addEventListener("click", function() { document.getElementById('status-icon').addEventListener('click', function () {
pingBackend(); pingBackend();
}) });
// send cookie // send cookie
document.getElementById("sendCookies").addEventListener("click", function() { document.getElementById('sendCookies').addEventListener('click', function () {
sendCookie(); sendCookie();
}) });
function sendCookie() { function sendCookie() {
console.log("popup send cookie"); console.log('popup send cookie');
function handleResponse(message) { function handleResponse(message) {
console.log("handle cookie response: " + JSON.stringify(message)); console.log('handle cookie response: ' + JSON.stringify(message));
let cookie_validated = message.cookie_validated; let cookie_validated = message.cookie_validated;
document.getElementById("sendCookiesStatus").innerText = "validated: " + cookie_validated document.getElementById('sendCookiesStatus').innerText = 'validated: ' + cookie_validated;
} }
function handleError(error) { function handleError(error) {
console.log(`Error: ${error}`); console.log(`Error: ${error}`);
} }
let checked = document.getElementById("sendCookies").checked; let checked = document.getElementById('sendCookies').checked;
let toStore = { let toStore = {
"sendCookies": { sendCookies: {
"checked": checked checked: checked,
} },
}; };
browserType.storage.local.set(toStore, function() { browserType.storage.local.set(toStore, function () {
console.log("stored option: " + JSON.stringify(toStore)); console.log('stored option: ' + JSON.stringify(toStore));
}) });
if (checked === false) { if (checked === false) {
return return;
} }
let sending = browserType.runtime.sendMessage({"sendCookie": true}); let sending = browserType.runtime.sendMessage({ sendCookie: true });
sending.then(handleResponse, handleError); sending.then(handleResponse, handleError);
} }
// send ping message to TA backend // send ping message to TA backend
function pingBackend() { function pingBackend() {
function handleResponse(message) {
function handleResponse(message) { if (message.response === 'pong') {
if (message.response === "pong") { setStatusIcon(true);
setStatusIcon(true); console.log('connection validated');
console.log("connection validated")
}
} }
}
function handleError(error) { function handleError(error) {
console.log(`Error: ${error}`); console.log(`Error: ${error}`);
setStatusIcon(false); setStatusIcon(false);
} }
console.log("ping TA server")
let sending = browserType.runtime.sendMessage({"verify": true});
sending.then(handleResponse, handleError);
console.log('ping TA server');
let sending = browserType.runtime.sendMessage({ verify: true });
sending.then(handleResponse, handleError);
} }
// add url to image // add url to image
function addUrl(access) { function addUrl(access) {
const url = `${access.url}:${access.port}`; const url = `${access.url}:${access.port}`;
document.getElementById("ta-url").setAttribute("href", url); document.getElementById('ta-url').setAttribute('href', url);
} }
function setCookieState() { function setCookieState() {
function handleResponse(message) {
function handleResponse(message) { console.log(message);
console.log(message); document.getElementById('sendCookies').checked = message.cookie_enabled;
document.getElementById("sendCookies").checked = message.cookie_enabled; if (message.validated_str) {
if (message.validated_str) { document.getElementById('sendCookiesStatus').innerText = message.validated_str;
document.getElementById("sendCookiesStatus").innerText = message.validated_str;
}
} }
}
function handleError(error) { function handleError(error) {
console.log(`Error: ${error}`); console.log(`Error: ${error}`);
} }
console.log("set cookie state"); console.log('set cookie state');
let sending = browserType.runtime.sendMessage({"cookieState": true}); let sending = browserType.runtime.sendMessage({ cookieState: true });
sending.then(handleResponse, handleError) sending.then(handleResponse, handleError);
document.getElementById("sendCookies").checked = true; document.getElementById('sendCookies').checked = true;
} }
// change status icon based on connection status // change status icon based on connection status
function setStatusIcon(connected) { function setStatusIcon(connected) {
let statusIcon = document.getElementById('status-icon');
let statusIcon = document.getElementById("status-icon") if (connected) {
if (connected == true) { statusIcon.innerHTML = '&#9745;';
statusIcon.innerHTML = "&#9745;"; statusIcon.style.color = 'green';
statusIcon.style.color = "green"; } else {
} else { statusIcon.innerHTML = '&#9746;';
statusIcon.innerHTML = "&#9746;"; statusIcon.style.color = 'red';
statusIcon.style.color = "red"; }
}
} }
// fill in form // fill in form
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener('DOMContentLoaded', async () => {
function onGot(item) {
function onGot(item) { if (!item.access) {
if (!item.access) { console.log('no access details found');
console.log("no access details found"); setStatusIcon(false);
setStatusIcon(false); return;
return
}
let { url, port } = item.access;
let fullUrl = url;
if (!(url.startsWith('http://') && port === '80')) {
fullUrl += `:${port}`;
}
document.getElementById("full-url").value = fullUrl;
document.getElementById("api-key").value = item.access.apiKey;
pingBackend();
addUrl(item.access);
};
function setCookiesOptions(result) {
if (!result.sendCookies || result.sendCookies.checked === false) {
console.log("sync cookies not set");
return
}
console.log("set options: " + JSON.stringify(result));
setCookieState();
} }
let { url, port } = item.access;
let fullUrl = url;
if (!(url.startsWith('http://') && port === '80')) {
fullUrl += `:${port}`;
}
document.getElementById('full-url').value = fullUrl;
document.getElementById('api-key').value = item.access.apiKey;
pingBackend();
addUrl(item.access);
}
function onError(error) { function setCookiesOptions(result) {
console.log(`Error: ${error}`); if (!result.sendCookies || result.sendCookies.checked === false) {
}; console.log('sync cookies not set');
return;
}
console.log('set options: ' + JSON.stringify(result));
setCookieState();
}
browserType.storage.local.get("access", function(result) { browserType.storage.local.get('access', function (result) {
onGot(result) onGot(result);
}); });
browserType.storage.local.get("sendCookies", function(result) { browserType.storage.local.get('sendCookies', function (result) {
setCookiesOptions(result) setCookiesOptions(result);
}) });
});
})

View File

@ -2,23 +2,24 @@
content script running on youtube.com content script running on youtube.com
*/ */
let browserType = getBrowser(); 'use strict';
let browserType = getBrowser();
// boilerplate to dedect browser type api // boilerplate to dedect browser type api
function getBrowser() { function getBrowser() {
if (typeof chrome !== "undefined") { if (typeof chrome !== 'undefined') {
if (typeof browser !== "undefined") { if (typeof browser !== 'undefined') {
console.log("detected firefox"); console.log('detected firefox');
return browser; return browser;
} else {
console.log("detected chrome");
return chrome;
}
} else { } else {
console.log("failed to dedect browser"); console.log('detected chrome');
throw "browser detection error" return chrome;
}; }
} else {
console.log('failed to dedect browser');
throw 'browser detection error';
}
} }
const downloadIcon = `<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" const downloadIcon = `<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
@ -100,308 +101,295 @@ viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="pres
</g> </g>
</g> </g>
</g> </g>
</svg>` </svg>`;
function buildButtonDiv() { function buildButtonDiv() {
var buttonDiv = document.createElement("div"); let buttonDiv = document.createElement('div');
buttonDiv.setAttribute("id", "ta-channel-button"); buttonDiv.setAttribute('id', 'ta-channel-button');
Object.assign(buttonDiv.style, { Object.assign(buttonDiv.style, {
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
backgroundColor: "#00202f", backgroundColor: '#00202f',
color: "#fff", color: '#fff',
fontSize: "14px", fontSize: '14px',
padding: "5px", padding: '5px',
margin: "0 5px", margin: '0 5px',
borderRadius: "8px", borderRadius: '8px',
}); });
return buttonDiv return buttonDiv;
} }
function buildSubLink(channelContainer) { function buildSubLink(channelContainer) {
var subLink = document.createElement("span"); let subLink = document.createElement('span');
subLink.innerText = "Subscribe"; subLink.innerText = 'Subscribe';
subLink.addEventListener('click', e => { subLink.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
var currentLocation = window.location.href; let currentLocation = window.location.href;
console.log("subscribe to: " + currentLocation); console.log('subscribe to: ' + currentLocation);
sendUrl(currentLocation, "subscribe", subLink); sendUrl(currentLocation, 'subscribe', subLink);
}); });
subLink.addEventListener("mouseover", e => { subLink.addEventListener('mouseover', e => {
let subText let subText;
if (window.location.pathname == "/watch") { if (window.location.pathname === '/watch') {
var currentLocation = window.location.href; let currentLocation = window.location.href;
subText = currentLocation; subText = currentLocation;
} else { } else {
subText = channelContainer.querySelector("#text").textContent; subText = channelContainer.querySelector('#text').textContent;
}; }
e.target.title = "TA Subscribe: " + subText; e.target.title = 'TA Subscribe: ' + subText;
}); });
Object.assign(subLink.style, { Object.assign(subLink.style, {
padding: "5px", padding: '5px',
cursor: "pointer", cursor: 'pointer',
}); });
return subLink return subLink;
} }
function buildSpacer() { function buildSpacer() {
var spacer = document.createElement("span"); let spacer = document.createElement('span');
spacer.innerText = "|"; spacer.innerText = '|';
return spacer return spacer;
} }
function buildDlLink(channelContainer) { function buildDlLink(channelContainer) {
var dlLink = document.createElement("span"); let dlLink = document.createElement('span');
dlLink.innerHTML = downloadIcon; dlLink.innerHTML = downloadIcon;
dlLink.addEventListener('click', e => { dlLink.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
var currentLocation = window.location.href; let currentLocation = window.location.href;
console.log("download: " + currentLocation) console.log('download: ' + currentLocation);
sendUrl(currentLocation, "download", dlLink); sendUrl(currentLocation, 'download', dlLink);
}); });
dlLink.addEventListener("mouseover", e => { dlLink.addEventListener('mouseover', e => {
let subText let subText;
if (window.location.pathname == "/watch") { if (window.location.pathname === '/watch') {
var currentLocation = window.location.href; let currentLocation = window.location.href;
subText = currentLocation; subText = currentLocation;
} else { } else {
subText = channelContainer.querySelector("#text").textContent; subText = channelContainer.querySelector('#text').textContent;
}; }
e.target.title = "TA Download: " + subText; e.target.title = 'TA Download: ' + subText;
}); });
Object.assign(dlLink.style, { Object.assign(dlLink.style, {
filter: "invert()", filter: 'invert()',
width: "20px", width: '20px',
padding: "0 5px", padding: '0 5px',
cursor: "pointer", cursor: 'pointer',
}); });
return dlLink return dlLink;
} }
function buildChannelButton(channelContainer) { function buildChannelButton(channelContainer) {
let buttonDiv = buildButtonDiv();
var buttonDiv = buildButtonDiv() let subLink = buildSubLink(channelContainer);
buttonDiv.appendChild(subLink);
var subLink = buildSubLink(channelContainer);
buttonDiv.appendChild(subLink)
var spacer = buildSpacer() let spacer = buildSpacer();
buttonDiv.appendChild(spacer); buttonDiv.appendChild(spacer);
var dlLink = buildDlLink(channelContainer) let dlLink = buildDlLink(channelContainer);
buttonDiv.appendChild(dlLink); buttonDiv.appendChild(dlLink);
return buttonDiv
return buttonDiv;
} }
function getChannelContainers() { function getChannelContainers() {
var nodes = document.querySelectorAll("#inner-header-container, #owner"); let nodes = document.querySelectorAll('#inner-header-container, #owner');
return nodes return nodes;
} }
function getThubnailContainers() { function getThubnailContainers() {
var nodes = document.querySelectorAll('#thumbnail'); let nodes = document.querySelectorAll('#thumbnail');
return nodes return nodes;
} }
function buildVideoButton(thumbContainer) { function buildVideoButton(thumbContainer) {
var thumbLink = thumbContainer?.href; let thumbLink = thumbContainer?.href;
if (!thumbLink) return; if (!thumbLink) return;
if (thumbLink.includes('list=') || thumbLink.includes('/shorts/')) return; if (thumbLink.includes('list=') || thumbLink.includes('/shorts/')) return;
var dlButton = document.createElement("a"); let dlButton = document.createElement('a');
dlButton.setAttribute("id", "ta-video-button"); dlButton.setAttribute('id', 'ta-video-button');
dlButton.href = '#' dlButton.href = '#';
dlButton.addEventListener('click', e => {
e.preventDefault();
let videoLink = thumbContainer.href;
console.log("download: " + videoLink);
sendUrl(videoLink, "download", dlButton)
});
dlButton.addEventListener('mouseover', e => {
Object.assign(dlButton.style, {
opacity: 1,
});
let videoTitle = thumbContainer.href;
e.target.title = "TA download: " + videoTitle;
})
dlButton.addEventListener('mouseout', e => {
Object.assign(dlButton.style, {
opacity: 0,
});
})
dlButton.addEventListener('click', e => {
e.preventDefault();
let videoLink = thumbContainer.href;
console.log('download: ' + videoLink);
sendUrl(videoLink, 'download', dlButton);
});
dlButton.addEventListener('mouseover', e => {
Object.assign(dlButton.style, { Object.assign(dlButton.style, {
display: "flex", opacity: 1,
position: "absolute",
top: "5px",
left: "5px",
alignItems: "center",
backgroundColor: "#00202f",
color: "#fff",
fontSize: "1.4rem",
textDecoration: "none",
borderRadius: "8px",
cursor: "pointer",
opacity: 0,
transition: "all 0.3s ease 0.3s",
}); });
let videoTitle = thumbContainer.href;
var dlIcon = document.createElement("span"); e.target.title = 'TA download: ' + videoTitle;
dlIcon.innerHTML = downloadIcon; });
Object.assign(dlIcon.style, { dlButton.addEventListener('mouseout', () => {
filter: "invert()", Object.assign(dlButton.style, {
width: "20px", opacity: 0,
padding: "10px 13px",
}); });
});
dlButton.appendChild(dlIcon);
return dlButton Object.assign(dlButton.style, {
display: 'flex',
position: 'absolute',
top: '5px',
left: '5px',
alignItems: 'center',
backgroundColor: '#00202f',
color: '#fff',
fontSize: '1.4rem',
textDecoration: 'none',
borderRadius: '8px',
cursor: 'pointer',
opacity: 0,
transition: 'all 0.3s ease 0.3s',
});
let dlIcon = document.createElement('span');
dlIcon.innerHTML = downloadIcon;
Object.assign(dlIcon.style, {
filter: 'invert()',
width: '20px',
padding: '10px 13px',
});
dlButton.appendChild(dlIcon);
return dlButton;
} }
// fix positioning of #owner div to fit new button // fix positioning of #owner div to fit new button
function adjustOwner(channelContainer) { function adjustOwner(channelContainer) {
let sponsorButton = channelContainer.querySelector('#sponsor-button'); let sponsorButton = channelContainer.querySelector('#sponsor-button');
if (sponsorButton === null) { if (sponsorButton === null) {
return channelContainer return channelContainer;
} }
let variableMinWidth let variableMinWidth;
if (sponsorButton.hasChildNodes()) { if (sponsorButton.hasChildNodes()) {
variableMinWidth = '140px'; variableMinWidth = '140px';
} else { } else {
variableMinWidth = '45px'; variableMinWidth = '45px';
} }
Object.assign(channelContainer.firstElementChild.style, { Object.assign(channelContainer.firstElementChild.style, {
minWidth: variableMinWidth minWidth: variableMinWidth,
}) });
Object.assign(channelContainer.style, { Object.assign(channelContainer.style, {
minWidth: 'calc(40% + 50px)' minWidth: 'calc(40% + 50px)',
}) });
return channelContainer return channelContainer;
} }
function ensureTALinks() { function ensureTALinks() {
let channelContainerNodes = getChannelContainers();
var channelContainerNodes = getChannelContainers() for (let channelContainer of channelContainerNodes) {
channelContainer = adjustOwner(channelContainer);
if (channelContainer.hasTA) continue;
let channelButton = buildChannelButton(channelContainer);
channelContainer.appendChild(channelButton);
channelContainer.hasTA = true;
}
for (var channelContainer of channelContainerNodes) { let thumbContainerNodes = getThubnailContainers();
channelContainer = adjustOwner(channelContainer);
if (channelContainer.hasTA) continue;
var channelButton = buildChannelButton(channelContainer);
channelContainer.appendChild(channelButton);
channelContainer.hasTA = true;
}
var thumbContainerNodes = getThubnailContainers();
for (var thumbContainer of thumbContainerNodes) {
if (thumbContainer.hasTA) continue;
var videoButton = buildVideoButton(thumbContainer);
if (videoButton == null) continue;
thumbContainer.parentElement.appendChild(videoButton);
thumbContainer.hasTA = true;
}
for (let thumbContainer of thumbContainerNodes) {
if (thumbContainer.hasTA) continue;
let videoButton = buildVideoButton(thumbContainer);
if (videoButton == null) continue;
thumbContainer.parentElement.appendChild(videoButton);
thumbContainer.hasTA = true;
}
} }
function buttonError(button) { function buttonError(button) {
let buttonSpan = button.querySelector("span"); let buttonSpan = button.querySelector('span');
if (buttonSpan === null) { if (buttonSpan === null) {
buttonSpan = button buttonSpan = button;
} }
buttonSpan.style.filter = "invert(19%) sepia(93%) saturate(7472%) hue-rotate(359deg) brightness(105%) contrast(113%)"; buttonSpan.style.filter =
buttonSpan.style.color = "red"; 'invert(19%) sepia(93%) saturate(7472%) hue-rotate(359deg) brightness(105%) contrast(113%)';
buttonSpan.style.color = 'red';
button.style.opacity = 1; button.style.opacity = 1;
button.addEventListener('mouseout', e => { button.addEventListener('mouseout', () => {
Object.assign(button.style, { Object.assign(button.style, {
opacity: 1, opacity: 1,
}); });
}) });
} }
function buttonSuccess(button) { function buttonSuccess(button) {
let buttonSpan = button.querySelector("span"); let buttonSpan = button.querySelector('span');
if (buttonSpan === null) { if (buttonSpan === null) {
buttonSpan = button; buttonSpan = button;
} }
if (buttonSpan.innerHTML === "Subscribe") { if (buttonSpan.innerHTML === 'Subscribe') {
buttonSpan.innerHTML = "Success"; buttonSpan.innerHTML = 'Success';
setTimeout(() => { setTimeout(() => {
buttonSpan.innerHTML = "Subscribe"; buttonSpan.innerHTML = 'Subscribe';
}, 2000); }, 2000);
} else { } else {
buttonSpan.innerHTML = checkmarkIcon; buttonSpan.innerHTML = checkmarkIcon;
setTimeout(() => { setTimeout(() => {
buttonSpan.innerHTML = downloadIcon; buttonSpan.innerHTML = downloadIcon;
}, 2000); }, 2000);
} }
} }
function sendUrl(url, action, button) { function sendUrl(url, action, button) {
function handleResponse(message) {
function handleResponse(message) { console.log('sendUrl response: ' + JSON.stringify(message));
console.log("sendUrl response: " + JSON.stringify(message)); if (message === null || message.detail === 'Invalid token.') {
if (message === null || message.detail === "Invalid token.") { buttonError(button);
buttonError(button); } else {
} else { buttonSuccess(button);
buttonSuccess(button);
}
} }
}
function handleError(error) { function handleError(error) {
console.log("error"); console.log('error');
console.log(JSON.stringify(error)); console.log(JSON.stringify(error));
} }
let payload = { let payload = {
"youtube": { youtube: {
"url": url, url: url,
"action": action, action: action,
} },
} };
console.log("youtube link: " + JSON.stringify(payload)); console.log('youtube link: ' + JSON.stringify(payload));
let sending = browserType.runtime.sendMessage(payload);
sending.then(handleResponse, handleError);
};
let sending = browserType.runtime.sendMessage(payload);
sending.then(handleResponse, handleError);
}
let throttleBlock; let throttleBlock;
const throttle = (callback, time) => { const throttle = (callback, time) => {
if (throttleBlock) return; if (throttleBlock) return;
throttleBlock = true; throttleBlock = true;
setTimeout(() => { setTimeout(() => {
callback(); callback();
throttleBlock = false; throttleBlock = false;
}, time); }, time);
}; };
let observer = new MutationObserver(list => { let observer = new MutationObserver(list => {
if (list.some(i => i.type === 'childList' && i.addedNodes.length > 0)) { if (list.some(i => i.type === 'childList' && i.addedNodes.length > 0)) {
throttle(ensureTALinks, 700); throttle(ensureTALinks, 700);
} }
}); });
observer.observe(document.body, { attributes: false, childList: true, subtree: true }); observer.observe(document.body, { attributes: false, childList: true, subtree: true });

1960
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "tubearchivist-browser-extension",
"private": true,
"scripts": {
"lint": "eslint 'extension/**/*.js'",
"format": "prettier --write 'extension/**/*.js'"
},
"devDependencies": {
"eslint": "^8.26.0",
"prettier": "^2.7.1",
"eslint-config-prettier": "^8.5.0"
},
"prettier": {
"singleQuote": true,
"arrowParens": "avoid",
"printWidth": 100
}
}