257 lines
7.2 KiB
TypeScript
257 lines
7.2 KiB
TypeScript
|
import {browser, Manifest} from 'webextension-polyfill-ts';
|
||
|
import {themeColors, ThemeKey} from './theme-colors';
|
||
|
|
||
|
export interface UserLabel {
|
||
|
color: string;
|
||
|
id: number;
|
||
|
text: string;
|
||
|
priority: number;
|
||
|
username: string;
|
||
|
}
|
||
|
|
||
|
export interface Settings {
|
||
|
data: {
|
||
|
latestActiveFeatureTab: string;
|
||
|
userLabels: UserLabel[];
|
||
|
version?: string;
|
||
|
};
|
||
|
features: {
|
||
|
backToTop: boolean;
|
||
|
debug: boolean;
|
||
|
jumpToNewComment: boolean;
|
||
|
userLabels: boolean;
|
||
|
[index: string]: boolean;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
export const defaultSettings: Settings = {
|
||
|
data: {
|
||
|
latestActiveFeatureTab: 'debug',
|
||
|
userLabels: []
|
||
|
},
|
||
|
features: {
|
||
|
backToTop: true,
|
||
|
debug: false,
|
||
|
jumpToNewComment: true,
|
||
|
userLabels: true
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Keep a local variable here for the debug logging, this way we don't have to
|
||
|
// call `getSettings()` each time we potentially want to log something. It gets
|
||
|
// set each time `getSettings()` is called so it's always accurate.
|
||
|
let debug = true;
|
||
|
|
||
|
export async function getSettings(): Promise<Settings> {
|
||
|
// TODO: Replace local storage with sync storage once the extension has been
|
||
|
// deployed to AMO and we get a permanent ID. https://bugzil.la/1323228
|
||
|
const localSettings: any = await browser.storage.local.get(defaultSettings);
|
||
|
const settings: Settings = {
|
||
|
data: {...defaultSettings.data, ...localSettings.data},
|
||
|
features: {...defaultSettings.features, ...localSettings.features}
|
||
|
};
|
||
|
debug = localSettings.features.debug;
|
||
|
// If we're in development, force debug output.
|
||
|
if (getManifest().nodeEnv === 'development') {
|
||
|
debug = true;
|
||
|
}
|
||
|
|
||
|
return settings;
|
||
|
}
|
||
|
|
||
|
export async function setSettings(
|
||
|
newSettings: Partial<Settings>
|
||
|
): Promise<void> {
|
||
|
return browser.storage.local.set(newSettings);
|
||
|
}
|
||
|
|
||
|
export function log(message: any, override = false): void {
|
||
|
let overrideStyle = '';
|
||
|
let prefix = '[TRX]';
|
||
|
if (override) {
|
||
|
prefix = '%c' + prefix;
|
||
|
overrideStyle = 'background-color: #dc322f; margin-right: 9px;';
|
||
|
}
|
||
|
|
||
|
if (debug || override || getManifest().nodeEnv === 'development') {
|
||
|
console.debug(prefix, overrideStyle, message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Helper function to convert kebab-case strings to camelCase ones.
|
||
|
// Primarily for converting element IDs to Object keys.
|
||
|
export function kebabToCamel(input: string): string {
|
||
|
let output = '';
|
||
|
for (const part of input.split('-')) {
|
||
|
output += part[0].toUpperCase();
|
||
|
output += part.slice(1);
|
||
|
}
|
||
|
|
||
|
return output[0].toLowerCase() + output.slice(1);
|
||
|
}
|
||
|
|
||
|
// The opposite of `kebabToCamel()`.
|
||
|
export function camelToKebab(input: string): string {
|
||
|
const uppercaseMatches: RegExpMatchArray | null = input.match(/[A-Z]/g);
|
||
|
// If there are no uppercase letters in the input, just return it.
|
||
|
if (uppercaseMatches === null) {
|
||
|
return input;
|
||
|
}
|
||
|
|
||
|
// Find all the indexes of where uppercase letters are.
|
||
|
const uppercaseIndexes: number[] = [];
|
||
|
for (const match of uppercaseMatches) {
|
||
|
const latestIndex: number =
|
||
|
typeof uppercaseIndexes[uppercaseIndexes.length - 1] === 'undefined'
|
||
|
? 0
|
||
|
: uppercaseIndexes[uppercaseIndexes.length - 1];
|
||
|
uppercaseIndexes.push(input.indexOf(match, latestIndex + 1));
|
||
|
}
|
||
|
|
||
|
// Convert each section up to the next uppercase letter to lowercase with
|
||
|
// a dash between each section.
|
||
|
let output = '';
|
||
|
let previousIndex = 0;
|
||
|
for (const index of uppercaseIndexes) {
|
||
|
output += input.slice(previousIndex, index).toLowerCase();
|
||
|
output += '-';
|
||
|
previousIndex = index;
|
||
|
}
|
||
|
|
||
|
output += input.slice(previousIndex).toLowerCase();
|
||
|
return output;
|
||
|
}
|
||
|
|
||
|
// This utility function should only be used in cases where we know for certain
|
||
|
// that the wanted element is going to exist.
|
||
|
export function querySelector<T extends Element>(selector: string): T {
|
||
|
return document.querySelector<T>(selector)!;
|
||
|
}
|
||
|
|
||
|
export function createElementFromString<T extends Element>(input: string): T {
|
||
|
const template: HTMLTemplateElement = document.createElement('template');
|
||
|
template.innerHTML = input.trim();
|
||
|
return template.content.firstElementChild! as T;
|
||
|
}
|
||
|
|
||
|
export function isInTopicListing(): boolean {
|
||
|
return document.querySelector('.topic-listing') !== null;
|
||
|
}
|
||
|
|
||
|
export function getManifest(): {nodeEnv?: string} & Manifest.ManifestBase {
|
||
|
const manifest: Manifest.ManifestBase = browser.runtime.getManifest();
|
||
|
return {...manifest};
|
||
|
}
|
||
|
|
||
|
export function getCurrentThemeKey(): ThemeKey {
|
||
|
const body: HTMLBodyElement = querySelector('body');
|
||
|
const classes: string | null = body.getAttribute('class');
|
||
|
if (classes === null || !classes.includes('theme-')) {
|
||
|
return 'white';
|
||
|
}
|
||
|
|
||
|
const themeIndex: number = classes.indexOf('theme-');
|
||
|
const themeKey: ThemeKey = kebabToCamel(
|
||
|
classes.slice(
|
||
|
themeIndex + 'theme-'.length,
|
||
|
classes.includes(' ', themeIndex)
|
||
|
? classes.indexOf(' ', themeIndex)
|
||
|
: classes.length
|
||
|
)
|
||
|
) as ThemeKey;
|
||
|
if (typeof themeColors[themeKey] === 'undefined') {
|
||
|
log(
|
||
|
`Attempted to retrieve theme key that's not defined: "${themeKey}" Using the white theme as fallback.`,
|
||
|
true
|
||
|
);
|
||
|
return 'white';
|
||
|
}
|
||
|
|
||
|
return themeKey;
|
||
|
}
|
||
|
|
||
|
// Adapted from https://stackoverflow.com/a/12043228/12251171.
|
||
|
export function isColorBright(color: string): boolean {
|
||
|
if (color.startsWith('#')) {
|
||
|
color = color.slice(1);
|
||
|
}
|
||
|
|
||
|
if (color.length === 4) {
|
||
|
color = color.slice(0, 3);
|
||
|
}
|
||
|
|
||
|
if (color.length === 8) {
|
||
|
color = color.slice(0, 6);
|
||
|
}
|
||
|
|
||
|
if (color.length === 3) {
|
||
|
color = color
|
||
|
.split('')
|
||
|
.map(val => val.repeat(2))
|
||
|
.join('');
|
||
|
}
|
||
|
|
||
|
const red: number = parseInt(color.slice(0, 2), 16);
|
||
|
const green: number = parseInt(color.slice(2, 4), 16);
|
||
|
const blue: number = parseInt(color.slice(4, 6), 16);
|
||
|
const brightness: number = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||
|
return brightness > 128;
|
||
|
}
|
||
|
|
||
|
export function appendStyleAttribute(element: Element, styles: string): void {
|
||
|
const existingStyles: string | null = element.getAttribute('style');
|
||
|
if (existingStyles === null) {
|
||
|
element.setAttribute('style', styles);
|
||
|
} else {
|
||
|
element.setAttribute('style', `${existingStyles} ${styles}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function isValidHexColor(color: string): boolean {
|
||
|
// Overly verbose validation for 3/4/6/8-character hex colors.
|
||
|
if (
|
||
|
/^#[a-f0-9]{6}$/i.exec(color) === null &&
|
||
|
/^#[a-f0-9]{3}$/i.exec(color) === null &&
|
||
|
/^#[a-f0-9]{8}$/i.exec(color) === null &&
|
||
|
/^#[a-f0-9]{4}$/i.exec(color) === null
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
export function flashMessage(message: string, error = false): void {
|
||
|
if (document.querySelector('.trx-flash-message') !== null) {
|
||
|
log(
|
||
|
`A flash message already exists, skipping requested one with message:\n${message}`
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const messageElement: HTMLDivElement = createElementFromString(
|
||
|
`<div class="trx-flash-message">${message}</div>`
|
||
|
);
|
||
|
if (error) {
|
||
|
messageElement.classList.add('trx-flash-error');
|
||
|
}
|
||
|
|
||
|
let isRemoved = false;
|
||
|
messageElement.addEventListener('click', (): void => {
|
||
|
messageElement.remove();
|
||
|
isRemoved = true;
|
||
|
});
|
||
|
document.body.append(messageElement);
|
||
|
setTimeout(() => {
|
||
|
messageElement.classList.add('trx-opaque');
|
||
|
}, 50);
|
||
|
setTimeout(() => {
|
||
|
if (!isRemoved) {
|
||
|
messageElement.classList.remove('trx-opaque');
|
||
|
setTimeout(() => {
|
||
|
messageElement.remove();
|
||
|
}, 500);
|
||
|
}
|
||
|
}, 5000);
|
||
|
}
|