diff --git a/.editorconfig b/.editorconfig index b4b0bcb..1c3e0d5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/jsconfig.json b/jsconfig.json index 77187e1..3f97f09 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -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 } diff --git a/manifest.json b/manifest.json index 58bf932..8ca34e3 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 366bfd1..82dbc12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/package.json b/package.json index 53fe0c6..32762e5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ui/menu/MenuCheckboxItem.svelte b/src/components/ui/menu/MenuCheckboxItem.svelte new file mode 100644 index 0000000..75eb520 --- /dev/null +++ b/src/components/ui/menu/MenuCheckboxItem.svelte @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/src/components/ui/menu/MenuRadioItem.svelte b/src/components/ui/menu/MenuRadioItem.svelte index 54bb9a4..1672400 100644 --- a/src/components/ui/menu/MenuRadioItem.svelte +++ b/src/components/ui/menu/MenuRadioItem.svelte @@ -32,5 +32,6 @@ width: 16px; height: 16px; margin-right: 6px; + flex-shrink: 0; } diff --git a/src/content/tags.js b/src/content/tags.js index 232bb7c..d5c3edc 100644 --- a/src/content/tags.js +++ b/src/content/tags.js @@ -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(); diff --git a/src/lib/components/MaintenancePopup.js b/src/lib/components/MaintenancePopup.js index bb967e5..25ee29c 100644 --- a/src/lib/components/MaintenancePopup.js +++ b/src/lib/components/MaintenancePopup.js @@ -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(); diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.js index 728e9b2..37c45ca 100644 --- a/src/lib/components/TagDropdownWrapper.js +++ b/src/lib/components/TagDropdownWrapper.js @@ -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); + } + }) +} diff --git a/src/lib/extension/EntitiesTransporter.ts b/src/lib/extension/EntitiesTransporter.ts new file mode 100644 index 0000000..598edbb --- /dev/null +++ b/src/lib/extension/EntitiesTransporter.ts @@ -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 = + (new (id: string, settings: Record) => T) + & typeof StorageEntity; + +export default class EntitiesTransporter { + readonly #targetEntityConstructor: EntityConstructor; + + constructor(entityConstructor: EntityConstructor) { + 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 | null { + let jsonObject: Record | null = null; + + try { + jsonObject = JSON.parse(jsonString); + } catch (e) { + + } + + if (typeof jsonObject !== "object") { + throw new TypeError("Should be an object!"); + } + + return jsonObject + } +} diff --git a/src/lib/extension/entities/MaintenanceProfile.js b/src/lib/extension/entities/MaintenanceProfile.js index 0e0e59f..0f9a637 100644 --- a/src/lib/extension/entities/MaintenanceProfile.js +++ b/src/lib/extension/entities/MaintenanceProfile.js @@ -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; diff --git a/src/lib/extension/transporting/exporters.js b/src/lib/extension/transporting/exporters.js new file mode 100644 index 0000000..f74e15f --- /dev/null +++ b/src/lib/extension/transporting/exporters.js @@ -0,0 +1,26 @@ +/** + * @type {Map Record)>} + */ +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} + */ +export function exportEntityToObject(entityInstance, entityName) { + if (!entitiesExporters.has(entityName)) { + throw new Error(`Missing exporter for entity: ${entityName}`); + } + + return entitiesExporters.get(entityName).call(null, entityInstance); +} diff --git a/src/lib/extension/transporting/validators.js b/src/lib/extension/transporting/validators.js new file mode 100644 index 0000000..cb41e3d --- /dev/null +++ b/src/lib/extension/transporting/validators.js @@ -0,0 +1,39 @@ +/** + * Map of validators for each entity. Function should throw the error if validation failed. + * @type {Map 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); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8c3d976..27ae80e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,9 +1,26 @@ + {#if activeProfile} + + Active Profile: {activeProfile.settings.name} + +
+ {/if} Tagging Profiles
Preferences diff --git a/src/routes/features/maintenance/[id]/export/+page.svelte b/src/routes/features/maintenance/[id]/export/+page.svelte index e533014..d4e2191 100644 --- a/src/routes/features/maintenance/[id]/export/+page.svelte +++ b/src/routes/features/maintenance/[id]/export/+page.svelte @@ -1,11 +1,13 @@