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

Merge pull request #20 from koloml/feature/configuration-section

Added the preferences section, implemented preferences to toggle properties suggestions
This commit is contained in:
2024-08-07 03:59:05 +04:00
committed by GitHub
14 changed files with 309 additions and 53 deletions

View File

@@ -0,0 +1,12 @@
<script>
/** @type {string|undefined} */
export let name = undefined;
/** @type {boolean} */
export let checked;
</script>
<input type="checkbox" {name} bind:checked={checked}>
<span>
<slot></slot>
</span>

View File

@@ -1,7 +1,7 @@
<script>
/** @type {string|undefined} */
export let label;
export let label = undefined;
</script>
<label class="control">
@@ -17,6 +17,8 @@
}
.control {
padding: 5px 0;
:global(textarea) {
width: 100%;
resize: vertical;

View File

@@ -0,0 +1,40 @@
<script>
/**
* @type {string[]|Record<string, string>}
*/
export let options = [];
/** @type {string|undefined} */
export let name = undefined;
/** @type {string|undefined} */
export let id = undefined;
/** @type {string|undefined} */
export let value = undefined;
/** @type {Record<string, string>} */
const optionPairs = {};
if (Array.isArray(options)) {
for (let option of options) {
optionPairs[option] = option;
}
} else if (options && typeof options === 'object') {
Object.keys(options).forEach((key) => {
optionPairs[key] = options[key];
})
}
</script>
<select {name} {id} bind:value={value}>
{#each Object.entries(optionPairs) as [value, label]}
<option {value}>{label}</option>
{/each}
</select>
<style lang="scss">
select {
width: 100%;
}
</style>

View File

@@ -272,12 +272,12 @@ export class MaintenancePopup extends BaseComponent {
}
});
const unsubscribeFromMaintenanceSettings = MaintenanceSettings.subscribe(settings => {
if (settings.activeProfileId === lastActiveProfileId) {
const unsubscribeFromMaintenanceSettings = this.#maintenanceSettings.subscribe(settings => {
if (settings.activeProfile === lastActiveProfileId) {
return;
}
lastActiveProfileId = settings.activeProfileId;
lastActiveProfileId = settings.activeProfile;
this.#maintenanceSettings
.resolveActiveProfileAsObject()

View File

@@ -1,5 +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";
export class SearchWrapper extends BaseComponent {
/** @type {HTMLInputElement|null} */
@@ -8,6 +9,10 @@ export class SearchWrapper extends BaseComponent {
#lastParsedSearchValue = null;
/** @type {Token[]} */
#cachedParsedQuery = [];
#searchSettings = new SearchSettings();
#arePropertiesSuggestionsEnabled = false;
/** @type {"start"|"end"} */
#propertiesSuggestionsPosition = "start";
build() {
this.#searchField = this.container.querySelector('input[name=q]');
@@ -15,6 +20,16 @@ export class SearchWrapper extends BaseComponent {
init() {
this.#searchField.addEventListener('input', this.#onInputFindProperties.bind(this));
this.#searchSettings.resolvePropertiesSuggestionsEnabled()
.then(isEnabled => this.#arePropertiesSuggestionsEnabled = isEnabled);
this.#searchSettings.resolvePropertiesSuggestionsPosition()
.then(position => this.#propertiesSuggestionsPosition = position);
this.#searchSettings.subscribe(settings => {
this.#arePropertiesSuggestionsEnabled = settings.suggestProperties;
this.#propertiesSuggestionsPosition = settings.suggestPropertiesPosition;
});
}
/**
@@ -22,6 +37,11 @@ export class SearchWrapper extends BaseComponent {
* @param {InputEvent} event Source event to find the input element from.
*/
#onInputFindProperties(event) {
// Ignore events until option is enabled.
if (!this.#arePropertiesSuggestionsEnabled) {
return;
}
const currentFragment = this.#findCurrentTagFragment();
if (!currentFragment) {
@@ -111,7 +131,20 @@ export class SearchWrapper extends BaseComponent {
}
const listContainer = autocompleteContainer.querySelector('ul');
listContainer.prepend(...suggestedListItems);
switch (this.#propertiesSuggestionsPosition) {
case "start":
listContainer.prepend(...suggestedListItems);
break;
case "end":
listContainer.append(...suggestedListItems);
break;
default:
console.warn("Invalid position for property suggestions!");
}
autocompleteContainer.style.position = 'absolute';
autocompleteContainer.style.left = `${targetInput.offsetLeft}px`;

View File

@@ -0,0 +1,79 @@
import ConfigurationController from "$lib/extension/ConfigurationController.js";
export default class CacheableSettings {
/** @type {ConfigurationController} */
#controller;
/** @type {Map<string, any>} */
#cachedValues = new Map();
/** @type {function[]} */
#disposables = [];
constructor(settingsNamespace) {
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]);
}
})
);
}
/**
* @template SettingType
* @param {string} settingName
* @param {SettingType} defaultValue
* @return {Promise<SettingType>}
* @protected
*/
async _resolveSetting(settingName, defaultValue) {
if (this.#cachedValues.has(settingName)) {
return this.#cachedValues.get(settingName);
}
const settingValue = await this.#controller.readSetting(settingName, defaultValue);
this.#cachedValues.set(settingName, settingValue);
return settingValue;
}
/**
* @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>}
* @protected
*/
async _writeSetting(settingName, value, force = false) {
if (
!force
&& this.#cachedValues.has(settingName)
&& this.#cachedValues.get(settingName) === value
) {
return;
}
return this.#controller.writeSetting(settingName, value);
}
/**
* Subscribe to the changes made to the storage.
* @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);
this.#disposables.push(unsubscribeCallback);
return unsubscribeCallback;
}
dispose() {
for (let disposeCallback of this.#disposables) {
disposeCallback();
}
}
}

View File

@@ -1,21 +1,10 @@
import ConfigurationController from "$lib/extension/ConfigurationController.js";
import MaintenanceProfile from "$lib/extension/entities/MaintenanceProfile.js";
import CacheableSettings from "$lib/extension/base/CacheableSettings.js";
export default class MaintenanceSettings {
#isInitialized = false;
#activeProfileId = null;
export default class MaintenanceSettings extends CacheableSettings {
constructor() {
void this.#initializeSettings();
}
async #initializeSettings() {
MaintenanceSettings.#controller.subscribeToChanges(settings => {
this.#activeProfileId = settings.activeProfile || null;
});
this.#activeProfileId = await MaintenanceSettings.#controller.readSetting("activeProfile", null);
this.#isInitialized = true;
super("maintenance");
}
/**
@@ -24,18 +13,7 @@ export default class MaintenanceSettings {
* @return {Promise<string|null>}
*/
async resolveActiveProfileId() {
if (!this.#isInitialized && !this.#activeProfileId) {
this.#activeProfileId = await MaintenanceSettings.#controller.readSetting(
"activeProfile",
null
);
}
if (!this.#activeProfileId) {
return null;
}
return this.#activeProfileId;
return this._resolveSetting("activeProfile", null);
}
/**
@@ -59,31 +37,27 @@ export default class MaintenanceSettings {
* @return {Promise<void>}
*/
async setActiveProfileId(profileId) {
this.#activeProfileId = profileId;
await MaintenanceSettings.#controller.writeSetting("activeProfile", profileId);
await this._writeSetting("activeProfile", profileId);
}
/**
* Controller for interaction with the settings stored in the extension's storage.
*
* @type {ConfigurationController}
*/
static #controller = new ConfigurationController("maintenance");
/**
* Subscribe to the changes in the maintenance-related settings.
*
* @param {function({activeProfileId: string|null}): void} callback Callback to call when the settings change. The new settings are
* passed as an argument.
* @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.
*/
static subscribe(callback) {
return MaintenanceSettings.#controller.subscribeToChanges(settings => {
subscribe(callback) {
return super.subscribe(settings => {
callback({
activeProfileId: settings.activeProfile || null,
activeProfile: settings.activeProfile || null,
});
});
}
}
/**
* @typedef {Object} MaintenanceSettingsObject
* @property {string|null} activeProfile
*/

View File

@@ -0,0 +1,42 @@
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

@@ -6,5 +6,6 @@
<Menu>
<MenuItem href="/settings/maintenance">Tagging Profiles</MenuItem>
<hr>
<MenuItem href="/preferences">Preferences</MenuItem>
<MenuItem href="/about">About</MenuItem>
</Menu>

View File

@@ -0,0 +1,10 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
</script>
<Menu>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/preferences/search">Search</MenuItem>
</Menu>

View File

@@ -0,0 +1,35 @@
<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 {
searchPropertiesSuggestionsEnabled,
searchPropertiesSuggestionsPosition
} from "$stores/search-preferences.js";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import SelectField from "$components/ui/forms/SelectField.svelte";
const propertiesPositions = {
start: "At the start of the list",
end: "At the end of the list",
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/preferences">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl>
<CheckboxField bind:checked={$searchPropertiesSuggestionsEnabled}>
Auto-complete properties
</CheckboxField>
</FormControl>
{#if $searchPropertiesSuggestionsEnabled}
<FormControl label="Show completed properties:">
<SelectField bind:value={$searchPropertiesSuggestionsPosition}
options="{propertiesPositions}"></SelectField>
</FormControl>
{/if}
</FormContainer>

View File

@@ -30,17 +30,17 @@ maintenanceSettings.resolveActiveProfileId().then(activeProfileId => {
activeProfileStore.set(activeProfileId);
});
MaintenanceSettings.subscribe(settings => {
activeProfileStore.set(settings.activeProfileId || null);
maintenanceSettings.subscribe(settings => {
activeProfileStore.set(settings.activeProfile || null);
});
/**
* Active profile ID stored locally. Used to reset active profile once the existing profile was removed.
* @type {string|null}
*/
let lastActiveProfileId = null;
activeProfileStore.subscribe(profileId => {
if (profileId === lastActiveProfileId) {
return;
}
lastActiveProfileId = profileId;
void maintenanceSettings.setActiveProfileId(profileId);

View File

@@ -0,0 +1,24 @@
import {writable} from "svelte/store";
import SearchSettings from "$lib/extension/settings/SearchSettings.js";
export const searchPropertiesSuggestionsEnabled = writable(false);
/** @type {import('svelte/store').Writable<"start"|"end">} */
export const searchPropertiesSuggestionsPosition = writable('start');
const searchSettings = new SearchSettings();
Promise.allSettled([
// First we wait for all properties to load and save
searchSettings.resolvePropertiesSuggestionsEnabled().then(v => searchPropertiesSuggestionsEnabled.set(v)),
searchSettings.resolvePropertiesSuggestionsPosition().then(v => searchPropertiesSuggestionsPosition.set(v))
]).then(() => {
// And then we can start reading value changes from the writable objects
searchPropertiesSuggestionsEnabled.subscribe(value => {
void searchSettings.setPropertiesSuggestions(value);
});
searchPropertiesSuggestionsPosition.subscribe(value => {
void searchSettings.setPropertiesSuggestionsPosition(value);
});
})

View File

@@ -1,6 +1,6 @@
@use '../colors';
input, textarea {
input, textarea, select {
background: colors.$input-background;
border: 1px solid colors.$input-border;
color: colors.$text;
@@ -9,3 +9,7 @@ input, textarea {
padding: 0 6px;
line-height: 26px;
}
select {
min-height: 28px;
}