mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-24 07:12:57 +00:00
Merge pull request #67 from koloml/feature/option-to-auto-remove-invalid-tags
Detect invalid blacklisted tags from Furbooru and allow to remove them manually or automatically
This commit is contained in:
@@ -48,6 +48,7 @@ function wrapScriptIntoIIFE() {
|
||||
*/
|
||||
function makeAliases(rootDir) {
|
||||
return {
|
||||
"$config": path.resolve(rootDir, 'src/config'),
|
||||
"$lib": path.resolve(rootDir, 'src/lib'),
|
||||
"$entities": path.resolve(rootDir, 'src/lib/extension/entities'),
|
||||
"$styles": path.resolve(rootDir, 'src/styles'),
|
||||
|
||||
66
src/config/tags.ts
Normal file
66
src/config/tags.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const tagsBlacklist: string[] = [
|
||||
"anthro art",
|
||||
"anthro artist",
|
||||
"anthro cute",
|
||||
"anthro furry",
|
||||
"anthro nsfw",
|
||||
"anthro oc",
|
||||
"anthroart",
|
||||
"anthroartist",
|
||||
"anthrofurry",
|
||||
"anthronsfw",
|
||||
"anthrooc",
|
||||
"art",
|
||||
"artist",
|
||||
"artwork",
|
||||
"cringe",
|
||||
"cringeworthy",
|
||||
"cute art",
|
||||
"cute artwork",
|
||||
"cute furry",
|
||||
"downvotes galore",
|
||||
"drama in comments",
|
||||
"drama in the comments",
|
||||
"fandom",
|
||||
"furries",
|
||||
"furry anthro",
|
||||
"furry art",
|
||||
"furry artist",
|
||||
"furry artwork",
|
||||
"furry character",
|
||||
"furry community",
|
||||
"furry cute",
|
||||
"furry fandom",
|
||||
"furry nsfw",
|
||||
"furry oc",
|
||||
"furryanthro",
|
||||
"furryart",
|
||||
"furryartist",
|
||||
"furryartwork",
|
||||
"furrynsfw",
|
||||
"furryoc",
|
||||
"image",
|
||||
"no tag",
|
||||
"not tagged",
|
||||
"notag",
|
||||
"notags",
|
||||
"nsfw anthro",
|
||||
"nsfw art",
|
||||
"nsfw artist",
|
||||
"nsfw artwork",
|
||||
"nsfw",
|
||||
"nsfwanthro",
|
||||
"nsfwart",
|
||||
"nsfwartist",
|
||||
"nsfwartwork",
|
||||
"paywall",
|
||||
"rcf community",
|
||||
"sfw",
|
||||
"solo oc",
|
||||
"tag me",
|
||||
"tag needed",
|
||||
"tag your shit",
|
||||
"tagme",
|
||||
"upvotes galore",
|
||||
"wall of faves"
|
||||
];
|
||||
@@ -3,6 +3,16 @@ import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
|
||||
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
|
||||
import {getComponent} from "$lib/components/base/ComponentUtils.js";
|
||||
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI.js";
|
||||
import {tagsBlacklist} from "$config/tags.ts";
|
||||
|
||||
class BlackListedTagsEncounteredError extends Error {
|
||||
/**
|
||||
* @param {string} tagName
|
||||
*/
|
||||
constructor(tagName) {
|
||||
super(`This tag is blacklisted and prevents submission: ${tagName}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaintenancePopup extends BaseComponent {
|
||||
/** @type {HTMLElement} */
|
||||
@@ -11,6 +21,9 @@ export class MaintenancePopup extends BaseComponent {
|
||||
/** @type {HTMLElement[]} */
|
||||
#tagsList = [];
|
||||
|
||||
/** @type {Map<string, HTMLElement>} */
|
||||
#suggestedInvalidTags = new Map();
|
||||
|
||||
/** @type {MaintenanceProfile|null} */
|
||||
#activeProfile = null;
|
||||
|
||||
@@ -89,11 +102,16 @@ export class MaintenancePopup extends BaseComponent {
|
||||
/** @type {string[]} */
|
||||
const activeProfileTagsList = this.#activeProfile?.settings.tags || [];
|
||||
|
||||
for (let tagElement of this.#tagsList) {
|
||||
for (const tagElement of this.#tagsList) {
|
||||
tagElement.remove();
|
||||
}
|
||||
|
||||
for (const tagElement of this.#suggestedInvalidTags.values()) {
|
||||
tagElement.remove();
|
||||
}
|
||||
|
||||
this.#tagsList = new Array(activeProfileTagsList.length);
|
||||
this.#suggestedInvalidTags.clear();
|
||||
|
||||
const currentPostTags = this.#mediaBoxTools.mediaBox.tagsAndAliases;
|
||||
|
||||
@@ -109,6 +127,12 @@ export class MaintenancePopup extends BaseComponent {
|
||||
tagElement.classList.toggle('is-present', isPresent);
|
||||
tagElement.classList.toggle('is-missing', !isPresent);
|
||||
tagElement.classList.toggle('is-aliased', isPresent && currentPostTags.get(tagName) !== tagName);
|
||||
|
||||
// Just to prevent duplication, we need to include this tag to the map of suggested invalid tags
|
||||
if (tagsBlacklist.includes(tagName)) {
|
||||
MaintenancePopup.#markTagAsInvalid(tagElement);
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,6 +212,8 @@ export class MaintenancePopup extends BaseComponent {
|
||||
|
||||
let maybeTagsAndAliasesAfterUpdate;
|
||||
|
||||
const shouldAutoRemove = await MaintenancePopup.#maintenanceSettings.resolveStripBlacklistedTags();
|
||||
|
||||
try {
|
||||
maybeTagsAndAliasesAfterUpdate = await MaintenancePopup.#scrapedAPI.updateImageTags(
|
||||
this.#mediaBoxTools.mediaBox.imageId,
|
||||
@@ -200,11 +226,27 @@ export class MaintenancePopup extends BaseComponent {
|
||||
tagsList.add(tagName);
|
||||
}
|
||||
|
||||
if (shouldAutoRemove) {
|
||||
for (let tagName of tagsBlacklist) {
|
||||
tagsList.delete(tagName);
|
||||
}
|
||||
} else {
|
||||
for (let tagName of tagsList) {
|
||||
if (tagsBlacklist.includes(tagName)) {
|
||||
throw new BlackListedTagsEncounteredError(tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagsList;
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('Tags submission failed:', e);
|
||||
if (e instanceof BlackListedTagsEncounteredError) {
|
||||
this.#revealInvalidTags();
|
||||
} else {
|
||||
console.warn('Tags submission failed:', e);
|
||||
}
|
||||
|
||||
MaintenancePopup.#notifyAboutPendingSubmission(false);
|
||||
this.emit('maintenance-state-change', 'failed');
|
||||
@@ -228,6 +270,36 @@ export class MaintenancePopup extends BaseComponent {
|
||||
this.#isSubmitting = false;
|
||||
}
|
||||
|
||||
#revealInvalidTags() {
|
||||
const tagsAndAliases = this.#mediaBoxTools.mediaBox.tagsAndAliases;
|
||||
|
||||
if (!tagsAndAliases) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstTagInList = this.#tagsList[0];
|
||||
|
||||
for (let tagName of tagsBlacklist) {
|
||||
if (tagsAndAliases.has(tagName)) {
|
||||
if (this.#suggestedInvalidTags.has(tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagElement = MaintenancePopup.#buildTagElement(tagName);
|
||||
MaintenancePopup.#markTagAsInvalid(tagElement);
|
||||
tagElement.classList.add('is-present');
|
||||
|
||||
this.#suggestedInvalidTags.set(tagName, tagElement);
|
||||
|
||||
if (firstTagInList && firstTagInList.isConnected) {
|
||||
this.#tagsListElement.insertBefore(tagElement, firstTagInList);
|
||||
} else {
|
||||
this.#tagsListElement.appendChild(tagElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
@@ -248,6 +320,15 @@ export class MaintenancePopup extends BaseComponent {
|
||||
return tagElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the tag with red color.
|
||||
* @param {HTMLElement} tagElement Element to mark.
|
||||
*/
|
||||
static #markTagAsInvalid(tagElement) {
|
||||
tagElement.dataset.tagCategory = 'error';
|
||||
tagElement.setAttribute('data-tag-category', 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller with maintenance settings.
|
||||
* @type {MaintenanceSettings}
|
||||
|
||||
@@ -3,6 +3,7 @@ import CacheableSettings from "$lib/extension/base/CacheableSettings.ts";
|
||||
|
||||
interface MaintenanceSettingsFields {
|
||||
activeProfile: string | null;
|
||||
stripBlacklistedTags: boolean;
|
||||
}
|
||||
|
||||
export default class MaintenanceSettings extends CacheableSettings<MaintenanceSettingsFields> {
|
||||
@@ -27,6 +28,10 @@ export default class MaintenanceSettings extends CacheableSettings<MaintenanceSe
|
||||
.find(profile => profile.id === resolvedProfileId) || null;
|
||||
}
|
||||
|
||||
async resolveStripBlacklistedTags() {
|
||||
return this._resolveSetting('stripBlacklistedTags', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active maintenance profile.
|
||||
*
|
||||
@@ -36,4 +41,8 @@ export default class MaintenanceSettings extends CacheableSettings<MaintenanceSe
|
||||
async setActiveProfileId(profileId: string | null): Promise<void> {
|
||||
await this._writeSetting("activeProfile", profileId);
|
||||
}
|
||||
|
||||
async setStripBlacklistedTags(isEnabled: boolean) {
|
||||
await this._writeSetting('stripBlacklistedTags', isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<Menu>
|
||||
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/preferences/tags">Tagging</MenuItem>
|
||||
<MenuItem href="/preferences/search">Search</MenuItem>
|
||||
<MenuItem href="/preferences/misc">Misc & Tools</MenuItem>
|
||||
<hr>
|
||||
|
||||
20
src/routes/preferences/tags/+page.svelte
Normal file
20
src/routes/preferences/tags/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
|
||||
import FormContainer from "$components/ui/forms/FormContainer.svelte";
|
||||
import FormControl from "$components/ui/forms/FormControl.svelte";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import { stripBlacklistedTagsEnabled } from "$stores/maintenance-preferences.ts";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem icon="arrow-left" href="/preferences">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
<FormContainer>
|
||||
<FormControl>
|
||||
<CheckboxField bind:checked={$stripBlacklistedTagsEnabled}>
|
||||
Automatically remove black-listed tags from the images
|
||||
</CheckboxField>
|
||||
</FormControl>
|
||||
</FormContainer>
|
||||
18
src/stores/maintenance-preferences.ts
Normal file
18
src/stores/maintenance-preferences.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {writable} from "svelte/store";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.ts";
|
||||
|
||||
export const stripBlacklistedTagsEnabled = writable(true);
|
||||
|
||||
const maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
Promise
|
||||
.all([
|
||||
maintenanceSettings.resolveStripBlacklistedTags().then(v => stripBlacklistedTagsEnabled.set(v ?? true))
|
||||
])
|
||||
.then(() => {
|
||||
maintenanceSettings.subscribe(settings => {
|
||||
stripBlacklistedTagsEnabled.set(typeof settings.stripBlacklistedTags === 'boolean' ? settings.stripBlacklistedTags : true);
|
||||
});
|
||||
|
||||
stripBlacklistedTagsEnabled.subscribe(v => maintenanceSettings.setStripBlacklistedTags(v));
|
||||
});
|
||||
@@ -25,6 +25,27 @@ $tag-background: #1b3c21;
|
||||
$tag-count-background: #2d6236;
|
||||
$tag-text: #4aa158;
|
||||
|
||||
$tag-rating-text: #418dd9;
|
||||
$tag-rating-background: darken($tag-rating-text, 35%);
|
||||
$tag-spoiler-text: #d49b39;
|
||||
$tag-spoiler-background: darken($tag-spoiler-text, 34%);
|
||||
$tag-origin-text: #6f66d6;
|
||||
$tag-origin-background: darken($tag-origin-text, 40%);
|
||||
$tag-oc-text: #b157b7;
|
||||
$tag-oc-background: darken($tag-oc-text, 33%);
|
||||
$tag-error-text: #d45460;
|
||||
$tag-error-background: desaturate(darken($tag-error-text, 38%), 6%);
|
||||
$tag-character-text: #4aaabf;
|
||||
$tag-character-background: darken($tag-character-text, 33%);
|
||||
$tag-content-official-text: #b9b541;
|
||||
$tag-content-official-background: desaturate(darken($tag-content-official-text, 29%),2%);
|
||||
$tag-content-fanmade-text: #cc8eb5;
|
||||
$tag-content-fanmade-background: darken($tag-content-fanmade-text, 40%);
|
||||
$tag-species-text: #b16b50;
|
||||
$tag-species-background: darken($tag-species-text, 35%);
|
||||
$tag-body-type-text: #b8b8b8;
|
||||
$tag-body-type-background: desaturate(darken($tag-body-type-text, 35%), 10%);
|
||||
|
||||
$input-background: #26232d;
|
||||
$input-border: #5c5a61;
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
color: colors.$tag-background;
|
||||
}
|
||||
|
||||
&[data-tag-category=error]:hover {
|
||||
background: colors.$tag-error-text;
|
||||
color: colors.$tag-error-background;
|
||||
}
|
||||
|
||||
&.is-missing:not(.is-added),
|
||||
&.is-present.is-removed {
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -14,6 +14,7 @@ const config = {
|
||||
name: Date.now().toString(36)
|
||||
},
|
||||
alias: {
|
||||
"$config": "./src/config",
|
||||
"$components": "./src/components",
|
||||
"$styles": "./src/styles",
|
||||
"$stores": "./src/stores",
|
||||
|
||||
Reference in New Issue
Block a user