1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-23 23:02:58 +00:00

Merge pull request #65 from koloml/release/0.3.4

Release: 0.3.4
This commit is contained in:
2024-12-16 16:43:16 +04:00
committed by GitHub
26 changed files with 362 additions and 179 deletions

View File

@@ -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'),

View File

@@ -1,7 +1,7 @@
{
"name": "Furbooru Tagging Assistant",
"description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.",
"version": "0.3.3",
"version": "0.3.4",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city"
@@ -39,6 +39,9 @@
],
"js": [
"src/content/header.js"
],
"css": [
"src/styles/content/header.scss"
]
},
{

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.3.3",
"version": "0.3.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "furbooru-tagging-assistant",
"version": "0.3.3",
"version": "0.3.4",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.1",
"lz-string": "^1.5.0"

View File

@@ -1,6 +1,6 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.3.3",
"version": "0.3.4",
"private": true,
"scripts": {
"build": "npm run build:popup && npm run build:extension",

66
src/config/tags.ts Normal file
View 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"
];

View File

@@ -1,6 +1,6 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import MiscSettings from "$lib/extension/settings/MiscSettings.js";
import MiscSettings from "$lib/extension/settings/MiscSettings.ts";
import {FullscreenViewer} from "$lib/components/FullscreenViewer.js";
export class ImageShowFullscreenButton extends BaseComponent {

View File

@@ -1,8 +1,18 @@
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.ts";
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}

View File

@@ -1,6 +1,6 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {QueryLexer, QuotedTermToken, TermToken, Token} from "$lib/booru/search/QueryLexer.js";
import SearchSettings from "$lib/extension/settings/SearchSettings.js";
import SearchSettings from "$lib/extension/settings/SearchSettings.ts";
export class SearchWrapper extends BaseComponent {
/** @type {HTMLInputElement|null} */
@@ -289,6 +289,10 @@ export class SearchWrapper extends BaseComponent {
suggestionItem.dataset.value = suggestedTerm;
suggestionItem.innerText = suggestedTerm;
const propertyIcon = document.createElement('i');
propertyIcon.classList.add('fa', 'fa-info-circle');
suggestionItem.insertAdjacentElement('afterbegin', propertyIcon);
suggestionItem.addEventListener('mouseover', () => {
SearchWrapper.#findAndResetSelectedSuggestion(suggestionItem);
suggestionItem.classList.add('autocomplete__item--selected');

View File

@@ -1,6 +1,6 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.ts";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
const isTagEditorProcessedKey = Symbol();

View File

@@ -1,20 +1,20 @@
import ConfigurationController from "$lib/extension/ConfigurationController.js";
export default class CacheableSettings {
/** @type {ConfigurationController} */
#controller;
/** @type {Map<string, any>} */
#cachedValues = new Map();
/** @type {function[]} */
#disposables = [];
export default class CacheableSettings<Fields> {
#controller: ConfigurationController;
#cachedValues: Map<keyof Fields, any> = new Map();
#disposables: Function[] = [];
constructor(settingsNamespace) {
constructor(settingsNamespace: string) {
this.#controller = new ConfigurationController(settingsNamespace);
this.#disposables.push(
this.#controller.subscribeToChanges(settings => {
for (const key of Object.keys(settings)) {
this.#cachedValues.set(key, settings[key]);
this.#cachedValues.set(
key as keyof Fields,
settings[key]
);
}
})
);
@@ -27,12 +27,12 @@ export default class CacheableSettings {
* @return {Promise<SettingType>}
* @protected
*/
async _resolveSetting(settingName, defaultValue) {
protected async _resolveSetting<Key extends keyof Fields>(settingName: Key, defaultValue: Fields[Key]): Promise<Fields[Key]> {
if (this.#cachedValues.has(settingName)) {
return this.#cachedValues.get(settingName);
}
const settingValue = await this.#controller.readSetting(settingName, defaultValue);
const settingValue = await this.#controller.readSetting(settingName as string, defaultValue);
this.#cachedValues.set(settingName, settingValue);
@@ -40,13 +40,12 @@ export default class CacheableSettings {
}
/**
* @param {string} settingName Name of the setting to write.
* @param {*} value Value to pass.
* @param {boolean} [force=false] Ignore the cache and force the update.
* @return {Promise<void>}
* @param settingName Name of the setting to write.
* @param value Value to pass.
* @param force Ignore the cache and force the update.
* @protected
*/
async _writeSetting(settingName, value, force = false) {
async _writeSetting<Key extends keyof Fields>(settingName: Key, value: Fields[Key], force: boolean = false): Promise<void> {
if (
!force
&& this.#cachedValues.has(settingName)
@@ -55,7 +54,10 @@ export default class CacheableSettings {
return;
}
return this.#controller.writeSetting(settingName, value);
return this.#controller.writeSetting(
settingName as string,
value
);
}
/**
@@ -63,8 +65,8 @@ export default class CacheableSettings {
* @param {function(Object): void} callback Callback which will receive list of settings.
* @return {function(): void} Unsubscribe function.
*/
subscribe(callback) {
const unsubscribeCallback = this.#controller.subscribeToChanges(callback);
subscribe(callback: (settings: Partial<Fields>) => void): () => void {
const unsubscribeCallback = this.#controller.subscribeToChanges(callback as (fields: Record<string, any>) => void);
this.#disposables.push(unsubscribeCallback);

View File

@@ -1,63 +0,0 @@
import ConfigurationController from "$lib/extension/ConfigurationController.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import CacheableSettings from "$lib/extension/base/CacheableSettings.js";
export default class MaintenanceSettings extends CacheableSettings {
constructor() {
super("maintenance");
}
/**
* Set the active maintenance profile.
*
* @return {Promise<string|null>}
*/
async resolveActiveProfileId() {
return this._resolveSetting("activeProfile", null);
}
/**
* Get the active maintenance profile if it is set.
*
* @return {Promise<MaintenanceProfile|null>}
*/
async resolveActiveProfileAsObject() {
const resolvedProfileId = await this.resolveActiveProfileId();
return (await MaintenanceProfile.readAll())
.find(profile => profile.id === resolvedProfileId) || null;
}
/**
* Set the active maintenance profile.
*
* @param {string|null} profileId ID of the profile to set as active. If `null`, the active profile will be considered
* unset.
*
* @return {Promise<void>}
*/
async setActiveProfileId(profileId) {
await this._writeSetting("activeProfile", profileId);
}
/**
* Subscribe to the changes in the maintenance-related settings.
*
* @param {function(MaintenanceSettingsObject): void} callback Callback to call when the settings change. The new
* settings are passed as an argument.
*
* @return {function(): void} Unsubscribe function.
*/
subscribe(callback) {
return super.subscribe(settings => {
callback({
activeProfile: settings.activeProfile || null,
});
});
}
}
/**
* @typedef {Object} MaintenanceSettingsObject
* @property {string|null} activeProfile
*/

View File

@@ -0,0 +1,48 @@
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import CacheableSettings from "$lib/extension/base/CacheableSettings.ts";
interface MaintenanceSettingsFields {
activeProfile: string | null;
stripBlacklistedTags: boolean;
}
export default class MaintenanceSettings extends CacheableSettings<MaintenanceSettingsFields> {
constructor() {
super("maintenance");
}
/**
* Set the active maintenance profile.
*/
async resolveActiveProfileId() {
return this._resolveSetting("activeProfile", null);
}
/**
* Get the active maintenance profile if it is set.
*/
async resolveActiveProfileAsObject(): Promise<MaintenanceProfile | null> {
const resolvedProfileId = await this.resolveActiveProfileId();
return (await MaintenanceProfile.readAll())
.find(profile => profile.id === resolvedProfileId) || null;
}
async resolveStripBlacklistedTags() {
return this._resolveSetting('stripBlacklistedTags', false);
}
/**
* Set the active maintenance profile.
*
* @param profileId ID of the profile to set as active. If `null`, the active profile will be considered
* unset.
*/
async setActiveProfileId(profileId: string | null): Promise<void> {
await this._writeSetting("activeProfile", profileId);
}
async setStripBlacklistedTags(isEnabled: boolean) {
await this._writeSetting('stripBlacklistedTags', isEnabled);
}
}

View File

@@ -1,39 +0,0 @@
import CacheableSettings from "$lib/extension/base/CacheableSettings.js";
export default class MiscSettings extends CacheableSettings {
constructor() {
super("misc");
}
/**
* @return {Promise<boolean>}
*/
async resolveFullscreenViewerEnabled() {
return this._resolveSetting("fullscreenViewer", true);
}
/**
* @param {boolean} isEnabled
* @return {Promise<void>}
*/
async setFullscreenViewerEnabled(isEnabled) {
return this._writeSetting("fullscreenViewer", isEnabled);
}
/**
* @param {function(MiscSettingsObject): void} callback
* @return {function(): void}
*/
subscribe(callback) {
return super.subscribe(settings => {
callback({
fullscreenViewer: settings.fullscreenViewer ?? true,
})
});
}
}
/**
* @typedef {Object} MiscSettingsObject
* @property {boolean} fullscreenViewer
*/

View File

@@ -0,0 +1,19 @@
import CacheableSettings from "$lib/extension/base/CacheableSettings.ts";
interface MiscSettingsFields {
fullscreenViewer: boolean;
}
export default class MiscSettings extends CacheableSettings<MiscSettingsFields> {
constructor() {
super("misc");
}
async resolveFullscreenViewerEnabled() {
return this._resolveSetting("fullscreenViewer", true);
}
async setFullscreenViewerEnabled(isEnabled: boolean) {
return this._writeSetting("fullscreenViewer", isEnabled);
}
}

View File

@@ -1,42 +0,0 @@
import CacheableSettings from "$lib/extension/base/CacheableSettings.js";
export default class SearchSettings extends CacheableSettings {
constructor() {
super("search");
}
async resolvePropertiesSuggestionsEnabled() {
return this._resolveSetting("suggestProperties", false);
}
async resolvePropertiesSuggestionsPosition() {
return this._resolveSetting("suggestPropertiesPosition", "start");
}
async setPropertiesSuggestions(isEnabled) {
return this._writeSetting("suggestProperties", isEnabled);
}
async setPropertiesSuggestionsPosition(position) {
return this._writeSetting("suggestPropertiesPosition", position);
}
/**
* @param {function(SearchSettingsObject): void} callback
* @return {function(): void}
*/
subscribe(callback) {
return super.subscribe(rawSettings => {
callback({
suggestProperties: rawSettings.suggestProperties ?? false,
suggestPropertiesPosition: rawSettings.suggestPropertiesPosition ?? "start",
});
});
}
}
/**
* @typedef {Object} SearchSettingsObject
* @property {boolean} suggestProperties
* @property {"start"|"end"} suggestPropertiesPosition
*/

View File

@@ -0,0 +1,28 @@
import CacheableSettings from "$lib/extension/base/CacheableSettings.ts";
interface SearchSettingsFields {
suggestProperties: boolean;
suggestPropertiesPosition: "start" | "end";
}
export default class SearchSettings extends CacheableSettings<SearchSettingsFields> {
constructor() {
super("search");
}
async resolvePropertiesSuggestionsEnabled() {
return this._resolveSetting("suggestProperties", false);
}
async resolvePropertiesSuggestionsPosition() {
return this._resolveSetting("suggestPropertiesPosition", "start");
}
async setPropertiesSuggestions(isEnabled: boolean) {
return this._writeSetting("suggestProperties", isEnabled);
}
async setPropertiesSuggestionsPosition(position: "start" | "end") {
return this._writeSetting("suggestPropertiesPosition", position);
}
}

View File

@@ -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>

View 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>

View 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));
});

View File

@@ -1,6 +1,6 @@
import {writable} from "svelte/store";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.ts";
/**
* Store for working with maintenance profiles in the Svelte popup.

View File

@@ -1,5 +1,5 @@
import {writable} from "svelte/store";
import MiscSettings from "$lib/extension/settings/MiscSettings.js";
import MiscSettings from "$lib/extension/settings/MiscSettings.ts";
export const fullScreenViewerEnabled = writable(true);
@@ -13,6 +13,6 @@ Promise.allSettled([
});
miscSettings.subscribe(settings => {
fullScreenViewerEnabled.set(settings.fullscreenViewer);
fullScreenViewerEnabled.set(Boolean(settings.fullscreenViewer));
});
});

View File

@@ -1,5 +1,5 @@
import {writable} from "svelte/store";
import SearchSettings from "$lib/extension/settings/SearchSettings.js";
import SearchSettings from "$lib/extension/settings/SearchSettings.ts";
export const searchPropertiesSuggestionsEnabled = writable(false);
@@ -23,7 +23,7 @@ Promise.allSettled([
});
searchSettings.subscribe(settings => {
searchPropertiesSuggestionsEnabled.set(settings.suggestProperties);
searchPropertiesSuggestionsPosition.set(settings.suggestPropertiesPosition);
searchPropertiesSuggestionsEnabled.set(Boolean(settings.suggestProperties));
searchPropertiesSuggestionsPosition.set(settings.suggestPropertiesPosition || 'start');
});
})

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
.autocomplete {
&__item {
&--property {
i {
margin-right: .5em;
}
}
}
}

View File

@@ -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;

View File

@@ -14,6 +14,7 @@ const config = {
name: Date.now().toString(36)
},
alias: {
"$config": "./src/config",
"$components": "./src/components",
"$styles": "./src/styles",
"$stores": "./src/stores",