mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-23 23:02:58 +00:00
@@ -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'),
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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
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"
|
||||
];
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
48
src/lib/extension/settings/MaintenanceSettings.ts
Normal file
48
src/lib/extension/settings/MaintenanceSettings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
19
src/lib/extension/settings/MiscSettings.ts
Normal file
19
src/lib/extension/settings/MiscSettings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
28
src/lib/extension/settings/SearchSettings.ts
Normal file
28
src/lib/extension/settings/SearchSettings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
9
src/styles/content/header.scss
Normal file
9
src/styles/content/header.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.autocomplete {
|
||||
&__item {
|
||||
&--property {
|
||||
i {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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