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

Merge pull request #45 from koloml/release/0.3.1

Release: 0.3.1
This commit is contained in:
2024-11-12 16:35:19 +04:00
committed by GitHub
18 changed files with 440 additions and 95 deletions

View File

@@ -231,6 +231,178 @@ ij_javascript_while_brace_force = never
ij_javascript_while_on_new_line = false
ij_javascript_wrap_comments = false
[{*.ts,*.tsx}]
indent_size = 2
tab_width = 2
ij_continuation_indent_size = 2
ij_typescript_align_imports = false
ij_typescript_align_multiline_array_initializer_expression = false
ij_typescript_align_multiline_binary_operation = false
ij_typescript_align_multiline_chained_methods = false
ij_typescript_align_multiline_extends_list = false
ij_typescript_align_multiline_for = true
ij_typescript_align_multiline_parameters = true
ij_typescript_align_multiline_parameters_in_calls = false
ij_typescript_align_multiline_ternary_operation = false
ij_typescript_align_object_properties = 0
ij_typescript_align_union_types = false
ij_typescript_align_var_statements = 0
ij_typescript_array_initializer_new_line_after_left_brace = false
ij_typescript_array_initializer_right_brace_on_new_line = false
ij_typescript_array_initializer_wrap = off
ij_typescript_assignment_wrap = off
ij_typescript_binary_operation_sign_on_next_line = false
ij_typescript_binary_operation_wrap = off
ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
ij_typescript_blank_lines_after_imports = 1
ij_typescript_blank_lines_around_class = 1
ij_typescript_blank_lines_around_field = 0
ij_typescript_blank_lines_around_function = 1
ij_typescript_blank_lines_around_method = 1
ij_typescript_block_brace_style = end_of_line
ij_typescript_block_comment_add_space = false
ij_typescript_block_comment_at_first_column = true
ij_typescript_call_parameters_new_line_after_left_paren = false
ij_typescript_call_parameters_right_paren_on_new_line = false
ij_typescript_call_parameters_wrap = off
ij_typescript_catch_on_new_line = false
ij_typescript_chained_call_dot_on_new_line = true
ij_typescript_class_brace_style = end_of_line
ij_typescript_comma_on_new_line = false
ij_typescript_do_while_brace_force = never
ij_typescript_else_on_new_line = false
ij_typescript_enforce_trailing_comma = keep
ij_typescript_extends_keyword_wrap = off
ij_typescript_extends_list_wrap = off
ij_typescript_field_prefix = _
ij_typescript_file_name_style = relaxed
ij_typescript_finally_on_new_line = false
ij_typescript_for_brace_force = never
ij_typescript_for_statement_new_line_after_left_paren = false
ij_typescript_for_statement_right_paren_on_new_line = false
ij_typescript_for_statement_wrap = off
ij_typescript_force_quote_style = false
ij_typescript_force_semicolon_style = false
ij_typescript_function_expression_brace_style = end_of_line
ij_typescript_if_brace_force = never
ij_typescript_import_merge_members = global
ij_typescript_import_prefer_absolute_path = global
ij_typescript_import_sort_members = true
ij_typescript_import_sort_module_name = false
ij_typescript_import_use_node_resolution = true
ij_typescript_imports_wrap = on_every_item
ij_typescript_indent_case_from_switch = true
ij_typescript_indent_chained_calls = true
ij_typescript_indent_package_children = 0
ij_typescript_jsx_attribute_value = braces
ij_typescript_keep_blank_lines_in_code = 2
ij_typescript_keep_first_column_comment = true
ij_typescript_keep_indents_on_empty_lines = false
ij_typescript_keep_line_breaks = true
ij_typescript_keep_simple_blocks_in_one_line = false
ij_typescript_keep_simple_methods_in_one_line = false
ij_typescript_line_comment_add_space = true
ij_typescript_line_comment_at_first_column = false
ij_typescript_method_brace_style = end_of_line
ij_typescript_method_call_chain_wrap = off
ij_typescript_method_parameters_new_line_after_left_paren = false
ij_typescript_method_parameters_right_paren_on_new_line = false
ij_typescript_method_parameters_wrap = off
ij_typescript_object_literal_wrap = on_every_item
ij_typescript_object_types_wrap = on_every_item
ij_typescript_parentheses_expression_new_line_after_left_paren = false
ij_typescript_parentheses_expression_right_paren_on_new_line = false
ij_typescript_place_assignment_sign_on_next_line = false
ij_typescript_prefer_as_type_cast = false
ij_typescript_prefer_explicit_types_function_expression_returns = false
ij_typescript_prefer_explicit_types_function_returns = false
ij_typescript_prefer_explicit_types_vars_fields = false
ij_typescript_prefer_parameters_wrap = false
ij_typescript_property_prefix =
ij_typescript_reformat_c_style_comments = false
ij_typescript_space_after_colon = true
ij_typescript_space_after_comma = true
ij_typescript_space_after_dots_in_rest_parameter = false
ij_typescript_space_after_generator_mult = true
ij_typescript_space_after_property_colon = true
ij_typescript_space_after_quest = true
ij_typescript_space_after_type_colon = true
ij_typescript_space_after_unary_not = false
ij_typescript_space_before_async_arrow_lparen = true
ij_typescript_space_before_catch_keyword = true
ij_typescript_space_before_catch_left_brace = true
ij_typescript_space_before_catch_parentheses = true
ij_typescript_space_before_class_lbrace = true
ij_typescript_space_before_class_left_brace = true
ij_typescript_space_before_colon = true
ij_typescript_space_before_comma = false
ij_typescript_space_before_do_left_brace = true
ij_typescript_space_before_else_keyword = true
ij_typescript_space_before_else_left_brace = true
ij_typescript_space_before_finally_keyword = true
ij_typescript_space_before_finally_left_brace = true
ij_typescript_space_before_for_left_brace = true
ij_typescript_space_before_for_parentheses = true
ij_typescript_space_before_for_semicolon = false
ij_typescript_space_before_function_left_parenth = true
ij_typescript_space_before_generator_mult = false
ij_typescript_space_before_if_left_brace = true
ij_typescript_space_before_if_parentheses = true
ij_typescript_space_before_method_call_parentheses = false
ij_typescript_space_before_method_left_brace = true
ij_typescript_space_before_method_parentheses = false
ij_typescript_space_before_property_colon = false
ij_typescript_space_before_quest = true
ij_typescript_space_before_switch_left_brace = true
ij_typescript_space_before_switch_parentheses = true
ij_typescript_space_before_try_left_brace = true
ij_typescript_space_before_type_colon = false
ij_typescript_space_before_unary_not = false
ij_typescript_space_before_while_keyword = true
ij_typescript_space_before_while_left_brace = true
ij_typescript_space_before_while_parentheses = true
ij_typescript_spaces_around_additive_operators = true
ij_typescript_spaces_around_arrow_function_operator = true
ij_typescript_spaces_around_assignment_operators = true
ij_typescript_spaces_around_bitwise_operators = true
ij_typescript_spaces_around_equality_operators = true
ij_typescript_spaces_around_logical_operators = true
ij_typescript_spaces_around_multiplicative_operators = true
ij_typescript_spaces_around_relational_operators = true
ij_typescript_spaces_around_shift_operators = true
ij_typescript_spaces_around_unary_operator = false
ij_typescript_spaces_within_array_initializer_brackets = false
ij_typescript_spaces_within_brackets = false
ij_typescript_spaces_within_catch_parentheses = false
ij_typescript_spaces_within_for_parentheses = false
ij_typescript_spaces_within_if_parentheses = false
ij_typescript_spaces_within_imports = false
ij_typescript_spaces_within_interpolation_expressions = false
ij_typescript_spaces_within_method_call_parentheses = false
ij_typescript_spaces_within_method_parentheses = false
ij_typescript_spaces_within_object_literal_braces = false
ij_typescript_spaces_within_object_type_braces = true
ij_typescript_spaces_within_parentheses = false
ij_typescript_spaces_within_switch_parentheses = false
ij_typescript_spaces_within_type_assertion = false
ij_typescript_spaces_within_union_types = true
ij_typescript_spaces_within_while_parentheses = false
ij_typescript_special_else_if_treatment = true
ij_typescript_ternary_operation_signs_on_next_line = false
ij_typescript_ternary_operation_wrap = off
ij_typescript_union_types_wrap = on_every_item
ij_typescript_use_chained_calls_group_indents = false
ij_typescript_use_double_quotes = true
ij_typescript_use_explicit_js_extension = auto
ij_typescript_use_import_type = auto
ij_typescript_use_path_mapping = always
ij_typescript_use_public_modifier = false
ij_typescript_use_semicolon_after_statement = true
ij_typescript_var_declaration_wrap = normal
ij_typescript_while_brace_force = never
ij_typescript_while_on_new_line = false
ij_typescript_wrap_comments = false
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
ij_html_align_attributes = true

View File

@@ -9,10 +9,7 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"allowImportingTsExtensions": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

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.0",
"version": "0.3.1",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city"
@@ -48,6 +48,7 @@
"*://*.furbooru.org/images/*/tag_changes",
"*://*.furbooru.org/images/*/tag_changes?*",
"*://*.furbooru.org/search?*",
"*://*.furbooru.org/tags",
"*://*.furbooru.org/tags?*",
"*://*.furbooru.org/tags/*",
"*://*.furbooru.org/profiles/*/tag_changes",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.3.0",
"version": "0.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "furbooru-tagging-assistant",
"version": "0.3.0",
"version": "0.3.1",
"dependencies": {
"lz-string": "^1.5.0"
},

View File

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

View File

@@ -0,0 +1,37 @@
<script>
import MenuLink from "$components/ui/menu/MenuItem.svelte";
/**
* @type {boolean}
*/
export let checked;
/**
* @type {string|undefined}
*/
export let name = undefined;
/**
* @type {string|undefined}
*/
export let value = undefined;
/**
* @type {string|null}
*/
export let href = null;
</script>
<MenuLink {href}>
<input type="checkbox" {name} {value} {checked} on:input on:click|stopPropagation>
<slot></slot>
</MenuLink>
<style lang="scss">
:global(.menu-item) input {
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
</style>

View File

@@ -32,5 +32,6 @@
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
</style>

View File

@@ -1,5 +1,7 @@
import {wrapTagDropdown} from "$lib/components/TagDropdownWrapper.js";
import {watchTagDropdownsInTagsEditor, wrapTagDropdown} from "$lib/components/TagDropdownWrapper.js";
for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) {
wrapTagDropdown(tagDropdownElement);
}
watchTagDropdownsInTagsEditor();

View File

@@ -286,7 +286,13 @@ export class MaintenancePopup extends BaseComponent {
this.#maintenanceSettings
.resolveActiveProfileAsObject()
.then(callback);
.then(profileOrNull => {
if (profileOrNull) {
lastActiveProfileId = profileOrNull.id;
}
callback(profileOrNull);
});
return () => {
unsubscribeFromProfilesChanges();

View File

@@ -1,6 +1,9 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
const isTagEditorProcessedKey = Symbol();
class TagDropdownWrapper extends BaseComponent {
/**
@@ -187,5 +190,41 @@ class TagDropdownWrapper extends BaseComponent {
}
export function wrapTagDropdown(element) {
// Skip initialization when tag component is already wrapped
if (getComponent(element)) {
return;
}
new TagDropdownWrapper(element).initialize();
}
export function watchTagDropdownsInTagsEditor() {
// We only need to watch for new editor elements if there is a tag editor present on the page
if (!document.querySelector('#image_tags_and_source')) {
return;
}
document.body.addEventListener('mouseover', event => {
/** @type {HTMLElement} */
const targetElement = event.target;
if (targetElement[isTagEditorProcessedKey]) {
return;
}
/** @type {HTMLElement|null} */
const closestTagEditor = targetElement.closest('#image_tags_and_source');
if (!closestTagEditor || closestTagEditor[isTagEditorProcessedKey]) {
targetElement[isTagEditorProcessedKey] = true;
return;
}
targetElement[isTagEditorProcessedKey] = true;
closestTagEditor[isTagEditorProcessedKey] = true;
for (const tagDropdownElement of closestTagEditor.querySelectorAll('.tag.dropdown')) {
wrapTagDropdown(tagDropdownElement);
}
})
}

View File

@@ -0,0 +1,73 @@
import {validateImportedEntity} from "$lib/extension/transporting/validators.js";
import {exportEntityToObject} from "$lib/extension/transporting/exporters.js";
import StorageEntity from "./base/StorageEntity.js";
import {compressToEncodedURIComponent, decompressFromEncodedURIComponent} from "lz-string";
type EntityConstructor<T extends StorageEntity> =
(new (id: string, settings: Record<string, any>) => T)
& typeof StorageEntity;
export default class EntitiesTransporter<EntityType extends StorageEntity> {
readonly #targetEntityConstructor: EntityConstructor<EntityType>;
constructor(entityConstructor: EntityConstructor<EntityType>) {
this.#targetEntityConstructor = entityConstructor;
}
importFromJSON(jsonString: string): EntityType {
const importedObject = this.#tryParsingAsJSON(jsonString);
if (!importedObject) {
throw new Error('Invalid JSON!');
}
validateImportedEntity(
importedObject,
this.#targetEntityConstructor._entityName
);
return new this.#targetEntityConstructor(
importedObject.id,
importedObject
);
}
importFromCompressedJSON(compressedJsonString: string): EntityType {
return this.importFromJSON(
decompressFromEncodedURIComponent(compressedJsonString)
)
}
exportToJSON(entityObject: EntityType): string {
if (!(entityObject instanceof this.#targetEntityConstructor)) {
throw new TypeError('Transporter should be connected to the same entity to export!');
}
const exportableObject = exportEntityToObject(
entityObject,
this.#targetEntityConstructor._entityName
);
return JSON.stringify(exportableObject, null, 2);
}
exportToCompressedJSON(entityObject: EntityType): string {
return compressToEncodedURIComponent(this.exportToJSON(entityObject));
}
#tryParsingAsJSON(jsonString: string): Record<string, any> | null {
let jsonObject: Record<string, any> | null = null;
try {
jsonObject = JSON.parse(jsonString);
} catch (e) {
}
if (typeof jsonObject !== "object") {
throw new TypeError("Should be an object!");
}
return jsonObject
}
}

View File

@@ -1,6 +1,5 @@
import StorageEntity from "$lib/extension/base/StorageEntity.js";
import EntitiesController from "$lib/extension/EntitiesController.js";
import {compressToEncodedURIComponent, decompressFromEncodedURIComponent} from "lz-string";
/**
* @typedef {Object} MaintenanceProfileSettings
@@ -30,26 +29,6 @@ class MaintenanceProfile extends StorageEntity {
return super.settings;
}
/**
* Export the profile to the formatted JSON.
*
* @type {string}
*/
toJSON() {
return JSON.stringify({
v: 1,
id: this.id,
name: this.settings.name,
tags: this.settings.tags,
}, null, 2);
}
toCompressedJSON() {
return compressToEncodedURIComponent(
this.toJSON()
);
}
static _entityName = "profiles";
/**
@@ -79,62 +58,6 @@ class MaintenanceProfile extends StorageEntity {
callback
);
}
/**
* Validate and import the profile from the JSON.
* @param {string} exportedString JSON for profile.
* @return {MaintenanceProfile} Maintenance profile imported from the JSON. Note that profile is not automatically
* saved.
* @throws {Error} When version is unsupported or format is invalid.
*/
static importFromJSON(exportedString) {
let importedObject;
try {
importedObject = JSON.parse(exportedString);
} catch (e) {
// Error will be sent later, since empty string could be parsed as nothing without raising the error.
}
if (!importedObject) {
throw new Error('Invalid JSON!');
}
if (importedObject.v !== 1) {
throw new Error('Unsupported version!');
}
if (
!importedObject.id
|| typeof importedObject.id !== "string"
|| !importedObject.name
|| typeof importedObject.name !== "string"
|| !importedObject.tags
|| !Array.isArray(importedObject.tags)
) {
throw new Error('Invalid profile format detected!');
}
return new MaintenanceProfile(
importedObject.id,
{
name: importedObject.name,
tags: importedObject.tags,
}
);
}
/**
* Validate and import the profile from the compressed JSON string.
* @param {string} compressedString
* @return {MaintenanceProfile}
* @throws {Error} When version is unsupported or format is invalid.
*/
static importFromCompressedJSON(compressedString) {
return this.importFromJSON(
decompressFromEncodedURIComponent(compressedString)
);
}
}
export default MaintenanceProfile;

View File

@@ -0,0 +1,26 @@
/**
* @type {Map<string, ((entity: import('../base/StorageEntity.js').default) => Record<string, any>)>}
*/
const entitiesExporters = new Map([
['profiles', /** @param {import('../entities/MaintenanceProfile.js').default} entity */entity => {
return {
v: 1,
id: entity.id,
name: entity.settings.name,
tags: entity.settings.tags,
}
}]
])
/**
* @param entityInstance
* @param {string} entityName
* @returns {Record<string, *>}
*/
export function exportEntityToObject(entityInstance, entityName) {
if (!entitiesExporters.has(entityName)) {
throw new Error(`Missing exporter for entity: ${entityName}`);
}
return entitiesExporters.get(entityName).call(null, entityInstance);
}

View File

@@ -0,0 +1,39 @@
/**
* Map of validators for each entity. Function should throw the error if validation failed.
* @type {Map<string, ((importedObject: Object) => void)>}
*/
const entitiesValidators = new Map([
['profiles', importedObject => {
if (importedObject.v !== 1) {
throw new Error('Unsupported version!');
}
if (
!importedObject.id
|| typeof importedObject.id !== "string"
|| !importedObject.name
|| typeof importedObject.name !== "string"
|| !importedObject.tags
|| !Array.isArray(importedObject.tags)
) {
throw new Error('Invalid profile format detected!');
}
}]
])
/**
* Validate the structure of the entity.
* @param {Object} importedObject Object imported from JSON.
* @param {string} entityName Name of the entity to validate. Should be loaded from the entity class.
* @throws {Error} Error in case validation failed with the reason stored in the message.
*/
export function validateImportedEntity(importedObject, entityName) {
if (!entitiesValidators.has(entityName)) {
console.error(`Trying to validate entity without the validator present! Entity name: ${entityName}`);
return;
}
entitiesValidators
.get(entityName)
.call(null, importedObject);
}

View File

@@ -1,9 +1,26 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { activeProfileStore, maintenanceProfilesStore } from "$stores/maintenance-profiles-store.js";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
/** @type {import('$lib/extension/entities/MaintenanceProfile.js').default|undefined} */
let activeProfile;
$: activeProfile = $maintenanceProfilesStore.find(profile => profile.id === $activeProfileStore);
function turnOffActiveProfile() {
$activeProfileStore = null;
}
</script>
<Menu>
{#if activeProfile}
<MenuCheckboxItem checked on:input={turnOffActiveProfile} href="/features/maintenance/{activeProfile.id}">
Active Profile: {activeProfile.settings.name}
</MenuCheckboxItem>
<hr>
{/if}
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
<hr>
<MenuItem href="/preferences">Preferences</MenuItem>

View File

@@ -1,11 +1,13 @@
<script>
import {page} from "$app/stores";
import {goto} from "$app/navigation";
import {maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { maintenanceProfilesStore } from "$stores/maintenance-profiles-store.js";
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 EntitiesTransporter from "$lib/extension/EntitiesTransporter.ts";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
const profileId = $page.params.id;
@@ -14,6 +16,7 @@
*/
const profile = $maintenanceProfilesStore.find(profile => profile.id === profileId);
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
/** @type {string} */
let exportedProfile = '';
/** @type {string} */
@@ -22,8 +25,8 @@
if (!profile) {
goto('/features/maintenance/');
} else {
exportedProfile = profile.toJSON();
compressedProfile = profile.toCompressedJSON();
exportedProfile = profilesTransporter.exportToJSON(profile);
compressedProfile = profilesTransporter.exportToCompressedJSON(profile);
}
let isCompressedProfileShown = true;

View File

@@ -7,6 +7,9 @@
import ProfileView from "$components/maintenance/ProfileView.svelte";
import {maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";
import {goto} from "$app/navigation";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter.ts";
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
/** @type {string} */
let importedString = '';
@@ -32,10 +35,10 @@
try {
if (importedString.trim().startsWith('{')) {
candidateProfile = MaintenanceProfile.importFromJSON(importedString);
candidateProfile = profilesTransporter.importFromJSON(importedString);
}
candidateProfile = MaintenanceProfile.importFromCompressedJSON(importedString);
candidateProfile = profilesTransporter.importFromCompressedJSON(importedString);
} catch (error) {
errorMessage = error instanceof Error
? error.message

View File

@@ -19,6 +19,12 @@ const config = {
"$stores": "./src/stores",
"$entities": "./src/lib/extension/entities",
},
typescript: {
config: config => {
config.compilerOptions = config.compilerOptions || {};
config.compilerOptions.allowImportingTsExtension = true
}
}
},
preprocess: [
vitePreprocess({