Fix all the things.
* Switch from Parcel to Vite. * Rewrite the Settings handling. * Fix a bug that made the extension not work on first load. * A bunch of other stuff, too lazy to write it all out.
This commit is contained in:
parent
58b5192d5a
commit
52696fe50e
|
@ -4,6 +4,10 @@ logs
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
|
@ -16,11 +20,12 @@ lib-cov
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
# nyc test coverage
|
# nyc test coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
@ -39,12 +44,21 @@ jspm_packages/
|
||||||
# TypeScript v1 declaration files
|
# TypeScript v1 declaration files
|
||||||
typings/
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
# Optional REPL history
|
# Optional REPL history
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
|
@ -56,17 +70,42 @@ typings/
|
||||||
|
|
||||||
# dotenv environment variables file
|
# dotenv environment variables file
|
||||||
.env
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
# next.js build output
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
.next
|
.next
|
||||||
|
|
||||||
# Profile directories
|
# Nuxt.js build / generate output
|
||||||
chromium/
|
.nuxt
|
||||||
firefox/
|
dist
|
||||||
|
|
||||||
# Parcel cache
|
# Gatsby files
|
||||||
.cache/
|
.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
|
||||||
|
|
||||||
# Output directory
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Build output directories
|
||||||
build/
|
build/
|
||||||
web-ext-artifacts/
|
web-ext-artifacts/
|
||||||
|
|
||||||
|
# Firefox profile directory
|
||||||
|
firefox/
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
_
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
yarn test
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
yarn test
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License
|
The MIT License
|
||||||
|
|
||||||
Copyright 2019-2020 Tildes Community and Contributors
|
Copyright 2019-2022 Tildes Community and Contributors
|
||||||
https://gitlab.com/tildes-community/tildes-reextended
|
https://gitlab.com/tildes-community/tildes-reextended
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
|
80
package.json
80
package.json
|
@ -8,73 +8,51 @@
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "NODE_ENV=development parcel 'source/manifest.json' -d 'build/' --no-hmr",
|
"start": "vite build -m development --watch",
|
||||||
"start": "web-ext run --source-dir build/ --bc",
|
"clean": "trash build web-ext-artifacts",
|
||||||
"start:chromium": "yarn start --chromium-profile chromium/ --keep-profile-changes --target chromium --start-url \"chrome://extensions\"",
|
"build": "pnpm clean && vite build && web-ext build --source-dir build && pnpm zip-source",
|
||||||
"start:firefox": "yarn start --firefox-profile firefox/ --keep-profile-changes --target firefox-desktop --start-url \"about:debugging#/runtime/this-firefox\"",
|
"zip-source": "git archive --format zip --output web-ext-artifacts/tildes_reextended-source.zip HEAD",
|
||||||
"clean": "trash .cache build/ web-ext-artifacts/",
|
"test": "xo && stylelint 'source/**/*.scss' && tsc"
|
||||||
"build": "yarn clean && parcel build 'source/manifest.json' -d 'build/' && web-ext build --source-dir 'build/' && yarn zip-source",
|
|
||||||
"zip-source": "zip -r web-ext-artifacts/tildes_reextended-source.zip README.md yarn.lock tsconfig.json package.json .gitignore LICENSE source/",
|
|
||||||
"test": "xo && stylelint 'source/scss/**/*.scss' && tsc --noEmit",
|
|
||||||
"postinstall": "husky install"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caret-pos": "^2.0.0",
|
"caret-pos": "^2.0.0",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.1",
|
||||||
"htm": "^3.0.4",
|
"htm": "^3.1.0",
|
||||||
"modern-normalize": "^1.0.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"platform": "^1.3.6",
|
"platform": "^1.3.6",
|
||||||
"preact": "^10.5.12",
|
"preact": "^10.6.6",
|
||||||
"webextension-polyfill-ts": "^0.25.0"
|
"webextension-polyfill": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debounce": "^1.2.0",
|
"@preact/preset-vite": "^2.1.7",
|
||||||
"@types/platform": "^1.3.3",
|
"@types/debounce": "^1.2.1",
|
||||||
"eslint-config-xo-typescript": "^0.38.0",
|
"@types/platform": "^1.3.4",
|
||||||
"husky": "5",
|
"@types/webextension-polyfill": "^0.8.2",
|
||||||
"parcel-bundler": "^1.12.4",
|
"postcss": "^8.4.6",
|
||||||
"parcel-plugin-web-extension": "^1.6.1",
|
"sass": "^1.49.8",
|
||||||
"sass": "^1.32.8",
|
"stylelint": "^14.5.1",
|
||||||
"stylelint": "^13.11.0",
|
"stylelint-config-standard-scss": "^3.0.0",
|
||||||
"stylelint-config-xo-scss": "^0.14.0",
|
"trash-cli": "^5.0.0",
|
||||||
"stylelint-config-xo-space": "^0.15.1",
|
"typescript": "^4.5.5",
|
||||||
"trash-cli": "^4.0.0",
|
"vite": "^2.8.4",
|
||||||
"typescript": "^4.1.5",
|
"vite-plugin-web-extension": "^1.1.2",
|
||||||
"web-ext": "^5.5.0",
|
"web-ext": "^6.7.0",
|
||||||
"web-ext-types": "^3.2.1",
|
"xo": "^0.48.0"
|
||||||
"xo": "^0.38.1"
|
|
||||||
},
|
},
|
||||||
"stylelint": {
|
"stylelint": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-xo-scss",
|
"stylelint-config-standard-scss"
|
||||||
"stylelint-config-xo-space"
|
|
||||||
],
|
|
||||||
"ignoreFiles": [
|
|
||||||
"source/**/*.ts",
|
|
||||||
"build/**"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"scss/at-rule-no-unknown": null,
|
"no-descending-specificity": null,
|
||||||
"at-rule-empty-line-before": null,
|
"string-quotes": "single"
|
||||||
"at-rule-no-unknown": null,
|
|
||||||
"block-no-empty": null,
|
|
||||||
"no-descending-specificity": null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
"globals": [
|
|
||||||
"document",
|
|
||||||
"window"
|
|
||||||
],
|
|
||||||
"prettier": true,
|
"prettier": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-implicit-any-catch": "off",
|
"@typescript-eslint/naming-convention": "off"
|
||||||
"@typescript-eslint/no-loop-func": "off",
|
|
||||||
"node/file-extension-in-import": "off"
|
|
||||||
},
|
},
|
||||||
"space": true
|
"space": true
|
||||||
},
|
}
|
||||||
"browserslist": [
|
|
||||||
"last 2 Chrome versions"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,33 +0,0 @@
|
||||||
import {browser} from 'webextension-polyfill-ts';
|
|
||||||
import {
|
|
||||||
defaultSettings,
|
|
||||||
getManifest,
|
|
||||||
getSettings,
|
|
||||||
setSettings,
|
|
||||||
versionAsNumber
|
|
||||||
} from '.';
|
|
||||||
|
|
||||||
// Add listeners to open the options page when:
|
|
||||||
// * The extension icon is clicked.
|
|
||||||
// * The extension is installed.
|
|
||||||
browser.browserAction.onClicked.addListener(openOptionsPage);
|
|
||||||
browser.runtime.onInstalled.addListener(async () => {
|
|
||||||
const manifest = getManifest();
|
|
||||||
const settings = await getSettings();
|
|
||||||
const versionGotUpdated =
|
|
||||||
versionAsNumber(manifest.version) >
|
|
||||||
versionAsNumber(settings.data.version ?? defaultSettings.data.version!);
|
|
||||||
|
|
||||||
if (versionGotUpdated) {
|
|
||||||
settings.data.version = manifest.version;
|
|
||||||
await setSettings(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versionGotUpdated || manifest.nodeEnv === 'development') {
|
|
||||||
await openOptionsPage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openOptionsPage() {
|
|
||||||
await browser.runtime.openOptionsPage();
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
import {log} from '../utilities/logging.js';
|
||||||
|
|
||||||
|
log('Debug logging is enabled.');
|
||||||
|
|
||||||
|
// Open the options page when the extension icon is clicked.
|
||||||
|
browser.browserAction.onClicked.addListener(openOptionsPage);
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener(async () => {
|
||||||
|
// Always automatically open the options page in development.
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
await openOptionsPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openOptionsPage() {
|
||||||
|
await browser.runtime.openOptionsPage();
|
||||||
|
}
|
|
@ -1,23 +1,24 @@
|
||||||
|
import './scss/scripts.scss';
|
||||||
|
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {render} from 'preact';
|
import {render} from 'preact';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutocompleteFeature,
|
AutocompleteFeature,
|
||||||
BackToTopFeature,
|
BackToTopFeature,
|
||||||
extractAndSaveGroups,
|
|
||||||
getSettings,
|
|
||||||
initialize,
|
|
||||||
JumpToNewCommentFeature,
|
JumpToNewCommentFeature,
|
||||||
log,
|
UserLabelsFeature,
|
||||||
runHideVotesFeature,
|
runHideVotesFeature,
|
||||||
runMarkdownToolbarFeature,
|
runMarkdownToolbarFeature,
|
||||||
TRXComponent,
|
} from './scripts/exports.js';
|
||||||
UserLabelsFeature
|
import Settings from './settings.js';
|
||||||
} from '.';
|
import {extractGroups, initializeGlobals, log} from './utilities/exports.js';
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
async function initialize() {
|
||||||
const start = window.performance.now();
|
const start = window.performance.now();
|
||||||
initialize();
|
initializeGlobals();
|
||||||
const settings = await getSettings();
|
const settings = await Settings.fromSyncStorage();
|
||||||
|
window.TildesReExtended.debug = settings.features.debug;
|
||||||
|
|
||||||
// Any features that will use `settings.data.knownGroups` should be added to
|
// Any features that will use `settings.data.knownGroups` should be added to
|
||||||
// this array so that when groups are changed on Tildes, TRX can still update
|
// this array so that when groups are changed on Tildes, TRX can still update
|
||||||
|
@ -25,7 +26,11 @@ window.addEventListener('load', async () => {
|
||||||
const usesKnownGroups = [settings.features.autocomplete];
|
const usesKnownGroups = [settings.features.autocomplete];
|
||||||
// Only when any of the features that uses this data try to save the groups.
|
// Only when any of the features that uses this data try to save the groups.
|
||||||
if (usesKnownGroups.some((value) => value)) {
|
if (usesKnownGroups.some((value) => value)) {
|
||||||
settings.data.knownGroups = await extractAndSaveGroups(settings);
|
const knownGroups = extractGroups();
|
||||||
|
if (knownGroups !== undefined) {
|
||||||
|
settings.data.knownGroups = knownGroups;
|
||||||
|
await settings.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object to hold the active components we are going to render.
|
// Object to hold the active components we are going to render.
|
||||||
|
@ -68,14 +73,18 @@ window.addEventListener('load', async () => {
|
||||||
// The jump to new comment button must come right before
|
// The jump to new comment button must come right before
|
||||||
// the back to top button. The CSS depends on them being in this order.
|
// the back to top button. The CSS depends on them being in this order.
|
||||||
render(
|
render(
|
||||||
html`<div id="trx-container">
|
html`
|
||||||
${components.jumpToNewComment} ${components.backToTop}
|
<div id="trx-container">
|
||||||
${components.autocomplete} ${components.userLabels}
|
${components.jumpToNewComment} ${components.backToTop}
|
||||||
</div>`,
|
${components.autocomplete} ${components.userLabels}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
document.body,
|
document.body,
|
||||||
replacement
|
replacement,
|
||||||
);
|
);
|
||||||
|
|
||||||
const initializedIn = window.performance.now() - start;
|
const initializedIn = window.performance.now() - start;
|
||||||
log(`Initialized in approximately ${initializedIn} milliseconds.`);
|
log(`Initialized in approximately ${initializedIn} milliseconds.`);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
void initialize();
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {createContext} from 'preact';
|
|
||||||
import {Settings} from './settings';
|
|
||||||
|
|
||||||
export type TRXComponent = ReturnType<typeof html>;
|
|
||||||
|
|
||||||
type AppContextValues = {
|
|
||||||
settings: Settings;
|
|
||||||
setActiveFeature: (feature: string) => void;
|
|
||||||
toggleFeature: (feature: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// We create this context with null as we'll create the state and the other
|
|
||||||
// functions inside App itself. See `settings-page.ts` for that.
|
|
||||||
export const AppContext = createContext<AppContextValues>(null!);
|
|
||||||
|
|
||||||
export * from './scripts';
|
|
||||||
export * from './settings';
|
|
||||||
export * from './utilities';
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"nodeEnv": "development"
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Tildes ReExtended",
|
"name": "Tildes ReExtended",
|
||||||
"description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.",
|
"description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"downloads",
|
"downloads",
|
||||||
"storage",
|
"storage",
|
||||||
|
@ -11,23 +11,23 @@
|
||||||
],
|
],
|
||||||
"content_security_policy": "script-src 'self'; object-src 'self'; style-src 'unsafe-inline'",
|
"content_security_policy": "script-src 'self'; object-src 'self'; style-src 'unsafe-inline'",
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"./assets/**"
|
"assets/**"
|
||||||
],
|
],
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "./assets/tildes-reextended-128.png"
|
"128": "assets/tildes-reextended-128.png"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"./background.ts"
|
"background/background.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"128": "./assets/tildes-reextended-128.png"
|
"128": "assets/tildes-reextended-128.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "./index.html",
|
"page": "options/index.html",
|
||||||
"open_in_tab": true
|
"open_in_tab": true
|
||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
],
|
],
|
||||||
"run_at": "document_end",
|
"run_at": "document_end",
|
||||||
"css": [
|
"css": [
|
||||||
"./scss/scripts.scss"
|
"generated:style.css"
|
||||||
],
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"./content-scripts.ts"
|
"content-scripts.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import Settings from '../../settings.js';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
log,
|
||||||
|
isValidHexColor,
|
||||||
|
isValidTildesUsername,
|
||||||
|
} from '../../utilities/exports.js';
|
||||||
|
import {SettingProps, Setting} from './index.js';
|
||||||
|
|
||||||
|
async function logSettings() {
|
||||||
|
log(await Settings.fromSyncStorage(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importFileHandler(event: Event): Promise<void> {
|
||||||
|
// Grab the imported files (if any).
|
||||||
|
const fileList = (event.target as HTMLInputElement).files;
|
||||||
|
|
||||||
|
if (fileList === null) {
|
||||||
|
log('No file imported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new window.FileReader();
|
||||||
|
|
||||||
|
reader.addEventListener('load', async (): Promise<void> => {
|
||||||
|
let data: Partial<Settings>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
data = JSON.parse(reader.result!.toString()) as Partial<Settings>;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
log(error, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await Settings.fromSyncStorage();
|
||||||
|
if (typeof data.data !== 'undefined') {
|
||||||
|
if (typeof data.data.userLabels !== 'undefined') {
|
||||||
|
settings.data.userLabels = [];
|
||||||
|
|
||||||
|
for (const label of data.data.userLabels) {
|
||||||
|
if (
|
||||||
|
typeof label.username === 'undefined' ||
|
||||||
|
!isValidTildesUsername(label.username)
|
||||||
|
) {
|
||||||
|
log(`Invalid username in imported labels: ${label.username}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.data.userLabels.push({
|
||||||
|
color: isValidHexColor(label.color) ? label.color : '#f0f',
|
||||||
|
id: settings.data.userLabels.length + 1,
|
||||||
|
priority: Number.isNaN(label.priority) ? 0 : label.priority,
|
||||||
|
text: typeof label.text === 'undefined' ? 'Label' : label.text,
|
||||||
|
username: label.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.data.hideVotes !== 'undefined') {
|
||||||
|
settings.data.hideVotes = data.data.hideVotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.features !== 'undefined') {
|
||||||
|
settings.features = {...data.features};
|
||||||
|
}
|
||||||
|
|
||||||
|
await settings.save();
|
||||||
|
log('Successfully imported your settings, reloading the page to apply.');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.addEventListener('error', (): void => {
|
||||||
|
log(reader.error, true);
|
||||||
|
reader.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsText(fileList[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportSettings(event: MouseEvent): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const settings = await Settings.fromSyncStorage();
|
||||||
|
const settingsBlob = new window.Blob([JSON.stringify(settings, null, 2)], {
|
||||||
|
type: 'text/json',
|
||||||
|
});
|
||||||
|
|
||||||
|
const objectURL = URL.createObjectURL(settingsBlob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await browser.downloads.download({
|
||||||
|
filename: 'tildes-reextended-settings.json',
|
||||||
|
url: objectURL,
|
||||||
|
saveAs: true,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
log(error);
|
||||||
|
} finally {
|
||||||
|
// According to MDN, when creating an object URL we should also revoke it
|
||||||
|
// when "it's safe to do so" to prevent excessive memory/storage use.
|
||||||
|
// 60 seconds should probably be enough time to download the settings.
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(objectURL);
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AboutSetting(props: SettingProps): TRXComponent {
|
||||||
|
const importSettings = () => {
|
||||||
|
document.querySelector<HTMLElement>('#import-settings')!.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const communityLink = html`
|
||||||
|
<${Link}
|
||||||
|
url="https://gitlab.com/tildes-community"
|
||||||
|
text="Tildes Community project"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const criusLink = html`
|
||||||
|
<${Link} url="https://tildes.net/user/crius" text="Crius" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
const gitlabIssuesLink = html`
|
||||||
|
<${Link}
|
||||||
|
url="https://gitlab.com/tildes-community/tildes-reextended/-/issues"
|
||||||
|
text="GitLab issue tracker"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const gitlabLicenseLink = html`
|
||||||
|
<${Link}
|
||||||
|
url="https://gitlab.com/tildes-community/tildes-reextended/blob/main/LICENSE"
|
||||||
|
text="MIT License"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageCommunityLink = html`
|
||||||
|
<${Link}
|
||||||
|
url="https://tildes.net/user/Community/new_message"
|
||||||
|
text="message Community"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tildesExtendedLink = html`
|
||||||
|
<${Link}
|
||||||
|
url="https://github.com/theCrius/tildes-extended"
|
||||||
|
text="Tildes Extended"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
This feature will make debugging logs output to the console when
|
||||||
|
enabled.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Tildes ReExtended is a from-scratch recreation of the original${' '}
|
||||||
|
${tildesExtendedLink} web extension by ${criusLink}. Open-sourced${' '}
|
||||||
|
with the ${gitlabLicenseLink} and maintained as a ${communityLink}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To report bugs or request new features use the links at the bottom of
|
||||||
|
this page, check out the ${gitlabIssuesLink} or${' '}
|
||||||
|
${messageCommunityLink}${' '} on Tildes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="divider" />
|
||||||
|
|
||||||
|
<div class="import-export">
|
||||||
|
<p>
|
||||||
|
Note that importing settings will delete and overwrite your existing
|
||||||
|
ones.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="import-settings"
|
||||||
|
onChange=${importFileHandler}
|
||||||
|
class="trx-hidden"
|
||||||
|
accept="application/json"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
<button onClick=${importSettings} class="button">
|
||||||
|
Import Settings
|
||||||
|
</button>
|
||||||
|
<button onClick=${exportSettings} class="button">
|
||||||
|
Export Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider" />
|
||||||
|
|
||||||
|
<details class="misc-utilities">
|
||||||
|
<summary>Danger Zone</summary>
|
||||||
|
|
||||||
|
<div class="inner">
|
||||||
|
<button onClick=${logSettings} class="button">Log Settings</button>
|
||||||
|
|
||||||
|
<button onClick=${Settings.nuke} class="button destructive">
|
||||||
|
Remove All Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {Setting, SettingProps} from './index.js';
|
||||||
|
|
||||||
|
export function AutocompleteSetting(props: SettingProps): TRXComponent {
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
Adds autocompletion in textareas for user mentions (starting with${' '}
|
||||||
|
<code>@</code>) and groups (starting with <code>~</code>).
|
||||||
|
</p>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {Setting, SettingProps} from './index.js';
|
||||||
|
|
||||||
|
export function BackToTopSetting(props: SettingProps): TRXComponent {
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
Adds a hovering button to the bottom-right of all pages once you've
|
||||||
|
scrolled down far enough that, when clicked, will scroll you back to the
|
||||||
|
top of the page.
|
||||||
|
</p>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export {AboutSetting} from './about.js';
|
||||||
|
export {AutocompleteSetting} from './autocomplete.js';
|
||||||
|
export {BackToTopSetting} from './back-to-top.js';
|
||||||
|
export {HideVotesSetting} from './hide-votes.js';
|
||||||
|
export {JumpToNewCommentSetting} from './jump-to-new-comment.js';
|
||||||
|
export {MarkdownToolbarSetting} from './markdown-toolbar.js';
|
||||||
|
export {UserLabelsSetting} from './user-labels.js';
|
|
@ -1,12 +1,8 @@
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {useContext, useState} from 'preact/hooks';
|
import {useContext, useState} from 'preact/hooks';
|
||||||
import {
|
|
||||||
AppContext,
|
import {AppContext} from '../context.js';
|
||||||
setSettings,
|
import {Setting, SettingProps} from './index.js';
|
||||||
Setting,
|
|
||||||
SettingProps,
|
|
||||||
TRXComponent
|
|
||||||
} from '../..';
|
|
||||||
|
|
||||||
export function HideVotesSetting(props: SettingProps): TRXComponent {
|
export function HideVotesSetting(props: SettingProps): TRXComponent {
|
||||||
const {settings} = useContext(AppContext);
|
const {settings} = useContext(AppContext);
|
||||||
|
@ -17,7 +13,7 @@ export function HideVotesSetting(props: SettingProps): TRXComponent {
|
||||||
setChecked(checked);
|
setChecked(checked);
|
||||||
|
|
||||||
settings.data.hideVotes = checked;
|
settings.data.hideVotes = checked;
|
||||||
void setSettings(settings);
|
void settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox labels and "targets". The targets should match the keys as defined
|
// Checkbox labels and "targets". The targets should match the keys as defined
|
||||||
|
@ -26,7 +22,7 @@ export function HideVotesSetting(props: SettingProps): TRXComponent {
|
||||||
{label: 'Your comments', target: 'ownComments'},
|
{label: 'Your comments', target: 'ownComments'},
|
||||||
{label: 'Your topics', target: 'ownTopics'},
|
{label: 'Your topics', target: 'ownTopics'},
|
||||||
{label: "Other's comments", target: 'comments'},
|
{label: "Other's comments", target: 'comments'},
|
||||||
{label: "Other's topics", target: 'topics'}
|
{label: "Other's topics", target: 'topics'},
|
||||||
].map(
|
].map(
|
||||||
({label, target}) =>
|
({label, target}) =>
|
||||||
html`
|
html`
|
||||||
|
@ -42,16 +38,18 @@ export function HideVotesSetting(props: SettingProps): TRXComponent {
|
||||||
${label}
|
${label}
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
`
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`<${Setting} ...${props}>
|
return html`
|
||||||
<p class="info">
|
<${Setting} ...${props}>
|
||||||
Hides vote counts from topics and comments of yourself or other people.
|
<p class="info">
|
||||||
</p>
|
Hides vote counts from topics and comments of yourself or other people.
|
||||||
|
</p>
|
||||||
|
|
||||||
<ul class="checkbox-list">
|
<ul class="checkbox-list">
|
||||||
${checkboxes}
|
${checkboxes}
|
||||||
</ul>
|
</ul>
|
||||||
<//>`;
|
<//>
|
||||||
|
`;
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import {html} from 'htm/preact';
|
import {Component} from 'preact';
|
||||||
import {useContext} from 'preact/hooks';
|
import {useContext} from 'preact/hooks';
|
||||||
import {AppContext, TRXComponent} from '../..';
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {AppContext} from '../context.js';
|
||||||
|
|
||||||
export type SettingProps = {
|
export type SettingProps = {
|
||||||
children: TRXComponent | undefined;
|
children: TRXComponent | undefined;
|
||||||
|
@ -10,20 +12,25 @@ export type SettingProps = {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Header(props: SettingProps): TRXComponent {
|
class Header extends Component<SettingProps> {
|
||||||
const context = useContext(AppContext);
|
render() {
|
||||||
const enabled = props.enabled ? 'Enabled' : 'Disabled';
|
const {props} = this;
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
const enabled = props.enabled ? 'Enabled' : 'Disabled';
|
||||||
|
|
||||||
return html`<header>
|
return html`
|
||||||
<h2>${props.title}</h2>
|
<header>
|
||||||
<button
|
<h2>${props.title}</h2>
|
||||||
onClick="${() => {
|
<button
|
||||||
context.toggleFeature(props.feature);
|
onClick="${() => {
|
||||||
}}"
|
context.toggleFeature(props.feature);
|
||||||
>
|
}}"
|
||||||
${enabled}
|
>
|
||||||
</button>
|
${enabled}
|
||||||
</header>`;
|
</button>
|
||||||
|
</header>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A base component for all the settings, this adds the header and the
|
// A base component for all the settings, this adds the header and the
|
||||||
|
@ -44,11 +51,3 @@ export function Setting(props: SettingProps): TRXComponent {
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './about';
|
|
||||||
export * from './autocomplete';
|
|
||||||
export * from './back-to-top';
|
|
||||||
export * from './hide-votes';
|
|
||||||
export * from './jump-to-new-comment';
|
|
||||||
export * from './markdown-toolbar';
|
|
||||||
export * from './user-labels';
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {Setting, SettingProps} from './index.js';
|
||||||
|
|
||||||
|
export function JumpToNewCommentSetting(props: SettingProps): TRXComponent {
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
Adds a hovering button to the bottom-right of pages with new comments
|
||||||
|
that, when clicked, will scroll you to the next new comment.
|
||||||
|
</p>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {Link} from '../../utilities/exports.js';
|
||||||
|
import {Setting, SettingProps} from './index.js';
|
||||||
|
|
||||||
|
export function MarkdownToolbarSetting(props: SettingProps): TRXComponent {
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
Adds a toolbar with a selection of Markdown snippets that when used will
|
||||||
|
insert the according Markdown where your cursor is. Particularly useful
|
||||||
|
for the${' '}
|
||||||
|
<${Link}
|
||||||
|
url="https://docs.tildes.net/instructions/text-formatting#expandable-sections"
|
||||||
|
text="expandable section"
|
||||||
|
/>
|
||||||
|
/spoilerbox syntax. If you have text selected, the Markdown will be
|
||||||
|
inserted around your text.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
A full list of the snippets is available${' '}
|
||||||
|
<${Link}
|
||||||
|
url="https://gitlab.com/tildes-community/tildes-reextended/-/issues/12"
|
||||||
|
text="on GitLab"
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import {Setting, SettingProps} from './index.js';
|
||||||
|
|
||||||
|
export function UserLabelsSetting(props: SettingProps): TRXComponent {
|
||||||
|
return html`
|
||||||
|
<${Setting} ...${props}>
|
||||||
|
<p class="info">
|
||||||
|
Adds a way to create customizable labels to users. Wherever a link to a
|
||||||
|
person's profile is available, a <code>[+]</code> will be put next to
|
||||||
|
it. Clicking on that will bring up a dialog to add a new label and
|
||||||
|
clicking on existing labels will bring up the same dialog to edit them.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>View Customizable Values</summary>
|
||||||
|
<ul class="user-label-values">
|
||||||
|
<li><b>Username</b>: who to apply the label 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 values are hex colors or <code>transparent</code>.
|
||||||
|
<br />
|
||||||
|
Colors based on your current Tildes theme are also available in the
|
||||||
|
dropdown menu.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Text</b>: the text to go in the label. If left empty the label
|
||||||
|
will show as a 12 by 12 pixel square instead.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {createContext} from 'preact';
|
||||||
|
|
||||||
|
import Settings from '../settings.js';
|
||||||
|
|
||||||
|
type AppContextValues = {
|
||||||
|
settings: Settings;
|
||||||
|
setActiveFeature: (feature: string) => void;
|
||||||
|
toggleFeature: (feature: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppContext = createContext<AppContextValues>(null!);
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {
|
||||||
|
AboutSetting,
|
||||||
|
AutocompleteSetting,
|
||||||
|
BackToTopSetting,
|
||||||
|
HideVotesSetting,
|
||||||
|
JumpToNewCommentSetting,
|
||||||
|
MarkdownToolbarSetting,
|
||||||
|
UserLabelsSetting,
|
||||||
|
} from './components/exports.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The array of features available in TRX.
|
||||||
|
* * The index exists to sort the sidebar listing.
|
||||||
|
* * The key should match the corresponding key from `Settings.features`.
|
||||||
|
* * The value should be the header title for display.
|
||||||
|
* * The component function should return the corresponding settings components.
|
||||||
|
*/
|
||||||
|
export const features = [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'autocomplete',
|
||||||
|
value: 'Autocomplete',
|
||||||
|
component: () => AutocompleteSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'backToTop',
|
||||||
|
value: 'Back To Top',
|
||||||
|
component: () => BackToTopSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'hideVotes',
|
||||||
|
value: 'Hide Votes',
|
||||||
|
component: () => HideVotesSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'jumpToNewComment',
|
||||||
|
value: 'Jump To New Comment',
|
||||||
|
component: () => JumpToNewCommentSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'markdownToolbar',
|
||||||
|
value: 'Markdown Toolbar',
|
||||||
|
component: () => MarkdownToolbarSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
key: 'userLabels',
|
||||||
|
value: 'User Labels',
|
||||||
|
component: () => UserLabelsSetting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
key: 'debug',
|
||||||
|
value: 'About & Info',
|
||||||
|
component: () => AboutSetting,
|
||||||
|
},
|
||||||
|
].sort((a, b) => a.index - b.index);
|
|
@ -5,18 +5,17 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Tildes ReExtended</title>
|
<title>Tildes ReExtended</title>
|
||||||
<link rel="shortcut icon" href="./assets/tildes-reextended-128.png"
|
<link rel="shortcut icon" href="../assets/tildes-reextended-128.png"
|
||||||
type="image/png">
|
type="image/png">
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet" href="../scss/modern-normalize.scss">
|
||||||
href="../node_modules/modern-normalize/modern-normalize.css">
|
<link rel="stylesheet" href="../scss/index.scss">
|
||||||
<link rel="stylesheet" href="./scss/index.scss">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
This web extension does not work without JavaScript, sorry. :(
|
This web extension does not work without JavaScript, sorry. :(
|
||||||
</noscript>
|
</noscript>
|
||||||
<script src="./settings-page.ts"></script>
|
<script type="module" src="./options.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,161 @@
|
||||||
|
import {html} from 'htm/preact';
|
||||||
|
import {Component, render} from 'preact';
|
||||||
|
|
||||||
|
import Settings from '../settings.js';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
createReportTemplate,
|
||||||
|
initializeGlobals,
|
||||||
|
} from '../utilities/exports.js';
|
||||||
|
import {AppContext} from './context.js';
|
||||||
|
import {features} from './features.js';
|
||||||
|
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
initializeGlobals();
|
||||||
|
const settings = await Settings.fromSyncStorage();
|
||||||
|
|
||||||
|
render(
|
||||||
|
html`<${App} manifest=${settings.manifest()} settings=${settings} />`,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
manifest: TRXManifest;
|
||||||
|
settings: Settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
activeFeature: string;
|
||||||
|
enabledFeatures: Set<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
class App extends Component<Props, State> {
|
||||||
|
state: State;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const {settings} = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
activeFeature: settings.data.latestActiveFeatureTab,
|
||||||
|
enabledFeatures: this.getEnabledFeatures(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnabledFeatures = (): Set<string> => {
|
||||||
|
return new Set(
|
||||||
|
Object.entries(this.props.settings.features)
|
||||||
|
.filter(([_, value]) => value)
|
||||||
|
.map(([key, _]) => key),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
setActiveFeature = (feature: string) => {
|
||||||
|
const {settings} = this.props;
|
||||||
|
settings.data.latestActiveFeatureTab = feature;
|
||||||
|
void settings.save();
|
||||||
|
|
||||||
|
this.setState({activeFeature: feature});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleFeature = (feature: string) => {
|
||||||
|
const {settings} = this.props;
|
||||||
|
settings.features[feature] = !settings.features[feature];
|
||||||
|
void settings.save();
|
||||||
|
|
||||||
|
const features = this.getEnabledFeatures();
|
||||||
|
this.setState({enabledFeatures: features});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {manifest, settings} = this.props;
|
||||||
|
const {activeFeature, enabledFeatures} = this.state;
|
||||||
|
|
||||||
|
// Create the version link for the header.
|
||||||
|
const version = manifest.version;
|
||||||
|
const versionURL = encodeURI(
|
||||||
|
`https://gitlab.com/tildes-community/tildes-reextended/-/tags/${version}`,
|
||||||
|
);
|
||||||
|
const versionLink = html`
|
||||||
|
<${Link} class="version" text="v${version}" url="${versionURL}" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create the GitLab report a bug link for the footer.
|
||||||
|
const gitlabTemplate = createReportTemplate('gitlab', version);
|
||||||
|
const gitlabURL = encodeURI(
|
||||||
|
`https://gitlab.com/tildes-community/tildes-reextended/issues/new?issue[description]=${gitlabTemplate}`,
|
||||||
|
);
|
||||||
|
const gitlabLink = html`<${Link} text="GitLab" url="${gitlabURL}" />`;
|
||||||
|
|
||||||
|
// Create the Tildes report a bug link for the footer.
|
||||||
|
const tildesReportTemplate = createReportTemplate('tildes', version);
|
||||||
|
const tildesURL = encodeURI(
|
||||||
|
`https://tildes.net/user/Community/new_message?subject=Tildes ReExtended Bug&message=${tildesReportTemplate}`,
|
||||||
|
);
|
||||||
|
const tildesLink = html`<${Link} text="Tildes" url="${tildesURL}" />`;
|
||||||
|
|
||||||
|
const asideElements = features.map(
|
||||||
|
({key, value}) =>
|
||||||
|
html`
|
||||||
|
<li
|
||||||
|
key=${key}
|
||||||
|
class="${activeFeature === key ? 'active' : ''}
|
||||||
|
${enabledFeatures.has(key) ? 'enabled' : ''}"
|
||||||
|
onClick="${() => {
|
||||||
|
this.setActiveFeature(key);
|
||||||
|
}}"
|
||||||
|
>
|
||||||
|
${value}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mainElements = features.map(
|
||||||
|
({key, value, component}) =>
|
||||||
|
html`
|
||||||
|
<${component()}
|
||||||
|
class="${activeFeature === key ? '' : 'trx-hidden'}"
|
||||||
|
enabled="${enabledFeatures.has(key)}"
|
||||||
|
feature=${key}
|
||||||
|
key=${key}
|
||||||
|
title="${value}"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<${AppContext.Provider}
|
||||||
|
value=${{
|
||||||
|
settings,
|
||||||
|
setActiveFeature: this.setActiveFeature,
|
||||||
|
toggleFeature: this.toggleFeature,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>
|
||||||
|
<img src="../assets/tildes-reextended-128.png" />
|
||||||
|
Tildes ReExtended
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
${versionLink}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<aside class="page-aside">
|
||||||
|
<ul>
|
||||||
|
${asideElements}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<main class="page-main">${mainElements}</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<p>Report a bug via ${gitlabLink} or ${tildesLink}.</p>
|
||||||
|
<p>© Tildes Community and Contributors</p>
|
||||||
|
</footer>
|
||||||
|
<//>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import {offset, Offset} from 'caret-pos';
|
import {offset, Offset} from 'caret-pos';
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {Component} from 'preact';
|
import {Component} from 'preact';
|
||||||
import {log, querySelectorAll, Settings} from '..';
|
|
||||||
|
import Settings from '../settings.js';
|
||||||
|
import {log, querySelectorAll} from '../utilities/exports.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
@ -11,11 +13,11 @@ type State = {
|
||||||
groups: Set<string>;
|
groups: Set<string>;
|
||||||
groupsHidden: boolean;
|
groupsHidden: boolean;
|
||||||
groupsMatches: Set<string>;
|
groupsMatches: Set<string>;
|
||||||
groupsPosition: Offset | null;
|
groupsPosition: Offset | undefined;
|
||||||
usernames: Set<string>;
|
usernames: Set<string>;
|
||||||
usernamesHidden: boolean;
|
usernamesHidden: boolean;
|
||||||
usernamesMatches: Set<string>;
|
usernamesMatches: Set<string>;
|
||||||
usernamesPosition: Offset | null;
|
usernamesPosition: Offset | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AutocompleteFeature extends Component<Props, State> {
|
export class AutocompleteFeature extends Component<Props, State> {
|
||||||
|
@ -24,27 +26,27 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
|
|
||||||
// Get all the groups without their leading tildes.
|
// Get all the groups without their leading tildes.
|
||||||
const groups = props.settings.data.knownGroups.map((value) =>
|
const groups = props.settings.data.knownGroups.map((value) =>
|
||||||
value.startsWith('~') ? value.slice(1) : value
|
value.startsWith('~') ? value.slice(1) : value,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all the usernames on the page without their leading @s, and get
|
// Get all the usernames on the page without their leading @s, and get
|
||||||
// all the username from the saved user labels.
|
// all the username from the saved user labels.
|
||||||
const usernames = [
|
const usernames = [
|
||||||
...querySelectorAll('.link-user').map((value) =>
|
...querySelectorAll('.link-user').map((value) =>
|
||||||
value.textContent!.replace(/^@/, '').toLowerCase()
|
value.textContent!.replace(/^@/, '').toLowerCase(),
|
||||||
),
|
),
|
||||||
...props.settings.data.userLabels.map((value) => value.username)
|
...props.settings.data.userLabels.map((value) => value.username),
|
||||||
].sort((a, b) => a.localeCompare(b));
|
].sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
groups: new Set(groups),
|
groups: new Set(groups),
|
||||||
groupsHidden: true,
|
groupsHidden: true,
|
||||||
groupsMatches: new Set(groups),
|
groupsMatches: new Set(groups),
|
||||||
groupsPosition: null,
|
groupsPosition: undefined,
|
||||||
usernames: new Set(usernames),
|
usernames: new Set(usernames),
|
||||||
usernamesHidden: true,
|
usernamesHidden: true,
|
||||||
usernamesMatches: new Set(usernames),
|
usernamesMatches: new Set(usernames),
|
||||||
usernamesPosition: null
|
usernamesPosition: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a keydown listener for the entire page.
|
// Add a keydown listener for the entire page.
|
||||||
|
@ -52,7 +54,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
|
|
||||||
log(
|
log(
|
||||||
`Autocomplete: Initialized with ${this.state.groups.size} groups and ` +
|
`Autocomplete: Initialized with ${this.state.groups.size} groups and ` +
|
||||||
`${this.state.usernames.size} usernames.`
|
`${this.state.usernames.size} usernames.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +69,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
const createHandler = (
|
const createHandler = (
|
||||||
prefix: string,
|
prefix: string,
|
||||||
target: string,
|
target: string,
|
||||||
values: Set<string>
|
values: Set<string>,
|
||||||
) => {
|
) => {
|
||||||
const dataAttribute = `data-trx-autocomplete-${target}`;
|
const dataAttribute = `data-trx-autocomplete-${target}`;
|
||||||
|
|
||||||
|
@ -89,7 +91,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
event: KeyboardEvent,
|
event: KeyboardEvent,
|
||||||
prefix: string,
|
prefix: string,
|
||||||
target: string,
|
target: string,
|
||||||
values: Set<string>
|
values: Set<string>,
|
||||||
) => {
|
) => {
|
||||||
const textarea = event.target as HTMLTextAreaElement;
|
const textarea = event.target as HTMLTextAreaElement;
|
||||||
const text = textarea.value;
|
const text = textarea.value;
|
||||||
|
@ -120,7 +122,7 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
|
|
||||||
// Find all the values that match the input using `includes`.
|
// Find all the values that match the input using `includes`.
|
||||||
const matches = new Set<string>(
|
const matches = new Set<string>(
|
||||||
[...values].filter((value) => value.includes(input.toLowerCase()))
|
[...values].filter((value) => value.includes(input.toLowerCase())),
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there are no matches, return early.
|
// If there are no matches, return early.
|
||||||
|
@ -138,11 +140,11 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
update = (target: string, matches: Set<string>) => {
|
update = (target: string, matches: Set<string>) => {
|
||||||
if (target === 'groups') {
|
if (target === 'groups') {
|
||||||
this.setState({
|
this.setState({
|
||||||
groupsMatches: matches
|
groupsMatches: matches,
|
||||||
});
|
});
|
||||||
} else if (target === 'usernames') {
|
} else if (target === 'usernames') {
|
||||||
this.setState({
|
this.setState({
|
||||||
usernamesMatches: matches
|
usernamesMatches: matches,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -151,12 +153,12 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
if (target === 'groups') {
|
if (target === 'groups') {
|
||||||
this.setState({
|
this.setState({
|
||||||
groupsHidden: false,
|
groupsHidden: false,
|
||||||
groupsPosition: position
|
groupsPosition: position,
|
||||||
});
|
});
|
||||||
} else if (target === 'usernames') {
|
} else if (target === 'usernames') {
|
||||||
this.setState({
|
this.setState({
|
||||||
usernamesHidden: false,
|
usernamesHidden: false,
|
||||||
usernamesPosition: position
|
usernamesPosition: position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -172,10 +174,10 @@ export class AutocompleteFeature extends Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
// Create the list of groups and usernames.
|
// Create the list of groups and usernames.
|
||||||
const groups = [...this.state.groupsMatches].map(
|
const groups = [...this.state.groupsMatches].map(
|
||||||
(value) => html`<li>~${value}</li>`
|
(value) => html`<li>~${value}</li>`,
|
||||||
);
|
);
|
||||||
const usernames = [...this.state.usernamesMatches].map(
|
const usernames = [...this.state.usernamesMatches].map(
|
||||||
(value) => html`<li>@${value}</li>`
|
(value) => html`<li>@${value}</li>`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the CSS class whether or not to hide the autocomplete.
|
// Create the CSS class whether or not to hide the autocomplete.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import debounce from 'debounce';
|
import debounce from 'debounce';
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {Component} from 'preact';
|
import {Component} from 'preact';
|
||||||
import {log} from '..';
|
|
||||||
|
import {log} from '../utilities/exports.js';
|
||||||
|
|
||||||
type Props = Record<string, unknown>;
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ export class BackToTopFeature extends Component<Props, State> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.state = {
|
this.state = {
|
||||||
hidden: true
|
hidden: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a "debounced" handler to the scroll listener, this will make it so
|
// Add a "debounced" handler to the scroll listener, this will make it so
|
||||||
|
@ -37,12 +38,14 @@ export class BackToTopFeature extends Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const hidden = this.state.hidden ? 'trx-hidden' : '';
|
const hidden = this.state.hidden ? 'trx-hidden' : '';
|
||||||
|
|
||||||
return html`<a
|
return html`
|
||||||
id="trx-back-to-top"
|
<a
|
||||||
class="btn btn-primary ${hidden}"
|
id="trx-back-to-top"
|
||||||
onClick=${this.scrollToTop}
|
class="btn btn-primary ${hidden}"
|
||||||
>
|
onClick=${this.scrollToTop}
|
||||||
Back To Top
|
>
|
||||||
</a>`;
|
Back To Top
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './autocomplete.js';
|
||||||
|
export * from './back-to-top.js';
|
||||||
|
export * from './hide-votes.js';
|
||||||
|
export * from './jump-to-new-comment.js';
|
||||||
|
export * from './markdown-toolbar.js';
|
||||||
|
export * from './user-labels.js';
|
|
@ -1,4 +1,5 @@
|
||||||
import {log, querySelectorAll, Settings} from '..';
|
import Settings from '../settings.js';
|
||||||
|
import {log, querySelectorAll} from '../utilities/exports.js';
|
||||||
|
|
||||||
export function runHideVotesFeature(settings: Settings) {
|
export function runHideVotesFeature(settings: Settings) {
|
||||||
const observer = new window.MutationObserver(() => {
|
const observer = new window.MutationObserver(() => {
|
||||||
|
@ -10,7 +11,7 @@ export function runHideVotesFeature(settings: Settings) {
|
||||||
function startObserver() {
|
function startObserver() {
|
||||||
observer.observe(document, {
|
observer.observe(document, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,14 +25,14 @@ function hideVotes(settings: Settings) {
|
||||||
if (settings.data.hideVotes.comments) {
|
if (settings.data.hideVotes.comments) {
|
||||||
const commentVotes = querySelectorAll(
|
const commentVotes = querySelectorAll(
|
||||||
'.btn-post-action[data-ic-put-to*="/vote"]:not(.trx-votes-hidden)',
|
'.btn-post-action[data-ic-put-to*="/vote"]:not(.trx-votes-hidden)',
|
||||||
'.btn-post-action[data-ic-delete-from*="/vote"]:not(.trx-votes-hidden)'
|
'.btn-post-action[data-ic-delete-from*="/vote"]:not(.trx-votes-hidden)',
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const vote of commentVotes) {
|
for (const vote of commentVotes) {
|
||||||
vote.classList.add('trx-votes-hidden');
|
vote.classList.add('trx-votes-hidden');
|
||||||
vote.textContent = vote.textContent!.slice(
|
vote.textContent = vote.textContent!.slice(
|
||||||
0,
|
0,
|
||||||
vote.textContent!.indexOf(' ')
|
vote.textContent!.indexOf(' '),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export * from './autocomplete';
|
|
||||||
export * from './back-to-top';
|
|
||||||
export * from './hide-votes';
|
|
||||||
export * from './jump-to-new-comment';
|
|
||||||
export * from './markdown-toolbar';
|
|
||||||
export * from './user-labels';
|
|
|
@ -1,13 +1,14 @@
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {Component} from 'preact';
|
import {Component} from 'preact';
|
||||||
import {log, querySelector, querySelectorAll} from '..';
|
|
||||||
|
import {log, querySelector, querySelectorAll} from '../utilities/exports.js';
|
||||||
|
|
||||||
type Props = Record<string, unknown>;
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
newCommentCount: number;
|
newCommentCount: number;
|
||||||
previousComment: HTMLElement | null;
|
previousComment: HTMLElement | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class JumpToNewCommentFeature extends Component<Props, State> {
|
export class JumpToNewCommentFeature extends Component<Props, State> {
|
||||||
|
@ -19,14 +20,14 @@ export class JumpToNewCommentFeature extends Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
newCommentCount,
|
newCommentCount,
|
||||||
previousComment: null
|
previousComment: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (newCommentCount === 0) {
|
if (newCommentCount === 0) {
|
||||||
log('Jump To New Comment: 0 new comments found, not doing anything.');
|
log('Jump To New Comment: 0 new comments found, not doing anything.');
|
||||||
} else {
|
} else {
|
||||||
log(
|
log(
|
||||||
`Jump To New Comment: Initialized for ${newCommentCount} new comments.`
|
`Jump To New Comment: Initialized for ${newCommentCount} new comments.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +38,7 @@ export class JumpToNewCommentFeature extends Component<Props, State> {
|
||||||
this.state.previousComment?.classList.remove('is-comment-new');
|
this.state.previousComment?.classList.remove('is-comment-new');
|
||||||
|
|
||||||
const newestComment = document.querySelector<HTMLElement>(
|
const newestComment = document.querySelector<HTMLElement>(
|
||||||
'.comment.is-comment-new'
|
'.comment.is-comment-new',
|
||||||
);
|
);
|
||||||
|
|
||||||
// If there are no new comments left, hide the button.
|
// If there are no new comments left, hide the button.
|
||||||
|
@ -67,12 +68,14 @@ export class JumpToNewCommentFeature extends Component<Props, State> {
|
||||||
const commentsLeft = querySelectorAll('.comment.is-comment-new').length;
|
const commentsLeft = querySelectorAll('.comment.is-comment-new').length;
|
||||||
const hidden = this.state.hidden ? 'trx-hidden' : '';
|
const hidden = this.state.hidden ? 'trx-hidden' : '';
|
||||||
|
|
||||||
return html`<a
|
return html`
|
||||||
id="trx-jump-to-new-comment"
|
<a
|
||||||
class="btn btn-primary ${hidden}"
|
id="trx-jump-to-new-comment"
|
||||||
onClick="${this.jump}"
|
class="btn btn-primary ${hidden}"
|
||||||
>
|
onClick="${this.jump}"
|
||||||
Jump To New Comment (${commentsLeft}/${this.state.newCommentCount})
|
>
|
||||||
</a>`;
|
Jump To New Comment (${commentsLeft}/${this.state.newCommentCount})
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {render} from 'preact';
|
import {render} from 'preact';
|
||||||
import {log, querySelectorAll, TRXComponent} from '..';
|
|
||||||
|
import {log, querySelectorAll} from '../utilities/exports.js';
|
||||||
|
|
||||||
type MarkdownSnippet = {
|
type MarkdownSnippet = {
|
||||||
dropdown: boolean;
|
dropdown: boolean;
|
||||||
|
@ -13,64 +14,64 @@ const snippets: MarkdownSnippet[] = [
|
||||||
{
|
{
|
||||||
dropdown: false,
|
dropdown: false,
|
||||||
markdown: '[<>]()',
|
markdown: '[<>]()',
|
||||||
name: 'Link'
|
name: 'Link',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: false,
|
dropdown: false,
|
||||||
markdown: '```\n<>\n```',
|
markdown: '```\n<>\n```',
|
||||||
name: 'Code'
|
name: 'Code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: false,
|
dropdown: false,
|
||||||
markdown: '~~<>~~',
|
markdown: '~~<>~~',
|
||||||
name: 'Strikethrough'
|
name: 'Strikethrough',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: false,
|
dropdown: false,
|
||||||
markdown:
|
markdown:
|
||||||
'<details>\n<summary>Click to expand spoiler.</summary>\n\n<>\n</details>',
|
'<details>\n<summary>Click to expand spoiler.</summary>\n\n<>\n</details>',
|
||||||
name: 'Spoilerbox'
|
name: 'Spoilerbox',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '**<>**',
|
markdown: '**<>**',
|
||||||
name: 'Bold'
|
name: 'Bold',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '\n\n---\n\n<>',
|
markdown: '\n\n---\n\n<>',
|
||||||
name: 'Horizontal Divider'
|
name: 'Horizontal Divider',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '`<>`',
|
markdown: '`<>`',
|
||||||
name: 'Inline Code'
|
name: 'Inline Code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '*<>*',
|
markdown: '*<>*',
|
||||||
name: 'Italic'
|
name: 'Italic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '1. <>',
|
markdown: '1. <>',
|
||||||
name: 'Ordered List'
|
name: 'Ordered List',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '<small><></small>',
|
markdown: '<small><></small>',
|
||||||
name: 'Small'
|
name: 'Small',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
markdown: '* <>',
|
markdown: '* <>',
|
||||||
name: 'Unordered List'
|
name: 'Unordered List',
|
||||||
}
|
},
|
||||||
].map(({dropdown, markdown, name}) => ({
|
].map(({dropdown, markdown, name}) => ({
|
||||||
dropdown,
|
dropdown,
|
||||||
name,
|
name,
|
||||||
index: markdown.indexOf('<>'),
|
index: markdown.indexOf('<>'),
|
||||||
markdown: markdown.replace(/<>/, '')
|
markdown: markdown.replace(/<>/, ''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function runMarkdownToolbarFeature() {
|
export function runMarkdownToolbarFeature() {
|
||||||
|
@ -85,7 +86,7 @@ export function runMarkdownToolbarFeature() {
|
||||||
function startObserver() {
|
function startObserver() {
|
||||||
observer.observe(document, {
|
observer.observe(document, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,14 +110,14 @@ function addToolbarsToTextareas() {
|
||||||
|
|
||||||
const menu = form.querySelector<HTMLElement>('.tab-markdown-mode')!;
|
const menu = form.querySelector<HTMLElement>('.tab-markdown-mode')!;
|
||||||
const textarea = form.querySelector<HTMLElement>(
|
const textarea = form.querySelector<HTMLElement>(
|
||||||
'textarea[name="markdown"]'
|
'textarea[name="markdown"]',
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
const snippetButtons = snippets
|
const snippetButtons = snippets
|
||||||
.filter((snippet) => !snippet.dropdown)
|
.filter((snippet) => !snippet.dropdown)
|
||||||
.map(
|
.map(
|
||||||
(snippet) =>
|
(snippet) =>
|
||||||
html`<${snippetButton} snippet=${snippet} textarea=${textarea} />`
|
html`<${snippetButton} snippet=${snippet} textarea=${textarea} />`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the buttons inside the tab menu so they appear
|
// Render the buttons inside the tab menu so they appear
|
||||||
|
@ -132,7 +133,7 @@ function addToolbarsToTextareas() {
|
||||||
render(
|
render(
|
||||||
html`<${snippetDropdown} textarea=${textarea} />`,
|
html`<${snippetDropdown} textarea=${textarea} />`,
|
||||||
menuParent,
|
menuParent,
|
||||||
dropdownPlaceholder
|
dropdownPlaceholder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,37 +149,41 @@ function snippetButton(props: Required<Props>): TRXComponent {
|
||||||
insertSnippet(props);
|
insertSnippet(props);
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`<li class="tab-item">
|
return html`
|
||||||
<button class="btn btn-link" onClick="${click}">
|
<li class="tab-item">
|
||||||
${props.snippet.name}
|
<button class="btn btn-link" onClick="${click}">
|
||||||
</button>
|
${props.snippet.name}
|
||||||
</li>`;
|
</button>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snippetDropdown(props: Props): TRXComponent {
|
function snippetDropdown(props: Props): TRXComponent {
|
||||||
const options = snippets.map(
|
const options = snippets.map(
|
||||||
(snippet) => html`<option value="${snippet.name}">${snippet.name}</option>`
|
(snippet) => html`<option value="${snippet.name}">${snippet.name}</option>`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const change = (event: Event) => {
|
const change = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const snippet = snippets.find(
|
const snippet = snippets.find(
|
||||||
(value) => value.name === (event.target as HTMLSelectElement).value
|
(value) => value.name === (event.target as HTMLSelectElement).value,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
insertSnippet({
|
insertSnippet({
|
||||||
...props,
|
...props,
|
||||||
snippet
|
snippet,
|
||||||
});
|
});
|
||||||
|
|
||||||
(event.target as HTMLSelectElement).selectedIndex = 0;
|
(event.target as HTMLSelectElement).selectedIndex = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`<select class="form-select" onChange=${change}>
|
return html`
|
||||||
<option>More…</option>
|
<select class="form-select" onChange=${change}>
|
||||||
${options}
|
<option>More…</option>
|
||||||
</select>`;
|
${options}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertSnippet(props: Required<Props>) {
|
function insertSnippet(props: Required<Props>) {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import debounce from 'debounce';
|
import debounce from 'debounce';
|
||||||
import {Component, render} from 'preact';
|
import {Component, render} from 'preact';
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
|
|
||||||
|
import Settings from '../settings.js';
|
||||||
import {
|
import {
|
||||||
createElementFromString,
|
createElementFromString,
|
||||||
isColorBright,
|
isColorBright,
|
||||||
isValidHexColor,
|
isValidHexColor,
|
||||||
log,
|
log,
|
||||||
querySelectorAll,
|
querySelectorAll,
|
||||||
setSettings,
|
themeColors,
|
||||||
Settings,
|
} from '../utilities/exports.js';
|
||||||
themeColors
|
|
||||||
} from '..';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
@ -20,9 +20,9 @@ type State = {
|
||||||
color: string;
|
color: string;
|
||||||
selectedColor: string;
|
selectedColor: string;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
id: number | null;
|
id: number | undefined;
|
||||||
priority: number;
|
priority: number;
|
||||||
target: HTMLElement | null;
|
target: HTMLElement | undefined;
|
||||||
text: string;
|
text: string;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@ const colorPattern: string = [
|
||||||
'[a-f\\d]{6}|',
|
'[a-f\\d]{6}|',
|
||||||
'[a-f\\d]{4}|',
|
'[a-f\\d]{4}|',
|
||||||
'[a-f\\d]{3})',
|
'[a-f\\d]{3})',
|
||||||
'|transparent)$' // "Transparent" is also allowed in the input.
|
'|transparent)$', // "Transparent" is also allowed in the input.
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
export class UserLabelsFeature extends Component<Props, State> {
|
export class UserLabelsFeature extends Component<Props, State> {
|
||||||
|
@ -48,12 +48,12 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
color: selectedColor,
|
color: selectedColor,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
id: null,
|
id: undefined,
|
||||||
text: '',
|
text: '',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
selectedColor,
|
selectedColor,
|
||||||
target: null,
|
target: undefined,
|
||||||
username: ''
|
username: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const count = this.addLabelsToUsernames(querySelectorAll('.link-user'));
|
const count = this.addLabelsToUsernames(querySelectorAll('.link-user'));
|
||||||
|
@ -94,7 +94,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
const userLabels = sortedLabels.filter(
|
const userLabels = sortedLabels.filter(
|
||||||
(value) =>
|
(value) =>
|
||||||
value.username === username &&
|
value.username === username &&
|
||||||
(onlyID === undefined ? true : value.id === onlyID)
|
(onlyID === undefined ? true : value.id === onlyID),
|
||||||
);
|
);
|
||||||
|
|
||||||
const addLabel = html`
|
const addLabel = html`
|
||||||
|
@ -174,10 +174,10 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
target,
|
target,
|
||||||
username,
|
username,
|
||||||
color: selectedColor,
|
color: selectedColor,
|
||||||
id: null,
|
id: undefined,
|
||||||
text: '',
|
text: '',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
selectedColor
|
selectedColor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -190,12 +190,12 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
const label = this.props.settings.data.userLabels.find(
|
const label = this.props.settings.data.userLabels.find(
|
||||||
(value) => value.id === id
|
(value) => value.id === id,
|
||||||
);
|
);
|
||||||
if (label === undefined) {
|
if (label === undefined) {
|
||||||
log(
|
log(
|
||||||
'User Labels: Tried to edit label with ID that could not be found.',
|
'User Labels: Tried to edit label with ID that could not be found.',
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
this.setState({
|
this.setState({
|
||||||
hidden: false,
|
hidden: false,
|
||||||
target,
|
target,
|
||||||
...label
|
...label,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -233,11 +233,11 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
|
|
||||||
priorityChange = (event: Event) => {
|
priorityChange = (event: Event) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
priority: Number((event.target as HTMLInputElement).value)
|
priority: Number((event.target as HTMLInputElement).value),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
save = (event: MouseEvent) => {
|
save = async (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const {color, id, text, priority, username} = this.state;
|
const {color, id, text, priority, username} = this.state;
|
||||||
if (color === '' || username === '') {
|
if (color === '' || username === '') {
|
||||||
|
@ -247,7 +247,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
|
|
||||||
const {settings} = this.props;
|
const {settings} = this.props;
|
||||||
// If no ID is present then save a new label otherwise edit the existing one.
|
// If no ID is present then save a new label otherwise edit the existing one.
|
||||||
if (id === null) {
|
if (id === undefined) {
|
||||||
let newID = 1;
|
let newID = 1;
|
||||||
if (settings.data.userLabels.length > 0) {
|
if (settings.data.userLabels.length > 0) {
|
||||||
newID = settings.data.userLabels.sort((a, b) => b.id - a.id)[0].id + 1;
|
newID = settings.data.userLabels.sort((a, b) => b.id - a.id)[0].id + 1;
|
||||||
|
@ -258,13 +258,13 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
id: newID,
|
id: newID,
|
||||||
priority,
|
priority,
|
||||||
text,
|
text,
|
||||||
username
|
username,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addLabelsToUsernames(querySelectorAll('.link-user'), newID);
|
this.addLabelsToUsernames(querySelectorAll('.link-user'), newID);
|
||||||
} else {
|
} else {
|
||||||
const index = settings.data.userLabels.findIndex(
|
const index = settings.data.userLabels.findIndex(
|
||||||
(value) => value.id === id
|
(value) => value.id === id,
|
||||||
);
|
);
|
||||||
settings.data.userLabels.splice(index, 1);
|
settings.data.userLabels.splice(index, 1);
|
||||||
settings.data.userLabels.push({
|
settings.data.userLabels.push({
|
||||||
|
@ -272,7 +272,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
color,
|
color,
|
||||||
priority,
|
priority,
|
||||||
text,
|
text,
|
||||||
username
|
username,
|
||||||
});
|
});
|
||||||
|
|
||||||
const elements = querySelectorAll(`[data-trx-label-id="${id}"]`);
|
const elements = querySelectorAll(`[data-trx-label-id="${id}"]`);
|
||||||
|
@ -288,27 +288,27 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSettings(settings);
|
await settings.save();
|
||||||
this.props.settings = settings;
|
this.props.settings = settings;
|
||||||
this.hide();
|
this.hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
remove = (event: MouseEvent) => {
|
remove = async (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const {id} = this.state;
|
const {id} = this.state;
|
||||||
if (id === null) {
|
if (id === undefined) {
|
||||||
log('User Labels: Tried remove label when ID was null.');
|
log('User Labels: Tried remove label when ID was undefined.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {settings} = this.props;
|
const {settings} = this.props;
|
||||||
const index = settings.data.userLabels.findIndex(
|
const index = settings.data.userLabels.findIndex(
|
||||||
(value) => value.id === id
|
(value) => value.id === id,
|
||||||
);
|
);
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
log(
|
log(
|
||||||
`User Labels: Tried to remove label with ID ${id} that could not be found.`,
|
`User Labels: Tried to remove label with ID ${id} that could not be found.`,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -318,7 +318,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.data.userLabels.splice(index, 1);
|
settings.data.userLabels.splice(index, 1);
|
||||||
void setSettings(settings);
|
await settings.save();
|
||||||
this.props.settings = settings;
|
this.props.settings = settings;
|
||||||
this.hide();
|
this.hide();
|
||||||
};
|
};
|
||||||
|
@ -331,7 +331,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
<option value="${bodyStyle.getPropertyValue(value).trim()}">
|
<option value="${bodyStyle.getPropertyValue(value).trim()}">
|
||||||
${name}
|
${name}
|
||||||
</option>
|
</option>
|
||||||
`
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const bright = isColorBright(this.state.color) ? 'trx-bright' : '';
|
const bright = isColorBright(this.state.color) ? 'trx-bright' : '';
|
||||||
|
@ -342,7 +342,7 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
let left = 0;
|
let left = 0;
|
||||||
|
|
||||||
const target = this.state.target;
|
const target = this.state.target;
|
||||||
if (target !== null) {
|
if (target !== undefined) {
|
||||||
const bounds = target.getBoundingClientRect();
|
const bounds = target.getBoundingClientRect();
|
||||||
top = bounds.y + bounds.height + 4 + window.scrollY;
|
top = bounds.y + bounds.height + 4 + window.scrollY;
|
||||||
left = bounds.x + window.scrollX;
|
left = bounds.x + window.scrollX;
|
||||||
|
@ -351,80 +351,82 @@ export class UserLabelsFeature extends Component<Props, State> {
|
||||||
const position = `left: ${left}px; top: ${top}px;`;
|
const position = `left: ${left}px; top: ${top}px;`;
|
||||||
const previewStyle = `background-color: ${color}`;
|
const previewStyle = `background-color: ${color}`;
|
||||||
|
|
||||||
return html`<form class="trx-user-label-form ${hidden}" style="${position}">
|
return html`
|
||||||
<div class="trx-label-username-priority">
|
<form class="trx-user-label-form ${hidden}" style="${position}">
|
||||||
<label class="trx-label-username">
|
<div class="trx-label-username-priority">
|
||||||
Add New Label
|
<label class="trx-label-username">
|
||||||
<input
|
Add New Label
|
||||||
type="text"
|
<input
|
||||||
class="form-input"
|
type="text"
|
||||||
placeholder="Username"
|
class="form-input"
|
||||||
value="${username}"
|
placeholder="Username"
|
||||||
required
|
value="${username}"
|
||||||
/>
|
required
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="trx-label-priority">
|
<label class="trx-label-priority">
|
||||||
Priority
|
Priority
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
value="${priority}"
|
value="${priority}"
|
||||||
onChange=${this.priorityChange}
|
onChange=${this.priorityChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="trx-label-color-input">Pick A Color</label>
|
|
||||||
|
|
||||||
<div class="trx-label-grid">
|
|
||||||
<input
|
|
||||||
id="trx-label-color-input"
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
placeholder="Color"
|
|
||||||
value="${color}"
|
|
||||||
onInput=${debounce(this.colorChange, 250)}
|
|
||||||
pattern="${colorPattern}"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
value="${selectedColor}"
|
|
||||||
onChange="${this.colorChange}"
|
|
||||||
>
|
|
||||||
${themeSelectOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="trx-label-input">Label</label>
|
<label for="trx-label-color-input">Pick A Color</label>
|
||||||
|
|
||||||
<div class="trx-label-grid">
|
<div class="trx-label-grid">
|
||||||
<input
|
<input
|
||||||
id="trx-label-input"
|
id="trx-label-color-input"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder="Text"
|
placeholder="Color"
|
||||||
value="${label}"
|
value="${color}"
|
||||||
onInput=${debounce(this.labelChange, 250)}
|
onInput=${debounce(this.colorChange, 250)}
|
||||||
/>
|
pattern="${colorPattern}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="trx-label-preview ${bright}" style="${previewStyle}">
|
<select
|
||||||
<p>${label}</p>
|
class="form-select"
|
||||||
|
value="${selectedColor}"
|
||||||
|
onChange="${this.colorChange}"
|
||||||
|
>
|
||||||
|
${themeSelectOptions}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="trx-label-actions">
|
<div>
|
||||||
<a class="btn-post-action" onClick=${this.save}>Save</a>
|
<label for="trx-label-input">Label</label>
|
||||||
<a class="btn-post-action" onClick=${this.hide}>Close</a>
|
|
||||||
<a class="btn-post-action" onClick=${this.remove}>Remove</a>
|
<div class="trx-label-grid">
|
||||||
</div>
|
<input
|
||||||
</form>`;
|
id="trx-label-input"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Text"
|
||||||
|
value="${label}"
|
||||||
|
onInput=${debounce(this.labelChange, 250)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="trx-label-preview ${bright}" style="${previewStyle}">
|
||||||
|
<p>${label}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trx-label-actions">
|
||||||
|
<a class="btn-post-action" onClick=${this.save}>Save</a>
|
||||||
|
<a class="btn-post-action" onClick=${this.hide}>Close</a>
|
||||||
|
<a class="btn-post-action" onClick=${this.remove}>Remove</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
$accents: (
|
$accents: (
|
||||||
'red' #dc322f,
|
'red' #dc322f,
|
||||||
|
@ -10,14 +12,16 @@ body {
|
||||||
'magenta' #d33682,
|
'magenta' #d33682,
|
||||||
);
|
);
|
||||||
|
|
||||||
--background-primary: #{adjust-color(#002b36, $lightness: -5%)};
|
--background-primary: #{color.adjust(#002b36, $lightness: -5%)};
|
||||||
--background-secondary: #002b36;
|
--background-secondary: #002b36;
|
||||||
--background-tertiary: #000;
|
--background-tertiary: #000;
|
||||||
--foreground: #fdf6e3;
|
--foreground: #fdf6e3;
|
||||||
|
|
||||||
@each $name, $color in $accents {
|
@each $name, $color in $accents {
|
||||||
|
/* stylelint-disable custom-property-pattern */
|
||||||
--#{$name}: #{$color};
|
--#{$name}: #{$color};
|
||||||
--light-#{$name}: #{adjust-color($color, $lightness: 10%)};
|
--light-#{$name}: #{color.adjust($color, $lightness: 10%)};
|
||||||
--dark-#{$name}: #{adjust-color($color, $lightness: -10%)};
|
--dark-#{$name}: #{color.adjust($color, $lightness: -10%)};
|
||||||
|
/* stylelint-enable */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
.button {
|
.button {
|
||||||
--button-color: var(--blue);
|
--button-color: var(--blue);
|
||||||
--button-color-alt: var(--dark-blue);
|
--button-color-alt: var(--dark-blue);
|
||||||
|
|
||||||
background-color: var(--button-color);
|
background-color: var(--button-color);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|
|
@ -95,7 +95,7 @@ details {
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: $large-breakpoint / 4 auto;
|
grid-template-columns: calc($large-breakpoint / 4) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-aside {
|
.page-aside {
|
||||||
|
@ -142,5 +142,6 @@ details {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable no-invalid-position-at-import-rule */
|
||||||
@import 'shared';
|
@import 'shared';
|
||||||
@import 'settings';
|
@import 'settings';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
@import '../../node_modules/modern-normalize/modern-normalize.css';
|
|
@ -1,7 +1,9 @@
|
||||||
|
// Scripts
|
||||||
@import 'scripts/autocomplete';
|
@import 'scripts/autocomplete';
|
||||||
@import 'scripts/back-to-top';
|
@import 'scripts/back-to-top';
|
||||||
@import 'scripts/jump-to-new-comment';
|
@import 'scripts/jump-to-new-comment';
|
||||||
@import 'scripts/markdown-toolbar';
|
@import 'scripts/markdown-toolbar';
|
||||||
@import 'scripts/user-labels';
|
@import 'scripts/user-labels';
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
@import 'shared';
|
@import 'shared';
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {render} from 'preact';
|
|
||||||
import {useState} from 'preact/hooks';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AppContext,
|
|
||||||
createReportTemplate,
|
|
||||||
features,
|
|
||||||
getManifest,
|
|
||||||
getSettings,
|
|
||||||
initialize,
|
|
||||||
Link,
|
|
||||||
setSettings,
|
|
||||||
Settings,
|
|
||||||
TRXManifest
|
|
||||||
} from '.';
|
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
|
||||||
initialize();
|
|
||||||
|
|
||||||
render(
|
|
||||||
html`<${App} manifest=${getManifest()} settings=${await getSettings()} />`,
|
|
||||||
document.body
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
manifest: TRXManifest;
|
|
||||||
settings: Settings;
|
|
||||||
};
|
|
||||||
|
|
||||||
function App(props: Props) {
|
|
||||||
const {manifest, settings} = props;
|
|
||||||
|
|
||||||
// Create some state to set the active feature tab.
|
|
||||||
const [activeFeature, _setActiveFeature] = useState(
|
|
||||||
settings.data.latestActiveFeatureTab
|
|
||||||
);
|
|
||||||
function setActiveFeature(feature: string) {
|
|
||||||
// Update the state and save the settings.
|
|
||||||
_setActiveFeature(feature);
|
|
||||||
settings.data.latestActiveFeatureTab = feature;
|
|
||||||
void setSettings(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create some state to set the enabled features.
|
|
||||||
const [enabledFeatures, _setFeature] = useState(
|
|
||||||
new Set(
|
|
||||||
Object.entries(settings.features)
|
|
||||||
.filter(([_, value]) => value)
|
|
||||||
.map(([key, _]) => key)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
function toggleFeature(feature: string) {
|
|
||||||
settings.features[feature] = !settings.features[feature];
|
|
||||||
const features = new Set(
|
|
||||||
Object.entries(settings.features)
|
|
||||||
.filter(([_, value]) => value)
|
|
||||||
.map(([key, _]) => key)
|
|
||||||
);
|
|
||||||
_setFeature(features);
|
|
||||||
void setSettings(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the version link for the header.
|
|
||||||
const version = manifest.version;
|
|
||||||
const versionURL = encodeURI(
|
|
||||||
`https://gitlab.com/tildes-community/tildes-reextended/-/tags/${version}`
|
|
||||||
);
|
|
||||||
const versionLink = html`<${Link}
|
|
||||||
class="version"
|
|
||||||
text="v${version}"
|
|
||||||
url="${versionURL}"
|
|
||||||
/>`;
|
|
||||||
|
|
||||||
// Create the GitLab report a bug link for the footer.
|
|
||||||
const gitlabTemplate = createReportTemplate('gitlab', version);
|
|
||||||
const gitlabURL = encodeURI(
|
|
||||||
`https://gitlab.com/tildes-community/tildes-reextended/issues/new?issue[description]=${gitlabTemplate}`
|
|
||||||
);
|
|
||||||
const gitlabLink = html`<${Link} text="GitLab" url="${gitlabURL}" />`;
|
|
||||||
|
|
||||||
// Create the Tildes report a bug link for the footer.
|
|
||||||
const tildesReportTemplate = createReportTemplate('tildes', version);
|
|
||||||
const tildesURL = encodeURI(
|
|
||||||
`https://tildes.net/user/Community/new_message?subject=Tildes ReExtended Bug&message=${tildesReportTemplate}`
|
|
||||||
);
|
|
||||||
const tildesLink = html`<${Link} text="Tildes" url="${tildesURL}" />`;
|
|
||||||
|
|
||||||
const asideElements = features.map(
|
|
||||||
({key, value}) =>
|
|
||||||
html`<li
|
|
||||||
key=${key}
|
|
||||||
class="${activeFeature === key ? 'active' : ''}
|
|
||||||
${enabledFeatures.has(key) ? 'enabled' : ''}"
|
|
||||||
onClick="${() => {
|
|
||||||
setActiveFeature(key);
|
|
||||||
}}"
|
|
||||||
>
|
|
||||||
${value}
|
|
||||||
</li>`
|
|
||||||
);
|
|
||||||
|
|
||||||
const mainElements = features.map(
|
|
||||||
({key, value, component}) =>
|
|
||||||
html`<${component()}
|
|
||||||
class="${activeFeature === key ? '' : 'trx-hidden'}"
|
|
||||||
enabled="${enabledFeatures.has(key)}"
|
|
||||||
feature=${key}
|
|
||||||
key=${key}
|
|
||||||
title="${value}"
|
|
||||||
/>`
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<${AppContext.Provider} value=${{
|
|
||||||
settings,
|
|
||||||
setActiveFeature,
|
|
||||||
toggleFeature
|
|
||||||
}}>
|
|
||||||
<header class="page-header">
|
|
||||||
<h1>
|
|
||||||
<img src="./assets/tildes-reextended-128.png" />
|
|
||||||
Tildes ReExtended
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
${versionLink}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<aside class="page-aside"><ul>${asideElements}</ul></aside>
|
|
||||||
<main class="page-main">${mainElements}</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="page-footer">
|
|
||||||
<p>Report a bug via ${gitlabLink} or ${tildesLink}.</p>
|
|
||||||
<p>© Tildes Community and Contributors</p>
|
|
||||||
</footer>
|
|
||||||
</${AppContext.Provider}>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not export anything from this file, otherwise if a content script
|
|
||||||
// somehow imports anything that is also connected to this file, it will try
|
|
||||||
// to run on Tildes as well. This file is solely for the extension options page!
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
import {log} from './utilities/exports.js';
|
||||||
|
|
||||||
|
export default class Settings {
|
||||||
|
public static async fromSyncStorage(): Promise<Settings> {
|
||||||
|
const settings = new Settings();
|
||||||
|
const defaultsObject = {
|
||||||
|
data: settings.data,
|
||||||
|
features: settings.features,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sync = (await browser.storage.sync.get(
|
||||||
|
defaultsObject,
|
||||||
|
)) as typeof defaultsObject;
|
||||||
|
settings.data = sync.data;
|
||||||
|
settings.features = sync.features;
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static manifest(): TRXManifest {
|
||||||
|
return browser.runtime.getManifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async nuke(event?: MouseEvent): Promise<void> {
|
||||||
|
if (event !== undefined) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
window.confirm(
|
||||||
|
'Are you sure you want to delete your data? There is no way to recover it once it has been deleted.',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await browser.storage.sync.clear();
|
||||||
|
log(
|
||||||
|
'Data removed, reloading this page to reinitialize default settings.',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public data: {
|
||||||
|
hideVotes: {
|
||||||
|
[index: string]: boolean;
|
||||||
|
comments: boolean;
|
||||||
|
topics: boolean;
|
||||||
|
ownComments: boolean;
|
||||||
|
ownTopics: boolean;
|
||||||
|
};
|
||||||
|
knownGroups: string[];
|
||||||
|
latestActiveFeatureTab: string;
|
||||||
|
userLabels: UserLabel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
public features: {
|
||||||
|
[index: string]: boolean;
|
||||||
|
autocomplete: boolean;
|
||||||
|
backToTop: boolean;
|
||||||
|
debug: boolean;
|
||||||
|
hideVotes: boolean;
|
||||||
|
jumpToNewComment: boolean;
|
||||||
|
markdownToolbar: boolean;
|
||||||
|
userLabels: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.data = {
|
||||||
|
hideVotes: {
|
||||||
|
comments: true,
|
||||||
|
topics: true,
|
||||||
|
ownComments: true,
|
||||||
|
ownTopics: true,
|
||||||
|
},
|
||||||
|
// If groups are added or removed from Tildes this does not necessarily need
|
||||||
|
// to be updated. There is a helper function available to update it whenever
|
||||||
|
// the user goes to "/groups", where all the groups are easily available.
|
||||||
|
// Features that use this data should be added to the `usesKnownGroups`
|
||||||
|
// array that is near the top of `content-scripts.ts`.
|
||||||
|
knownGroups: [
|
||||||
|
'~anime',
|
||||||
|
'~arts',
|
||||||
|
'~books',
|
||||||
|
'~comp',
|
||||||
|
'~creative',
|
||||||
|
'~design',
|
||||||
|
'~enviro',
|
||||||
|
'~finance',
|
||||||
|
'~food',
|
||||||
|
'~games',
|
||||||
|
'~games.game_design',
|
||||||
|
'~games.tabletop',
|
||||||
|
'~health',
|
||||||
|
'~health.coronavirus',
|
||||||
|
'~hobbies',
|
||||||
|
'~humanities',
|
||||||
|
'~lgbt',
|
||||||
|
'~life',
|
||||||
|
'~misc',
|
||||||
|
'~movies',
|
||||||
|
'~music',
|
||||||
|
'~news',
|
||||||
|
'~science',
|
||||||
|
'~space',
|
||||||
|
'~sports',
|
||||||
|
'~talk',
|
||||||
|
'~tech',
|
||||||
|
'~test',
|
||||||
|
'~tildes',
|
||||||
|
'~tildes.official',
|
||||||
|
'~tv',
|
||||||
|
],
|
||||||
|
latestActiveFeatureTab: 'debug',
|
||||||
|
userLabels: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.features = {
|
||||||
|
autocomplete: true,
|
||||||
|
backToTop: true,
|
||||||
|
debug: false,
|
||||||
|
hideVotes: false,
|
||||||
|
jumpToNewComment: true,
|
||||||
|
markdownToolbar: true,
|
||||||
|
userLabels: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public manifest(): TRXManifest {
|
||||||
|
return Settings.manifest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async nuke(event?: MouseEvent): Promise<void> {
|
||||||
|
await Settings.nuke(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
await browser.storage.sync.set(this);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,112 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {
|
|
||||||
getSettings,
|
|
||||||
exportSettings,
|
|
||||||
importFileHandler,
|
|
||||||
Link,
|
|
||||||
log,
|
|
||||||
removeAllData,
|
|
||||||
Setting,
|
|
||||||
SettingProps,
|
|
||||||
TRXComponent
|
|
||||||
} from '../..';
|
|
||||||
|
|
||||||
async function logSettings() {
|
|
||||||
log(await getSettings(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AboutSetting(props: SettingProps): TRXComponent {
|
|
||||||
const importSettings = () => {
|
|
||||||
document.querySelector<HTMLElement>('#import-settings')!.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const communityLink = html`
|
|
||||||
<${Link}
|
|
||||||
url="https://gitlab.com/tildes-community"
|
|
||||||
text="Tildes Community project"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const criusLink = html`
|
|
||||||
<${Link} url="https://tildes.net/user/crius" text="Crius" />
|
|
||||||
`;
|
|
||||||
|
|
||||||
const gitlabIssuesLink = html`
|
|
||||||
<${Link}
|
|
||||||
url="https://gitlab.com/tildes-community/tildes-reextended/-/issues"
|
|
||||||
text="GitLab issue tracker"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const gitlabLicenseLink = html`
|
|
||||||
<${Link}
|
|
||||||
url="https://gitlab.com/tildes-community/tildes-reextended/blob/main/LICENSE"
|
|
||||||
text="MIT License"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const messageCommunityLink = html`
|
|
||||||
<${Link}
|
|
||||||
url="https://tildes.net/user/Community/new_message"
|
|
||||||
text="message Community"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const tildesExtendedLink = html`
|
|
||||||
<${Link}
|
|
||||||
url="https://github.com/theCrius/tildes-extended"
|
|
||||||
text="Tildes Extended"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
This feature will make debugging logs output to the console when enabled.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Tildes ReExtended is a from-scratch recreation of the original${' '}
|
|
||||||
${tildesExtendedLink} web extension by ${criusLink}. Open-sourced${' '}
|
|
||||||
with the ${gitlabLicenseLink} and maintained as a ${communityLink}.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
To report bugs or request new features use the links at the bottom of this
|
|
||||||
page, check out the ${gitlabIssuesLink} or ${messageCommunityLink}${' '}
|
|
||||||
on Tildes.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="divider" />
|
|
||||||
|
|
||||||
<div class="import-export">
|
|
||||||
<p>
|
|
||||||
Note that importing settings will delete and overwrite your existing
|
|
||||||
ones.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="import-settings"
|
|
||||||
onChange=${importFileHandler}
|
|
||||||
class="trx-hidden"
|
|
||||||
accept="application/json"
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
<button onClick=${importSettings} class="button">Import Settings</button>
|
|
||||||
<button onClick=${exportSettings} class="button">Export Settings</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider" />
|
|
||||||
|
|
||||||
<details class="misc-utilities">
|
|
||||||
<summary>Danger Zone</summary>
|
|
||||||
|
|
||||||
<div class="inner">
|
|
||||||
<button onClick=${logSettings} class="button">Log Settings</button>
|
|
||||||
|
|
||||||
<button onClick=${removeAllData} class="button destructive">
|
|
||||||
Remove All Data
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {Setting, SettingProps, TRXComponent} from '../..';
|
|
||||||
|
|
||||||
export function AutocompleteSetting(props: SettingProps): TRXComponent {
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
Adds autocompletion in textareas for user mentions (starting with${' '}
|
|
||||||
<code>@</code>) and groups (starting with <code>~</code>).
|
|
||||||
</p>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {Setting, SettingProps, TRXComponent} from '../..';
|
|
||||||
|
|
||||||
export function BackToTopSetting(props: SettingProps): TRXComponent {
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
Adds a hovering button to the bottom-right of all pages once you've
|
|
||||||
scrolled down far enough that, when clicked, will scroll you back to the
|
|
||||||
top of the page.
|
|
||||||
</p>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {Setting, SettingProps, TRXComponent} from '../..';
|
|
||||||
|
|
||||||
export function JumpToNewCommentSetting(props: SettingProps): TRXComponent {
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
Adds a hovering button to the bottom-right of pages with new comments
|
|
||||||
that, when clicked, will scroll you to the next new comment.
|
|
||||||
</p>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {Link, Setting, SettingProps, TRXComponent} from '../..';
|
|
||||||
|
|
||||||
export function MarkdownToolbarSetting(props: SettingProps): TRXComponent {
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
Adds a toolbar with a selection of Markdown snippets that when used will
|
|
||||||
insert the according Markdown where your cursor is. Particularly useful
|
|
||||||
for the${' '}
|
|
||||||
<${Link}
|
|
||||||
url="https://docs.tildes.net/instructions/text-formatting#expandable-sections"
|
|
||||||
text="expandable section"
|
|
||||||
/>
|
|
||||||
/spoilerbox syntax. If you have text selected, the Markdown will be
|
|
||||||
inserted around your text.
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
A full list of the snippets is available${' '}
|
|
||||||
<${Link}
|
|
||||||
url="https://gitlab.com/tildes-community/tildes-reextended/-/issues/12"
|
|
||||||
text="on GitLab"
|
|
||||||
/>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import {html} from 'htm/preact';
|
|
||||||
import {Setting, SettingProps, TRXComponent} from '../..';
|
|
||||||
|
|
||||||
export function UserLabelsSetting(props: SettingProps): TRXComponent {
|
|
||||||
return html`<${Setting} ...${props}>
|
|
||||||
<p class="info">
|
|
||||||
Adds a way to create customizable labels to users. Wherever a link to a
|
|
||||||
person's profile is available, a <code>[+]</code> will be put next to it.
|
|
||||||
Clicking on that will bring up a dialog to add a new label and clicking on
|
|
||||||
existing labels will bring up the same dialog to edit them.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>View Customizable Values</summary>
|
|
||||||
<ul class="user-label-values">
|
|
||||||
<li><b>Username</b>: who to apply the label 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 values are hex colors or <code>transparent</code>.
|
|
||||||
<br />
|
|
||||||
Colors based on your current Tildes theme are also available in the
|
|
||||||
dropdown menu.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b>Text</b>: the text to go in the label. If left empty the label will
|
|
||||||
show as a 12 by 12 pixel square instead.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
import {
|
|
||||||
AboutSetting,
|
|
||||||
AutocompleteSetting,
|
|
||||||
BackToTopSetting,
|
|
||||||
HideVotesSetting,
|
|
||||||
JumpToNewCommentSetting,
|
|
||||||
MarkdownToolbarSetting,
|
|
||||||
Settings,
|
|
||||||
UserLabelsSetting
|
|
||||||
} from '..';
|
|
||||||
|
|
||||||
export const defaultSettings: Settings = {
|
|
||||||
data: {
|
|
||||||
hideVotes: {
|
|
||||||
comments: true,
|
|
||||||
topics: true,
|
|
||||||
ownComments: true,
|
|
||||||
ownTopics: true
|
|
||||||
},
|
|
||||||
// If groups are added or removed from Tildes this does not necessarily need
|
|
||||||
// to be updated. There is a helper function available to update it whenever
|
|
||||||
// the user goes to "/groups", where all the groups are easily available.
|
|
||||||
// Features that use this data should be added to the `usesKnownGroups`
|
|
||||||
// array that is near the top of `content-scripts.ts`.
|
|
||||||
knownGroups: [
|
|
||||||
'~anime',
|
|
||||||
'~arts',
|
|
||||||
'~books',
|
|
||||||
'~comp',
|
|
||||||
'~creative',
|
|
||||||
'~design',
|
|
||||||
'~enviro',
|
|
||||||
'~finance',
|
|
||||||
'~food',
|
|
||||||
'~games',
|
|
||||||
'~games.game_design',
|
|
||||||
'~games.tabletop',
|
|
||||||
'~health',
|
|
||||||
'~health.coronavirus',
|
|
||||||
'~hobbies',
|
|
||||||
'~humanities',
|
|
||||||
'~lgbt',
|
|
||||||
'~life',
|
|
||||||
'~misc',
|
|
||||||
'~movies',
|
|
||||||
'~music',
|
|
||||||
'~news',
|
|
||||||
'~science',
|
|
||||||
'~space',
|
|
||||||
'~sports',
|
|
||||||
'~talk',
|
|
||||||
'~tech',
|
|
||||||
'~test',
|
|
||||||
'~tildes',
|
|
||||||
'~tildes.official',
|
|
||||||
'~tv'
|
|
||||||
],
|
|
||||||
latestActiveFeatureTab: 'debug',
|
|
||||||
userLabels: [],
|
|
||||||
version: '0.0.0'
|
|
||||||
},
|
|
||||||
features: {
|
|
||||||
autocomplete: true,
|
|
||||||
backToTop: true,
|
|
||||||
debug: false,
|
|
||||||
hideVotes: false,
|
|
||||||
jumpToNewComment: true,
|
|
||||||
markdownToolbar: true,
|
|
||||||
userLabels: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultActiveFeature = defaultSettings.data.latestActiveFeatureTab;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The array of features available in TRX.
|
|
||||||
* * The index exists to sort the sidebar listing.
|
|
||||||
* * The key should match the corresponding key from `Settings.features`.
|
|
||||||
* * The value should be the header title for display.
|
|
||||||
* * The component function should return the corresponding settings components.
|
|
||||||
*/
|
|
||||||
export const features = [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'autocomplete',
|
|
||||||
value: 'Autocomplete',
|
|
||||||
component: () => AutocompleteSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'backToTop',
|
|
||||||
value: 'Back To Top',
|
|
||||||
component: () => BackToTopSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'hideVotes',
|
|
||||||
value: 'Hide Votes',
|
|
||||||
component: () => HideVotesSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'jumpToNewComment',
|
|
||||||
value: 'Jump To New Comment',
|
|
||||||
component: () => JumpToNewCommentSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'markdownToolbar',
|
|
||||||
value: 'Markdown Toolbar',
|
|
||||||
component: () => MarkdownToolbarSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
key: 'userLabels',
|
|
||||||
value: 'User Labels',
|
|
||||||
component: () => UserLabelsSetting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 1,
|
|
||||||
key: 'debug',
|
|
||||||
value: 'About & Info',
|
|
||||||
component: () => AboutSetting
|
|
||||||
}
|
|
||||||
].sort((a, b) => a.index - b.index);
|
|
|
@ -1,30 +0,0 @@
|
||||||
import {browser} from 'webextension-polyfill-ts';
|
|
||||||
import {getSettings, log} from '..';
|
|
||||||
|
|
||||||
export async function exportSettings(event: MouseEvent): Promise<void> {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const settings = await getSettings();
|
|
||||||
const settingsBlob = new window.Blob([JSON.stringify(settings, null, 2)], {
|
|
||||||
type: 'text/json'
|
|
||||||
});
|
|
||||||
|
|
||||||
const objectURL = URL.createObjectURL(settingsBlob);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await browser.downloads.download({
|
|
||||||
filename: 'tildes-reextended-settings.json',
|
|
||||||
url: objectURL,
|
|
||||||
saveAs: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
log(error);
|
|
||||||
} finally {
|
|
||||||
// According to MDN, when creating an object URL we should also revoke it
|
|
||||||
// when "it's safe to do so" to prevent excessive memory/storage use.
|
|
||||||
// 60 seconds should probably be enough time to download the settings.
|
|
||||||
setTimeout(() => {
|
|
||||||
URL.revokeObjectURL(objectURL);
|
|
||||||
}, 60 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
import {
|
|
||||||
getSettings,
|
|
||||||
log,
|
|
||||||
isValidHexColor,
|
|
||||||
isValidTildesUsername,
|
|
||||||
setSettings,
|
|
||||||
Settings
|
|
||||||
} from '..';
|
|
||||||
|
|
||||||
export async function importFileHandler(event: Event): Promise<void> {
|
|
||||||
// Grab the imported files (if any).
|
|
||||||
const fileList: FileList | null = (event.target as HTMLInputElement).files;
|
|
||||||
|
|
||||||
if (fileList === null) {
|
|
||||||
log('No file imported.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new window.FileReader();
|
|
||||||
|
|
||||||
reader.addEventListener(
|
|
||||||
'load',
|
|
||||||
async (): Promise<void> => {
|
|
||||||
let data: Partial<Settings>;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
||||||
data = JSON.parse(reader.result!.toString());
|
|
||||||
} catch (error) {
|
|
||||||
log(error, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings: Settings = await getSettings();
|
|
||||||
const newSettings: Settings = {...settings};
|
|
||||||
|
|
||||||
if (typeof data.data !== 'undefined') {
|
|
||||||
if (typeof data.data.userLabels !== 'undefined') {
|
|
||||||
newSettings.data.userLabels = [];
|
|
||||||
|
|
||||||
for (const label of data.data.userLabels) {
|
|
||||||
if (
|
|
||||||
typeof label.username === 'undefined' ||
|
|
||||||
!isValidTildesUsername(label.username)
|
|
||||||
) {
|
|
||||||
log(`Invalid username in imported labels: ${label.username}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
newSettings.data.userLabels.push({
|
|
||||||
color: isValidHexColor(label.color) ? label.color : '#f0f',
|
|
||||||
id: newSettings.data.userLabels.length + 1,
|
|
||||||
priority: Number.isNaN(label.priority) ? 0 : label.priority,
|
|
||||||
text: typeof label.text === 'undefined' ? 'Label' : label.text,
|
|
||||||
username: label.username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.data.hideVotes !== 'undefined') {
|
|
||||||
newSettings.data.hideVotes = data.data.hideVotes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.features !== 'undefined') {
|
|
||||||
newSettings.features = {...data.features};
|
|
||||||
}
|
|
||||||
|
|
||||||
await setSettings(newSettings);
|
|
||||||
log('Successfully imported your settings, reloading the page to apply.');
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
reader.addEventListener('error', (): void => {
|
|
||||||
log(reader.error, true);
|
|
||||||
reader.abort();
|
|
||||||
});
|
|
||||||
|
|
||||||
reader.readAsText(fileList[0]);
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
import {browser, Manifest} from 'webextension-polyfill-ts';
|
|
||||||
import {defaultSettings, log} from '..';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserLabel type definition.
|
|
||||||
*/
|
|
||||||
export type UserLabel = {
|
|
||||||
color: string;
|
|
||||||
id: number;
|
|
||||||
priority: number;
|
|
||||||
text: string;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User extension settings.
|
|
||||||
*/
|
|
||||||
export type Settings = {
|
|
||||||
data: {
|
|
||||||
hideVotes: {
|
|
||||||
[index: string]: boolean;
|
|
||||||
comments: boolean;
|
|
||||||
topics: boolean;
|
|
||||||
ownComments: boolean;
|
|
||||||
ownTopics: boolean;
|
|
||||||
};
|
|
||||||
knownGroups: string[];
|
|
||||||
latestActiveFeatureTab: string;
|
|
||||||
userLabels: UserLabel[];
|
|
||||||
version?: string;
|
|
||||||
};
|
|
||||||
features: {
|
|
||||||
[index: string]: boolean;
|
|
||||||
autocomplete: boolean;
|
|
||||||
backToTop: boolean;
|
|
||||||
debug: boolean;
|
|
||||||
hideVotes: boolean;
|
|
||||||
jumpToNewComment: boolean;
|
|
||||||
markdownToolbar: boolean;
|
|
||||||
userLabels: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches and returns the user extension settings.
|
|
||||||
*/
|
|
||||||
export async function getSettings(): Promise<Settings> {
|
|
||||||
const syncSettings: any = await browser.storage.sync.get(defaultSettings);
|
|
||||||
const settings: Settings = {
|
|
||||||
data: {...defaultSettings.data, ...syncSettings.data},
|
|
||||||
features: {...defaultSettings.features, ...syncSettings.features}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window?.TildesReExtended !== undefined) {
|
|
||||||
window.TildesReExtended.debug = settings.features.debug;
|
|
||||||
// If we're in development, force debug output.
|
|
||||||
if (getManifest().nodeEnv === 'development') {
|
|
||||||
window.TildesReExtended.debug = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the user extension settings.
|
|
||||||
* @param newSettings The new settings to save.
|
|
||||||
*/
|
|
||||||
export async function setSettings(newSettings: Settings): Promise<void> {
|
|
||||||
return browser.storage.sync.set(newSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tildes ReExtended WebExtension manifest type definition.
|
|
||||||
*/
|
|
||||||
export type TRXManifest = {nodeEnv?: string} & Manifest.ManifestBase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the WebExtension manifest.
|
|
||||||
*/
|
|
||||||
export function getManifest(): TRXManifest {
|
|
||||||
const manifest: Manifest.ManifestBase = browser.runtime.getManifest();
|
|
||||||
return {...manifest};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all user extension settings and reloads the page.
|
|
||||||
* @param event The mouse click event.
|
|
||||||
*/
|
|
||||||
export async function removeAllData(event: MouseEvent): Promise<void> {
|
|
||||||
event.preventDefault();
|
|
||||||
if (
|
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
!window.confirm(
|
|
||||||
'Are you sure you want to delete your data? There is no way to ' +
|
|
||||||
'recover it once it has been deleted.'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.storage.sync.clear();
|
|
||||||
log(
|
|
||||||
'Data removed, reloading this page to reinitialize default settings.',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './components';
|
|
||||||
export * from './defaults';
|
|
||||||
export * from './export';
|
|
||||||
export * from './import';
|
|
|
@ -1,15 +1,30 @@
|
||||||
// A fix for TypeScript so it sees this file as a module and thus allows to
|
import {html} from 'htm/preact';
|
||||||
// modify the global scope.
|
import browser from 'webextension-polyfill';
|
||||||
export {};
|
|
||||||
|
|
||||||
// See the initialize function located in `utilities/index.ts` for actual code.
|
|
||||||
|
|
||||||
type TildesReExtended = {
|
|
||||||
debug: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
TildesReExtended: TildesReExtended;
|
TildesReExtended: {
|
||||||
|
debug: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly DEV: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TRXComponent = ReturnType<typeof html>;
|
||||||
|
|
||||||
|
type TRXManifest = browser.Manifest.ManifestBase;
|
||||||
|
|
||||||
|
type UserLabel = {
|
||||||
|
color: string;
|
||||||
|
id: number;
|
||||||
|
priority: number;
|
||||||
|
text: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
/**
|
/** Returns whether a hex color is "bright". */
|
||||||
* Returns whether a hex color is "bright".
|
|
||||||
* @param color The hex color.
|
|
||||||
*/
|
|
||||||
export function isColorBright(color: string): boolean {
|
export function isColorBright(color: string): boolean {
|
||||||
if (color.startsWith('#')) {
|
if (color.startsWith('#')) {
|
||||||
color = color.slice(1);
|
color = color.slice(1);
|
||||||
|
@ -40,50 +37,50 @@ export function isColorBright(color: string): boolean {
|
||||||
return brightness > 128;
|
return brightness > 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS custom properties from the Tildes themes.
|
/** CSS custom properties from the Tildes themes. */
|
||||||
export const themeColors = [
|
export const themeColors = [
|
||||||
{
|
{
|
||||||
name: 'Background Primary',
|
name: 'Background Primary',
|
||||||
value: '--background-primary-color'
|
value: '--background-primary-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Background Secondary',
|
name: 'Background Secondary',
|
||||||
value: '--background-secondary-color'
|
value: '--background-secondary-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Foreground Primary',
|
name: 'Foreground Primary',
|
||||||
value: '--foreground-primary-color'
|
value: '--foreground-primary-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Foreground Secondary',
|
name: 'Foreground Secondary',
|
||||||
value: '--foreground-secondary-color'
|
value: '--foreground-secondary-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Exemplary',
|
name: 'Exemplary',
|
||||||
value: '--comment-label-exemplary-color'
|
value: '--comment-label-exemplary-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Off-topic',
|
name: 'Off-topic',
|
||||||
value: '--comment-label-offtopic-color'
|
value: '--comment-label-offtopic-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Joke',
|
name: 'Joke',
|
||||||
value: '--comment-label-joke-color'
|
value: '--comment-label-joke-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Noise',
|
name: 'Noise',
|
||||||
value: '--comment-label-noise-color'
|
value: '--comment-label-noise-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Malice',
|
name: 'Malice',
|
||||||
value: '--comment-label-malice-color'
|
value: '--comment-label-malice-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Mine',
|
name: 'Mine',
|
||||||
value: '--stripe-mine-color'
|
value: '--stripe-mine-color',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Official',
|
name: 'Official',
|
||||||
value: '--alert-color'
|
value: '--alert-color',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import {html} from 'htm/preact';
|
import {html} from 'htm/preact';
|
||||||
import {TRXComponent} from '..';
|
|
||||||
|
|
||||||
type LinkProps = {
|
type Props = {
|
||||||
class: string;
|
class: string;
|
||||||
text: string;
|
text: string;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** An `<a />` helper component with `target="_blank"` and `rel="noopener"`. */
|
||||||
* A `<a />` helper component with `target="_blank"` and `rel="noopener"`.
|
export function Link(props: Props): TRXComponent {
|
||||||
* @param props Link properties.
|
|
||||||
*/
|
|
||||||
export function Link(props: LinkProps): TRXComponent {
|
|
||||||
return html`
|
return html`
|
||||||
<a
|
<a
|
||||||
class="${props.class}"
|
class="${props.class}"
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Creates an HTML Element from a given string. Only use this when using
|
||||||
|
* `htm/preact` isn't practical.
|
||||||
|
*/
|
||||||
|
export function createElementFromString<T extends Element>(input: string): T {
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = input.trim();
|
||||||
|
return template.content.firstElementChild as T;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export * from './color.js';
|
||||||
|
export * from './components/link.js';
|
||||||
|
export * from './elements.js';
|
||||||
|
export * from './globals.js';
|
||||||
|
export * from './groups.js';
|
||||||
|
export * from './logging.js';
|
||||||
|
export * from './query-selectors.js';
|
||||||
|
export * from './report-a-bug.js';
|
||||||
|
export * from './validators.js';
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function initializeGlobals() {
|
||||||
|
if (window.TildesReExtended === undefined) {
|
||||||
|
window.TildesReExtended = {
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,17 @@
|
||||||
import {log, querySelectorAll, setSettings, Settings} from '..';
|
import {log} from './logging.js';
|
||||||
|
import {querySelectorAll} from './query-selectors.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to extract and save the groups. Returns the current saved groups when
|
* Tries to extract and save the groups. Returns the current saved groups when
|
||||||
* the user is not in `/groups` and the new ones when they are in `/groups`.
|
* the user is not in `/groups` and the new ones when they are in `/groups`.
|
||||||
* @param settings The user's extension settings.
|
|
||||||
*/
|
*/
|
||||||
export async function extractAndSaveGroups(
|
export function extractGroups(): string[] | undefined {
|
||||||
settings: Settings
|
|
||||||
): Promise<string[]> {
|
|
||||||
if (window.location.pathname !== '/groups') {
|
if (window.location.pathname !== '/groups') {
|
||||||
log('Not in "/groups", returning early.');
|
log('Not in "/groups", returning early.');
|
||||||
return settings.data.knownGroups;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups: string[] = querySelectorAll('.link-group').map(
|
return querySelectorAll('.link-group').map(
|
||||||
(value) => value.textContent!
|
(value) => value.textContent ?? '<unknown group>',
|
||||||
);
|
);
|
||||||
|
|
||||||
settings.data.knownGroups = groups;
|
|
||||||
await setSettings(settings);
|
|
||||||
log('Updated saved groups.', true);
|
|
||||||
return groups;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
/**
|
|
||||||
* Creates an HTMLElement from a given string. Only use this when using
|
|
||||||
* `htm/preact` isn't practical.
|
|
||||||
* @param input The HTML.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the global window with Tildes ReExtended-specific settings.
|
|
||||||
*/
|
|
||||||
export function initialize(): void {
|
|
||||||
if (window.TildesReExtended === undefined) {
|
|
||||||
window.TildesReExtended = {
|
|
||||||
debug: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs something to the console under the debug level.
|
|
||||||
* @param thing The thing to log.
|
|
||||||
* @param force If true, ignores whether or not debug logging is enabled.
|
|
||||||
*/
|
|
||||||
export function log(thing: any, force = false): void {
|
|
||||||
let overrideStyle = '';
|
|
||||||
let prefix = '[TRX]';
|
|
||||||
if (force) {
|
|
||||||
prefix = '%c' + prefix;
|
|
||||||
overrideStyle = 'background-color: #dc322f; margin-right: 9px;';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.TildesReExtended.debug || force) {
|
|
||||||
if (overrideStyle.length > 0) {
|
|
||||||
console.debug(prefix, overrideStyle, thing);
|
|
||||||
} else {
|
|
||||||
console.debug(prefix, thing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './color';
|
|
||||||
export * from './components';
|
|
||||||
export * from './groups';
|
|
||||||
export * from './query-selectors';
|
|
||||||
export * from './report-a-bug';
|
|
||||||
export * from './validators';
|
|
||||||
export * from './version';
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Logs something to the console under the debug level.
|
||||||
|
* @param thing The thing to log.
|
||||||
|
* @param force If true, ignores whether or not debug logging is enabled.
|
||||||
|
*/
|
||||||
|
export function log(thing: any, force = false): void {
|
||||||
|
let overrideStyle = '';
|
||||||
|
let prefix = '[TRX]';
|
||||||
|
if (force) {
|
||||||
|
prefix = '%c' + prefix;
|
||||||
|
overrideStyle = 'background-color: #dc322f; margin-right: 9px;';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.TildesReExtended?.debug || import.meta.env.DEV || force) {
|
||||||
|
if (overrideStyle.length > 0) {
|
||||||
|
console.debug(prefix, overrideStyle, thing);
|
||||||
|
} else {
|
||||||
|
console.debug(prefix, thing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,22 +4,16 @@
|
||||||
// The first function should only ever be used when we know for certain that
|
// The first function should only ever be used when we know for certain that
|
||||||
// the target element is going to exist.
|
// the target element is going to exist.
|
||||||
|
|
||||||
/**
|
/** Returns the first element found that matches the selector. */
|
||||||
* Returns the first element found that matches the selector.
|
|
||||||
* @param selector The selector.
|
|
||||||
*/
|
|
||||||
export function querySelector<T extends Element>(selector: string): T {
|
export function querySelector<T extends Element>(selector: string): T {
|
||||||
return document.querySelector<T>(selector)!;
|
return document.querySelector(selector)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns all elements found from all the selectors. */
|
||||||
* Returns all elements found from all the selectors.
|
|
||||||
* @param selectors The selectors.
|
|
||||||
*/
|
|
||||||
export function querySelectorAll<T extends Element>(
|
export function querySelectorAll<T extends Element>(
|
||||||
...selectors: string[]
|
...selectors: string[]
|
||||||
): T[] {
|
): T[] {
|
||||||
return selectors.flatMap((selector) =>
|
return selectors.flatMap((selector) =>
|
||||||
Array.from(document.querySelectorAll(selector))
|
Array.from(document.querySelectorAll(selector)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import platform from 'platform';
|
||||||
*/
|
*/
|
||||||
export function createReportTemplate(
|
export function createReportTemplate(
|
||||||
location: 'gitlab' | 'tildes',
|
location: 'gitlab' | 'tildes',
|
||||||
trxVersion: string
|
trxVersion: string,
|
||||||
): string {
|
): string {
|
||||||
let introText =
|
let introText =
|
||||||
"Thank you for taking the time to report a bug! Don't forget to fill in an\n appropriate title above, and make sure the information below is correct.";
|
"Thank you for taking the time to report a bug! Don't forget to fill in an\n appropriate title above, and make sure the information below is correct.";
|
||||||
|
@ -17,10 +17,10 @@ export function createReportTemplate(
|
||||||
'Thank you for taking the time to report a bug! Please make sure the\n information below is correct.';
|
'Thank you for taking the time to report a bug! Please make sure the\n information below is correct.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout: string = platform.layout!;
|
const layout = platform.layout ?? '<unknown>';
|
||||||
const name: string = platform.name!;
|
const name = platform.name ?? '<unknown>';
|
||||||
const os: string = platform.os?.toString()!;
|
const os = platform.os?.toString() ?? '<unknown>';
|
||||||
const version: string = platform.version!;
|
const version = platform.version ?? '<unknown>';
|
||||||
|
|
||||||
// Set the headers using HTML tags, these can't be with #-style Markdown
|
// 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.
|
// headers as they'll be interpreted as an ID instead of Markdown content.
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
/**
|
/** Check whether a hex color is valid. */
|
||||||
* Return whether the input is a valid hex color with a starting `#`.
|
|
||||||
* @param color The potential hex color.
|
|
||||||
*/
|
|
||||||
export function isValidHexColor(color: string): boolean {
|
export function isValidHexColor(color: string): boolean {
|
||||||
return (
|
return (
|
||||||
/^#(?:[a-f\d]{8}|[a-f\d]{6}|[a-f\d]{4}|[a-f\d]{3})$/i.exec(color) !== null
|
/^#(?:[a-f\d]{8}|[a-f\d]{6}|[a-f\d]{4}|[a-f\d]{3})$/i.exec(color) !== null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Check whether a username is a valid Tildes username. */
|
||||||
* Return whether the input is a valid Tildes username.
|
|
||||||
* @param username The potential username.
|
|
||||||
*/
|
|
||||||
export function isValidTildesUsername(username: string): boolean {
|
export function isValidTildesUsername(username: string): boolean {
|
||||||
// Validation copied from Tildes source code:
|
// Validation copied from Tildes source code:
|
||||||
// https://gitlab.com/tildes/tildes/blob/master/tildes/tildes/schemas/user.py
|
// https://gitlab.com/tildes/tildes/blob/master/tildes/tildes/schemas/user.py
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
/**
|
|
||||||
* Returns a version string as a number by removing the periods.
|
|
||||||
* @param version
|
|
||||||
*/
|
|
||||||
export function versionAsNumber(version: string): number {
|
|
||||||
return Number(version.replace(/\./g, ''));
|
|
||||||
}
|
|
|
@ -2,19 +2,18 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2019"
|
"ES2020"
|
||||||
],
|
],
|
||||||
"module": "commonjs",
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noEmit": true,
|
||||||
"outDir": "build/",
|
"outDir": "build/",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "es6",
|
"target": "esnext"
|
||||||
"typeRoots": [
|
|
||||||
"node_modules/@types",
|
|
||||||
"node_modules/web-ext-types"
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"source/**/*.ts",
|
"source/**/*.ts",
|
||||||
|
"vite.config.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules/"
|
"node_modules/"
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
import url from 'node:url';
|
||||||
|
|
||||||
|
import preact from '@preact/preset-vite';
|
||||||
|
import {defineConfig} from 'vite';
|
||||||
|
import webExtension from 'vite-plugin-web-extension';
|
||||||
|
|
||||||
|
const currentDir = path.dirname(url.fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const buildDir = path.join(currentDir, 'build');
|
||||||
|
const sourceDir = path.join(currentDir, 'source');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: buildDir,
|
||||||
|
sourcemap: 'inline',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
preact(),
|
||||||
|
webExtension({
|
||||||
|
assets: 'assets',
|
||||||
|
browser: 'firefox',
|
||||||
|
manifest: path.join(sourceDir, 'manifest.json'),
|
||||||
|
webExtConfig: {
|
||||||
|
browserConsole: true,
|
||||||
|
firefoxProfile: 'firefox/',
|
||||||
|
keepProfileChanges: true,
|
||||||
|
startUrl: 'about:debugging#/runtime/this-firefox',
|
||||||
|
target: 'firefox-desktop',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
root: sourceDir,
|
||||||
|
});
|
Loading…
Reference in New Issue