Compare commits

..
Author SHA1 Message Date
koloml 906959205e Merge pull request #189 from koloml/chore/sonar-fixes
Applying some of the suggestions from SonarQube
2026-10-17 05:51:52 +02:00
koloml 37a8e77f97 Sonar: Replacing regular for with for..of 2026-10-17 05:48:50 +02:00
koloml 971f9b8a60 Removed unused import 2026-10-17 05:47:22 +02:00
koloml 05349feafc Sonar: Check for undefined without typeof 2026-10-17 05:47:22 +02:00
koloml 304c824045 Sonar: Explicitly marking some members as readonly 2026-10-17 05:47:22 +02:00
koloml 0eafa074f5 Sonar: Accessing/removing data attributes only through dataset 2026-10-17 05:47:22 +02:00
koloml ace783afc3 Sonar: Removed useless void 2026-10-17 05:47:22 +02:00
koloml 72a0d64809 Merge pull request #188 from koloml/bugfix/duplicating-blacklisted-tags
Tags Toggler: Prevent duplicated tags from appearing, only reveal existing blacklisted tags
2026-10-17 05:46:29 +02:00
koloml c34c650005 Adding general guard to prevent duplicates altogether 2026-10-17 05:15:44 +02:00
koloml af348621df Only reveal invalid blacklisted tags that actually present 2026-10-17 05:15:11 +02:00
24 changed files with 69 additions and 162 deletions

No files matched your search

+1 -2
View File
@@ -30,8 +30,7 @@
],
"js": [
"src/content/deps/amd.ts",
"src/content/user-details.ts",
"src/content/fun.ts"
"src/content/user-details.ts"
]
},
{
+1 -1
View File
@@ -77,7 +77,7 @@ export class BaseComponent<ContainerType extends HTMLElement = HTMLElement> {
): () => void {
this.#container.addEventListener(event, listener, options);
return () => void this.#container.removeEventListener(event, listener, options);
return () => this.#container.removeEventListener(event, listener, options);
}
/**
@@ -11,15 +11,15 @@ import type { TaggingProfilePopup } from "$content/components/extension/profiles
export class FullscreenViewer extends BaseComponent {
#currentMediaBox: MediaBox | null = null;
#videoElement: HTMLVideoElement = document.createElement('video');
#imageElement: HTMLImageElement = document.createElement('img');
#spinnerElement: HTMLElement = document.createElement('i');
#sizeSelectorElement: HTMLSelectElement = document.createElement('select');
#closeButtonElement: HTMLElement = document.createElement('i');
#tagsPopupElement: HTMLElement = document.createElement('div');
#imageTagsToggler: TagsListToggler = TagsListToggler.create();
#profileTagsToggler: TagsListToggler = TagsListToggler.create();
#tagsPopupHider: HTMLElement = document.createElement('div');
readonly #videoElement: HTMLVideoElement = document.createElement('video');
readonly #imageElement: HTMLImageElement = document.createElement('img');
readonly #spinnerElement: HTMLElement = document.createElement('i');
readonly #sizeSelectorElement: HTMLSelectElement = document.createElement('select');
readonly #closeButtonElement: HTMLElement = document.createElement('i');
readonly #tagsPopupElement: HTMLElement = document.createElement('div');
readonly #imageTagsToggler: TagsListToggler = TagsListToggler.create();
readonly #profileTagsToggler: TagsListToggler = TagsListToggler.create();
readonly #tagsPopupHider: HTMLElement = document.createElement('div');
#activeProfileListenerUnsubscriber: UnsubscribeFunction | null = null;
#touchId: number | null = null;
#startX: number | null = null;
@@ -412,7 +412,7 @@ export class FullscreenViewer extends BaseComponent {
}
#togglePopupVisibility(maybeState: boolean | undefined = undefined) {
if (typeof maybeState === 'undefined') {
if (maybeState === undefined) {
const isHidden = this.#tagsPopupElement.classList.toggle(FullscreenViewer.#collapsedPopupState);
void FullscreenViewer.#preferences.fullscreenViewerPopupShown.set(!isHidden);
return;
@@ -425,21 +425,21 @@ export class FullscreenViewer extends BaseComponent {
return url.endsWith('.mp4') || url.endsWith('.webm');
}
static #preferences = new MiscPreferences();
static readonly #preferences = new MiscPreferences();
static #offsetProperty = '--offset';
static #opacityProperty = '--opacity';
static #shownState = 'shown';
static #swipeState = 'swiped';
static #minRequiredDistance = 50;
static #collapsedPopupState = 'is-collapsed';
static readonly #offsetProperty = '--offset';
static readonly #opacityProperty = '--opacity';
static readonly #shownState = 'shown';
static readonly #swipeState = 'swiped';
static readonly #minRequiredDistance = 50;
static readonly #collapsedPopupState = 'is-collapsed';
static #previewSizes: Record<FullscreenViewerSize, string> = {
static readonly #previewSizes: Record<FullscreenViewerSize, string> = {
full: 'Full',
large: 'Large',
medium: 'Medium',
small: 'Small'
}
static #fallbackSize = 'large';
static readonly #fallbackSize = 'large';
}
@@ -81,5 +81,5 @@ export class ImageShowFullscreenButton extends BaseComponent {
return viewer;
}
static #preferences = new MiscPreferences();
static readonly #preferences = new MiscPreferences();
}
@@ -5,8 +5,8 @@ import { createFontAwesomeIcon } from "$lib/dom-utils";
import { sortEntitiesByField } from "$lib/utils";
export default class EditorPresetsBlock extends BaseComponent {
#presetsTable = document.createElement('table');
#presetBlocks: PresetTableRow[] = [];
readonly #presetsTable = document.createElement('table');
readonly #presetBlocks: PresetTableRow[] = [];
#tags: Set<string> = new Set();
protected build() {
@@ -207,6 +207,6 @@ export default class PresetTableRow extends BaseComponent {
return new this(document.createElement('tr'), preset);
}
static #tagMissingClassName = 'is-missing';
static #presetWarningClassName = 'has-warning';
static readonly #tagMissingClassName = 'is-missing';
static readonly #presetWarningClassName = 'has-warning';
}
@@ -16,14 +16,14 @@ import { EVENT_TAG_TOGGLED } from "$content/components/events/tags-list-toggler-
export class TaggingProfilePopup extends BaseComponent {
#tagsListToggler = TagsListToggler.create();
readonly #tagsListToggler = TagsListToggler.create();
#activeProfile: TaggingProfile | null = null;
#mediaBoxTools: MediaBoxTools | null = null;
#isSubmissionLocked: boolean = false;
#isPlanningToSubmit: boolean = false;
#isSubmitting: boolean = false;
#tagsSubmissionTimer: Timeout | null = null;
#emitter = emitterAt(this);
readonly #emitter = emitterAt(this);
/**
* @protected
@@ -211,7 +211,7 @@ export class TaggingProfilePopup extends BaseComponent {
return;
}
this.#tagsListToggler.prependTags(tagsBlacklist, activeTags);
this.#tagsListToggler.prependTags(tagsBlacklist.filter(tagName => activeTags.has(tagName)), activeTags);
}
static create(): HTMLElement {
@@ -226,7 +226,7 @@ export class TaggingProfilePopup extends BaseComponent {
/**
* Controller with maintenance settings.
*/
static #preferences = new TaggingProfilesPreferences();
static readonly #preferences = new TaggingProfilesPreferences();
/**
* Subscribe to all necessary feeds to watch for every active profile change. Additionally, will execute the callback
@@ -298,9 +298,9 @@ export class TaggingProfilePopup extends BaseComponent {
});
}
static #scrapedAPI = new ScrapedAPI();
static readonly #scrapedAPI = new ScrapedAPI();
static #delayBeforeSubmissionMs = 500;
static readonly #delayBeforeSubmissionMs = 500;
/**
* Amount of pending submissions or NULL if logic was not yet initialized.
@@ -10,6 +10,7 @@ export default class TagsListToggler extends BaseComponent {
#invalidTagsList: Map<string, HTMLElement> = new Map();
#tagsToAdd: Set<string> = new Set();
#tagsToRemove: Set<string> = new Set();
#renderedTags: Set<string> = new Set();
protected build() {
this.container.classList.add('tags-list');
@@ -34,6 +35,7 @@ export default class TagsListToggler extends BaseComponent {
this.#tagsList = [];
this.#invalidTagsList = new Map();
this.#renderedTags = new Set();
this.#renderAndInsertTags(tags, activeTags);
}
@@ -58,14 +60,18 @@ export default class TagsListToggler extends BaseComponent {
#renderAndInsertTags(toggleableTags: string[], activeTags: Set<string>, shouldPrepend: boolean = false) {
toggleableTags.sort((a, b) => a.localeCompare(b));
let lastInserted: HTMLElement|null = null;
let lastInserted: HTMLElement | null = null;
const hasStoredChanges = this.hasChanges;
for (let tagIndex = 0; tagIndex < toggleableTags.length; tagIndex++) {
const tagName = toggleableTags[tagIndex];
const tagElement = TagsListToggler.#buildTagElement(tagName);
for (const tagName of toggleableTags) {
if (this.#renderedTags.has(tagName)) {
continue;
}
this.#renderedTags.add(tagName);
const tagElement = TagsListToggler.#buildTagElement(tagName);
this.#tagsList.push(tagElement);
const isPresent = activeTags.has(tagName);
@@ -206,7 +212,6 @@ export default class TagsListToggler extends BaseComponent {
*/
static #markTagElementWithCategory(tagElement: HTMLElement, category: string) {
tagElement.dataset.tagCategory = category;
tagElement.setAttribute('data-tag-category', category);
}
static create(): TagsListToggler {
@@ -214,11 +219,11 @@ export default class TagsListToggler extends BaseComponent {
return new TagsListToggler(tagsListContainer);
}
static #containsClass = 'is-present';
static #missingClass = 'is-missing';
static readonly #containsClass = 'is-present';
static readonly #missingClass = 'is-missing';
static #addedClass = 'is-added';
static #removedClass = 'is-removed';
static readonly #addedClass = 'is-added';
static readonly #removedClass = 'is-removed';
}
export class BlackListedTagsEncounteredError extends Error {
@@ -112,14 +112,14 @@ export class BlockCommunication extends BaseComponent {
);
}
static #preferences = new TagsPreferences();
static readonly #preferences = new TagsPreferences();
/**
* Map of links to their original texts. These texts need to be stored here to make them restorable. Keys is a link
* element and value is a text.
* @private
*/
static #originalTagLinkTexts: WeakMap<HTMLElement, string> = new WeakMap();
static readonly #originalTagLinkTexts: WeakMap<HTMLElement, string> = new WeakMap();
static findAndInitializeAll() {
for (const container of document.querySelectorAll<HTMLElement>('.block.communication')) {
@@ -88,14 +88,12 @@ export class TagDropdown extends BaseComponent {
// Make sure original category is properly stored.
this.originalCategory;
this.container.dataset.tagCategory = targetCategory;
if (targetCategory) {
this.container.setAttribute('data-tag-category', targetCategory);
this.container.dataset.tagCategory = targetCategory;
return;
}
this.container.removeAttribute('data-tag-category');
delete this.container.dataset.tagCategory;
}
/**
@@ -203,7 +201,7 @@ export class TagDropdown extends BaseComponent {
await this.#activeProfile.save();
}
static #preferences = new TaggingProfilesPreferences();
static readonly #preferences = new TaggingProfilesPreferences();
/**
* Watch for changes to active profile.
@@ -262,8 +260,8 @@ export class TagDropdown extends BaseComponent {
return dropdownLink;
}
static #categoriesResolver = new CustomCategoriesResolver();
static #processedElements: WeakSet<HTMLElement> = new WeakSet();
static readonly #categoriesResolver = new CustomCategoriesResolver();
static readonly #processedElements: WeakSet<HTMLElement> = new WeakSet();
static #findAll(parentNode: ParentNode = document): NodeListOf<HTMLElement> {
return parentNode.querySelectorAll('.tag.dropdown');
+2 -3
View File
@@ -7,8 +7,8 @@ import EditorPresetsBlock from "$content/components/extension/presets/EditorPres
import { EVENT_PRESET_TAG_CHANGE_APPLIED, type PresetTagChange } from "$content/components/events/preset-block-events";
export class TagsForm extends BaseComponent {
#togglePresetsButton: HTMLButtonElement = document.createElement('button');
#presetsList = EditorPresetsBlock.create();
readonly #togglePresetsButton: HTMLButtonElement = document.createElement('button');
readonly #presetsList = EditorPresetsBlock.create();
#plainEditorTextarea: HTMLTextAreaElement | null = null;
#fancyEditorInput: HTMLInputElement | null = null;
#tagsSet: Set<string> = new Set();
@@ -127,7 +127,6 @@ export class TagsForm extends BaseComponent {
const categoryName = tagCategories.get(tagName)!;
tagElement.dataset.tagCategory = categoryName;
tagElement.setAttribute('data-tag-category', categoryName);
}
}
@@ -104,7 +104,7 @@ export default class DupeDiff extends BaseComponent {
*
* Keys are the icons and values are the patterns extension should check for each URL.
*/
static #iconsToPatternsMap = new Map<string[] | string, string[] | string>([
static readonly #iconsToPatternsMap = new Map<string[] | string, string[] | string>([
[
['artstation', brandsSubtype],
'artstation.com'
@@ -4,9 +4,9 @@ import DupeImage from "$content/components/philomena/dupe/DupeImage";
import { ratingTags } from "$config/tags";
export default class DupeReportRow extends BaseComponent {
#leftImage: DupeImage;
#rightImage: DupeImage;
#difference: DupeDiff;
readonly #leftImage: DupeImage;
readonly #rightImage: DupeImage;
readonly #difference: DupeDiff;
constructor(leftCell: HTMLElement, rightCell: HTMLElement, diffCell: HTMLElement, reportOptions: HTMLElement) {
super(reportOptions);
@@ -2,8 +2,8 @@ import { BaseComponent } from "$content/components/base/BaseComponent";
export class ImageListInfo extends BaseComponent {
#tagElement: HTMLElement | null = null;
#impliedTags: string[] = [];
#showUntaggedImplicationsButton: HTMLAnchorElement = document.createElement('a');
readonly #impliedTags: string[] = [];
readonly #showUntaggedImplicationsButton: HTMLAnchorElement = document.createElement('a');
protected build() {
const sectionAfterImage = this.container.querySelector('.tag-info__image + .flex__grow');
@@ -71,5 +71,5 @@ export class ImageListInfo extends BaseComponent {
location.assign(url.href);
}
static #implicationsStarterText = 'Implies:';
static readonly #implicationsStarterText = 'Implies:';
}
+1 -1
View File
@@ -38,7 +38,7 @@ amdLite.define = (name, dependencies, originalCallback) => {
// Workaround for the entry script not returning anything causing AMD-Lite to send warning about modules not
// being loaded/not existing.
return typeof callbackResult !== 'undefined' ? callbackResult : {};
return callbackResult !== undefined ? callbackResult : {};
});
// Schedule the auto run on the next available frame. Firefox and Chromium have a lot of differences in how they
-34
View File
@@ -1,34 +0,0 @@
import roundFeatherFontUrl from "../assets/icon/fonts/roundfeather-regular-1.001.ttf?url";
import FunPreferences from "$lib/extension/preferences/FunPreferences";
const preferences = new FunPreferences();
const url = (browser ?? chrome).runtime.getURL(roundFeatherFontUrl);
const avaliFontDefinitionStyleTag = document.createElement('style');
// language=CSS
avaliFontDefinitionStyleTag.textContent = `
@font-face {
font-family: Roundfeather;
url('${url}') format('truetype');
}
:root {
--font-family-base: Roundfeather, verdana, arial, helvetica, sans-serif !important;
--font-family-monospace: Roundfeather, "Droid Sans Mono", monospace !important;
}
`;
function refreshAvaliFont(isFontEnabled: boolean) {
if (!isFontEnabled) {
avaliFontDefinitionStyleTag.remove();
return;
}
document.head.append(avaliFontDefinitionStyleTag);
}
preferences.subscribe(settings => {
refreshAvaliFont(settings.useAvaliFont ?? false);
});
preferences.useAvaliFont.get().then(refreshAvaliFont);
+1 -1
View File
@@ -101,7 +101,7 @@ export default class BulkEntitiesTransporter {
&& Array.isArray(targetObject.elements);
}
static #transporters: TransportersMapping = {
static readonly #transporters: TransportersMapping = {
profiles: new EntitiesTransporter(TaggingProfile),
groups: new EntitiesTransporter(TagGroup),
presets: new EntitiesTransporter(TagEditorPreset),
@@ -5,9 +5,9 @@ import { emit } from "$content/components/events/comms";
import { EVENT_TAG_GROUP_RESOLVED } from "$content/components/events/tag-dropdown-events";
export default class CustomCategoriesResolver {
#exactGroupMatches = new Map<string, TagGroup>();
#regExpGroupMatches = new Map<RegExp, TagGroup>();
#tagDropdowns: TagDropdown[] = [];
readonly #exactGroupMatches = new Map<string, TagGroup>();
readonly #regExpGroupMatches = new Map<RegExp, TagGroup>();
readonly #tagDropdowns: TagDropdown[] = [];
#nextQueuedUpdate: Timeout | null = null;
constructor() {
@@ -125,5 +125,5 @@ export default class CustomCategoriesResolver {
);
}
static #unprocessedTagsTimeout = 0;
static readonly #unprocessedTagsTimeout = 0;
}
-1
View File
@@ -2,7 +2,6 @@ import { validateImportedEntity } from "$lib/extension/transporting/validators";
import { exportEntityToObject } from "$lib/extension/transporting/exporters";
import StorageEntity from "$lib/extension/base/StorageEntity";
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
import type { ImportableElement } from "$lib/extension/transporting/importables";
/**
* Status of the last import.
@@ -1,16 +0,0 @@
import CacheablePreferences, { PreferenceField, type WithFields } from "$lib/extension/base/CacheablePreferences";
interface FunPreferencesFields {
useAvaliFont: boolean;
}
export default class FunPreferences extends CacheablePreferences<FunPreferencesFields> implements WithFields<FunPreferencesFields> {
constructor() {
super('fun');
}
readonly useAvaliFont = new PreferenceField(this, {
field: "useAvaliFont",
defaultValue: false,
});
}
@@ -78,5 +78,5 @@ export default class PostParser extends PageParser {
);
}
static tagsInputName = 'image[tag_input]';
static readonly tagsInputName = 'image[tag_input]';
}
-1
View File
@@ -11,7 +11,6 @@
<hr>
<MenuItem href="/preferences/tags">Tagging</MenuItem>
<MenuItem href="/preferences/misc">Misc & Tools</MenuItem>
<MenuItem href="/preferences/fun">Fun</MenuItem>
<hr>
<MenuItem href="/preferences/debug">Debug</MenuItem>
</Menu>
-31
View File
@@ -1,31 +0,0 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import { useAvaliFont } from "$stores/preferences/fun";
import Notice from "$components/ui/Notice.svelte";
</script>
<Menu>
<MenuItem href="/preferences" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<Notice level="warning">
These features are not to mean be really useful. Just a little bit of fun.
</Notice>
<Menu>
<hr>
</Menu>
<FormContainer>
<FormControl>
<CheckboxField bind:checked={$useAvaliFont}>
<strong>Display everything in Avali Scratch</strong><br>
<small>
This will apply the special font across the whole website, making it unusable unless you can read Avali Scratch
alphabet. Unfortunately, text will flicker on every page load.
</small>
</CheckboxField>
</FormControl>
</FormContainer>
-11
View File
@@ -1,11 +0,0 @@
import FunPreferences from "$lib/extension/preferences/FunPreferences";
import { PreferenceSync } from "$lib/store-sync";
import { writable } from "svelte/store";
export const useAvaliFont = writable<boolean>(false);
const preferences = new FunPreferences();
void PreferenceSync.for(preferences)
.connect(preferences.useAvaliFont, useAvaliFont, {updateToStore: Boolean})
.startSync()