This commit is contained in:
Vipra 2023-05-26 00:07:14 +02:00
commit e75051b509
10 changed files with 402 additions and 341 deletions

26
.github/workflows/codequality.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Code quality
on:
pull_request:
push:
branches:
- main
jobs:
format:
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Install dependencies
run: npm ci
- name: Format files using prettier
run: npm run format
- uses: stefanzweifel/git-auto-commit-action@v4
name: Commit possible changes
with:
commit_message: "Format files using prettier"

View file

@ -1,6 +0,0 @@
{
"endOfLine": "lf",
"printWidth": 100,
"useTabs": true,
"trailingComma": "all"
}

View file

@ -1,23 +1,27 @@
# ProToots (v0.99)
A Firefox extension which displays an author's pronouns next to their name on Mastodon.
![A Mastodon screenshot showing off pronouns next to a person's name](documentation/firefox_ehHwJufMau.png)
## Download/Installation
### ⚠️ This is a **pre-release, temporary extension**! It will be **removed when you restart your browser**! ⚠️
We're working on a proper firefox store release already.
We're working on a proper firefox store release already.
To install go to [the releases page](https://github.com/ItsVipra/ProToots/releases) and follow the instructions there.
## FAQ
Why does it need permission for all websites?
> The addon needs to determine whether or not the site you are currently browsing is a Mastodon server. For that to work, it requires access to all sites. Otherwise, each existing Mastodon server would have to be explicitly added.
## setup
## setup
- install web-ext with `npm install --global web-ext`
- optionally:
- run `web-ext run --firefox-profile='$ProfileNameOfYourChoosing' --profile-create-if-mising`
- open that profile in firefox, log into fedi
- after that when you run `web-ext run -p='$ProfileNameOfYourChoosing'` you should be logged into your fedi account
- install web-ext with `npm install --global web-ext`
- optionally:
- run `web-ext run --firefox-profile='$ProfileNameOfYourChoosing' --profile-create-if-mising`
- open that profile in firefox, log into fedi
- after that when you run `web-ext run -p='$ProfileNameOfYourChoosing'` you should be logged into your fedi account
- run the extension with `web-ext run -u -u="yourinstancehere"`

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "ProToots",
"version": "0.99",
"manifest_version": 2,
"name": "ProToots",
"version": "0.99",
"icons":{
"48": "src/icons/icon small_size/icon small_size.png",
@ -21,27 +21,22 @@
"default_popup": "src/options/options.html"
},
"content_scripts": [
{
"matches": [
"*://*/*"
],
"js" : [
"src/content_scripts/protoots.js"
],
"css": ["src/styles/proplate.css"],
"run_at": "document_start"
}
],
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["src/content_scripts/protoots.js"],
"css": ["src/styles/proplate.css"],
"run_at": "document_start"
}
],
"options_ui": {
"page": "src/options/options.html"
},
"browser_specific_settings": {
"gecko": {
"id": "protoots@trans.rights"
}
}
"options_ui": {
"page": "src/options/options.html"
},
"browser_specific_settings": {
"gecko": {
"id": "protoots@trans.rights"
}
}
}

View file

@ -1,6 +1,15 @@
{
"scripts": {
"format": "prettier --write --ignore-path .gitignore ."
},
"devDependencies": {
"prettier": "^2.8.8",
"web-ext": "^7.6.2"
},
"prettier": {
"endOfLine": "lf",
"printWidth": 100,
"useTabs": true,
"trailingComma": "all"
}
}

View file

@ -4,31 +4,31 @@
//document.body.display-name__html.append();
// const max_age = 8.64e7
const max_age = 24*60*60*1000 //time after which cached pronouns should be checked again: 24h
const host_name = location.hostname
const max_age = 24 * 60 * 60 * 1000; //time after which cached pronouns should be checked again: 24h
const host_name = location.host;
//before anything else, check whether we're on a Mastodon page
checkSite()
checkSite();
let logging;
function error() {
if (logging) console.error(arguments);
if (logging) console.error(arguments);
}
function warn() {
if (logging) console.warn(arguments);
if (logging) console.warn(arguments);
}
function log() {
if (logging) console.log(arguments);
if (logging) console.log(arguments);
}
function info() {
if (logging) console.info(arguments);
if (logging) console.info(arguments);
}
function debug() {
if (logging) console.debug(arguments);
if (logging) console.debug(arguments);
}
// log("hey vippy, du bist cute <3")
@ -38,19 +38,24 @@ function debug() {
* If so creates an 'readystatechange' EventListener, with callback to main()
*/
async function checkSite() {
await browser.storage.sync.get("logging").then((res) => {logging = res['logging']}, () => {logging = true});
let requestDest = location.protocol + '//' + host_name + '/api/v1/instance'
let response = await fetch(requestDest)
await browser.storage.sync.get("logging").then(
(res) => {
logging = res["logging"];
},
() => {
logging = true;
},
);
let requestDest = location.protocol + "//" + host_name + "/api/v1/instance";
let response = await fetch(requestDest);
if (response) {
// debug('checksite response got', {'response' : response.json()})
if (response) {
// debug('checksite response got', {'response' : response.json()})
document.addEventListener('readystatechange', main)
}
else {
warn('Not a Mastodon instance')
}
document.addEventListener("readystatechange", main);
} else {
warn("Not a Mastodon instance");
}
}
/**
@ -60,38 +65,35 @@ async function checkSite() {
*
*/
function main() {
// debug('selection for id mastodon', {'result': document.querySelector("#mastodon")})
if (document.querySelector("#mastodon")) {
log('Mastodon instance, activating Protoots')
// debug('selection for id mastodon', {'result': document.querySelector("#mastodon")})
if (document.querySelector("#mastodon")) {
log("Mastodon instance, activating Protoots");
let lastUrl = location.href
new MutationObserver((mutations) => {
const url = location.href
if (url !== lastUrl) {
lastUrl = url
onUrlChange()
}
let lastUrl = location.href;
new MutationObserver((mutations) => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
onUrlChange();
}
for (const m of mutations) {
m.addedNodes.forEach(n => {
if (!(n instanceof HTMLElement))
return
for (const m of mutations) {
m.addedNodes.forEach((n) => {
if (!(n instanceof HTMLElement)) return;
if (n.className == "column") {
debug("found a column: ", n)
createObserver(n)
//TODO: yet another bad hack, pls fix
//TODO: doesn't work when going from detailed-status to detailed-status
document.querySelectorAll(".detailed-status").forEach(el => addProplate(el))
}
})
}
}).observe(document, { subtree: true, childList: true })}
else {
warn('Not a Mastodon instance')
}
if (n.className == "column") {
debug("found a column: ", n);
createObserver(n);
//TODO: yet another bad hack, pls fix
//TODO: doesn't work when going from detailed-status to detailed-status
document.querySelectorAll(".detailed-status").forEach((el) => addProplate(el));
}
});
}
}).observe(document, { subtree: true, childList: true });
} else {
warn("Not a Mastodon instance");
}
}
/**
@ -102,55 +104,56 @@ function main() {
* @param {string} account_name The account name, used for caching. Should have the "@" prefix.
*/
async function fetchPronouns(statusID, account_name) {
// log(`searching for ${account_name}`);
// log(`searching for ${account_name}`);
let cacheResult = await browser.storage.local.get().then(getSuccess, onError);
let cacheResult = await browser.storage.local.get().then(getSuccess, onError);
if (account_name[0] == '@') account_name = account_name.substring(1);
// if the username doesn't contain an @ (i.e. the post we're looking at is from this instance)
// append the host name to it, to avoid cache overlap between instances
if (!account_name.includes("@")) {
account_name = account_name + "@" + host_name
}
if (account_name[0] == "@") account_name = account_name.substring(1);
// if the username doesn't contain an @ (i.e. the post we're looking at is from this instance)
// append the host name to it, to avoid cache overlap between instances
if (!account_name.includes("@")) {
account_name = account_name + "@" + host_name;
}
// debug(cacheResult);
// debug(cacheResult);
if (Object.keys(cacheResult).length == 0) {
let pronounsCache = {}
await browser.storage.local.set({pronounsCache}).then(setSuccess, onError);
warn('created pronounsCache in storage');
}
if (Object.keys(cacheResult).length == 0) {
let pronounsCache = {};
await browser.storage.local.set({ pronounsCache }).then(setSuccess, onError);
warn("created pronounsCache in storage");
}
let cacheKeys = Object.keys(cacheResult["pronounsCache"])
let cacheKeys = Object.keys(cacheResult["pronounsCache"]);
if (cacheKeys.includes(account_name)) {
let entryValue = cacheResult["pronounsCache"][account_name].value
let entryTimestamp = cacheResult["pronounsCache"][account_name].timestamp
if ((Date.now() - entryTimestamp) < max_age) {
info(`${account_name} in cache:`, {'cache entry': cacheResult["pronounsCache"][account_name]} )
return entryValue
}
else {
info(`${account_name} entry is stale, refreshing`);
}
}
if (cacheKeys.includes(account_name)) {
let entryValue = cacheResult["pronounsCache"][account_name].value;
let entryTimestamp = cacheResult["pronounsCache"][account_name].timestamp;
if (Date.now() - entryTimestamp < max_age) {
info(`${account_name} in cache:`, {
"cache entry": cacheResult["pronounsCache"][account_name],
});
return entryValue;
} else {
info(`${account_name} entry is stale, refreshing`);
}
}
if (!statusID) {
console.warn(`Could not fetch pronouns for user ${account_name}, because no status ID was passed. This is an issue we're working on.`)
return;
}
if (!statusID) {
console.warn(
`Could not fetch pronouns for user ${account_name}, because no status ID was passed. This is an issue we're working on.`,
);
return;
}
info(`${account_name} not in cache, fetching status`)
let status = await fetchStatus(statusID);
info(`${account_name} not in cache, fetching status`);
let status = await fetchStatus(statusID);
let PronounField = getPronounField(status, account_name);
if (PronounField == 'null') {
//TODO: if no field check bio
info(`no pronouns found for ${account_name}, cached null`);
}
return PronounField
let PronounField = getPronounField(status, account_name);
if (PronounField == "null") {
//TODO: if no field check bio
info(`no pronouns found for ${account_name}, cached null`);
}
return PronounField;
}
/**
@ -160,16 +163,16 @@ async function fetchPronouns(statusID, account_name) {
* @returns {Promise<object>} Contents of the status in json form.
*/
async function fetchStatus(statusID) {
const accessToken = await getActiveAccessToken();
//fetch status from home server with access token
const response = await fetch(`${location.protocol}//${host_name}/api/v1/statuses/${statusID}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const accessToken = await getActiveAccessToken();
//fetch status from home server with access token
const response = await fetch(`https://${host_name}/api/v1/statuses/${statusID}`, { headers: { 'Authorization': `Bearer ${accessToken}` } });
let status = await response.json();
return status;
let status = await response.json();
return status;
}
/**
* Searches for fields labelled "pronouns" in the statuses' author.
* If found returns the value of said field.
@ -179,31 +182,28 @@ async function fetchStatus(statusID) {
* @returns {string} Author pronouns if found. Otherwise returns "null"
*/
function getPronounField(status, account_name) {
debug(status)
// get account from status and pull out fields
let account = status["account"];
let fields = account["fields"];
debug(status);
// get account from status and pull out fields
let account = status["account"];
let fields = account["fields"];
for (let field of fields) {
//match fields against "pronouns"
//TODO: multiple languages
if (field['name'].toLowerCase().includes("pronouns")) {
debug(`${account['acct']}: ${field['value']}`);
for (let field of fields) {
//match fields against "pronouns"
//TODO: multiple languages
if (field["name"].toLowerCase().includes("pronouns")) {
debug(`${account["acct"]}: ${field["value"]}`);
if (!(field['value'].includes("a href"))) { //filter links
let pronounSet = generatePronounSet(account_name, field['value'])
cachePronouns(account_name, pronounSet)
return(field['value']);
}
let pronounSet = generatePronounSet(account_name, field["value"]);
cachePronouns(account_name, pronounSet);
return field["value"];
}
}
}
}
//if not returned by this point no field with pronouns was found
let pronounSet = generatePronounSet(account_name, "null");
//if not returned by this point no field with pronouns was found
let pronounSet = generatePronounSet(account_name, 'null')
cachePronouns(account_name, pronounSet)
return "null"
cachePronouns(account_name, pronounSet);
return "null";
}
/**
@ -214,7 +214,7 @@ function getPronounField(status, account_name) {
* @returns {object} Object containing account name, timestamp and pronouns
*/
function generatePronounSet(account, value) {
return { 'acct': account, 'timestamp': Date.now(), 'value': value }; //TODO: this should be just account right
return { acct: account, timestamp: Date.now(), value: value }; //TODO: this should be just account right
}
/**
@ -224,24 +224,22 @@ function generatePronounSet(account, value) {
* @param {{ acct: any; timestamp: number; value: any; }} set The data to cache.
*/
async function cachePronouns(account, set) {
let result = await browser.storage.local.get("pronounsCache").then(getSuccess, onError);
let pronounsCache = result['pronounsCache']
pronounsCache[account] = set
await browser.storage.local.set({pronounsCache}).then(setSuccess, onError);
debug(`caching ${account}`)
// return
let result = await browser.storage.local.get("pronounsCache").then(getSuccess, onError);
let pronounsCache = result["pronounsCache"];
pronounsCache[account] = set;
await browser.storage.local.set({ pronounsCache }).then(setSuccess, onError);
debug(`caching ${account}`);
// return
}
function getSuccess(result) {
return result;
return result;
}
function setSuccess() {
}
function setSuccess() {}
function onError(error) {
error('Failed save to storage!');
error("Failed save to storage!");
}
/**
@ -254,21 +252,24 @@ function onError(error) {
*
* @param {Element | HTMLElement} element The status where the element should be added.
*/
async function addProplate (element) {
if (!(element instanceof HTMLElement)) return;
async function addProplate(element) {
if (!(element instanceof HTMLElement)) return;
//check whether element has already had a proplate added
if (element.hasAttribute("protoots-checked")) return
//check whether element has already had a proplate added
if (element.hasAttribute("protoots-checked")) return;
//if not add the attribute
element.setAttribute("protoots-checked", "true")
let statusId = element.dataset.id
//if not add the attribute
element.setAttribute("protoots-checked", "true");
let statusId = element.dataset.id;
if (!statusId) {
// We don't have a status ID, pronouns might not be in cache
warn("The element passed to addProplate does not have a data-id attribute, although it should have one.", element);
warn(
"The element passed to addProplate does not have a data-id attribute, although it should have one.",
element,
);
}
let accountNameEl = element.querySelector(".display-name__account");
let accountNameEl = element.querySelector(".display-name__account");
if (!accountNameEl) {
warn(
"The element passed to addProplate does not have a .display-name__account, although it should have one.",
@ -276,14 +277,14 @@ async function addProplate (element) {
);
return;
}
let accountName = accountNameEl.textContent
if (!accountName){
warn("Could not extract the account name from the element.")
let accountName = accountNameEl.textContent;
if (!accountName) {
warn("Could not extract the account name from the element.");
return;
}
//get the name element and apply CSS
let nametagEl =/** @type {HTMLElement|null} */ (element.querySelector(".display-name__html"));
//get the name element and apply CSS
let nametagEl = /** @type {HTMLElement|null} */ (element.querySelector(".display-name__html"));
if (!nametagEl) {
warn(
"The element passed to addProplate does not have a .display-name__html, although it should have one.",
@ -295,66 +296,77 @@ async function addProplate (element) {
nametagEl.style.display = "flex";
nametagEl.style.alignItems = "baseline";
//create plate
const proplate = document.createElement("span");
let pronouns = await fetchPronouns(statusId, accountName)
if (pronouns == "null" && !logging) {return}
proplate.textContent = pronouns;
//create plate
const proplate = document.createElement("span");
let pronouns = await fetchPronouns(statusId, accountName);
if (pronouns == "null" && !logging) {
return;
}
proplate.innerHTML = sanitizePronouns(pronouns);
proplate.classList.add("protoots-proplate");
if ((host_name == "queer.group" && (accountName == "@vivien" || accountName == "@jasmin")) || accountName == "@jasmin@queer.group" || accountName == "@vivien@queer.group") {
//i think you can figure out what this does on your own
proplate.classList.add("pog")
}
if (
(host_name == "queer.group" && (accountName == "@vivien" || accountName == "@jasmin")) ||
accountName == "@jasmin@queer.group" ||
accountName == "@vivien@queer.group"
) {
//i think you can figure out what this does on your own
proplate.classList.add("pog");
}
//add plate to nametag
nametagEl.appendChild(proplate);
//add plate to nametag
nametagEl.appendChild(proplate);
}
function createObserver(element) {
// select column as observation target
// const targetNode = document.querySelector(".column");
const targetNode = element
// select column as observation target
// const targetNode = document.querySelector(".column");
const targetNode = element;
// observe childlist and subtree events
// docs: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
const config = { childList: true, subtree: true, attributes: true };
// observe childlist and subtree events
// docs: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
const config = { childList: true, subtree: true, attributes: true };
// define callback inline
const callback = (/** @type {MutationRecord[]} */ mutationList, /** @type {MutationObserver} */ observer) => {
for (const mutation of mutationList) {
// define callback inline
const callback = (
/** @type {MutationRecord[]} */ mutationList,
/** @type {MutationObserver} */ observer,
) => {
for (const mutation of mutationList) {
mutation.addedNodes.forEach((n) => {
if (!(n instanceof HTMLElement)) return;
mutation.addedNodes.forEach((n) => {
if (!(n instanceof HTMLElement)) return;
//case for all the other normal statuses
if (
containsClass(n.classList, "status") &&
!containsClass(n.classList, "status__prepend")
) {
//|| containsClass(n.classList, "detailed-status"))) {
addProplate(n);
} else {
//for nodes that have a broken classlist
let statusElement = n.querySelector(".status");
if (statusElement) {
addProplate(statusElement);
}
//potential solution for dirty hack?
// debug(".status not found looking for .detailed-status", {"element:": n})
// statusElement = n.querySelector(".detailed-status")
// if (statusElement != null) {
// addProplate(statusElement);
// }
}
});
}
//TODO: bad hack, please remove
document.querySelectorAll(".status").forEach((el) => addProplate(el));
document.querySelectorAll(".detailed-status").forEach((el) => addProplate(el));
};
//case for all the other normal statuses
if (containsClass(n.classList, "status") && !containsClass(n.classList, "status__prepend")) { //|| containsClass(n.classList, "detailed-status"))) {
addProplate(n);
}
else {
//for nodes that have a broken classlist
let statusElement = n.querySelector(".status")
if (statusElement) {
addProplate(statusElement);
}
//potential solution for dirty hack?
// debug(".status not found looking for .detailed-status", {"element:": n})
// statusElement = n.querySelector(".detailed-status")
// if (statusElement != null) {
// addProplate(statusElement);
// }
}
})
}
//TODO: bad hack, please remove
document.querySelectorAll(".status").forEach(el => addProplate(el));
document.querySelectorAll(".detailed-status").forEach(el => addProplate(el));
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
}
/**
@ -362,9 +374,11 @@ function createObserver(element) {
* Creates a new MutationObserver for each column on the page.
*/
function onUrlChange() {
//select all columns for advanced interface
document.querySelectorAll(".column").forEach(el => {createObserver(el)});
// createObserver();
//select all columns for advanced interface
document.querySelectorAll(".column").forEach((el) => {
createObserver(el);
});
// createObserver();
}
/**
@ -373,15 +387,15 @@ function onUrlChange() {
* @returns Whether the classList contains the class.
*/
function containsClass(classList, cl) {
if (!classList|| !cl) return false;
if (!classList || !cl) return false;
for (const c of classList) {
if (c === cl) {
return true
}
}
for (const c of classList) {
if (c === cl) {
return true;
}
}
return false
return false;
}
/**
@ -391,13 +405,28 @@ return false
async function getActiveAccessToken() {
// Fortunately, Mastodon provides the initial state in a <script> element at the beginning of the page.
// Besides a lot of other information, it contains the access token for the current user.
const initialStateEl = document.getElementById("initial-state")
const initialStateEl = document.getElementById("initial-state");
if (!initialStateEl) {
error("user not logged in yet")
error("user not logged in yet");
return "";
}
// Parse the JSON inside the script tag and extract the meta element from it.
const { meta } = JSON.parse(initialStateEl.innerText)
const { meta } = JSON.parse(initialStateEl.innerText);
return meta.access_token;
}
/**
* Sanitizes the pronoun field by removing various long information parts.
* As of today, this just removes custom emojis from the field.
*
* @param {string} str The input string.
* @returns The sanitized string.
*/
function sanitizePronouns(str) {
// Remove all custom emojis with the :shortcode: format.
str = str.replace(/:[\w_]+:/gi, "");
// Finally, remove leading and trailing whitespace.
return str.trim();
}

View file

@ -1,74 +1,75 @@
body {
margin: 0;
--background: hsl(249, 11%, 90%);
--hover: hsl(249, 11%, 75%);
--border: hsl(0, 0%, 50%);
--link: hsl(0, 0%, 25%);
font-family: system-ui, sans-serif;
margin: 0;
--background: hsl(249, 11%, 90%);
--hover: hsl(249, 11%, 75%);
--border: hsl(0, 0%, 50%);
--link: hsl(0, 0%, 25%);
}
.protoots-settings{
margin-bottom: 0;
.protoots-settings {
margin-bottom: 0;
}
.row {
background-color: var(--background);
margin: 0.5em;
border-radius: 0.5em;
display: flex;
justify-content: space-between;
background-color: var(--background);
margin: 0.5em;
border-radius: 0.5em;
display: flex;
justify-content: space-between;
}
.switch{
display: block;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
padding: 0.25em;
padding-left: 0.75em;
margin-top: 0.25em;
.switch {
display: block;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
padding: 0.25em;
padding-left: 0.75em;
margin-top: 0.25em;
}
input{
margin: 0.75em;
input {
margin: 0.75em;
}
.description {
/* up right down left */
padding: 0 0 0 0.75em;
margin: 0 0 0.5em 0;
/* up right down left */
padding: 0 0 0 0.75em;
margin: 0 0 0.5em 0;
}
.footer {
background-color: var(--background);
padding: 1em;
border-top: 1px dashed var(--border);
display: flex;
align-items: baseline;
justify-content: space-between;
background-color: var(--background);
padding: 1em;
border-top: 1px dashed var(--border);
display: flex;
align-items: baseline;
justify-content: space-between;
}
button {
padding: 0.75em 1em;
border-radius: 0.5em;
border: 1px solid var(--border);
cursor: pointer;
padding: 0.75em 1em;
border-radius: 0.5em;
border: 1px solid var(--border);
cursor: pointer;
}
button:hover {
background-color: var(--hover);
background-color: var(--hover);
}
a {
color: var(--link);
color: var(--link);
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1c1b22;
color: hsl(0, 0%, 90%);
--background: hsl(249, 11%, 25%);
--hover: hsl(249, 11%, 75%);
--border: hsl(0, 0%, 90%);
--link: hsl(0, 0%, 75%);
}
}
body {
background-color: #1c1b22;
color: hsl(0, 0%, 90%);
--background: hsl(249, 11%, 25%);
--hover: hsl(249, 11%, 75%);
--border: hsl(0, 0%, 90%);
--link: hsl(0, 0%, 75%);
}
}

View file

@ -1,25 +1,26 @@
<html>
<head>
<link rel="stylesheet" href="options.css">
</head>
<body>
<form class="protoots-settings">
<div class="row">
<div class="text">
<label for="logging" class="switch">Enable logging</label>
<p id="logging-description" class="description">Log debug information to browser console</p>
</div>
<input type="checkbox" id="logging" name="logging" aria-describedby="logging-description">
<head>
<link rel="stylesheet" href="options.css" />
</head>
<body>
<form class="protoots-settings">
<div class="row">
<div class="text">
<label for="logging" class="switch">Enable logging</label>
<p id="logging-description" class="description">
Log debug information to browser console
</p>
</div>
<input type="checkbox" id="logging" name="logging" aria-describedby="logging-description" />
</div>
<div class="footer">
<button type="submit">Save</button>
<a target="_blank" href="https://github.com/ItsVipra/ProToots"
>More info / help on Github</a
>
</div>
</form>
</div>
<div class="footer">
<button type="submit">Save</button>
<a target="_blank" href="https://github.com/ItsVipra/ProToots">More info / help on Github</a>
</div>
</form>
<script src="options.js"></script>
</body>
</html>
<script src="options.js"></script>
</body>
</html>

View file

@ -1,22 +1,22 @@
function saveOptions(e) {
e.preventDefault();
browser.storage.sync.set({
logging: document.querySelector("#logging").checked
});
}
e.preventDefault();
browser.storage.sync.set({
logging: document.querySelector("#logging").checked,
});
}
function restoreOptions() {
function setCurrentChoice(result) {
document.querySelector("#logging").checked = result.logging || off;
}
function restoreOptions() {
function setCurrentChoice(result) {
document.querySelector("#logging").checked = result.logging || off;
}
function onError(error) {
console.log(`Error: ${error}`);
}
function onError(error) {
console.log(`Error: ${error}`);
}
let getting = browser.storage.sync.get("logging");
getting.then(setCurrentChoice, onError);
}
let getting = browser.storage.sync.get("logging");
getting.then(setCurrentChoice, onError);
}
document.addEventListener("DOMContentLoaded", restoreOptions);
document.querySelector("form").addEventListener("submit", saveOptions);
document.addEventListener("DOMContentLoaded", restoreOptions);
document.querySelector("form").addEventListener("submit", saveOptions);

View file

@ -17,7 +17,7 @@
}
/* dark theme on detailed status */
.theme-default .detailed-status .protoots-proplate {
.theme-default .detailed-status .protoots-proplate {
/* scrollable background */
background-color: #282c37;
@ -49,16 +49,18 @@
/* rainbow pronouns for jasmin and i cause why not */
.pog:hover {
background-image: linear-gradient(45deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%);
background-image: linear-gradient(
45deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
);
}