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