Version: 0.1.0
This commit is contained in:
commit
6802f57de5
|
@ -0,0 +1,72 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 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
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://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/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# Profile directories
|
||||
chromium/
|
||||
firefox/
|
||||
|
||||
# Parcel cache
|
||||
.cache/
|
||||
|
||||
# Output directory
|
||||
build/
|
||||
web-ext-artifacts/
|
|
@ -0,0 +1,32 @@
|
|||
## Bug Report
|
||||
<!--
|
||||
Thank you for taking the time to report a bug! Don't forget to fill in an
|
||||
appropriate title above and the information in the table below.
|
||||
-->
|
||||
### Info
|
||||
<!--
|
||||
If you click the "Report A Bug" link in the Tildes ReExtended options page,
|
||||
the table below will automatically be filled with your details. That might be
|
||||
easier than filling it out manually.
|
||||
-->
|
||||
|
||||
| Type | Value |
|
||||
|------|-------|
|
||||
| Operating System | |
|
||||
| Browser | |
|
||||
| Device | |
|
||||
|
||||
### The Problem
|
||||
<!--
|
||||
Please explain in sufficient detail what the problem is. When suitable,
|
||||
including an image or video showing the problem will also help immensely.
|
||||
-->
|
||||
|
||||
|
||||
### A Solution
|
||||
<!--
|
||||
If you know of any possible solutions, feel free to include them. If the
|
||||
solution is just something like "it should work" then you can safely omit
|
||||
this section.
|
||||
-->
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
Copyright 2019 Tildes Community and Contributors
|
||||
https://gitlab.com/tildes-community/tildes-reextended
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,48 @@
|
|||
# Tildes ReExtended
|
||||
|
||||
> An updated and reimagined recreation of [Crius' original Tildes Extended](https://github.com/theCrius/tildes-extended) web extension.
|
||||
|
||||
## Differences
|
||||
|
||||
### Removed Functionality
|
||||
|
||||
Large parts of the original Tildes Extended have been removed for various reasons:
|
||||
|
||||
* "Link In New Tab": this functionality now exists natively and can be configured [in your settings](https://tildes.net/settings).
|
||||
* "Markdown Preview": this too exists natively (although not a "live" preview). Another reason this isn't included is due to Tildes using a customized flavor of Markdown that is difficult to replicate accurately enough with what's available and keep up to date.
|
||||
* "Sticky Header": with the dedicated "Back To Top" button now I wasn't sure if there was a need for it, so it's left out.
|
||||
* "Custom Styles": this feature introduced many issues in Tildes Extended that while better and dedicated extensions exist such as [Stylus](https://add0n.com/stylus.html) that can reliably handle custom styles instead.
|
||||
|
||||
### Extended Functionality
|
||||
|
||||
Some functionality has also been extended more:
|
||||
|
||||
* [x] The "Back To Top" button has been separated out into its own feature. It used to be apart of the "Jump To New Comments" one.
|
||||
* [ ] The "Random Tildes Logo" feature now picks from theme-appropriate logos instead of a regular tilde character. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/7)
|
||||
* [ ] The "Jump To New Comment" button now uncollapses comments if the new one is collapsed or is inside a collapsed one. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/8)
|
||||
|
||||
#### User Labels
|
||||
|
||||
* [x] Multiple labels per person.
|
||||
* [x] Specify priority of labels.
|
||||
* [x] A dropdown with theme-appropriate colors for easy access.
|
||||
* [x] Able to pick any color you want.
|
||||
* [ ] Dedicated interface to add, edit, and remove labels. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/1)
|
||||
|
||||
### New Functionality
|
||||
|
||||
And various new features have been added such as:
|
||||
|
||||
* [ ] Hide (and unhide) topics from the topic listing. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/3)
|
||||
* [ ] Hide all vote counts. Or all but your own. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/4)
|
||||
* [ ] Anonymize usernames while adding a unique color to usernames for easy recognition. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/5)
|
||||
* [ ] Assign unique colors to people's usernames. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/6)
|
||||
* [ ] Export and import your settings. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/2)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under [MIT](License).
|
||||
|
||||
<div align="center">
|
||||
<img alt="Tildes ReExtended" src="source/assets/tildes-reextended-128.png" width="128px" height="128px">
|
||||
</div>
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"name": "tildes-reextended",
|
||||
"description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.",
|
||||
"license": "MIT",
|
||||
"version": "0.1.0",
|
||||
"repository": "https://gitlab.com/tildes-community/tildes-reextended",
|
||||
"authors": [
|
||||
"Bauke <me@bauke.xyz>"
|
||||
],
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"watch": "NODE_ENV=development parcel source/assets/manifest.json -d build/ --no-hmr",
|
||||
"start": "web-ext run --source-dir build/ --bc",
|
||||
"start:chromium": "yarn start --chromium-profile chromium/ --keep-profile-changes --target chromium --start-url \"chrome://extensions\"",
|
||||
"start:firefox": "yarn start --firefox-profile firefox/ --keep-profile-changes --target firefox-desktop --start-url \"about:debugging#/runtime/this-firefox\"",
|
||||
"build": "yarn clean && parcel build source/assets/manifest.json -d build/ && web-ext build --source-dir build/",
|
||||
"clean": "trash .cache build/ web-ext-artifacts/",
|
||||
"bump": "ts-node scripts/bump-version.ts && yarn build",
|
||||
"test": "xo && stylelint source/scss/"
|
||||
},
|
||||
"dependencies": {
|
||||
"debounce": "^1.2.0",
|
||||
"modern-normalize": "^0.5.0",
|
||||
"platform": "^1.3.5",
|
||||
"webextension-polyfill-ts": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/platform": "^1.3.2",
|
||||
"@types/prompts": "^2.0.3",
|
||||
"@types/semver": "^6.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.6.1",
|
||||
"@typescript-eslint/parser": "^2.6.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-xo": "^0.27.2",
|
||||
"eslint-config-xo-typescript": "^0.19.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"parcel-plugin-web-extension": "^1.5.2",
|
||||
"prompts": "^2.2.1",
|
||||
"sass": "^1.23.3",
|
||||
"semver": "^6.3.0",
|
||||
"simple-git": "^1.126.0",
|
||||
"stylelint": "^11.1.1",
|
||||
"stylelint-config-xo-scss": "^0.9.0",
|
||||
"stylelint-config-xo-space": "^0.13.0",
|
||||
"trash-cli": "^3.0.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"type-fest": "^0.8.1",
|
||||
"typescript": "^3.7.2",
|
||||
"web-ext": "^3.2.1",
|
||||
"web-ext-types": "^3.2.1",
|
||||
"xo": "^0.25.3"
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": [
|
||||
"stylelint-config-xo-scss",
|
||||
"stylelint-config-xo-space"
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"build/**"
|
||||
],
|
||||
"rules": {
|
||||
"scss/at-rule-no-unknown": null,
|
||||
"at-rule-no-unknown": null,
|
||||
"block-no-empty": null
|
||||
}
|
||||
},
|
||||
"xo": {
|
||||
"extends": [
|
||||
"xo-typescript"
|
||||
],
|
||||
"extensions": [
|
||||
"ts"
|
||||
],
|
||||
"global": [
|
||||
"confirm",
|
||||
"document",
|
||||
"performance",
|
||||
"window"
|
||||
],
|
||||
"ignores": [
|
||||
"build/",
|
||||
"public/"
|
||||
],
|
||||
"prettier": true,
|
||||
"rules": {
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"capitalized-comments": "off",
|
||||
"no-await-in-loop": "off"
|
||||
},
|
||||
"space": true
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 Chrome versions"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import {promises as fs} from 'fs';
|
||||
import {join} from 'path';
|
||||
import prompts from 'prompts';
|
||||
import semver from 'semver';
|
||||
import git from 'simple-git/promise';
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const manifestJSONPath: string = join(
|
||||
__dirname,
|
||||
'../source/assets/manifest.json'
|
||||
);
|
||||
const packageJSONPath: string = join(__dirname, '../package.json');
|
||||
|
||||
const manifestJSON: any = JSON.parse((await fs.readFile(
|
||||
manifestJSONPath,
|
||||
'UTF8'
|
||||
)) as string);
|
||||
|
||||
const packageJSON: any = JSON.parse((await fs.readFile(
|
||||
packageJSONPath,
|
||||
'UTF8'
|
||||
)) as string);
|
||||
|
||||
if (manifestJSON.version !== packageJSON.version) {
|
||||
console.log(
|
||||
`manifest.json and package.json versions are not the same:\n${manifestJSON.version} | ${packageJSON.version}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion: string = manifestJSON.version;
|
||||
const input = await prompts({
|
||||
message: 'Bump major, minor or patch?',
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
choices: [
|
||||
{
|
||||
title: 'Major',
|
||||
description: `${currentVersion} -> ${semver.inc(
|
||||
currentVersion,
|
||||
'major'
|
||||
)}`,
|
||||
value: 'major'
|
||||
},
|
||||
{
|
||||
title: 'Minor',
|
||||
description: `${currentVersion} -> ${semver.inc(
|
||||
currentVersion,
|
||||
'minor'
|
||||
)}`,
|
||||
value: 'minor'
|
||||
},
|
||||
{
|
||||
title: 'Patch',
|
||||
description: `${currentVersion} -> ${semver.inc(
|
||||
currentVersion,
|
||||
'patch'
|
||||
)}`,
|
||||
value: 'patch'
|
||||
}
|
||||
] as Array<prompts.Choice & {description?: string}> | undefined,
|
||||
initial: 1
|
||||
});
|
||||
|
||||
switch (input.type) {
|
||||
case 'major':
|
||||
break;
|
||||
case 'minor':
|
||||
break;
|
||||
case 'patch':
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown input: ${input.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newVersion: string | null = semver.inc(currentVersion, input.type);
|
||||
if (newVersion === null) {
|
||||
console.log(
|
||||
`Something went wrong with semver incrementing ${currentVersion}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(
|
||||
'Running in development, not writing JSONs to file or committing the changes.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const repository: git.SimpleGit = git(join(__dirname, '../'));
|
||||
const status: git.StatusResult = await repository.status();
|
||||
if (status.staged.length > 0 || status.created.length > 0) {
|
||||
console.log(
|
||||
`Git repository has ${status.staged.length}/${status.created.length} staged/created files, commit these or unstage them then run this script again.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Bumping ${currentVersion} to ${newVersion}, writing JSONs to file.`
|
||||
);
|
||||
manifestJSON.version = newVersion;
|
||||
await fs.writeFile(
|
||||
manifestJSONPath,
|
||||
JSON.stringify(manifestJSON, null, 2) + '\n'
|
||||
);
|
||||
|
||||
packageJSON.version = newVersion;
|
||||
await fs.writeFile(
|
||||
packageJSONPath,
|
||||
JSON.stringify(packageJSON, null, 2) + '\n'
|
||||
);
|
||||
|
||||
console.log('Committing changed files and tagging version.');
|
||||
await repository.add([manifestJSONPath, packageJSONPath]);
|
||||
await repository.commit(`Version: ${newVersion}`);
|
||||
await repository.addAnnotatedTag(`${newVersion}`, `Version ${newVersion}`);
|
||||
})();
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"nodeEnv": "development"
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/webextension",
|
||||
"manifest_version": 2,
|
||||
"name": "Tildes ReExtended",
|
||||
"description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.",
|
||||
"version": "0.1.0",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"*://tildes.net/*"
|
||||
],
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'; style-src 'unsafe-inline'",
|
||||
"web_accessible_resources": [
|
||||
"./**"
|
||||
],
|
||||
"icons": {
|
||||
"128": "./tildes-reextended-128.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"../ts/background.ts"
|
||||
]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "./tildes-reextended-128.png"
|
||||
}
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "../html/options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://tildes.net/*"
|
||||
],
|
||||
"run_at": "document_end",
|
||||
"css": [
|
||||
"../scss/scripts/jump-to-new-comment.scss",
|
||||
"../scss/scripts/back-to-top.scss",
|
||||
"../scss/scripts/user-labels.scss"
|
||||
],
|
||||
"js": [
|
||||
"../ts/scripts/jump-to-new-comment.ts",
|
||||
"../ts/scripts/back-to-top.ts",
|
||||
"../ts/scripts/user-labels.ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 963 B |
|
@ -0,0 +1,26 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="128" viewBox="0 0 100 100">
|
||||
<!-- Default Tildes logo with Solarized colors: -->
|
||||
<rect fill="#002b36" width="100" height="100"/>
|
||||
<g>
|
||||
<rect fill="#859900" width="12.5" height="12.5" x="0" y="50"/>
|
||||
<rect fill="#2aa198" width="12.5" height="12.5" x="12.5" y="37.5"/>
|
||||
<rect fill="#268bd2" width="12.5" height="12.5" x="25" y="25"/>
|
||||
<rect fill="#6c71c4" width="12.5" height="12.5" x="37.5" y="37.5"/>
|
||||
<rect fill="#d33682" width="12.5" height="12.5" x="50" y="50"/>
|
||||
<rect fill="#dc322f" width="12.5" height="12.5" x="62.5" y="62.5"/>
|
||||
<rect fill="#cb4b16" width="12.5" height="12.5" x="75" y="50"/>
|
||||
<rect fill="#b58900" width="12.5" height="12.5" x="87.5" y="37.5"/>
|
||||
</g>
|
||||
<!-- Bottom-left extension with Dracula colors: -->
|
||||
<g transform="translate(0 37.5)">
|
||||
<rect fill="#50fa7b" width="12.5" height="12.5" x="12.5" y="37.5"/>
|
||||
<rect fill="#8be9fd" width="12.5" height="12.5" x="25" y="25"/>
|
||||
<rect fill="#6272a4" width="12.5" height="12.5" x="37.5" y="37.5"/>
|
||||
</g>
|
||||
<!-- Top-right extension with Zenburn colors: -->
|
||||
<g transform="translate(0 -37.5)">
|
||||
<rect fill="#e06c75" width="12.5" height="12.5" x="50" y="50"/>
|
||||
<rect fill="#be5046" width="12.5" height="12.5" x="62.5" y="62.5"/>
|
||||
<rect fill="#d19a66" width="12.5" height="12.5" x="75" y="50"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,184 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Tildes ReExtended Options</title>
|
||||
<link href="../assets/tildes-reextended-128.png" rel="shortcut icon"
|
||||
type="image/x-icon">
|
||||
<link href="../scss/options.scss" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<header id="header">
|
||||
<img src="../assets/tildes-reextended-128.png"
|
||||
alt="Tildes ReExtended Logo">
|
||||
<h1>
|
||||
Tildes <span class="red-re">Re</span>Extended
|
||||
</h1>
|
||||
<a id="version" target="_blank" rel="noopener"></a>
|
||||
</header>
|
||||
<main id="main">
|
||||
<div id="settings-list">
|
||||
<a id="back-to-top-list">
|
||||
Back To Top
|
||||
</a>
|
||||
<a id="jump-to-new-comment-list">
|
||||
Jump To New Comment
|
||||
</a>
|
||||
<a id="user-labels-list">
|
||||
User Labels
|
||||
</a>
|
||||
<a id="debug-list">
|
||||
About & Info
|
||||
</a>
|
||||
</div>
|
||||
<div id="settings-content">
|
||||
<div id="back-to-top" class="setting">
|
||||
<header>
|
||||
<h2>Back To Top</h2>
|
||||
<button id="back-to-top-button"></button>
|
||||
</header>
|
||||
<div class="content">
|
||||
<p>
|
||||
Adds a hovering button to the bottom-right of all pages (once
|
||||
you've scrolled past a certain point) that when clicked will
|
||||
scroll you back to the top.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="jump-to-new-comment" class="setting">
|
||||
<header>
|
||||
<h2>Jump To New Comment</h2>
|
||||
<button id="jump-to-new-comment-button"></button>
|
||||
</header>
|
||||
<div class="content">
|
||||
<p>
|
||||
Adds a hovering button to the bottom-right of comment sections to
|
||||
automatically scroll to the next new comment.
|
||||
</p>
|
||||
<p>Requires you to have
|
||||
<a href="https://tildes.net/settings/comment_visits"
|
||||
target="_blank" rel="noopener">the "Mark New Comments"
|
||||
feature</a>
|
||||
enabled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user-labels" class="setting">
|
||||
<header>
|
||||
<h2>User Labels</h2>
|
||||
<button id="user-labels-button"></button>
|
||||
</header>
|
||||
<div class="content">
|
||||
<p>
|
||||
Adds the ability to add customizable labels to users. When in a
|
||||
comments section or in the topic listing a username is visible, a
|
||||
<code>[+]</code> will be next to it. Clicking on that will bring
|
||||
up a dialog to add a label. The values you can customize are:
|
||||
</p>
|
||||
<details>
|
||||
<summary></summary>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Username</b>:
|
||||
specifies who the label will be applied to.
|
||||
</li>
|
||||
<li>
|
||||
<b>Priority</b>:
|
||||
determines the order of labels. If multiple labels have the
|
||||
same priority they will be sorted alphabetically. In the topic
|
||||
listing only the highest priority label will be shown.
|
||||
</li>
|
||||
<li>
|
||||
<b>Color</b>:
|
||||
will set the background color of the label. The foreground
|
||||
color
|
||||
is calculated to be black or white depending on the brightness
|
||||
of the background color.<br>
|
||||
Valid color values are 3, 4, 6 or 8 character hex colors and a
|
||||
special "transparent" value for a transparent background.<br>
|
||||
Next to the color input there is also a dropdown menu with
|
||||
various names, these are colors taken from the Tildes theme
|
||||
you're using. Note that if you add a label with one of these
|
||||
colors they won't change when you switch themes.
|
||||
</li>
|
||||
<li>
|
||||
<b>Text</b>:
|
||||
the text that will go in your label. If left empty, the label
|
||||
will be a 12 by 12 pixel square instead.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<p>
|
||||
To edit or remove labels, click on the labels wherever you see
|
||||
them or
|
||||
use the menu below.<a class="asterisk-link"
|
||||
href="https://gitlab.com/tildes-community/tildes-reextended/issues/1"
|
||||
target="_blank" rel="noopener"><small>*WIP</small></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="debug" class="setting">
|
||||
<header>
|
||||
<h2>About & Info</h2>
|
||||
<button id="debug-button"></button>
|
||||
</header>
|
||||
<div class="content">
|
||||
<p>
|
||||
When this feature is enabled, debug information will be logged to
|
||||
the console.
|
||||
</p>
|
||||
<p>
|
||||
Tildes <span class="red-re">Re</span>Extended is a recreation of
|
||||
<a href="https://tildes.net/user/crius" target="_blank"
|
||||
rel="noopener">Crius</a>' <a
|
||||
href="https://github.com/theCrius/tildes-extended"
|
||||
target="_blank" rel="noopener">Tildes Extended</a> web
|
||||
extension, completely remade from scratch. Open-sourced under <a
|
||||
href="https://gitlab.com/tildes-community/tildes-reextended/blob/master/License"
|
||||
target="_blank" rel="noopener">the MIT license</a>
|
||||
and maintained as a
|
||||
<a href="https://gitlab.com/tildes-community" target="_blank"
|
||||
rel="noopener">
|
||||
Tildes Community project</a>.
|
||||
</p>
|
||||
<p>
|
||||
You can report bugs or request features in <a
|
||||
href="https://gitlab.com/tildes-community/tildes-reextended/issues"
|
||||
target="_blank" rel="noopener">the GitLab issue tracker</a>
|
||||
through the link at the bottom of this page or by
|
||||
<a href="https://tildes.net/user/Bauke/new_message">private
|
||||
messaging Bauke</a>
|
||||
on Tildes. If you're reporting a bug through a private message,
|
||||
please use the "Copy Bug Template" button below and fill out
|
||||
the template in your message.
|
||||
</p>
|
||||
</div>
|
||||
<div id="debug-buttons">
|
||||
<button id="copy-bug-template-button">
|
||||
Copy Bug Template
|
||||
</button>
|
||||
<button id="log-stored-data-button">
|
||||
Log Stored Data
|
||||
</button>
|
||||
<button id="remove-all-data-button">
|
||||
Remove All Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<a id="report-a-bug" target="_blank" rel="noopener">
|
||||
Report A Bug
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="../ts/options.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,242 @@
|
|||
// stylelint-disable-next-line scss/partial-no-import
|
||||
@import 'utilities';
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: darken($background, 5%);
|
||||
color: $foreground;
|
||||
font-family: sans-serif;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: lighten($blue, 10%);
|
||||
|
||||
&:hover {
|
||||
color: $magenta;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: darken($background, 5%);
|
||||
}
|
||||
|
||||
details {
|
||||
border: 0.25rem solid $blue;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
> summary {
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: 'Click to expand.';
|
||||
}
|
||||
}
|
||||
|
||||
&[open] > summary {
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&::after {
|
||||
content: 'Click to collapse.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
*:last-child > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.red-re {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
p > .red-re {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.asterisk-link {
|
||||
font-family: monospace;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
> img {
|
||||
height: 4rem;
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
> #version {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
#main {
|
||||
background-color: $background;
|
||||
border-top: 0.25rem solid $blue;
|
||||
border-bottom: 0.25rem solid $cyan;
|
||||
display: grid;
|
||||
grid-template-columns: 35% 65%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 1rem;
|
||||
|
||||
> a {
|
||||
background-color: lighten($background, 5%);
|
||||
border-bottom: 0.25rem solid darken($background, 5%);
|
||||
color: $foreground;
|
||||
padding: 2rem;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: darken($blue, 20%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: darken($blue, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
> header {
|
||||
border-bottom: 0.25rem solid $red;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
> h2 {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
> button {
|
||||
align-self: flex-start;
|
||||
background-color: $red;
|
||||
border: none;
|
||||
color: $foreground;
|
||||
padding: 0.5rem;
|
||||
min-width: 10rem;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($red, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.enabled > header {
|
||||
border-color: $green;
|
||||
|
||||
> button {
|
||||
background-color: $green;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($green, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
align-self: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
#copy-bug-template-button,
|
||||
#log-stored-data-button,
|
||||
#remove-all-data-button {
|
||||
align-self: flex-end;
|
||||
border: none;
|
||||
color: $foreground;
|
||||
padding: 0.5rem 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#copy-bug-template-button,
|
||||
#log-stored-data-button {
|
||||
background-color: $blue;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($blue, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
#remove-all-data-button {
|
||||
background-color: $red;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($red, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $large-breakpoint) {
|
||||
#wrapper {
|
||||
margin: 0 auto;
|
||||
width: $large-breakpoint;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
$background: #002b36;
|
||||
$foreground: #fdf6e3;
|
||||
|
||||
$red: #dc322f;
|
||||
$orange: #cb4b16;
|
||||
$yellow: #b58900;
|
||||
$green: #859900;
|
||||
$cyan: #2aa198;
|
||||
$blue: #268bd2;
|
||||
$violet: #6c71c4;
|
||||
$magenta: #d33682;
|
||||
|
||||
$small-breakpoint: 600px;
|
||||
$medium-breakpoint: 900px;
|
||||
$large-breakpoint: 1200px;
|
||||
$extra-large-breakpoint: 1800px;
|
||||
|
||||
.trx-flash-message {
|
||||
align-items: center;
|
||||
background-color: $background;
|
||||
color: $foreground;
|
||||
border: 0.25rem solid $blue;
|
||||
bottom: 1vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 1vw;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
padding: 1rem;
|
||||
position: fixed;
|
||||
transition: opacity 0.5s;
|
||||
width: 30vw;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.trx-opaque {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.trx-flash-error {
|
||||
background-color: darken($red, 25%);
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.trx-hidden {
|
||||
// stylelint-disable-next-line declaration-no-important
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.trx-offscreen {
|
||||
left: -250vw;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@mixin invalid($color) {
|
||||
.trx-invalid {
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@include invalid(#d91e18);
|
||||
|
||||
// stylelint-disable-next-line selector-class-pattern
|
||||
&.theme- {
|
||||
&atom-one-dark {
|
||||
@include invalid(#E06C75);
|
||||
}
|
||||
|
||||
&black {
|
||||
@include invalid(#f00);
|
||||
}
|
||||
|
||||
&dracula {
|
||||
@include invalid(#ff5555);
|
||||
}
|
||||
|
||||
&gruvbox-dark,
|
||||
&gruvbox-light {
|
||||
@include invalid(#fb4934);
|
||||
}
|
||||
|
||||
&solarized-dark,
|
||||
&solarized-light {
|
||||
@include invalid(#dc322f);
|
||||
}
|
||||
|
||||
&zenburn {
|
||||
@include invalid(#dc8c6c);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@import '~modern-normalize';
|
||||
// stylelint-disable-next-line scss/at-import-no-partial-leading-underscore
|
||||
@import '_options';
|
|
@ -0,0 +1,11 @@
|
|||
@import '../utilities';
|
||||
|
||||
#trx-back-to-top {
|
||||
bottom: 2vh;
|
||||
position: fixed;
|
||||
right: 2vw;
|
||||
}
|
||||
|
||||
#trx-jump-to-new-comment:not(.trx-hidden) + #trx-back-to-top {
|
||||
bottom: 6.5vh;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@import '../utilities';
|
||||
|
||||
#trx-jump-to-new-comment {
|
||||
bottom: 2vh;
|
||||
position: fixed;
|
||||
right: 2vw;
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
@import '../utilities';
|
||||
|
||||
.trx-user-label {
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
min-height: 12px;
|
||||
min-width: 12px;
|
||||
|
||||
&,
|
||||
&-add {
|
||||
font-style: initial;
|
||||
margin: 0 2px;
|
||||
padding: 0 2px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-add {
|
||||
background-color: #0002;
|
||||
}
|
||||
}
|
||||
|
||||
.trx-bright {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#trx-user-label-form {
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
width: 325px;
|
||||
|
||||
> div:nth-child(1),
|
||||
> div:nth-child(2) {
|
||||
display: flex;
|
||||
|
||||
> label,
|
||||
> input {
|
||||
&:nth-of-type(1) {
|
||||
margin-right: 4px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
width: calc(25% - 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#trx-user-label-form-color {
|
||||
display: flex;
|
||||
|
||||
> input {
|
||||
margin-right: 4px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
> select {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
#trx-user-label-input {
|
||||
display: flex;
|
||||
|
||||
> input {
|
||||
margin-right: 4px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
> #trx-user-label-preview {
|
||||
align-items: center;
|
||||
border: 1px solid;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 50%;
|
||||
|
||||
&.trx-bright {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#trx-user-label-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
body.theme-black {
|
||||
.trx-user-label-add {
|
||||
background-color: #fff2;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {browser} from 'webextension-polyfill-ts';
|
||||
|
||||
// Add listeners to open the options page when:
|
||||
// * The extension icon gets clicked.
|
||||
// * The extension first gets installed.
|
||||
browser.browserAction.onClicked.addListener(openOptionsPage);
|
||||
browser.runtime.onInstalled.addListener(openOptionsPage);
|
||||
|
||||
async function openOptionsPage(): Promise<void> {
|
||||
await browser.runtime.openOptionsPage();
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
import platform from 'platform';
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
import {
|
||||
getSettings,
|
||||
Settings,
|
||||
camelToKebab,
|
||||
log,
|
||||
kebabToCamel,
|
||||
querySelector,
|
||||
setSettings,
|
||||
createElementFromString,
|
||||
flashMessage
|
||||
} from './utilities';
|
||||
|
||||
window.addEventListener(
|
||||
'load',
|
||||
async (): Promise<void> => {
|
||||
// Grab the version anchor from the header and add the tag link to it.
|
||||
const versionSpan: HTMLAnchorElement = querySelector('#version');
|
||||
const {version} = browser.runtime.getManifest();
|
||||
versionSpan.setAttribute(
|
||||
'href',
|
||||
`https://gitlab.com/tildes-community/tildes-reextended/-/tags/v${version}`
|
||||
);
|
||||
versionSpan.textContent = `v${version}`;
|
||||
|
||||
// Grab the "Report A Bug" anchor and add a prefilled GitLab issue URL to it.
|
||||
const reportAnchor: HTMLAnchorElement = querySelector('#report-a-bug');
|
||||
reportAnchor.setAttribute(
|
||||
'href',
|
||||
encodeURI(
|
||||
`https://gitlab.com/tildes-community/tildes-reextended/issues/new?issue[description]=${createReportTemplate()}`
|
||||
)
|
||||
);
|
||||
|
||||
const copyBugTemplateButton: HTMLButtonElement = querySelector(
|
||||
'#copy-bug-template-button'
|
||||
);
|
||||
copyBugTemplateButton.addEventListener('click', copyBugTemplateHandler);
|
||||
|
||||
const logStoredDataButton: HTMLButtonElement = querySelector(
|
||||
'#log-stored-data-button'
|
||||
);
|
||||
logStoredDataButton.addEventListener('click', logStoredDataHandler);
|
||||
|
||||
const removeAllDataButton: HTMLButtonElement = querySelector(
|
||||
'#remove-all-data-button'
|
||||
);
|
||||
removeAllDataButton.addEventListener('click', removeAllDataHandler);
|
||||
|
||||
// Get the settings and add a bunch of behaviours for them.
|
||||
const settings: Settings = await getSettings();
|
||||
// log(settings);
|
||||
|
||||
// Set the latest feature to active.
|
||||
const latestActiveListItem: HTMLAnchorElement = querySelector(
|
||||
`#${settings.data.latestActiveFeatureTab}-list`
|
||||
);
|
||||
latestActiveListItem.classList.add('active');
|
||||
const latestActiveContent: HTMLDivElement = querySelector(
|
||||
`#${settings.data.latestActiveFeatureTab}`
|
||||
);
|
||||
latestActiveContent.classList.add('active');
|
||||
|
||||
for (const key in settings.features) {
|
||||
if (Object.hasOwnProperty.call(settings.features, key)) {
|
||||
const value: boolean = settings.features[key];
|
||||
// Convert the camelCase key to a kebab-case string.
|
||||
const id: string = camelToKebab(key);
|
||||
|
||||
const settingContent: HTMLDivElement | null = document.querySelector(
|
||||
`#${id}`
|
||||
);
|
||||
if (settingContent === null) {
|
||||
log(
|
||||
`New setting key:${key} id:${id} does not have an entry in the settings content!`,
|
||||
true
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
settingContent.classList.add('enabled');
|
||||
}
|
||||
|
||||
// Set the button text to enable/disable based on the current setting.
|
||||
const toggleButton: HTMLButtonElement = querySelector(`#${id}-button`);
|
||||
toggleButton.addEventListener('click', toggleButtonClickHandler);
|
||||
toggleButton.textContent = value === true ? 'Enabled' : 'Disabled';
|
||||
|
||||
// Add a checkmark to the list item if the feature is enabled.
|
||||
const listItem: HTMLAnchorElement = querySelector(`#${id}-list`);
|
||||
listItem.addEventListener('click', listItemClickHandler);
|
||||
if (value) {
|
||||
listItem.textContent += ' ✔';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof settings.data.version !== 'undefined') {
|
||||
if (settings.data.version !== version) {
|
||||
flashMessage(`Updated to ${version}!`);
|
||||
}
|
||||
}
|
||||
|
||||
settings.data.version = version;
|
||||
await setSettings(settings);
|
||||
}
|
||||
);
|
||||
|
||||
async function toggleButtonClickHandler(event: MouseEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
const settings: Settings = await getSettings();
|
||||
|
||||
// Convert the kebab-case ID to camelCase and remove the `-button` suffix.
|
||||
const wantedSettingKey: string = kebabToCamel(
|
||||
target.id.slice(0, target.id.lastIndexOf('-'))
|
||||
);
|
||||
|
||||
// Toggle the value and update it in the settings.
|
||||
const wantedSettingValue = !settings.features[wantedSettingKey];
|
||||
settings.features[wantedSettingKey] = wantedSettingValue;
|
||||
await setSettings(settings);
|
||||
|
||||
// Update the button text.
|
||||
target.textContent = wantedSettingValue === true ? 'Enabled' : 'Disabled';
|
||||
|
||||
// Grab the equivalent list item and update the checkmark.
|
||||
const listItem: HTMLAnchorElement = querySelector(
|
||||
`#${camelToKebab(wantedSettingKey)}-list`
|
||||
);
|
||||
if (wantedSettingValue) {
|
||||
listItem.textContent += ' ✔';
|
||||
} else {
|
||||
listItem.textContent = listItem.textContent!.slice(
|
||||
0,
|
||||
listItem.textContent!.lastIndexOf(' ')
|
||||
);
|
||||
}
|
||||
|
||||
const settingContent: HTMLDivElement = querySelector(
|
||||
`#${camelToKebab(wantedSettingKey)}`
|
||||
);
|
||||
if (wantedSettingValue) {
|
||||
settingContent.classList.add('enabled');
|
||||
} else {
|
||||
settingContent.classList.remove('enabled');
|
||||
}
|
||||
}
|
||||
|
||||
async function listItemClickHandler(event: MouseEvent): Promise<void> {
|
||||
const target: HTMLAnchorElement = event.target as HTMLAnchorElement;
|
||||
const id: string = target.id.slice(0, target.id.lastIndexOf('-'));
|
||||
const currentActiveListItem: HTMLAnchorElement = querySelector(
|
||||
'#settings-list > .active'
|
||||
);
|
||||
// If the currently selected item is the same as the new one, do nothing.
|
||||
if (target.id === currentActiveListItem.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the currently active settings content.
|
||||
const currentActiveContent: HTMLDivElement = querySelector(
|
||||
'#settings-content > .active'
|
||||
);
|
||||
currentActiveContent.classList.remove('active');
|
||||
|
||||
// And show the newly selected active settings content.
|
||||
const newActiveContent: HTMLDivElement = querySelector(`#${id}`);
|
||||
newActiveContent.classList.add('active');
|
||||
|
||||
// Remove the active style from the currently active settings list item.
|
||||
currentActiveListItem.classList.remove('active');
|
||||
|
||||
// And add it to the newly selected settings list item.
|
||||
target.classList.add('active');
|
||||
|
||||
// Update the latest active feature data.
|
||||
const settings: Settings = await getSettings();
|
||||
settings.data.latestActiveFeatureTab = id;
|
||||
await setSettings(settings);
|
||||
}
|
||||
|
||||
function copyBugTemplateHandler(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
const temporaryElement: HTMLTextAreaElement = createElementFromString(
|
||||
`<textarea>${createReportTemplate()}</textarea>`
|
||||
);
|
||||
temporaryElement.classList.add('trx-offscreen');
|
||||
document.body.append(temporaryElement);
|
||||
temporaryElement.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
flashMessage('Copied bug report template to clipboard.');
|
||||
} catch (error) {
|
||||
flashMessage(
|
||||
'Failed to copy bug report template to clipboard. Check the console for an error.',
|
||||
true
|
||||
);
|
||||
log(error, true);
|
||||
} finally {
|
||||
temporaryElement.remove();
|
||||
log('Removed temporary textarea from DOM.');
|
||||
}
|
||||
}
|
||||
|
||||
async function logStoredDataHandler(event: MouseEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
log(JSON.stringify(await getSettings(), null, 2), true);
|
||||
}
|
||||
|
||||
async function removeAllDataHandler(event: MouseEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
!confirm(
|
||||
'Are you sure you want to delete your data? There is no way to recover your data once it has been deleted.'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await browser.storage.local.clear();
|
||||
flashMessage(
|
||||
'Data removed, reloading this page to reinitialize default settings.'
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function createReportTemplate(): string {
|
||||
// Set the headers using HTML tags, these can't be with #-style Markdown
|
||||
// headers as they'll be interpreted as an ID instead of Markdown content
|
||||
// and so GitLab won't add it to the description.
|
||||
let reportTemplate = `<h2>Bug Report</h2>
|
||||
<!--
|
||||
Thank you for taking the time to report a bug! Don't forget to fill in an
|
||||
appropriate title above, and make sure the information below is correct.
|
||||
-->
|
||||
<h3>Info</h3>\n
|
||||
| Type | Value |
|
||||
|------|-------|
|
||||
| Operating System | ${platform.os} |
|
||||
| Browser | ${platform.name} ${platform.version} (${platform.layout}) |\n`;
|
||||
// The platform manufacturer and product can be null in certain cases (such as
|
||||
// desktops) so only when they're both not null include them.
|
||||
if (platform.manufacturer !== null && platform.product !== null) {
|
||||
reportTemplate += `| Device | ${platform.manufacturer} ${platform.product} |\n`;
|
||||
}
|
||||
|
||||
reportTemplate += `\n<h3>The Problem</h3>
|
||||
<!--
|
||||
Please explain in sufficient detail what the problem is. When suitable,
|
||||
including an image or video showing the problem will also help immensely.
|
||||
-->\n\n
|
||||
<h3>A Solution</h3>
|
||||
<!--
|
||||
If you know of any possible solutions, feel free to include them. If the
|
||||
solution is just something like "it should work" then you can safely omit
|
||||
this section.
|
||||
-->\n\n\n`;
|
||||
|
||||
return reportTemplate;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import debounce from 'debounce';
|
||||
import {
|
||||
getSettings,
|
||||
Settings,
|
||||
createElementFromString,
|
||||
querySelector
|
||||
} from '../utilities';
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const settings: Settings = await getSettings();
|
||||
if (!settings.features.backToTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the Back To Top button.
|
||||
const backToTopButton: HTMLAnchorElement = createElementFromString(
|
||||
'<a id="trx-back-to-top" class="btn btn-primary trx-hidden">Back To Top</a>'
|
||||
);
|
||||
backToTopButton.addEventListener('click', clickHandler);
|
||||
document.body.append(backToTopButton);
|
||||
|
||||
// Add a "debounced" handler to the scroll listener, this will make it so
|
||||
// the handler will only run after scrolling has ended for 150ms.
|
||||
window.addEventListener('scroll', debounce(scrollHandler, 150));
|
||||
// And finally run the handler once, in case the page was already scrolled
|
||||
// down when it got loaded.
|
||||
scrollHandler();
|
||||
})();
|
||||
|
||||
function scrollHandler(): void {
|
||||
const backToTopButton: HTMLAnchorElement = querySelector('#trx-back-to-top');
|
||||
const yPosition: number = window.scrollY;
|
||||
// TODO: See if 500 is a good position. Tildes Extended was originally at 250
|
||||
// but I think that might be a bit too little.
|
||||
if (yPosition > 500) {
|
||||
backToTopButton.classList.remove('trx-hidden');
|
||||
} else {
|
||||
backToTopButton.classList.add('trx-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function clickHandler(): void {
|
||||
window.scrollTo({behavior: 'smooth', top: 0});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
getSettings,
|
||||
Settings,
|
||||
createElementFromString,
|
||||
querySelector
|
||||
} from '../utilities';
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const settings: Settings = await getSettings();
|
||||
if (
|
||||
!settings.features.jumpToNewComment ||
|
||||
!window.location.pathname.startsWith('/~') ||
|
||||
document.querySelectorAll('.comment.is-comment-new').length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the Jump To New Comment button.
|
||||
const jumpToNewCommentButton: HTMLAnchorElement = createElementFromString(
|
||||
'<a id="trx-jump-to-new-comment" class="btn btn-primary">Jump To New Comment</a>'
|
||||
);
|
||||
jumpToNewCommentButton.addEventListener('click', clickHandler);
|
||||
document.body.append(jumpToNewCommentButton);
|
||||
})();
|
||||
|
||||
function clickHandler(): void {
|
||||
// Scroll to the first new comment and remove the `.is-comment-new` class
|
||||
// from it.
|
||||
const newestComment: HTMLElement = querySelector('.comment.is-comment-new');
|
||||
// TODO: Check if the new comment is collapsed or is inside a collapsed
|
||||
// comment and uncollapse it if so.
|
||||
newestComment.scrollIntoView({behavior: 'smooth'});
|
||||
// TODO: Don't immediately remove the class after scrolling to it. But remove
|
||||
// it when scrolling to the next new comment after this one. I've decided to
|
||||
// leave this as a TODO as it complicates the code a little bit and it's only
|
||||
// a QOL feature.
|
||||
newestComment.classList.remove('is-comment-new');
|
||||
|
||||
// If there's no new comments left, remove the button.
|
||||
if (document.querySelectorAll('.comment.is-comment-new').length === 0) {
|
||||
const jumpToNewCommentButton: HTMLAnchorElement = querySelector(
|
||||
'#trx-jump-to-new-comment'
|
||||
);
|
||||
jumpToNewCommentButton.remove();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
import {Except} from 'type-fest';
|
||||
import debounce from 'debounce';
|
||||
import {ColorKey, ThemeKey, themeColors} from '../theme-colors';
|
||||
import {
|
||||
getSettings,
|
||||
Settings,
|
||||
log,
|
||||
createElementFromString,
|
||||
UserLabel,
|
||||
isInTopicListing,
|
||||
getCurrentThemeKey,
|
||||
isColorBright,
|
||||
setSettings,
|
||||
appendStyleAttribute,
|
||||
querySelector
|
||||
} from '../utilities';
|
||||
import {
|
||||
getLabelForm,
|
||||
getLabelFormValues,
|
||||
hideLabelForm,
|
||||
getLabelFormID
|
||||
} from './user-labels/label-form';
|
||||
import {
|
||||
editLabelHandler,
|
||||
addLabelHandler,
|
||||
labelTextInputHandler,
|
||||
presetColorSelectHandler,
|
||||
labelColorInputHandler
|
||||
} from './user-labels/handlers';
|
||||
|
||||
let theme: typeof themeColors[ThemeKey];
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const settings: Settings = await getSettings();
|
||||
if (!settings.features.userLabels) {
|
||||
return;
|
||||
}
|
||||
|
||||
const themeKey: ThemeKey = getCurrentThemeKey();
|
||||
theme = themeColors[themeKey];
|
||||
addLabelsToUsernames(settings);
|
||||
const existingLabelForm: HTMLElement | null = document.querySelector(
|
||||
'#trx-user-label-form'
|
||||
);
|
||||
if (existingLabelForm !== null) {
|
||||
existingLabelForm.remove();
|
||||
}
|
||||
|
||||
const themeSelectOptions: string[] = [];
|
||||
for (const color in theme) {
|
||||
if (Object.hasOwnProperty.call(theme, color)) {
|
||||
themeSelectOptions.push(
|
||||
`<option value="${theme[color as ColorKey]}">${color}</option>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelFormTemplate = `<form id="trx-user-label-form" class="trx-hidden">
|
||||
<div>
|
||||
<label for="trx-user-label-form-username">Add New Label</label>
|
||||
<label for="trx-user-label-priority">Priority</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" id="trx-user-label-form-username" class="form-input" placeholder="Username">
|
||||
<input id="trx-user-label-priority" type="number" class="form-input" value="0">
|
||||
</div>
|
||||
<label>Pick A Color</label>
|
||||
<div id="trx-user-label-form-color">
|
||||
<input type="text" class="form-input" placeholder="Color">
|
||||
<select class="form-select">
|
||||
${themeSelectOptions.join('\n')}
|
||||
</select>
|
||||
</div>
|
||||
<label>Label</label>
|
||||
<div id="trx-user-label-input">
|
||||
<input type="text" class="form-input" placeholder="Label">
|
||||
<div id="trx-user-label-preview">
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="trx-user-label-actions">
|
||||
<a id="trx-user-label-save" class="btn-post-action">Save</a>
|
||||
<a id="trx-user-label-close" class="btn-post-action">Close</a>
|
||||
<a id="trx-user-label-remove" class="btn-post-action">Remove</a>
|
||||
</div>
|
||||
</form>`;
|
||||
const labelForm: HTMLFormElement = createElementFromString(labelFormTemplate);
|
||||
document.body.append(labelForm);
|
||||
labelForm.setAttribute(
|
||||
'style',
|
||||
`background-color: ${theme.background}; border-color: ${theme.foregroundAlt};`
|
||||
);
|
||||
|
||||
const labelColorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
labelColorInput.addEventListener(
|
||||
'keyup',
|
||||
debounce(labelColorInputHandler, 250)
|
||||
);
|
||||
|
||||
const presetColorSelect: HTMLSelectElement = querySelector(
|
||||
'#trx-user-label-form-color > select'
|
||||
);
|
||||
presetColorSelect.addEventListener('change', presetColorSelectHandler);
|
||||
presetColorSelect.value = theme.backgroundAlt;
|
||||
|
||||
const labelTextInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-input > input'
|
||||
);
|
||||
labelTextInput.addEventListener('keyup', labelTextInputHandler);
|
||||
|
||||
const labelPreview: HTMLDivElement = querySelector('#trx-user-label-preview');
|
||||
labelPreview.setAttribute(
|
||||
'style',
|
||||
`background-color: ${theme.background};` +
|
||||
`border-color: ${theme.foregroundAlt};`
|
||||
);
|
||||
|
||||
const formSaveButton: HTMLAnchorElement = querySelector(
|
||||
'#trx-user-label-save'
|
||||
);
|
||||
formSaveButton.addEventListener('click', saveUserLabel);
|
||||
|
||||
const formCloseButton: HTMLAnchorElement = querySelector(
|
||||
'#trx-user-label-close'
|
||||
);
|
||||
formCloseButton.addEventListener('click', hideLabelForm);
|
||||
|
||||
const formRemoveButton: HTMLAnchorElement = querySelector(
|
||||
'#trx-user-label-remove'
|
||||
);
|
||||
formRemoveButton.addEventListener('click', removeUserLabel);
|
||||
|
||||
const commentObserver = new window.MutationObserver(
|
||||
async (mutations: MutationRecord[]): Promise<void> => {
|
||||
const commentElements: HTMLElement[] = mutations
|
||||
.map(val => val.target as HTMLElement)
|
||||
.filter(
|
||||
val =>
|
||||
val.classList.contains('comment-itself') ||
|
||||
val.classList.contains('comment')
|
||||
);
|
||||
if (commentElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentObserver.disconnect();
|
||||
addLabelsToUsernames(await getSettings());
|
||||
startObserver();
|
||||
}
|
||||
);
|
||||
|
||||
function startObserver(): void {
|
||||
const topicComments: HTMLElement | null = document.querySelector(
|
||||
'.topic-comments'
|
||||
);
|
||||
if (topicComments !== null) {
|
||||
commentObserver.observe(topicComments, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const postListing: HTMLElement | null = document.querySelector(
|
||||
'.post-listing'
|
||||
);
|
||||
if (postListing !== null) {
|
||||
commentObserver.observe(postListing, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startObserver();
|
||||
})();
|
||||
|
||||
// TODO: Refactor this function to be able to only add labels to specific
|
||||
// elements. At the moment it goes through all `.link-user` elements which is
|
||||
// inefficient.
|
||||
function addLabelsToUsernames(settings: Settings): void {
|
||||
for (const element of [
|
||||
...document.querySelectorAll('.trx-user-label'),
|
||||
...document.querySelectorAll('.trx-user-label-add')
|
||||
]) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
for (const element of document.querySelectorAll('.link-user')) {
|
||||
const username: string = element
|
||||
.textContent!.replace(/@/g, '')
|
||||
.toLowerCase();
|
||||
const addLabelSpan: HTMLSpanElement = createElementFromString(
|
||||
`<span class="trx-user-label-add" data-trx-username="${username}">[+]</span>`
|
||||
);
|
||||
addLabelSpan.addEventListener('click', (event: MouseEvent): void =>
|
||||
addLabelHandler(event, addLabelSpan)
|
||||
);
|
||||
if (!isInTopicListing()) {
|
||||
element.insertAdjacentElement('afterend', addLabelSpan);
|
||||
appendStyleAttribute(addLabelSpan, `color: ${theme.foreground};`);
|
||||
}
|
||||
|
||||
const userLabels: UserLabel[] = settings.data.userLabels.filter(
|
||||
val => val.username === username
|
||||
);
|
||||
if (userLabels.length === 0) {
|
||||
if (
|
||||
isInTopicListing() &&
|
||||
(element.nextElementSibling === null ||
|
||||
!element.nextElementSibling.className.includes('trx-user-label'))
|
||||
) {
|
||||
element.insertAdjacentElement('afterend', addLabelSpan);
|
||||
appendStyleAttribute(addLabelSpan, `color: ${theme.foreground};`);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isInTopicListing()) {
|
||||
userLabels.sort((a, b) => b.priority - a.priority);
|
||||
} else {
|
||||
userLabels.sort((a, b): number => {
|
||||
if (a.priority !== b.priority) {
|
||||
return a.priority - b.priority;
|
||||
}
|
||||
|
||||
return b.text.localeCompare(a.text);
|
||||
});
|
||||
}
|
||||
|
||||
for (const userLabel of userLabels) {
|
||||
const userLabelSpan: HTMLSpanElement = createElementFromString(
|
||||
`<span class="trx-user-label" data-trx-user-label-id="${userLabel.id}">${userLabel.text}</span>`
|
||||
);
|
||||
userLabelSpan.addEventListener(
|
||||
'click',
|
||||
async (event: MouseEvent): Promise<void> =>
|
||||
editLabelHandler(event, userLabelSpan)
|
||||
);
|
||||
element.insertAdjacentElement('afterend', userLabelSpan);
|
||||
// Set the inline-style after the element gets added to the DOM, this
|
||||
// will prevent a CSP error saying inline-styles aren't permitted.
|
||||
userLabelSpan.setAttribute(
|
||||
'style',
|
||||
`background-color: ${userLabel.color};`
|
||||
);
|
||||
if (isColorBright(userLabel.color)) {
|
||||
userLabelSpan.classList.add('trx-bright');
|
||||
} else {
|
||||
userLabelSpan.classList.remove('trx-bright');
|
||||
}
|
||||
|
||||
if (isInTopicListing()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserLabel(): Promise<void> {
|
||||
const settings: Settings = await getSettings();
|
||||
const labelForm: HTMLFormElement = getLabelForm();
|
||||
const labelNoID: Except<UserLabel, 'id'> | undefined = getLabelFormValues();
|
||||
if (typeof labelNoID === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingIDString: string | null = labelForm.getAttribute(
|
||||
'data-trx-user-label-id'
|
||||
);
|
||||
if (existingIDString === null) {
|
||||
settings.data.userLabels.push({
|
||||
id: (await getHighestLabelID(settings)) + 1,
|
||||
...labelNoID
|
||||
});
|
||||
await setSettings(settings);
|
||||
hideLabelForm();
|
||||
addLabelsToUsernames(settings);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingID = Number(existingIDString);
|
||||
const existingLabel: UserLabel | undefined = settings.data.userLabels.find(
|
||||
val => val.id === existingID
|
||||
);
|
||||
if (typeof existingLabel === 'undefined') {
|
||||
log(`Tried to find label with ID that doesn't exist: ${existingID}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingLabelIndex: number = settings.data.userLabels.findIndex(
|
||||
val => val.id === existingID
|
||||
);
|
||||
settings.data.userLabels.splice(existingLabelIndex, 1);
|
||||
settings.data.userLabels.push({
|
||||
id: existingID,
|
||||
...labelNoID
|
||||
});
|
||||
await setSettings(settings);
|
||||
hideLabelForm();
|
||||
addLabelsToUsernames(settings);
|
||||
}
|
||||
|
||||
async function removeUserLabel(): Promise<void> {
|
||||
const labelNoID: Except<UserLabel, 'id'> | undefined = getLabelFormValues();
|
||||
if (typeof labelNoID === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const id: number | undefined = getLabelFormID();
|
||||
if (typeof id === 'undefined') {
|
||||
log('Attempted to remove user label without an ID.');
|
||||
hideLabelForm();
|
||||
return;
|
||||
}
|
||||
|
||||
const settings: Settings = await getSettings();
|
||||
const labelIndex: number = settings.data.userLabels.findIndex(
|
||||
val => val.id === id
|
||||
);
|
||||
if (typeof findLabelByID(id) === 'undefined') {
|
||||
log(
|
||||
`Attempted to remove user label with an ID that doesn't exist ${id}.`,
|
||||
true
|
||||
);
|
||||
hideLabelForm();
|
||||
return;
|
||||
}
|
||||
|
||||
settings.data.userLabels.splice(labelIndex, 1);
|
||||
await setSettings(settings);
|
||||
hideLabelForm();
|
||||
addLabelsToUsernames(settings);
|
||||
}
|
||||
|
||||
export async function findLabelByID(
|
||||
id: number,
|
||||
settings?: Settings
|
||||
): Promise<UserLabel | undefined> {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = await getSettings();
|
||||
}
|
||||
|
||||
return settings.data.userLabels.find(val => val.id === id);
|
||||
}
|
||||
|
||||
async function getHighestLabelID(settings?: Settings): Promise<number> {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = await getSettings();
|
||||
}
|
||||
|
||||
if (settings.data.userLabels.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.max(...settings.data.userLabels.map(val => val.id));
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
log,
|
||||
isValidHexColor,
|
||||
UserLabel,
|
||||
getCurrentThemeKey,
|
||||
querySelector
|
||||
} from '../../utilities';
|
||||
import {findLabelByID} from '../user-labels';
|
||||
import {themeColors, ThemeKey} from '../../theme-colors';
|
||||
import {
|
||||
getLabelForm,
|
||||
setLabelFormColor,
|
||||
setLabelFormPriority,
|
||||
setLabelFormUserID,
|
||||
setLabelFormUsername,
|
||||
setLabelFormText,
|
||||
setLabelFormTitle,
|
||||
showLabelForm,
|
||||
updatePreview
|
||||
} from './label-form';
|
||||
|
||||
const theme: typeof themeColors[ThemeKey] = themeColors[getCurrentThemeKey()];
|
||||
|
||||
export function addLabelHandler(
|
||||
event: MouseEvent,
|
||||
element: HTMLSpanElement
|
||||
): void {
|
||||
const labelForm = getLabelForm();
|
||||
labelForm.removeAttribute('data-trx-user-label-id');
|
||||
setLabelFormTitle('Add New Label');
|
||||
setLabelFormUsername(element.getAttribute('data-trx-username')!);
|
||||
setLabelFormPriority(0);
|
||||
setLabelFormColor(theme.backgroundAlt);
|
||||
setLabelFormText('');
|
||||
showLabelForm(element);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
export async function editLabelHandler(
|
||||
event: MouseEvent,
|
||||
element: HTMLSpanElement
|
||||
): Promise<void> {
|
||||
setLabelFormTitle('Edit Existing Label');
|
||||
const labelID = Number(element.getAttribute('data-trx-user-label-id')!);
|
||||
const label: UserLabel | undefined = await findLabelByID(labelID);
|
||||
if (typeof label === 'undefined') {
|
||||
log(`Tried to find label with ID that doesn't exist: ${labelID}`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLabelFormUserID(label.id);
|
||||
setLabelFormUsername(label.username.toLowerCase());
|
||||
setLabelFormPriority(label.priority);
|
||||
setLabelFormColor(label.color);
|
||||
setLabelFormText(label.text);
|
||||
showLabelForm(element);
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
export function labelColorInputHandler(): void {
|
||||
const labelColorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
let color: string = labelColorInput.value.toLowerCase();
|
||||
if (!color.startsWith('#') && !color.startsWith('t') && color.length > 0) {
|
||||
color = `#${color}`;
|
||||
labelColorInput.value = color;
|
||||
}
|
||||
|
||||
if (color !== 'transparent' && !isValidHexColor(color)) {
|
||||
log('Invalid color input, must be a valid 3/4/6/8-character hex color.');
|
||||
labelColorInput.classList.add('trx-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
labelColorInput.classList.remove('trx-invalid');
|
||||
updatePreview(color);
|
||||
}
|
||||
|
||||
export function labelTextInputHandler(): void {
|
||||
const labelTextInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-input > input'
|
||||
);
|
||||
updatePreview(undefined, labelTextInput.value);
|
||||
}
|
||||
|
||||
export function presetColorSelectHandler(): void {
|
||||
const labelColorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
const presetColorSelect: HTMLSelectElement = querySelector(
|
||||
'#trx-user-label-form-color > select'
|
||||
);
|
||||
labelColorInput.value = presetColorSelect.value;
|
||||
updatePreview(presetColorSelect.value);
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import {Except} from 'type-fest';
|
||||
import {
|
||||
appendStyleAttribute,
|
||||
UserLabel,
|
||||
log,
|
||||
isColorBright,
|
||||
getCurrentThemeKey,
|
||||
querySelector
|
||||
} from '../../utilities';
|
||||
import {themeColors, ThemeKey} from '../../theme-colors';
|
||||
|
||||
const theme: typeof themeColors[ThemeKey] = themeColors[getCurrentThemeKey()];
|
||||
|
||||
export function getLabelForm(): HTMLFormElement {
|
||||
return querySelector('#trx-user-label-form');
|
||||
}
|
||||
|
||||
export function showLabelForm(label: HTMLSpanElement): HTMLFormElement {
|
||||
const labelBounds: DOMRect = label.getBoundingClientRect();
|
||||
const labelForm = getLabelForm();
|
||||
const horizontalOffset: number = labelBounds.x + window.scrollX;
|
||||
const verticalOffset: number =
|
||||
labelBounds.y + labelBounds.height + 4 + window.scrollY;
|
||||
appendStyleAttribute(
|
||||
labelForm,
|
||||
`left: ${horizontalOffset}px; top: ${verticalOffset}px;`
|
||||
);
|
||||
labelForm.classList.remove('trx-hidden');
|
||||
return labelForm;
|
||||
}
|
||||
|
||||
export function hideLabelForm(): HTMLFormElement {
|
||||
const labelForm = getLabelForm();
|
||||
labelForm.classList.add('trx-hidden');
|
||||
return labelForm;
|
||||
}
|
||||
|
||||
export function setLabelFormTitle(title: string): void {
|
||||
const labelTitle: HTMLLabelElement = querySelector(
|
||||
'#trx-user-label-form > div:first-child > label:first-child'
|
||||
);
|
||||
labelTitle.textContent = title;
|
||||
}
|
||||
|
||||
export function setLabelFormUserID(id: number): void {
|
||||
getLabelForm().dataset.trxUserLabelId = String(id);
|
||||
}
|
||||
|
||||
export function setLabelFormUsername(username: string): void {
|
||||
const usernameInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-username'
|
||||
);
|
||||
usernameInput.value = username.toLowerCase();
|
||||
}
|
||||
|
||||
export function setLabelFormPriority(priority: number): void {
|
||||
const priorityInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-priority'
|
||||
);
|
||||
priorityInput.value = String(priority);
|
||||
}
|
||||
|
||||
export function setLabelFormColor(color: string): void {
|
||||
const colorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
colorInput.value = color;
|
||||
}
|
||||
|
||||
export function setLabelFormText(text: string): void {
|
||||
const textInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-input > input'
|
||||
);
|
||||
textInput.value = text;
|
||||
}
|
||||
|
||||
export function getLabelFormValues(): Except<UserLabel, 'id'> | undefined {
|
||||
const usernameInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-username'
|
||||
);
|
||||
const priorityInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-priority'
|
||||
);
|
||||
const colorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
const textInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-input > input'
|
||||
);
|
||||
|
||||
const data: Except<UserLabel, 'id'> = {
|
||||
color: colorInput.value.toLowerCase(),
|
||||
priority: Number(priorityInput.value),
|
||||
text: textInput.value,
|
||||
username: usernameInput.value.toLowerCase()
|
||||
};
|
||||
|
||||
if (data.username.length < 3) {
|
||||
log('No username was provided to add a label to.', true);
|
||||
usernameInput.classList.add('trx-invalid');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
usernameInput.classList.remove('trx-invalid');
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getLabelFormID(): number | undefined {
|
||||
const labelForm: HTMLFormElement = getLabelForm();
|
||||
const id: string | null = labelForm.getAttribute('data-trx-user-label-id');
|
||||
if (id === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Number(id);
|
||||
}
|
||||
|
||||
export function updatePreview(color?: string, text?: string): void {
|
||||
if (typeof color === 'undefined') {
|
||||
const labelColorInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-form-color > input'
|
||||
);
|
||||
color = labelColorInput.value;
|
||||
}
|
||||
|
||||
if (typeof text === 'undefined') {
|
||||
const labelTextInput: HTMLInputElement = querySelector(
|
||||
'#trx-user-label-input > input'
|
||||
);
|
||||
text = labelTextInput.value;
|
||||
}
|
||||
|
||||
const labelPreview: HTMLDivElement = querySelector('#trx-user-label-preview');
|
||||
labelPreview.setAttribute(
|
||||
'style',
|
||||
`background-color: ${color}; border-color: ${theme.foregroundAlt};`
|
||||
);
|
||||
labelPreview.firstElementChild!.textContent = text;
|
||||
|
||||
if (isColorBright(color)) {
|
||||
labelPreview.classList.add('trx-bright');
|
||||
} else {
|
||||
labelPreview.classList.remove('trx-bright');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
export type ThemeKey = keyof typeof themeColors;
|
||||
export type ColorKey = keyof typeof themeColors[ThemeKey];
|
||||
|
||||
export const themeColors = {
|
||||
atomOneDark: {
|
||||
background: '#282C34',
|
||||
backgroundAlt: '#21242b',
|
||||
foreground: '#ABB2BF',
|
||||
foregroundAlt: '#828997',
|
||||
cyan: '#56B6C2',
|
||||
blue: '#61AFEF',
|
||||
purple: '#C678DD',
|
||||
green: '#98C379',
|
||||
red: '#E06C75',
|
||||
orange: '#D19A66'
|
||||
},
|
||||
black: {
|
||||
background: '#000',
|
||||
backgroundAlt: '#222',
|
||||
foreground: '#ccc',
|
||||
foregroundAlt: '#888',
|
||||
cyan: '#2aa198',
|
||||
blue: '#268bd2',
|
||||
purple: '#6c71c4',
|
||||
green: '#859900',
|
||||
red: '#f00',
|
||||
orange: '#b58900'
|
||||
},
|
||||
dracula: {
|
||||
background: '#282a36',
|
||||
backgroundAlt: '#44475a',
|
||||
foreground: '#f8f8f2',
|
||||
foregroundAlt: '#6272a4',
|
||||
cyan: '#8be9fd',
|
||||
blue: '#6272a4',
|
||||
purple: '#bd93f9',
|
||||
green: '#50fa7b',
|
||||
red: '#ff5555',
|
||||
orange: '#ffb86c'
|
||||
},
|
||||
gruvboxDark: {
|
||||
background: '#282828',
|
||||
backgroundAlt: '#3c3836',
|
||||
foreground: '#fbf1c7',
|
||||
foregroundAlt: '#ebdbb2',
|
||||
cyan: '#689d6a',
|
||||
blue: '#458588',
|
||||
purple: '#b16286',
|
||||
green: '#98971a',
|
||||
red: '#fb4934',
|
||||
orange: '#fabd2f'
|
||||
},
|
||||
gruvboxLight: {
|
||||
background: '#fbf1c7',
|
||||
backgroundAlt: '#ebdbb2',
|
||||
foreground: '#282828',
|
||||
foregroundAlt: '#3c3836',
|
||||
cyan: '#689d6a',
|
||||
blue: '#458588',
|
||||
purple: '#b16286',
|
||||
green: '#98971a',
|
||||
red: '#fb4934',
|
||||
orange: '#fabd2f'
|
||||
},
|
||||
solarizedLight: {
|
||||
background: '#fdf6e3',
|
||||
backgroundAlt: '#eee8d5',
|
||||
foreground: '#657b83',
|
||||
foregroundAlt: '#93a1a1',
|
||||
cyan: '#2aa198',
|
||||
blue: '#268bd2',
|
||||
purple: '#6c71c4',
|
||||
green: '#859900',
|
||||
red: '#dc322f',
|
||||
orange: '#cb4b16'
|
||||
},
|
||||
solarizedDark: {
|
||||
background: '#002b36',
|
||||
backgroundAlt: '#073642',
|
||||
foreground: '#839496',
|
||||
foregroundAlt: '#586e75',
|
||||
cyan: '#2aa198',
|
||||
blue: '#268bd2',
|
||||
purple: '#6c71c4',
|
||||
green: '#859900',
|
||||
red: '#dc322f',
|
||||
orange: '#cb4b16'
|
||||
},
|
||||
white: {
|
||||
background: '#fff',
|
||||
backgroundAlt: '#eee',
|
||||
foreground: '#333',
|
||||
foregroundAlt: '#888',
|
||||
cyan: '#1e824c',
|
||||
blue: '#1460aa',
|
||||
purple: '#551a8b',
|
||||
green: '#4b6319',
|
||||
red: '#d91e18',
|
||||
orange: '#e66b00'
|
||||
},
|
||||
zenburn: {
|
||||
background: '#3f3f3f',
|
||||
backgroundAlt: '#4f4f4f',
|
||||
foreground: '#dcdccc',
|
||||
foregroundAlt: '#aaa',
|
||||
cyan: '#8cd0d3',
|
||||
blue: '#80d4aa',
|
||||
purple: '#bc8cbc',
|
||||
green: '#7f9f7f',
|
||||
red: '#dc8c6c',
|
||||
orange: '#efef8f'
|
||||
}
|
||||
};
|
|
@ -0,0 +1,256 @@
|
|||
import {browser, Manifest} from 'webextension-polyfill-ts';
|
||||
import {themeColors, ThemeKey} from './theme-colors';
|
||||
|
||||
export interface UserLabel {
|
||||
color: string;
|
||||
id: number;
|
||||
text: string;
|
||||
priority: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
data: {
|
||||
latestActiveFeatureTab: string;
|
||||
userLabels: UserLabel[];
|
||||
version?: string;
|
||||
};
|
||||
features: {
|
||||
backToTop: boolean;
|
||||
debug: boolean;
|
||||
jumpToNewComment: boolean;
|
||||
userLabels: boolean;
|
||||
[index: string]: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultSettings: Settings = {
|
||||
data: {
|
||||
latestActiveFeatureTab: 'debug',
|
||||
userLabels: []
|
||||
},
|
||||
features: {
|
||||
backToTop: true,
|
||||
debug: false,
|
||||
jumpToNewComment: true,
|
||||
userLabels: true
|
||||
}
|
||||
};
|
||||
|
||||
// Keep a local variable here for the debug logging, this way we don't have to
|
||||
// call `getSettings()` each time we potentially want to log something. It gets
|
||||
// set each time `getSettings()` is called so it's always accurate.
|
||||
let debug = true;
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
// TODO: Replace local storage with sync storage once the extension has been
|
||||
// deployed to AMO and we get a permanent ID. https://bugzil.la/1323228
|
||||
const localSettings: any = await browser.storage.local.get(defaultSettings);
|
||||
const settings: Settings = {
|
||||
data: {...defaultSettings.data, ...localSettings.data},
|
||||
features: {...defaultSettings.features, ...localSettings.features}
|
||||
};
|
||||
debug = localSettings.features.debug;
|
||||
// If we're in development, force debug output.
|
||||
if (getManifest().nodeEnv === 'development') {
|
||||
debug = true;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function setSettings(
|
||||
newSettings: Partial<Settings>
|
||||
): Promise<void> {
|
||||
return browser.storage.local.set(newSettings);
|
||||
}
|
||||
|
||||
export function log(message: any, override = false): void {
|
||||
let overrideStyle = '';
|
||||
let prefix = '[TRX]';
|
||||
if (override) {
|
||||
prefix = '%c' + prefix;
|
||||
overrideStyle = 'background-color: #dc322f; margin-right: 9px;';
|
||||
}
|
||||
|
||||
if (debug || override || getManifest().nodeEnv === 'development') {
|
||||
console.debug(prefix, overrideStyle, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert kebab-case strings to camelCase ones.
|
||||
// Primarily for converting element IDs to Object keys.
|
||||
export function kebabToCamel(input: string): string {
|
||||
let output = '';
|
||||
for (const part of input.split('-')) {
|
||||
output += part[0].toUpperCase();
|
||||
output += part.slice(1);
|
||||
}
|
||||
|
||||
return output[0].toLowerCase() + output.slice(1);
|
||||
}
|
||||
|
||||
// The opposite of `kebabToCamel()`.
|
||||
export function camelToKebab(input: string): string {
|
||||
const uppercaseMatches: RegExpMatchArray | null = input.match(/[A-Z]/g);
|
||||
// If there are no uppercase letters in the input, just return it.
|
||||
if (uppercaseMatches === null) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Find all the indexes of where uppercase letters are.
|
||||
const uppercaseIndexes: number[] = [];
|
||||
for (const match of uppercaseMatches) {
|
||||
const latestIndex: number =
|
||||
typeof uppercaseIndexes[uppercaseIndexes.length - 1] === 'undefined'
|
||||
? 0
|
||||
: uppercaseIndexes[uppercaseIndexes.length - 1];
|
||||
uppercaseIndexes.push(input.indexOf(match, latestIndex + 1));
|
||||
}
|
||||
|
||||
// Convert each section up to the next uppercase letter to lowercase with
|
||||
// a dash between each section.
|
||||
let output = '';
|
||||
let previousIndex = 0;
|
||||
for (const index of uppercaseIndexes) {
|
||||
output += input.slice(previousIndex, index).toLowerCase();
|
||||
output += '-';
|
||||
previousIndex = index;
|
||||
}
|
||||
|
||||
output += input.slice(previousIndex).toLowerCase();
|
||||
return output;
|
||||
}
|
||||
|
||||
// This utility function should only be used in cases where we know for certain
|
||||
// that the wanted element is going to exist.
|
||||
export function querySelector<T extends Element>(selector: string): T {
|
||||
return document.querySelector<T>(selector)!;
|
||||
}
|
||||
|
||||
export function createElementFromString<T extends Element>(input: string): T {
|
||||
const template: HTMLTemplateElement = document.createElement('template');
|
||||
template.innerHTML = input.trim();
|
||||
return template.content.firstElementChild! as T;
|
||||
}
|
||||
|
||||
export function isInTopicListing(): boolean {
|
||||
return document.querySelector('.topic-listing') !== null;
|
||||
}
|
||||
|
||||
export function getManifest(): {nodeEnv?: string} & Manifest.ManifestBase {
|
||||
const manifest: Manifest.ManifestBase = browser.runtime.getManifest();
|
||||
return {...manifest};
|
||||
}
|
||||
|
||||
export function getCurrentThemeKey(): ThemeKey {
|
||||
const body: HTMLBodyElement = querySelector('body');
|
||||
const classes: string | null = body.getAttribute('class');
|
||||
if (classes === null || !classes.includes('theme-')) {
|
||||
return 'white';
|
||||
}
|
||||
|
||||
const themeIndex: number = classes.indexOf('theme-');
|
||||
const themeKey: ThemeKey = kebabToCamel(
|
||||
classes.slice(
|
||||
themeIndex + 'theme-'.length,
|
||||
classes.includes(' ', themeIndex)
|
||||
? classes.indexOf(' ', themeIndex)
|
||||
: classes.length
|
||||
)
|
||||
) as ThemeKey;
|
||||
if (typeof themeColors[themeKey] === 'undefined') {
|
||||
log(
|
||||
`Attempted to retrieve theme key that's not defined: "${themeKey}" Using the white theme as fallback.`,
|
||||
true
|
||||
);
|
||||
return 'white';
|
||||
}
|
||||
|
||||
return themeKey;
|
||||
}
|
||||
|
||||
// Adapted from https://stackoverflow.com/a/12043228/12251171.
|
||||
export function isColorBright(color: string): boolean {
|
||||
if (color.startsWith('#')) {
|
||||
color = color.slice(1);
|
||||
}
|
||||
|
||||
if (color.length === 4) {
|
||||
color = color.slice(0, 3);
|
||||
}
|
||||
|
||||
if (color.length === 8) {
|
||||
color = color.slice(0, 6);
|
||||
}
|
||||
|
||||
if (color.length === 3) {
|
||||
color = color
|
||||
.split('')
|
||||
.map(val => val.repeat(2))
|
||||
.join('');
|
||||
}
|
||||
|
||||
const red: number = parseInt(color.slice(0, 2), 16);
|
||||
const green: number = parseInt(color.slice(2, 4), 16);
|
||||
const blue: number = parseInt(color.slice(4, 6), 16);
|
||||
const brightness: number = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||||
return brightness > 128;
|
||||
}
|
||||
|
||||
export function appendStyleAttribute(element: Element, styles: string): void {
|
||||
const existingStyles: string | null = element.getAttribute('style');
|
||||
if (existingStyles === null) {
|
||||
element.setAttribute('style', styles);
|
||||
} else {
|
||||
element.setAttribute('style', `${existingStyles} ${styles}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidHexColor(color: string): boolean {
|
||||
// Overly verbose validation for 3/4/6/8-character hex colors.
|
||||
if (
|
||||
/^#[a-f0-9]{6}$/i.exec(color) === null &&
|
||||
/^#[a-f0-9]{3}$/i.exec(color) === null &&
|
||||
/^#[a-f0-9]{8}$/i.exec(color) === null &&
|
||||
/^#[a-f0-9]{4}$/i.exec(color) === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function flashMessage(message: string, error = false): void {
|
||||
if (document.querySelector('.trx-flash-message') !== null) {
|
||||
log(
|
||||
`A flash message already exists, skipping requested one with message:\n${message}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageElement: HTMLDivElement = createElementFromString(
|
||||
`<div class="trx-flash-message">${message}</div>`
|
||||
);
|
||||
if (error) {
|
||||
messageElement.classList.add('trx-flash-error');
|
||||
}
|
||||
|
||||
let isRemoved = false;
|
||||
messageElement.addEventListener('click', (): void => {
|
||||
messageElement.remove();
|
||||
isRemoved = true;
|
||||
});
|
||||
document.body.append(messageElement);
|
||||
setTimeout(() => {
|
||||
messageElement.classList.add('trx-opaque');
|
||||
}, 50);
|
||||
setTimeout(() => {
|
||||
if (!isRemoved) {
|
||||
messageElement.classList.remove('trx-opaque');
|
||||
setTimeout(() => {
|
||||
messageElement.remove();
|
||||
}, 500);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "build/",
|
||||
"strict": true,
|
||||
"target": "es6",
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"node_modules/web-ext-types"
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"source/ts/**/*.ts",
|
||||
"scripts/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules/"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue