Unify cookies
This commit is contained in:
parent
5d48d60e96
commit
800d0cbd9f
14
README.md
14
README.md
@ -29,7 +29,7 @@ Imgur => [Rimgo](https://codeberg.org/video-prize-ranch/rimgo)\
|
||||
Wikipedia => [Wikiless](https://codeberg.org/orenom/wikiless)\
|
||||
Medium => [Scribe](https://sr.ht/~edwardloveall/scribe/)\
|
||||
Quora => [Quetre](https://github.com/zyachel/quetre)\
|
||||
IMDb => [Libremdb](https://github.com/zyachel/libremdb)\
|
||||
IMDb => [libremdb](https://github.com/zyachel/libremdb)\
|
||||
PeerTube => [SimpleerTube](https://git.sr.ht/~metalune/simpleweb_peertube)\
|
||||
LBRY/Odysee => [Librarian](https://codeberg.org/librarian/librarian), [LBRY Desktop](https://lbry.com/get)\
|
||||
Search => [SearXNG](https://github.com/searxng/searxng), [SearX](https://searx.github.io/searx/), [Whoogle](https://benbusby.com/projects/whoogle-search/), [LibreX](https://github.com/hnhx/librex/)\
|
||||
@ -72,21 +72,19 @@ npm update
|
||||
npm install
|
||||
```
|
||||
|
||||
If you are modifying any files ending with .pug, the pug cli needs to be installed with the following command (with root privileges):
|
||||
If you are modifying any files ending with .ejs, you need to run the following command to render html:
|
||||
|
||||
```
|
||||
npm install -g pug-cli
|
||||
npm run ejs
|
||||
```
|
||||
|
||||
and then run `npm run pug` to generate pages in the background.
|
||||
|
||||
### Build
|
||||
### Build the extention zip archive:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Test
|
||||
### Run automated tests
|
||||
|
||||
```
|
||||
npm run test
|
||||
@ -120,4 +118,4 @@ select `load unpacked extension`\
|
||||
select `src` folder
|
||||
|
||||
[Privacy Policy](Privacy-Policy.md)\
|
||||
Credits: [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)
|
||||
Forked from [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)
|
||||
|
@ -9,8 +9,6 @@
|
||||
"start": "web-ext run --browser-console --source-dir ./src/",
|
||||
"build": "web-ext build --overwrite-dest --source-dir ./src/",
|
||||
"test": "web-ext lint --source-dir ./src/ || true",
|
||||
"pug": "pug ./src/pages/options/*.pug ./src/pages/popup/ -P -w",
|
||||
"prettier": "npx prettier --write .",
|
||||
"instances": "python3 src/instances/get_instances.py; git update-index --assume-unchanged src/instances/blacklist.json src/instances/data.json",
|
||||
"ejs": "npx ejs src/pages/options/index.ejs -f src/config/config.json -o src/pages/options/index.html; npx ejs src/pages/popup/popup.ejs -f src/config/config.json -o src/pages/popup/popup.html"
|
||||
},
|
||||
|
@ -11,8 +11,8 @@ function isException(url) {
|
||||
|
||||
function init() {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get("exceptions", r => {
|
||||
exceptions = r.exceptions
|
||||
browser.storage.local.get("options", r => {
|
||||
exceptions = r.options.exceptions
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
@ -30,7 +30,7 @@ async function initDefaults() {
|
||||
url: [],
|
||||
regex: [],
|
||||
},
|
||||
theme: "DEFAULT",
|
||||
theme: "detect",
|
||||
popupServices: ["youtube", "twitter", "instagram", "tiktok", "imgur", "reddit", "quora", "translate", "maps"],
|
||||
autoRedirect: false,
|
||||
firstPartyIsolate: false,
|
||||
|
@ -19,25 +19,28 @@ function init() {
|
||||
return new Promise(async resolve => {
|
||||
// await getConfig()
|
||||
browser.storage.local.get(["options", "targets", "redirects", "blacklists"], r => {
|
||||
blacklists = r.blacklists
|
||||
redirects = r.redirects
|
||||
targets = r.targets
|
||||
options = r.options
|
||||
if (r.options) {
|
||||
blacklists = r.blacklists
|
||||
redirects = r.redirects
|
||||
targets = r.targets
|
||||
options = r.options
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await init()
|
||||
await getConfig()
|
||||
await init()
|
||||
|
||||
function fetchFrontendInstanceList(service, frontend) {
|
||||
let tmp = []
|
||||
if (!config.services[service].frontends[frontend].singleInstance) {
|
||||
if (config.services[service].frontends[frontend].instanceList) {
|
||||
for (const network in config.networks) {
|
||||
if (!redirects[frontend]) console.log(frontend)
|
||||
tmp.push(...redirects[frontend][network], ...options[frontend][network].custom)
|
||||
}
|
||||
} else if (config.services[service].frontends[frontend].singleInstance != undefined) tmp = config.services[service].frontends[frontend].singleInstance
|
||||
} else if (config.services[service].frontends[frontend].singleInstance) tmp = config.services[service].frontends[frontend].singleInstance
|
||||
return tmp
|
||||
}
|
||||
|
||||
@ -58,8 +61,8 @@ function all(service, frontend) {
|
||||
function regexArray(service, url) {
|
||||
let targets
|
||||
if (config.services[service].targets == "datajson") {
|
||||
browser.storage.local.get(`${service}Targets`, r => {
|
||||
targets = r[service + "Targets"]
|
||||
browser.storage.local.get("targets", r => {
|
||||
targets = r.targets[service]
|
||||
})
|
||||
} else {
|
||||
targets = config.services[service].targets
|
||||
@ -397,8 +400,10 @@ function initDefaults() {
|
||||
let redirects = JSON.parse(data)
|
||||
let options = r.options
|
||||
let targets = {}
|
||||
// let latency = {}
|
||||
for (const service in config.services) {
|
||||
options[service] = {}
|
||||
// latency[service] = {}
|
||||
if (config.services[service].targets == "datajson") {
|
||||
targets[service] = redirects[service]
|
||||
//delete dataJson[service]
|
||||
@ -423,19 +428,25 @@ function initDefaults() {
|
||||
}
|
||||
}
|
||||
}
|
||||
browser.storage.local.set({ redirects, options, targets })
|
||||
browser.storage.local.set({ redirects, options, targets /*, latency*/ })
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function computeService(url) {
|
||||
function computeService(url, returnFrontend) {
|
||||
for (const service in config.services) {
|
||||
if (regexArray(service, url)) {
|
||||
return service
|
||||
} else if (all(service).includes(utils.protocolHost(url))) {
|
||||
return service
|
||||
if (returnFrontend) return [service, null]
|
||||
else return service
|
||||
} else {
|
||||
for (const frontend in config.services[service].frontends) {
|
||||
if (all(service, frontend).includes(utils.protocolHost(url))) {
|
||||
if (returnFrontend) return [service, frontend]
|
||||
else return service
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
@ -524,7 +535,7 @@ function unifyPreferences(url) {
|
||||
|
||||
const frontend = config.services[currentService].frontends[currentFrontend]
|
||||
if ("cookies" in frontend.preferences) {
|
||||
for (const cookie in frontend.preferences.cookies) {
|
||||
for (const cookie of frontend.preferences.cookies) {
|
||||
await utils.copyCookie(currentFrontend, url, instancesList, cookie)
|
||||
}
|
||||
}
|
||||
|
@ -111,29 +111,27 @@ function protocolHost(url) {
|
||||
return `${url.protocol}//${url.host}`
|
||||
}
|
||||
|
||||
async function processDefaultCustomInstances(target, name, protocol, document) {
|
||||
let latencyKey = `${name}Latency`
|
||||
async function processDefaultCustomInstances(service, name, network, document) {
|
||||
let instancesLatency
|
||||
let nameProtocolElement = document.getElementById(name).getElementsByClassName(protocol)[0]
|
||||
let nameNetworkElement = document.getElementById(name).getElementsByClassName(network)[0]
|
||||
|
||||
let nameCustomInstances = []
|
||||
let nameCheckListElement = nameProtocolElement.getElementsByClassName("checklist")[0]
|
||||
let nameCheckListElement = nameNetworkElement.getElementsByClassName("checklist")[0]
|
||||
|
||||
await initBlackList()
|
||||
|
||||
let nameDefaultRedirects
|
||||
|
||||
let redirectsChecks = `${name}${camelCase(protocol)}RedirectsChecks`
|
||||
let customRedirects = `${name}${camelCase(protocol)}CustomRedirects`
|
||||
|
||||
let redirects
|
||||
let redirects, options
|
||||
|
||||
async function getFromStorage() {
|
||||
return new Promise(async resolve =>
|
||||
browser.storage.local.get([redirectsChecks, customRedirects, "redirects", latencyKey], r => {
|
||||
nameDefaultRedirects = r[redirectsChecks]
|
||||
nameCustomInstances = r[customRedirects]
|
||||
instancesLatency = r[latencyKey] ?? []
|
||||
browser.storage.local.get(["options", "redirects", "latency"], r => {
|
||||
nameDefaultRedirects = r.options[name][network].enabled
|
||||
nameCustomInstances = r.options[name][network].custom
|
||||
options = r.options
|
||||
if (r.latency) instancesLatency = r.latency[name] ?? []
|
||||
else instancesLatency = []
|
||||
redirects = r.redirects
|
||||
resolve()
|
||||
})
|
||||
@ -141,12 +139,11 @@ async function processDefaultCustomInstances(target, name, protocol, document) {
|
||||
}
|
||||
|
||||
await getFromStorage()
|
||||
if (nameCustomInstances === undefined) console.log(customRedirects)
|
||||
|
||||
function calcNameCheckBoxes() {
|
||||
let isTrue = true
|
||||
for (const item of redirects[name][protocol]) {
|
||||
if (nameDefaultRedirects === undefined) console.log(name + protocol + " is undefined")
|
||||
for (const item of redirects[name][network]) {
|
||||
if (nameDefaultRedirects === undefined) console.log(name + network + " is undefined")
|
||||
if (!nameDefaultRedirects.includes(item)) {
|
||||
isTrue = false
|
||||
break
|
||||
@ -156,14 +153,14 @@ async function processDefaultCustomInstances(target, name, protocol, document) {
|
||||
element.checked = nameDefaultRedirects.includes(element.className)
|
||||
}
|
||||
if (nameDefaultRedirects.length == 0) isTrue = false
|
||||
nameProtocolElement.getElementsByClassName("toggle-all")[0].checked = isTrue
|
||||
nameNetworkElement.getElementsByClassName("toggle-all")[0].checked = isTrue
|
||||
}
|
||||
nameCheckListElement.innerHTML = [
|
||||
`<div>
|
||||
<x data-localise="__MSG_toggleAll__">Toggle All</x>
|
||||
<input type="checkbox" class="toggle-all"/>
|
||||
</div>`,
|
||||
...redirects[name][protocol].map(x => {
|
||||
...redirects[name][network].map(x => {
|
||||
const cloudflare = cloudflareBlackList.includes(x) ? ' <span style="color:red;">cloudflare</span>' : ""
|
||||
const authenticate = authenticateBlackList.includes(x) ? ' <span style="color:orange;">authenticate</span>' : ""
|
||||
const offline = offlineBlackList.includes(x) ? ' <span style="color:grey;">offline</span>' : ""
|
||||
@ -188,31 +185,32 @@ async function processDefaultCustomInstances(target, name, protocol, document) {
|
||||
localise.localisePage()
|
||||
|
||||
calcNameCheckBoxes()
|
||||
nameProtocolElement.getElementsByClassName("toggle-all")[0].addEventListener("change", async event => {
|
||||
if (event.target.checked) nameDefaultRedirects = [...redirects[name][protocol]]
|
||||
nameNetworkElement.getElementsByClassName("toggle-all")[0].addEventListener("change", async event => {
|
||||
if (event.service.checked) nameDefaultRedirects = [...redirects[name][network]]
|
||||
else nameDefaultRedirects = []
|
||||
|
||||
browser.storage.local.set({ [redirectsChecks]: nameDefaultRedirects })
|
||||
options[service][network].enabled = nameDefaultRedirects
|
||||
browser.storage.local.set({ options })
|
||||
calcNameCheckBoxes()
|
||||
})
|
||||
|
||||
for (let element of nameCheckListElement.getElementsByTagName("input")) {
|
||||
if (element.className != "toggle-all")
|
||||
nameProtocolElement.getElementsByClassName(element.className)[0].addEventListener("change", async event => {
|
||||
if (event.target.checked) nameDefaultRedirects.push(element.className)
|
||||
nameNetworkElement.getElementsByClassName(element.className)[0].addEventListener("change", async event => {
|
||||
if (event.service.checked) nameDefaultRedirects.push(element.className)
|
||||
else {
|
||||
let index = nameDefaultRedirects.indexOf(element.className)
|
||||
if (index > -1) nameDefaultRedirects.splice(index, 1)
|
||||
}
|
||||
browser.storage.local.set({
|
||||
[redirectsChecks]: nameDefaultRedirects,
|
||||
})
|
||||
|
||||
options[service][network].enabled = nameDefaultRedirects
|
||||
browser.storage.local.set({ options })
|
||||
calcNameCheckBoxes()
|
||||
})
|
||||
}
|
||||
|
||||
function calcNameCustomInstances() {
|
||||
nameProtocolElement.getElementsByClassName("custom-checklist")[0].innerHTML = nameCustomInstances
|
||||
nameNetworkElement.getElementsByClassName("custom-checklist")[0].innerHTML = nameCustomInstances
|
||||
.map(
|
||||
x => `<div>
|
||||
${x}
|
||||
@ -227,24 +225,26 @@ async function processDefaultCustomInstances(target, name, protocol, document) {
|
||||
.join("\n")
|
||||
|
||||
for (const item of nameCustomInstances) {
|
||||
nameProtocolElement.getElementsByClassName(`clear-${item}`)[0].addEventListener("click", async () => {
|
||||
nameNetworkElement.getElementsByClassName(`clear-${item}`)[0].addEventListener("click", async () => {
|
||||
let index = nameCustomInstances.indexOf(item)
|
||||
if (index > -1) nameCustomInstances.splice(index, 1)
|
||||
browser.storage.local.set({ [customRedirects]: nameCustomInstances })
|
||||
options[service][network].custom = nameCustomInstances
|
||||
browser.storage.local.set({ options })
|
||||
calcNameCustomInstances()
|
||||
})
|
||||
}
|
||||
}
|
||||
calcNameCustomInstances()
|
||||
nameProtocolElement.getElementsByClassName("custom-instance-form")[0].addEventListener("submit", async event => {
|
||||
nameNetworkElement.getElementsByClassName("custom-instance-form")[0].addEventListener("submit", async event => {
|
||||
event.preventDefault()
|
||||
let nameCustomInstanceInput = nameProtocolElement.getElementsByClassName("custom-instance")[0]
|
||||
let nameCustomInstanceInput = nameNetworkElement.getElementsByClassName("custom-instance")[0]
|
||||
let url = new URL(nameCustomInstanceInput.value)
|
||||
let protocolHostVar = protocolHost(url)
|
||||
if (nameCustomInstanceInput.validity.valid && !redirects[name][protocol].includes(protocolHostVar)) {
|
||||
if (nameCustomInstanceInput.validity.valid && !redirects[name][network].includes(protocolHostVar)) {
|
||||
if (!nameCustomInstances.includes(protocolHostVar)) {
|
||||
nameCustomInstances.push(protocolHostVar)
|
||||
browser.storage.local.set({ [customRedirects]: nameCustomInstances })
|
||||
options[service][network].custom = nameCustomInstances
|
||||
browser.storage.local.set({ options })
|
||||
nameCustomInstanceInput.value = ""
|
||||
}
|
||||
calcNameCustomInstances()
|
||||
@ -299,9 +299,9 @@ async function testLatency(element, instances, frontend) {
|
||||
let myList = {}
|
||||
let latencyThreshold
|
||||
let redirectsChecks = []
|
||||
browser.storage.local.get(["latencyThreshold", `${frontend}ClearnetRedirectsChecks`], r => {
|
||||
latencyThreshold = r.latencyThreshold
|
||||
redirectsChecks = r[`${frontend}ClearnetRedirectsChecks`]
|
||||
browser.storage.local.get(["options"], r => {
|
||||
latencyThreshold = r.options.latencyThreshold
|
||||
redirectsChecks = r.options[frontend].clearnet.enabled
|
||||
})
|
||||
for (const href of instances)
|
||||
await ping(href).then(time => {
|
||||
@ -331,9 +331,9 @@ async function testLatency(element, instances, frontend) {
|
||||
|
||||
function copyCookie(frontend, targetUrl, urls, name) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get("firstPartyIsolate", r => {
|
||||
browser.storage.local.get("options", r => {
|
||||
let query
|
||||
if (!r.firstPartyIsolate)
|
||||
if (!r.options.firstPartyIsolate)
|
||||
query = {
|
||||
url: protocolHost(targetUrl),
|
||||
name: name,
|
||||
@ -348,7 +348,7 @@ function copyCookie(frontend, targetUrl, urls, name) {
|
||||
for (const cookie of cookies)
|
||||
if (cookie.name == name) {
|
||||
for (const url of urls) {
|
||||
const setQuery = r.firstPartyIsolate
|
||||
const setQuery = r.options.firstPartyIsolate
|
||||
? {
|
||||
url: url,
|
||||
name: name,
|
||||
@ -375,23 +375,21 @@ function copyCookie(frontend, targetUrl, urls, name) {
|
||||
|
||||
function getPreferencesFromToken(frontend, targetUrl, urls, name, endpoint) {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get("firstPartyIsolate", r => {
|
||||
const http = new XMLHttpRequest()
|
||||
const url = `${targetUrl}${endpoint}`
|
||||
http.open("GET", url, false)
|
||||
//http.setRequestHeader("Cookie", `${name}=${cookie.value}`)
|
||||
http.send(null)
|
||||
const preferences = JSON.parse(http.responseText)
|
||||
let formdata = new FormData()
|
||||
for (var key in preferences) formdata.append(key, preferences[key])
|
||||
for (const url of urls) {
|
||||
const http = new XMLHttpRequest()
|
||||
const url = `${targetUrl}${endpoint}`
|
||||
http.open("GET", url, false)
|
||||
http.setRequestHeader("Cookie", `${name}=${cookie.value}`)
|
||||
http.open("POST", `${url}/settings/stay`, false)
|
||||
http.send(null)
|
||||
const preferences = JSON.parse(http.responseText)
|
||||
let formdata = new FormData()
|
||||
for (var key in preferences) formdata.append(key, preferences[key])
|
||||
for (const url of urls) {
|
||||
const http = new XMLHttpRequest()
|
||||
http.open("POST", `${url}/settings/stay`, false)
|
||||
http.send(null)
|
||||
}
|
||||
resolve()
|
||||
return
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@ -427,7 +425,7 @@ function copyRaw(test, copyRawElement) {
|
||||
})
|
||||
}
|
||||
|
||||
function unify(test) {
|
||||
function unify() {
|
||||
return new Promise(resolve => {
|
||||
browser.tabs.query({ active: true, currentWindow: true }, async tabs => {
|
||||
let currTab = tabs[0]
|
||||
|
@ -184,7 +184,7 @@
|
||||
"instanceList": true
|
||||
}
|
||||
},
|
||||
"targets": ["^https?:\\/{2}(www\\.)?instagram\\.com"],
|
||||
"targets": ["^https?:\\/{2}(www\\.)?instagram\\.com\\/p\\/"],
|
||||
"name": "Instagram",
|
||||
"options": { "enabled": true },
|
||||
"imageType": "png",
|
||||
|
@ -121,7 +121,7 @@ async function redirectOfflineInstance(url, tabId) {
|
||||
let counter = 0
|
||||
|
||||
function isAutoRedirect() {
|
||||
return new Promise(resolve => browser.storage.local.get("autoRedirect", r => resolve(r.autoRedirect == true)))
|
||||
return new Promise(resolve => browser.storage.local.get("options", r => resolve(r.options.autoRedirect == true)))
|
||||
}
|
||||
|
||||
browser.webRequest.onResponseStarted.addListener(
|
||||
|
@ -99,7 +99,7 @@
|
||||
<div class="some-block option-block">
|
||||
<h4 data-localise="__MSG_theme__">Theme</h4>
|
||||
<select id="theme">
|
||||
<option value="DEFAULT" data-localise="__MSG_system__">System</option>
|
||||
<option value="detect" data-localise="__MSG_detect__">Detect</option>
|
||||
<option value="light" data-localise="__MSG_light__">Light</option>
|
||||
<option value="dark" data-localise="__MSG_dark__">Dark</option>
|
||||
</select>
|
||||
|
@ -4,8 +4,8 @@ import localise from "../../assets/javascripts/localise.js"
|
||||
|
||||
function changeTheme() {
|
||||
return new Promise(resolve => {
|
||||
browser.storage.local.get("theme", r => {
|
||||
switch (r.theme) {
|
||||
browser.storage.local.get("options", r => {
|
||||
switch (r.options.theme) {
|
||||
case "dark":
|
||||
document.body.classList.add("dark-theme")
|
||||
document.body.classList.remove("light-theme")
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="some-block option-block">
|
||||
<h4 data-localise="__MSG_theme__">Theme</h4>
|
||||
<select id="theme">
|
||||
<option value="DEFAULT" data-localise="__MSG_system__">System</option>
|
||||
<option value="detect" data-localise="__MSG_detect__">Detect</option>
|
||||
<option value="light" data-localise="__MSG_light__">Light</option>
|
||||
<option value="dark" data-localise="__MSG_dark__">Dark</option>
|
||||
</select>
|
||||
|
@ -241,7 +241,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor">
|
||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path>
|
||||
</svg></a></div>
|
||||
<div class="some-block" id="unify_div" title="Unify cookies across all selected instances"><a class="title button prevent" id="unify">
|
||||
<div class="some-block" id="unify_div" title="Unify preferences across all selected instances"><a class="title button prevent" id="unify">
|
||||
<h4 data-localise="__MSG_unifySettings__">Unify Settings</h4>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="currentColor">
|
||||
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"></path>
|
||||
|
@ -78,11 +78,11 @@ browser.storage.local.get("options", r => {
|
||||
return
|
||||
}
|
||||
|
||||
const currentService = serviceHelper.computeService(url)
|
||||
if (currentService != null) {
|
||||
divs[currentService].current.classList.remove("hide")
|
||||
divs[currentService].all.classList.add("hide")
|
||||
if (config.services[currentService].preferences != undefined) {
|
||||
const [service, frontend] = serviceHelper.computeService(url, true)
|
||||
if (service) {
|
||||
divs[service].current.classList.remove("hide")
|
||||
divs[service].all.classList.add("hide")
|
||||
if (config.services[service].frontends[frontend].preferences) {
|
||||
const unify = document.getElementById("unify")
|
||||
const textElement = document.getElementById("unify").getElementsByTagName("h4")[0]
|
||||
unify.addEventListener("click", () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user