mirror of
https://github.com/ItsVipra/ProToots
synced 2024-11-24 20:43:03 +00:00
initial commit
This commit is contained in:
commit
1e5e338c08
16 changed files with 6396 additions and 0 deletions
152
.gitignore
vendored
Normal file
152
.gitignore
vendored
Normal file
|
@ -0,0 +1,152 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||
|
||||
# web-ext-artifacts
|
||||
web-ext-artifacts
|
||||
|
||||
# note files
|
||||
TODO
|
||||
edgecases
|
||||
notes
|
1
.node-version
Normal file
1
.node-version
Normal file
|
@ -0,0 +1 @@
|
|||
v18.16.0
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"endOfLine": "lf",
|
||||
"printWidth": 100,
|
||||
"useTabs": true,
|
||||
"trailingComma": "all"
|
||||
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
}
|
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.insertFinalNewline": true
|
||||
}
|
20
README.md
Normal file
20
README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# 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
|
||||
uuuuhhh i dunno, i've probably sent it to you
|
||||
|
||||
## 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
|
||||
- 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"`
|
BIN
documentation/firefox_ehHwJufMau.png
Normal file
BIN
documentation/firefox_ehHwJufMau.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
42
manifest.json
Normal file
42
manifest.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ProToots",
|
||||
"version": "0.99",
|
||||
|
||||
|
||||
"description": "puts pronouns next to usernames on mastodon",
|
||||
"homepage_url": "https://github.com/ItsVipra/ProToots",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
|
||||
"browser_action": {
|
||||
"default_icon": "src/icons/beasts-32.png",
|
||||
"default_title": "ProToots",
|
||||
"default_popup": "src/options/options.html"
|
||||
},
|
||||
|
||||
"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@mastodon.com"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
5555
package-lock.json
generated
Normal file
5555
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
6
package.json
Normal file
6
package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"prettier": "^2.8.8",
|
||||
"web-ext": "^7.6.2"
|
||||
}
|
||||
}
|
403
src/content_scripts/protoots.js
Normal file
403
src/content_scripts/protoots.js
Normal file
|
@ -0,0 +1,403 @@
|
|||
// @ts-check
|
||||
//const proplate = document.createElement("bdi");
|
||||
//proplate.textContent = "pro/nouns";
|
||||
//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
|
||||
|
||||
//before anything else, check whether we're on a Mastodon page
|
||||
checkSite()
|
||||
let logging;
|
||||
|
||||
function error() {
|
||||
if (logging) console.error(arguments);
|
||||
}
|
||||
|
||||
function warn() {
|
||||
if (logging) console.warn(arguments);
|
||||
}
|
||||
|
||||
function log() {
|
||||
if (logging) console.log(arguments);
|
||||
}
|
||||
|
||||
function info() {
|
||||
if (logging) console.info(arguments);
|
||||
}
|
||||
|
||||
function debug() {
|
||||
if (logging) console.debug(arguments);
|
||||
}
|
||||
|
||||
// log("hey vippy, du bist cute <3")
|
||||
|
||||
/**
|
||||
* Checks whether site responds to Mastodon API Calls.
|
||||
* 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)
|
||||
|
||||
if (response) {
|
||||
// debug('checksite response got', {'response' : response.json()})
|
||||
|
||||
document.addEventListener('readystatechange', main)
|
||||
|
||||
}
|
||||
else {
|
||||
warn('Not a Mastodon instance')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the result of document.querySelector("#mastodon") and only creates a MutationObserver if the site is Mastodon.
|
||||
* Warns that site is not Mastodon otherwise.
|
||||
* - This prevents any additional code from being run.
|
||||
*
|
||||
*/
|
||||
function main() {
|
||||
// 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()
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches pronouns associated with account name.
|
||||
* If cache misses status is fetched from the instance.
|
||||
*
|
||||
* @param {string | undefined} statusID ID of the status being requested, in case cache misses.
|
||||
* @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}`);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// debug(cacheResult);
|
||||
|
||||
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"])
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches status by statusID from host_name with user's access token.
|
||||
*
|
||||
* @param {string} statusID ID of status being requested.
|
||||
* @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(`https://${host_name}/api/v1/statuses/${statusID}`, { headers: { 'Authorization': `Bearer ${accessToken}` } });
|
||||
|
||||
let status = await response.json();
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches for fields labelled "pronouns" in the statuses' author.
|
||||
* If found returns the value of said field.
|
||||
*
|
||||
* @param {string} status
|
||||
* @param {string} account_name
|
||||
* @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"];
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//if not returned by this point no field with pronouns was found
|
||||
let pronounSet = generatePronounSet(account_name, 'null')
|
||||
|
||||
cachePronouns(account_name, pronounSet)
|
||||
return "null"
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates object with pronoun related data to be saved to storage.
|
||||
*
|
||||
* @param {string} account Full account name as generated in fetchPronouns()
|
||||
* @param {string} value Contents of the found field's value
|
||||
* @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
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an entry to the "pronounsCache" object in local storage.
|
||||
*
|
||||
* @param {string} account The account ID
|
||||
* @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
|
||||
}
|
||||
|
||||
function getSuccess(result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function setSuccess() {
|
||||
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
error('Failed save to storage!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pro-plate to the element. The caller needs to ensure that the passed element
|
||||
* is defined and that it's either a:
|
||||
* - <article> with the "status" class or
|
||||
* - <article> with the "detailed-status" class.
|
||||
*
|
||||
* Although it's possible to pass raw {@type Element}s, the method only does things on elements of type {@type HTMLElement}.
|
||||
*
|
||||
* @param {Element | HTMLElement} element The status where the element should be added.
|
||||
*/
|
||||
async function addProplate (element) {
|
||||
if (!(element instanceof HTMLElement)) 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 (!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);
|
||||
}
|
||||
|
||||
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.",
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
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"));
|
||||
if (!nametagEl) {
|
||||
warn(
|
||||
"The element passed to addProplate does not have a .display-name__html, although it should have one.",
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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")
|
||||
}
|
||||
|
||||
//add plate to nametag
|
||||
nametagEl.appendChild(proplate);
|
||||
}
|
||||
|
||||
function createObserver(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 };
|
||||
|
||||
// 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;
|
||||
|
||||
//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);
|
||||
|
||||
// Start observing the target node for configured mutations
|
||||
observer.observe(targetNode, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by MutationObserver when the url changes.
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMTokenList} classList The class list.
|
||||
* @param {string} cl The class to check for.
|
||||
* @returns Whether the classList contains the class.
|
||||
*/
|
||||
function containsClass(classList, cl) {
|
||||
if (!classList|| !cl) return false;
|
||||
|
||||
for (const c of classList) {
|
||||
if (c === cl) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current access token for the user.
|
||||
* @returns {Promise<string>} The accessToken for the current user if we are logged in.
|
||||
*/
|
||||
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")
|
||||
if (!initialStateEl) {
|
||||
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)
|
||||
return meta.access_token;
|
||||
}
|
BIN
src/icons/beasts-32.png
Normal file
BIN
src/icons/beasts-32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
74
src/options/options.css
Normal file
74
src/options/options.css
Normal file
|
@ -0,0 +1,74 @@
|
|||
body {
|
||||
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;
|
||||
}
|
||||
|
||||
.row {
|
||||
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;
|
||||
}
|
||||
|
||||
input{
|
||||
margin: 0.75em;
|
||||
}
|
||||
|
||||
.description {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75em 1em;
|
||||
border-radius: 0.5em;
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
a {
|
||||
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%);
|
||||
}
|
||||
}
|
25
src/options/options.html
Normal file
25
src/options/options.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<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">
|
||||
|
||||
</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>
|
22
src/options/options.js
Normal file
22
src/options/options.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
function saveOptions(e) {
|
||||
e.preventDefault();
|
||||
browser.storage.sync.set({
|
||||
logging: document.querySelector("#logging").checked
|
||||
});
|
||||
}
|
||||
|
||||
function restoreOptions() {
|
||||
function setCurrentChoice(result) {
|
||||
document.querySelector("#logging").checked = result.logging || off;
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
|
||||
let getting = browser.storage.sync.get("logging");
|
||||
getting.then(setCurrentChoice, onError);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
document.querySelector("form").addEventListener("submit", saveOptions);
|
64
src/styles/proplate.css
Normal file
64
src/styles/proplate.css
Normal file
|
@ -0,0 +1,64 @@
|
|||
.protoots-proplate {
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-left: 0.5rem;
|
||||
display: inline-flex;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* dark theme */
|
||||
.theme-default .protoots-proplate {
|
||||
/* header background */
|
||||
background-color: #313543;
|
||||
|
||||
/* pulled from nowhere, basically just as low contrast as we can go while keeping AA rating */
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* dark theme on detailed status */
|
||||
.theme-default .detailed-status .protoots-proplate {
|
||||
/* scrollable background */
|
||||
background-color: #282c37;
|
||||
|
||||
/* pulled from nowhere, basically just as low contrast as we can go while keeping AA rating */
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* light theme */
|
||||
.theme-mastodon-light .protoots-proplate {
|
||||
/* body background */
|
||||
background-color: #eff3f5;
|
||||
/* search bar foreground */
|
||||
color: #282c37;
|
||||
}
|
||||
|
||||
/* high contrast */
|
||||
.theme-contrast .protoots-proplate {
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-macaron .protoots-proplate {
|
||||
background-color: #c8c4dd;
|
||||
}
|
||||
|
||||
.theme-fairy-floss .protoots-proplate {
|
||||
background-color: #474059;
|
||||
}
|
||||
|
||||
/* 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%);
|
||||
}
|
Loading…
Reference in a new issue