mirror of
https://github.com/insin/control-panel-for-twitter.git
synced 2025-06-18 14:45:31 -04:00

- Fixed Community Tweets being treated as Retweets - Fixed handling timelines in Communities tabs - Replaced hardcoding of the primary column selector Options: - Fixed options textarea styles - Fixed background in macOS Safari options popup in dark mode - Apply other settings changes after options input changes
565 lines
18 KiB
JavaScript
565 lines
18 KiB
JavaScript
import {defaultSettings} from './settings.js'
|
||
import {get, set} from './storage.js'
|
||
|
||
//#region Localisation
|
||
document.title = chrome.i18n.getMessage(`extensionName`)
|
||
|
||
for (let optionValue of [
|
||
'badges',
|
||
'comfortable',
|
||
'compact',
|
||
'default',
|
||
'hide',
|
||
'ignore',
|
||
'liked',
|
||
'recent',
|
||
'relevant',
|
||
'separate',
|
||
]) {
|
||
let label = chrome.i18n.getMessage(`option_${optionValue}`)
|
||
for (let $option of document.querySelectorAll(`option[value="${optionValue}"]`)) {
|
||
$option.textContent = label
|
||
}
|
||
}
|
||
|
||
for (let translationId of [
|
||
'addAddMutedWordMenuItemLabel_desktop',
|
||
'addAddMutedWordMenuItemLabel_mobile',
|
||
'customCssLabel',
|
||
'debugInfo',
|
||
'debugLabel',
|
||
'debugLogTimelineStatsLabel',
|
||
'debugOptionsLabel',
|
||
'defaultToFollowingLabel',
|
||
'defaultToLatestSearchLabel',
|
||
'disableHomeTimelineInfo',
|
||
'disableHomeTimelineLabel',
|
||
'disableTweetTextFormattingLabel',
|
||
'disabledHomeTimelineRedirectLabel',
|
||
'disabledHomeTimelineRedirectOption_messages',
|
||
'dontUseChirpFontLabel',
|
||
'dropdownMenuFontWeightLabel',
|
||
'enabled',
|
||
'experimentsOptionsLabel',
|
||
'exportConfigLabel',
|
||
'fastBlockLabel',
|
||
'followButtonStyleLabel',
|
||
'followButtonStyleOption_monochrome',
|
||
'followButtonStyleOption_themed',
|
||
'fullWidthContentInfo',
|
||
'fullWidthContentLabel',
|
||
'fullWidthMediaLabel',
|
||
'hideAccountSwitcherLabel',
|
||
'hideAdsNavLabel',
|
||
'hideAllMetricsLabel',
|
||
'hideBookmarkButtonLabel',
|
||
'hideBookmarkMetricsLabel',
|
||
'hideComposeTweetLabel',
|
||
'hideDiscoverSuggestionsLabel',
|
||
'hideExploreNavLabel',
|
||
'hideExploreNavWithSidebarLabel',
|
||
'hideExplorePageContentsLabel',
|
||
'hideFollowingMetricsLabel',
|
||
'hideForYouTimelineLabel',
|
||
'hideGrokLabel',
|
||
'hideGrokTweetsLabel',
|
||
'hideInlinePrompts',
|
||
'hideJobsLabel',
|
||
'hideLikeMetricsLabel',
|
||
'hideLiveBroadcastBarLabel',
|
||
'hideLiveBroadcastsLabel',
|
||
'hideMessagesBottomNavItemLabel',
|
||
'hideMessagesDrawerLabel',
|
||
'hideMetricsLabel',
|
||
'hideMonetizationNavLabel',
|
||
'hideMoreSlideOutMenuItemsOptionsLabel_desktop',
|
||
'hideMoreSlideOutMenuItemsOptionsLabel_mobile',
|
||
'hidePremiumRepliesLabel',
|
||
'hidePremiumUpsellsLabel',
|
||
'hideProfileHeaderMetricsLabel',
|
||
'hideProfileRetweetsLabel',
|
||
'hideQuoteTweetMetricsLabel',
|
||
'hideReplyMetricsLabel',
|
||
'hideRetweetMetricsLabel',
|
||
'hideSeeNewTweetsLabel',
|
||
'hideShareTweetButtonLabel',
|
||
'hideSidebarContentLabel',
|
||
'hideSpacesNavLabel',
|
||
'hideSubscriptionsLabel',
|
||
'hideSuggestedFollowsLabel',
|
||
'hideTimelineTweetBoxLabel',
|
||
'hideToggleNavigationLabel',
|
||
'hideTweetAnalyticsLinksLabel',
|
||
'hideUnavailableQuoteTweetsLabel',
|
||
'hideUnusedUiItemsOptionsLabel',
|
||
'hideVerifiedTabsLabel',
|
||
'hideViewsLabel',
|
||
'hideWhatsHappeningLabel',
|
||
'hideWhoToFollowEtcLabel',
|
||
'homeTimelineOptionsLabel',
|
||
'listRetweetsLabel',
|
||
'mutableQuoteTweetsLabel',
|
||
'navBaseFontSizeLabel',
|
||
'navDensityLabel',
|
||
'premiumBlueChecksLabel',
|
||
'premiumBlueChecksOption_replace',
|
||
'preventNextVideoAutoplayInfo',
|
||
'preventNextVideoAutoplayLabel',
|
||
'quoteTweetsLabel',
|
||
'redirectToTwitterLabel',
|
||
'reduceAlgorithmicContentOptionsLabel',
|
||
'reduceEngagementOptionsLabel',
|
||
'reducedInteractionModeInfo',
|
||
'reducedInteractionModeLabel',
|
||
'restoreLinkHeadlinesLabel',
|
||
'restoreOtherInteractionLinksLabel',
|
||
'restoreQuoteTweetsLinkLabel',
|
||
'restoreTweetSourceLabel',
|
||
'retweetsLabel',
|
||
'revertXBrandingLabel',
|
||
'showBookmarkButtonUnderFocusedTweetsLabel',
|
||
'showPremiumReplyBusinessLabel',
|
||
'showPremiumReplyFollowedByLabel',
|
||
'showPremiumReplyFollowersCountAmountLabel',
|
||
'showPremiumReplyFollowingLabel',
|
||
'showPremiumReplyGovernmentLabel',
|
||
'showRelevantPeopleLabel',
|
||
'sidebarLabel',
|
||
'sortRepliesLabel',
|
||
'tweakNewLayoutInfo',
|
||
'tweakNewLayoutLabel',
|
||
'tweakQuoteTweetsPageLabel',
|
||
'uiImprovementsOptionsLabel',
|
||
'uiTweaksOptionsLabel',
|
||
'unblurSensitiveContentLabel',
|
||
'uninvertFollowButtonsLabel',
|
||
'xFixesLabel',
|
||
]) {
|
||
document.getElementById(translationId).textContent = chrome.i18n.getMessage(translationId)
|
||
}
|
||
|
||
for (let translationClass of [
|
||
'hideBookmarksNavLabel',
|
||
'hideCommunitiesNavLabel',
|
||
'hideListsNavLabel',
|
||
'notificationsLabel',
|
||
'saveAndApplyButton',
|
||
]) {
|
||
let translation = chrome.i18n.getMessage(translationClass)
|
||
for (let $el of document.querySelectorAll(`.${translationClass}`)) {
|
||
$el.textContent = translation
|
||
}
|
||
}
|
||
|
||
for (let amount of [1_000, 10_000, 100_000, 1_000_000]) {
|
||
document.querySelector(`option[value="${amount}"]`).textContent = formatFollowerCount(amount)
|
||
}
|
||
//#endregion
|
||
|
||
/**
|
||
* Internal options which map directly to a form element.
|
||
* @type {import('./types').StoredConfigKey[]}
|
||
*/
|
||
const INTERNAL_CONFIG_FORM_KEYS = ['enabled', 'debug', 'debugLogTimelineStats']
|
||
/** @type {Set<string>} */
|
||
const INTERNAL_CONFIG_FORM_KEYSET = new Set(INTERNAL_CONFIG_FORM_KEYS)
|
||
|
||
/** @type {boolean} */
|
||
let desktop
|
||
/** @type {boolean} */
|
||
let mobile
|
||
|
||
const $body = document.body
|
||
|
||
if (navigator.userAgent.includes('Safari/') && !/Chrom(e|ium)\//.test(navigator.userAgent)) {
|
||
$body.classList.add('safari', /iP(ad|hone)/.test(navigator.userAgent) ? 'iOS' : 'macOS')
|
||
} else {
|
||
$body.classList.toggle('edge', navigator.userAgent.includes('Edg/'))
|
||
}
|
||
|
||
//#region Default config
|
||
/** @type {import("./types").StoredConfig} */
|
||
const defaultConfig = {
|
||
enabled: true,
|
||
debug: false,
|
||
debugLogTimelineStats: false,
|
||
// Default based on the platform if the main script hasn't run on Twitter yet
|
||
version: /(Android|iP(ad|hone))/.test(navigator.userAgent) ? 'mobile' : 'desktop',
|
||
}
|
||
//#endregion
|
||
|
||
//#region Config & variables
|
||
/**
|
||
* @type {import("./types").StoredConfig}
|
||
*/
|
||
let config
|
||
|
||
/**
|
||
* Checkbox group configuration for the version being used (mobile or desktop).
|
||
* @type {Map<string, string[]>}
|
||
*/
|
||
let checkboxGroups
|
||
|
||
// Page elements
|
||
let $experiments = /** @type {HTMLDetailsElement} */ (document.querySelector('details#experiments'))
|
||
let $exportConfig = document.querySelector('#export-config')
|
||
let $form = document.querySelector('form')
|
||
let $hideQuotesFrom = /** @type {HTMLDivElement} */ (document.querySelector('#hideQuotesFrom'))
|
||
let $hideQuotesFromDetails = /** @type {HTMLDetailsElement} */ (document.querySelector('details#hideQuotesFromDetails'))
|
||
let $hideQuotesFromLabel = /** @type {HTMLElement} */ (document.querySelector('#hideQuotesFromLabel'))
|
||
let $mutedQuotes = /** @type {HTMLDivElement} */ (document.querySelector('#mutedQuotes'))
|
||
let $mutedQuotesDetails = /** @type {HTMLDetailsElement} */ (document.querySelector('details#mutedQuotesDetails'))
|
||
let $mutedQuotesLabel = /** @type {HTMLElement} */ (document.querySelector('#mutedQuotesLabel'))
|
||
let $saveCustomCssButton = document.querySelector('button#saveCustomCss')
|
||
let $showPremiumReplyFollowersCountLabel = /** @type {HTMLElement} */ (document.querySelector('#showPremiumReplyFollowersCountLabel'))
|
||
//#endregion
|
||
|
||
//#region Utility functions
|
||
function exportConfig() {
|
||
let $a = document.createElement('a')
|
||
$a.download = 'control-panel-for-twitter-v4.12.1.config.txt'
|
||
$a.href = URL.createObjectURL(new Blob([
|
||
JSON.stringify(config, null, 2)
|
||
], {type: 'text/plain'}))
|
||
$a.click()
|
||
URL.revokeObjectURL($a.href)
|
||
}
|
||
|
||
function formatFollowerCount(num) {
|
||
let numFormat = Intl.NumberFormat(undefined, {notation: 'compact', compactDisplay: num < 1_000_000 ? 'short' : 'long'})
|
||
return numFormat.format(num)
|
||
}
|
||
|
||
/**
|
||
* @param {keyof HTMLElementTagNameMap} tagName
|
||
* @param {({[key: string]: any} | null)?} attributes
|
||
* @param {...any} children
|
||
* @returns {HTMLElement}
|
||
*/
|
||
function h(tagName, attributes, ...children) {
|
||
let $el = document.createElement(tagName)
|
||
|
||
if (attributes) {
|
||
for (let [prop, value] of Object.entries(attributes)) {
|
||
if (prop.startsWith('on') && typeof value == 'function') {
|
||
$el.addEventListener(prop.slice(2).toLowerCase(), value)
|
||
} else {
|
||
$el[prop] = value
|
||
}
|
||
}
|
||
}
|
||
|
||
for (let child of children) {
|
||
if (child == null || child === false) continue
|
||
if (child instanceof Node) {
|
||
$el.appendChild(child)
|
||
} else {
|
||
$el.insertAdjacentText('beforeend', String(child))
|
||
}
|
||
}
|
||
|
||
return $el
|
||
}
|
||
//#endregion
|
||
|
||
//#region Options page functions
|
||
/**
|
||
* Update the options page to match the current config.
|
||
*/
|
||
function applyConfig() {
|
||
mobile = config.version == 'mobile'
|
||
desktop = !mobile
|
||
$body.classList.toggle('mobile', mobile)
|
||
$body.classList.toggle('desktop', desktop)
|
||
checkboxGroups = new Map(Object.entries({
|
||
hideAllMetrics: [
|
||
'hideBookmarkMetrics',
|
||
'hideFollowingMetrics',
|
||
'hideLikeMetrics',
|
||
'hideReplyMetrics',
|
||
'hideRetweetMetrics',
|
||
'hideQuoteTweetMetrics',
|
||
'hideProfileHeaderMetrics',
|
||
]
|
||
}))
|
||
updateFormControls()
|
||
updateCheckboxGroups()
|
||
updateDisplay()
|
||
}
|
||
|
||
function onFormChanged(/** @type {Event} */ e) {
|
||
if (e.target instanceof HTMLTextAreaElement) return
|
||
|
||
/** @type {import("./types").StoredConfig} */
|
||
let changedConfig = Object.create(null)
|
||
/** @type {Partial<import("./types").UserSettings>} */
|
||
let changedSettings = Object.create(null)
|
||
|
||
// Handle input
|
||
let $el = /** @type {HTMLInputElement} */ (e.target)
|
||
if ($el.type == 'checkbox') {
|
||
// All internal config is currently checkbox toggles
|
||
if (INTERNAL_CONFIG_FORM_KEYSET.has($el.name)) {
|
||
config[$el.name] = changedConfig[$el.name] = $el.checked
|
||
}
|
||
// Checkbox group toggle
|
||
else if (checkboxGroups.has($el.name)) {
|
||
checkboxGroups.get($el.name).forEach(checkboxName => {
|
||
config.settings[checkboxName] = changedSettings[checkboxName] = $el.checked
|
||
updateFormControl($form.elements[checkboxName], $el.checked)
|
||
})
|
||
$el.indeterminate = false
|
||
}
|
||
// Individual checkbox toggle
|
||
else {
|
||
config.settings[$el.name] = changedSettings[$el.name] = $el.checked
|
||
updateCheckboxGroups()
|
||
}
|
||
} else {
|
||
config.settings[$el.name] = changedSettings[$el.name] = $el.value
|
||
}
|
||
|
||
// Apply other settings changes based on input
|
||
// Don't try to redirect the Home timeline to Notifications if both are disabled
|
||
if (changedSettings.hideNotifications &&
|
||
config.settings.disabledHomeTimelineRedirect == 'notifications') {
|
||
let key = 'disabledHomeTimelineRedirect'
|
||
$form.elements[key].value = config.settings[key] = changedSettings[key] = 'messages'
|
||
}
|
||
|
||
updateDisplay()
|
||
if (Object.keys(changedSettings).length > 0) {
|
||
changedConfig.settings = changedSettings
|
||
}
|
||
storeConfigChanges(changedConfig)
|
||
}
|
||
|
||
/**
|
||
* Listen for storage changes while the options page is open and update its
|
||
* controls to reflect them. We only store settings the user has interacted
|
||
* with.
|
||
* @param {{[key: string]: chrome.storage.StorageChange}} storageChanges
|
||
*/
|
||
function onStorageChanged(storageChanges) {
|
||
let changes = Object.fromEntries(
|
||
Object.entries(storageChanges).map(([key, {newValue}]) => [key, newValue])
|
||
)
|
||
let {settings: settingsChanges, ...configChanges} = changes
|
||
Object.assign(config, configChanges)
|
||
Object.assign(config.settings, settingsChanges)
|
||
applyConfig()
|
||
}
|
||
|
||
function saveCustomCss() {
|
||
let customCss = $form.elements['customCss'].value
|
||
if (config.settings.customCss == customCss) return
|
||
config.settings.customCss = customCss
|
||
storeConfigChanges({settings: {customCss}})
|
||
}
|
||
|
||
function shouldDisplayHideQuotesFrom() {
|
||
return config.settings.mutableQuoteTweets && config.settings.hideQuotesFrom.length > 0
|
||
}
|
||
|
||
function shouldDisplayMutedQuotes() {
|
||
return config.settings.mutableQuoteTweets && config.settings.mutedQuotes.length > 0
|
||
}
|
||
|
||
/**
|
||
* @param {Partial<import("./types").StoredConfig>} changes
|
||
*/
|
||
async function storeConfigChanges(changes) {
|
||
/** @type {Partial<import("./types").StoredConfig>} */
|
||
let changesToStore = {}
|
||
for (let key of Object.keys(defaultConfig)) {
|
||
if (Object.hasOwn(changes, key)) {
|
||
changesToStore[key] = changes[key]
|
||
}
|
||
}
|
||
try {
|
||
if (changes.settings) {
|
||
let {settings: storedSettings} = await get({settings: {}})
|
||
changesToStore.settings = {...storedSettings, ...changes.settings}
|
||
}
|
||
} catch(e) {
|
||
console.error('[options] error merging settings change', e)
|
||
}
|
||
try {
|
||
await set(changesToStore)
|
||
} catch(e) {
|
||
console.error('[options] error storing config change', e)
|
||
}
|
||
}
|
||
|
||
function updateCheckboxGroups() {
|
||
for (let [group, checkboxNames] of checkboxGroups.entries()) {
|
||
let checkedCount = checkboxNames.filter(name => config.settings[name]).length
|
||
$form.elements[group].checked = checkedCount == checkboxNames.length
|
||
$form.elements[group].indeterminate = checkedCount > 0 && checkedCount < checkboxNames.length;
|
||
}
|
||
}
|
||
|
||
function updateDisplay() {
|
||
$body.classList.toggle('chronological', config.settings.defaultToFollowing)
|
||
$body.classList.toggle('debugging', config.debug)
|
||
$body.classList.toggle('disabled', !config.enabled)
|
||
$body.classList.toggle('disabledHomeTimeline', config.settings.disableHomeTimeline)
|
||
$body.classList.toggle('fullWidthContent', config.settings.fullWidthContent)
|
||
$body.classList.toggle('hidingBookmarkButton', config.settings.hideBookmarkButton)
|
||
$body.classList.toggle('hidingExploreNav', config.settings.hideExploreNav)
|
||
$body.classList.toggle('hidingMetrics', config.settings.hideMetrics)
|
||
$body.classList.toggle('hidingNotifications', config.settings.hideNotifications == 'hide')
|
||
$body.classList.toggle('hidingQuotesFrom', shouldDisplayHideQuotesFrom())
|
||
$body.classList.toggle('hidingSuggestedFollows', config.settings.hideSidebarContent || config.settings.hideSuggestedFollows)
|
||
$body.classList.toggle('hidingTwitterBlueReplies', config.settings.hidePremiumReplies)
|
||
$body.classList.toggle('mutingQuotes', shouldDisplayMutedQuotes())
|
||
$body.classList.toggle('showingBlueReplyFollowersCount', config.settings.showPremiumReplyFollowersCount)
|
||
$body.classList.toggle('showingSidebarContent', !config.settings.hideSidebarContent)
|
||
$body.classList.toggle('tweakingNewLayout', config.settings.tweakNewLayout)
|
||
$body.classList.toggle('uninvertedFollowButtons', config.settings.uninvertFollowButtons)
|
||
$showPremiumReplyFollowersCountLabel.textContent = chrome.i18n.getMessage(
|
||
'showPremiumReplyFollowersCountLabel',
|
||
formatFollowerCount(Number(config.settings.showPremiumReplyFollowersCountAmount))
|
||
)
|
||
updateHideQuotesFromDisplay()
|
||
updateMutedQuotesDisplay()
|
||
}
|
||
|
||
function updateHideQuotesFromDisplay() {
|
||
if (!shouldDisplayHideQuotesFrom()) return
|
||
|
||
$hideQuotesFromLabel.textContent =
|
||
chrome.i18n.getMessage('hideQuotesFromLabel', String(config.settings.hideQuotesFrom.length))
|
||
|
||
if (!$hideQuotesFromDetails.open) return
|
||
|
||
while ($hideQuotesFrom.hasChildNodes()) $hideQuotesFrom.firstChild.remove()
|
||
for (let user of config.settings.hideQuotesFrom) {
|
||
$hideQuotesFrom.appendChild(
|
||
h('section', null,
|
||
h('label', {className: 'button'},
|
||
h('span', null, `@${user}`),
|
||
h('button', {
|
||
type: 'button',
|
||
onclick() {
|
||
config.settings.hideQuotesFrom = config.settings.hideQuotesFrom.filter(u => u != user)
|
||
updateDisplay()
|
||
storeConfigChanges({settings: {hideQuotesFrom: config.settings.hideQuotesFrom}})
|
||
}
|
||
}, chrome.i18n.getMessage('unmuteButtonText'))
|
||
)
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
function updateMutedQuotesDisplay() {
|
||
if (!shouldDisplayMutedQuotes()) return
|
||
|
||
$mutedQuotesLabel.textContent = chrome.i18n.getMessage('mutedTweetsLabel', String(config.settings.mutedQuotes.length))
|
||
|
||
if (!$mutedQuotesDetails.open) return
|
||
|
||
while ($mutedQuotes.hasChildNodes()) $mutedQuotes.firstChild.remove()
|
||
|
||
config.settings.mutedQuotes.forEach(({user, time, text}, index) => {
|
||
$mutedQuotes.appendChild(
|
||
h('section', null,
|
||
h('label', {className: 'button mutedQuote'},
|
||
h('div', null,
|
||
user,
|
||
' – ',
|
||
new Intl.DateTimeFormat([], {dateStyle: 'medium'}).format(new Date(time)),
|
||
text && h('p', {className: 'mb-0'}, text),
|
||
),
|
||
h('button', {
|
||
type: 'button',
|
||
onclick: () => {
|
||
config.settings.mutedQuotes = config.settings.mutedQuotes.filter((_, i) => i != index)
|
||
set({mutedQuotes: config.settings.mutedQuotes})
|
||
updateDisplay()
|
||
},
|
||
}, chrome.i18n.getMessage('unmuteButtonText'))
|
||
)
|
||
)
|
||
)
|
||
})
|
||
}
|
||
|
||
function updateFormControls() {
|
||
for (let key of INTERNAL_CONFIG_FORM_KEYSET) {
|
||
updateFormControl($form.elements[key], config[key])
|
||
}
|
||
for (let key of Object.keys(config.settings)) {
|
||
if (key in $form.elements) {
|
||
updateFormControl($form.elements[key], config.settings[key])
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateFormControl($control, value) {
|
||
if ($control instanceof RadioNodeList) {
|
||
// If a checkbox displays in multiple sections, update them all
|
||
$control.forEach(input => /** @type {HTMLInputElement} */ (input).checked = value)
|
||
}
|
||
else if ($control.type == 'checkbox') {
|
||
$control.checked = value
|
||
}
|
||
else {
|
||
$control.value = value
|
||
}
|
||
}
|
||
//#endregion
|
||
|
||
//#region Main
|
||
async function main() {
|
||
/** @type {Partial<import("./types").StoredConfig>} */
|
||
let {settings: storedSettings = {}, ...storedConfig} = await get()
|
||
config = {
|
||
...defaultConfig,
|
||
...storedConfig,
|
||
settings: {...defaultSettings, ...storedSettings}
|
||
}
|
||
|
||
$body.classList.toggle('debug', config.debug === true)
|
||
$experiments.open = Boolean(config.settings.tweakNewLayout || config.settings.customCss)
|
||
$exportConfig.addEventListener('click', exportConfig)
|
||
$form.addEventListener('change', onFormChanged)
|
||
$hideQuotesFromDetails.addEventListener('toggle', updateHideQuotesFromDisplay)
|
||
$mutedQuotesDetails.addEventListener('toggle', updateMutedQuotesDisplay)
|
||
$saveCustomCssButton.addEventListener('click', saveCustomCss)
|
||
chrome.storage.onChanged.addListener(onStorageChanged)
|
||
|
||
if (!config.debug) {
|
||
let $version = document.querySelector('#version')
|
||
let $debugCountdown = document.querySelector('#debugCountdown')
|
||
let debugCountdown = 5
|
||
|
||
function onClick(e) {
|
||
if (e.target === $version || $version.contains(/** @type {Node} */ (e.target))) {
|
||
debugCountdown--
|
||
} else {
|
||
debugCountdown = 5
|
||
}
|
||
|
||
if (debugCountdown == 0) {
|
||
$body.classList.add('debug')
|
||
$debugCountdown.textContent = ''
|
||
$form.removeEventListener('click', onClick)
|
||
}
|
||
else if (debugCountdown <= 3) {
|
||
$debugCountdown.textContent = ` (${debugCountdown})`
|
||
}
|
||
}
|
||
|
||
$form.addEventListener('click', onClick)
|
||
}
|
||
|
||
applyConfig()
|
||
}
|
||
|
||
main()
|
||
|
||
// Open a port for the background page to detect when the options page is closed
|
||
chrome.runtime.connect({name: 'options'})
|
||
//#endregion
|