mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2026-02-06 23:32:58 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bab919f0f8 | |||
| 72f901a2b7 | |||
| fd8efccfb3 | |||
| 3621bb9f0e | |||
| c15fae7c3d | |||
| 01e538c5c2 | |||
| 4375613768 | |||
| 3e05b1964d | |||
| 5092dc7f6d | |||
| 64dfac310e | |||
| 2da2716844 | |||
| 10b5bff377 | |||
| 198b9da407 | |||
| b5cdb0d81b | |||
| dc4f575576 | |||
| 0a947219d0 | |||
| e83d70fbd9 | |||
| 844853ff57 | |||
| 774409aac6 | |||
| 917775c5cd | |||
| a68c261b52 | |||
| f3a9694b1b | |||
| 2cb4c6b4b2 | |||
| bafdb68f1e | |||
| 02f9f3b36e | |||
| 57c505bee9 | |||
| 03512a6539 | |||
| f95eaacaaa | |||
| 38cb925fa4 | |||
| 7dd738d0e8 | |||
| 2f8a47b808 | |||
| d0c910d5bb | |||
| dc1e49e60c | |||
| 5e7e92614d | |||
| 727b2c81ff | |||
| 8059c93baa | |||
| 2f8d608e6b | |||
| 4635ccdb2b | |||
| 68d1d726af | |||
| e8c3e610eb | |||
| f9cb73bafc | |||
| 6bb3e83684 |
172
.editorconfig
172
.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
|
||||
|
||||
BIN
.github/assets/chrome.png
vendored
Normal file
BIN
.github/assets/chrome.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
.github/assets/firefox.png
vendored
Normal file
BIN
.github/assets/firefox.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1,5 +1,8 @@
|
||||
# Furbooru Tagging Assistant
|
||||
|
||||
[](https://addons.mozilla.org/en-US/firefox/addon/furbooru-tagging-assistant/)
|
||||
[](https://chromewebstore.google.com/detail/kpgaphaooaaodgodmnkamhmoedjcnfkj)
|
||||
|
||||
This is a browser extension written for the [Furbooru](https://furbooru.org) image-board. It gives you the ability to
|
||||
tag the images more easily and quickly.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.2.1",
|
||||
"version": "0.3.1",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "furbooru-tagging-assistant@thecore.city"
|
||||
@@ -23,7 +23,8 @@
|
||||
"*://*.furbooru.org/",
|
||||
"*://*.furbooru.org/images?*",
|
||||
"*://*.furbooru.org/search?*",
|
||||
"*://*.furbooru.org/tags/*"
|
||||
"*://*.furbooru.org/tags/*",
|
||||
"*://*.furbooru.org/galleries/*"
|
||||
],
|
||||
"js": [
|
||||
"src/content/listing.js"
|
||||
@@ -39,6 +40,32 @@
|
||||
"js": [
|
||||
"src/content/header.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"*://*.furbooru.org/images?*",
|
||||
"*://*.furbooru.org/images/*",
|
||||
"*://*.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",
|
||||
"*://*.furbooru.org/profiles/*/tag_changes?*",
|
||||
"*://*.furbooru.org/filters/*"
|
||||
],
|
||||
"js": [
|
||||
"src/content/tags.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"*://*.furbooru.org/images/*"
|
||||
],
|
||||
"js": [
|
||||
"src/content/tags-editor.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
|
||||
922
package-lock.json
generated
922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "furbooru-tagging-assistant",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:popup && npm run build:extension",
|
||||
@@ -17,10 +17,10 @@
|
||||
"@types/chrome": "^0.0.262",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"sass": "^1.71.0",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.6.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^5.4.9"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
93
src/components/debugging/StorageViewer.svelte
Normal file
93
src/components/debugging/StorageViewer.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script>
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import {storagesCollection} from "$stores/debug.js";
|
||||
import {goto} from "$app/navigation";
|
||||
import {findDeepObject} from "$lib/utils.js";
|
||||
|
||||
/** @type {string} */
|
||||
export let storage;
|
||||
|
||||
/** @type {string[]} */
|
||||
export let path;
|
||||
|
||||
/** @type {Object|null} */
|
||||
let targetStorage = null;
|
||||
/** @type {[string, string][]} */
|
||||
let breadcrumbs = [];
|
||||
/** @type {Object<string, any>|null} */
|
||||
let targetObject = null;
|
||||
let targetPathString = '';
|
||||
|
||||
$: {
|
||||
/** @type {[string, string][]} */
|
||||
const builtBreadcrumbs = [];
|
||||
|
||||
breadcrumbs = path.reduce((resultCrumbs, entry) => {
|
||||
let entryPath = entry;
|
||||
|
||||
if (resultCrumbs.length) {
|
||||
entryPath = resultCrumbs[resultCrumbs.length - 1][1] + "/" + entryPath;
|
||||
}
|
||||
|
||||
resultCrumbs.push([entry, entryPath]);
|
||||
|
||||
return resultCrumbs;
|
||||
}, builtBreadcrumbs);
|
||||
|
||||
targetPathString = path.join("/");
|
||||
|
||||
if (targetPathString.length) {
|
||||
targetPathString += "/";
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
targetStorage = $storagesCollection[storage];
|
||||
|
||||
if (!targetStorage) {
|
||||
goto("/preferences/debug/storage");
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
targetObject = targetStorage
|
||||
? findDeepObject(targetStorage, path)
|
||||
: null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/preferences/debug/storage" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
<p class="path">
|
||||
<span>/ <a href="/preferences/debug/storage/{storage}">{storage}</a></span>
|
||||
{#each breadcrumbs as [name, entryPath]}
|
||||
<span>/ <a href="/preferences/debug/storage/{storage}/{entryPath}/">{name}</a></span>
|
||||
{/each}
|
||||
</p>
|
||||
{#if targetObject}
|
||||
<Menu>
|
||||
<hr>
|
||||
{#each Object.entries(targetObject) as [key, value]}
|
||||
{#if targetObject[key] && typeof targetObject[key] === 'object'}
|
||||
<MenuItem href="/preferences/debug/storage/{storage}/{targetPathString}{key}">
|
||||
{key}: Object
|
||||
</MenuItem>
|
||||
{:else}
|
||||
<MenuItem>
|
||||
{key}: {typeof targetObject[key]} = {targetObject[key]}
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: .5em;
|
||||
}
|
||||
</style>
|
||||
37
src/components/ui/menu/MenuCheckboxItem.svelte
Normal file
37
src/components/ui/menu/MenuCheckboxItem.svelte
Normal 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>
|
||||
@@ -32,5 +32,6 @@
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
3
src/content/tags-editor.js
Normal file
3
src/content/tags-editor.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import {TagsForm} from "$lib/components/TagsForm.js";
|
||||
|
||||
TagsForm.watchForEditors();
|
||||
7
src/content/tags.js
Normal file
7
src/content/tags.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {watchTagDropdownsInTagsEditor, wrapTagDropdown} from "$lib/components/TagDropdownWrapper.js";
|
||||
|
||||
for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
|
||||
watchTagDropdownsInTagsEditor();
|
||||
211
src/lib/components/FullscreenViewer.js
Normal file
211
src/lib/components/FullscreenViewer.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
|
||||
|
||||
export class FullscreenViewer extends BaseComponent {
|
||||
/** @type {HTMLVideoElement} */
|
||||
#videoElement;
|
||||
/** @type {HTMLImageElement} */
|
||||
#imageElement;
|
||||
|
||||
/** @type {number|null} */
|
||||
#touchId = null;
|
||||
/** @type {number|null} */
|
||||
#startX = null;
|
||||
/** @type {number|null} */
|
||||
#startY = null;
|
||||
/** @type {boolean|null} */
|
||||
#isClosingSwipeStarted = null;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
build() {
|
||||
this.container.classList.add('fullscreen-viewer');
|
||||
|
||||
this.#videoElement = document.createElement('video');
|
||||
this.#imageElement = document.createElement('img');
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
init() {
|
||||
document.addEventListener('keydown', this.#onDocumentKeyPressed.bind(this));
|
||||
this.on('click', this.#close.bind(this));
|
||||
|
||||
this.on('touchstart', this.#onTouchStart.bind(this));
|
||||
this.on('touchmove', this.#onTouchMove.bind(this));
|
||||
this.on('touchend', this.#onTouchEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} event
|
||||
*/
|
||||
#onTouchStart(event) {
|
||||
if (this.#touchId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstTouch = event.touches.item(0);
|
||||
|
||||
if (!firstTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#touchId = firstTouch.identifier;
|
||||
this.#startX = firstTouch.clientX;
|
||||
this.#startY = firstTouch.clientY;
|
||||
this.container.classList.add(FullscreenViewer.#swipeState);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} event
|
||||
*/
|
||||
#onTouchEnd(event) {
|
||||
if (this.#touchId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endedTouch = Array.from(event.changedTouches)
|
||||
.find(touch => touch.identifier === this.#touchId);
|
||||
|
||||
if (!endedTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verticalDistance = Math.abs(endedTouch.clientY - this.#startY);
|
||||
const requiredClosingDistance = window.innerHeight / 3;
|
||||
|
||||
if (this.#isClosingSwipeStarted && verticalDistance > requiredClosingDistance) {
|
||||
this.#close();
|
||||
}
|
||||
|
||||
this.#touchId = null;
|
||||
this.#startX = null;
|
||||
this.#startY = null;
|
||||
this.#isClosingSwipeStarted = null;
|
||||
|
||||
this.container.classList.remove(FullscreenViewer.#swipeState);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.container.style.removeProperty(FullscreenViewer.#offsetProperty);
|
||||
this.container.style.removeProperty(FullscreenViewer.#opacityProperty);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TouchEvent} event
|
||||
*/
|
||||
#onTouchMove(event) {
|
||||
if (this.#touchId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#isClosingSwipeStarted === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const changedTouch of event.changedTouches) {
|
||||
if (changedTouch.identifier !== this.#touchId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const verticalDistance = changedTouch.clientY - this.#startY;
|
||||
|
||||
if (this.#isClosingSwipeStarted === null) {
|
||||
const horizontalDistance = changedTouch.clientX - this.#startX;
|
||||
|
||||
if (Math.abs(verticalDistance) >= FullscreenViewer.#minRequiredDistance) {
|
||||
this.#isClosingSwipeStarted = true;
|
||||
} else if (Math.abs(horizontalDistance) >= FullscreenViewer.#minRequiredDistance) {
|
||||
this.#isClosingSwipeStarted = false;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.container.style.setProperty(
|
||||
FullscreenViewer.#offsetProperty,
|
||||
verticalDistance.toString().concat('px')
|
||||
);
|
||||
|
||||
const maxDistance = window.innerHeight * 2;
|
||||
let opacity = 1;
|
||||
|
||||
if (verticalDistance !== 0) {
|
||||
opacity -= Math.min(1, Math.abs(verticalDistance) / maxDistance);
|
||||
}
|
||||
|
||||
this.container.style.setProperty(
|
||||
FullscreenViewer.#opacityProperty,
|
||||
opacity.toString()
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onDocumentKeyPressed(event) {
|
||||
if (event.code === 'Escape' || event.code === 'Esc') {
|
||||
this.#close();
|
||||
}
|
||||
}
|
||||
|
||||
#close() {
|
||||
this.container.classList.remove(FullscreenViewer.#shownState);
|
||||
document.body.style.overflow = null;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.#videoElement.volume = 0;
|
||||
this.#videoElement.pause();
|
||||
this.#videoElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
*/
|
||||
show(url) {
|
||||
requestAnimationFrame(() => {
|
||||
this.container.classList.add(FullscreenViewer.#shownState);
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
if (FullscreenViewer.#isVideoUrl(url)) {
|
||||
this.#imageElement.remove();
|
||||
|
||||
this.#videoElement.src = url;
|
||||
this.#videoElement.volume = 0;
|
||||
this.#videoElement.autoplay = true;
|
||||
this.#videoElement.loop = true;
|
||||
this.#videoElement.controls = true;
|
||||
|
||||
this.container.append(this.#videoElement);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.#videoElement.remove();
|
||||
|
||||
this.#imageElement.src = url;
|
||||
|
||||
this.container.append(this.#imageElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @return {boolean}
|
||||
*/
|
||||
static #isVideoUrl(url) {
|
||||
return url.endsWith('.mp4') || url.endsWith('.webm');
|
||||
}
|
||||
|
||||
static #offsetProperty = '--offset';
|
||||
static #opacityProperty = '--opacity';
|
||||
static #shownState = 'shown';
|
||||
static #swipeState = 'swiped';
|
||||
static #minRequiredDistance = 50;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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 {FullscreenViewer} from "$lib/components/FullscreenViewer.js";
|
||||
|
||||
export class ImageShowFullscreenButton extends BaseComponent {
|
||||
/**
|
||||
@@ -11,7 +12,6 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
|
||||
build() {
|
||||
this.container.innerText = '🔍';
|
||||
ImageShowFullscreenButton.#resolveFullscreenViewer();
|
||||
|
||||
ImageShowFullscreenButton.#miscSettings ??= new MiscSettings();
|
||||
}
|
||||
@@ -45,95 +45,36 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
}
|
||||
|
||||
#onButtonClicked() {
|
||||
const imageViewer = ImageShowFullscreenButton.#resolveFullscreenViewer();
|
||||
const largeSourceUrl = this.#mediaBoxTools.mediaBox.imageLinks.large;
|
||||
|
||||
let imageElement = imageViewer.querySelector('img');
|
||||
let videoElement = imageViewer.querySelector('video');
|
||||
|
||||
if (imageElement) {
|
||||
imageElement.remove();
|
||||
}
|
||||
|
||||
if (videoElement) {
|
||||
videoElement.remove();
|
||||
}
|
||||
|
||||
if (largeSourceUrl.endsWith('.webm') || largeSourceUrl.endsWith('.mp4')) {
|
||||
videoElement ??= document.createElement('video');
|
||||
videoElement.src = largeSourceUrl;
|
||||
videoElement.volume = 0;
|
||||
videoElement.autoplay = true;
|
||||
videoElement.loop = true;
|
||||
videoElement.controls = true;
|
||||
|
||||
imageViewer.appendChild(videoElement);
|
||||
} else {
|
||||
imageElement ??= document.createElement('img');
|
||||
imageElement.src = largeSourceUrl;
|
||||
|
||||
imageViewer.appendChild(imageElement);
|
||||
}
|
||||
|
||||
imageViewer.classList.add('shown');
|
||||
ImageShowFullscreenButton
|
||||
.#resolveViewer()
|
||||
.show(this.#mediaBoxTools.mediaBox.imageLinks.large);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HTMLElement|null}
|
||||
* @type {FullscreenViewer|null}
|
||||
*/
|
||||
static #fullscreenViewerElement = null;
|
||||
static #viewer = null;
|
||||
|
||||
/**
|
||||
* @return {HTMLElement}
|
||||
* @return {FullscreenViewer}
|
||||
*/
|
||||
static #resolveFullscreenViewer() {
|
||||
this.#fullscreenViewerElement ??= this.#buildFullscreenViewer();
|
||||
return this.#fullscreenViewerElement;
|
||||
static #resolveViewer() {
|
||||
this.#viewer ??= this.#buildViewer();
|
||||
return this.#viewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {HTMLElement}
|
||||
* @return {FullscreenViewer}
|
||||
*/
|
||||
static #buildFullscreenViewer() {
|
||||
static #buildViewer() {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('fullscreen-viewer');
|
||||
const viewer = new FullscreenViewer(element);
|
||||
|
||||
viewer.initialize();
|
||||
|
||||
document.body.append(element);
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
// When ESC pressed
|
||||
if (event.code === 'Escape' || event.code === 'Esc') {
|
||||
this.#closeFullscreenViewer(element);
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('click', () => {
|
||||
this.#closeFullscreenViewer(element);
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} [viewerElement]
|
||||
*/
|
||||
static #closeFullscreenViewer(viewerElement = null) {
|
||||
viewerElement ??= this.#resolveFullscreenViewer();
|
||||
viewerElement.classList.remove('shown');
|
||||
|
||||
/** @type {HTMLVideoElement} */
|
||||
const videoElement = viewerElement.querySelector('video');
|
||||
|
||||
if (!videoElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stopping and muting the video
|
||||
requestAnimationFrame(() => {
|
||||
videoElement.volume = 0;
|
||||
videoElement.pause();
|
||||
videoElement.remove();
|
||||
})
|
||||
return viewer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
230
src/lib/components/TagDropdownWrapper.js
Normal file
230
src/lib/components/TagDropdownWrapper.js
Normal file
@@ -0,0 +1,230 @@
|
||||
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 {
|
||||
/**
|
||||
* Container with dropdown elements to insert options into.
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
#dropdownContainer;
|
||||
|
||||
/**
|
||||
* Button to add or remove the current tag into/from the active profile.
|
||||
* @type {HTMLAnchorElement|null}
|
||||
*/
|
||||
#toggleOnExistingButton = null;
|
||||
|
||||
/**
|
||||
* Button to create a new profile, make it active and add the current tag into the active profile.
|
||||
* @type {HTMLAnchorElement|null}
|
||||
*/
|
||||
#addToNewButton = null;
|
||||
|
||||
/**
|
||||
* Local clone of the currently active profile used for updating the list of tags.
|
||||
* @type {MaintenanceProfile|null}
|
||||
*/
|
||||
#activeProfile = null;
|
||||
|
||||
/**
|
||||
* Is cursor currently entered the dropdown.
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isEntered = false;
|
||||
|
||||
build() {
|
||||
this.#dropdownContainer = this.container.querySelector('.dropdown__content');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.on('mouseenter', this.#onDropdownEntered.bind(this));
|
||||
this.on('mouseleave', this.#onDropdownLeft.bind(this));
|
||||
|
||||
TagDropdownWrapper.#watchActiveProfile(activeProfileOrNull => {
|
||||
this.#activeProfile = activeProfileOrNull;
|
||||
|
||||
if (this.#isEntered) {
|
||||
this.#updateButtons();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get #tagName() {
|
||||
return this.container.dataset.tagName;
|
||||
}
|
||||
|
||||
#onDropdownEntered() {
|
||||
this.#isEntered = true;
|
||||
this.#updateButtons();
|
||||
}
|
||||
|
||||
#onDropdownLeft() {
|
||||
this.#isEntered = false;
|
||||
}
|
||||
|
||||
#updateButtons() {
|
||||
if (!this.#activeProfile) {
|
||||
this.#addToNewButton ??= TagDropdownWrapper.#createDropdownLink(
|
||||
'Add to new tagging profile',
|
||||
this.#onAddToNewClicked.bind(this)
|
||||
);
|
||||
|
||||
if (!this.#addToNewButton.isConnected) {
|
||||
this.#dropdownContainer.append(this.#addToNewButton);
|
||||
}
|
||||
} else {
|
||||
this.#addToNewButton?.remove();
|
||||
}
|
||||
|
||||
if (this.#activeProfile) {
|
||||
this.#toggleOnExistingButton ??= TagDropdownWrapper.#createDropdownLink(
|
||||
'Add to existing tagging profile',
|
||||
this.#onToggleInExistingClicked.bind(this)
|
||||
);
|
||||
|
||||
const profileName = this.#activeProfile.settings.name;
|
||||
let profileSpecificButtonText = `Add to profile "${profileName}"`;
|
||||
|
||||
if (this.#activeProfile.settings.tags.includes(this.#tagName)) {
|
||||
profileSpecificButtonText = `Remove from profile "${profileName}"`;
|
||||
}
|
||||
|
||||
this.#toggleOnExistingButton.innerText = profileSpecificButtonText;
|
||||
|
||||
if (!this.#toggleOnExistingButton.isConnected) {
|
||||
this.#dropdownContainer.append(this.#toggleOnExistingButton);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.#toggleOnExistingButton?.remove();
|
||||
}
|
||||
|
||||
async #onAddToNewClicked() {
|
||||
const profile = new MaintenanceProfile(crypto.randomUUID(), {
|
||||
name: 'Temporary Profile (' + (new Date().toISOString()) + ')',
|
||||
tags: [this.#tagName]
|
||||
});
|
||||
|
||||
await profile.save();
|
||||
await TagDropdownWrapper.#maintenanceSettings.setActiveProfileId(profile.id);
|
||||
}
|
||||
|
||||
async #onToggleInExistingClicked() {
|
||||
if (!this.#activeProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagsList = new Set(this.#activeProfile.settings.tags);
|
||||
const targetTagName = this.#tagName;
|
||||
|
||||
if (tagsList.has(targetTagName)) {
|
||||
tagsList.delete(targetTagName);
|
||||
} else {
|
||||
tagsList.add(targetTagName);
|
||||
}
|
||||
|
||||
this.#activeProfile.settings.tags = Array.from(tagsList.values());
|
||||
|
||||
await this.#activeProfile.save();
|
||||
}
|
||||
|
||||
static #maintenanceSettings = new MaintenanceSettings();
|
||||
|
||||
/**
|
||||
* Watch for changes to active profile.
|
||||
* @param {(profile: MaintenanceProfile|null) => void} onActiveProfileChange Callback to call when profile was
|
||||
* changed.
|
||||
*/
|
||||
static #watchActiveProfile(onActiveProfileChange) {
|
||||
let lastActiveProfile;
|
||||
|
||||
this.#maintenanceSettings.subscribe((settings) => {
|
||||
lastActiveProfile = settings.activeProfile;
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
.then(onActiveProfileChange);
|
||||
});
|
||||
|
||||
MaintenanceProfile.subscribe(profiles => {
|
||||
const activeProfile = profiles
|
||||
.find(profile => profile.id === lastActiveProfile);
|
||||
|
||||
onActiveProfileChange(activeProfile);
|
||||
});
|
||||
|
||||
this.#maintenanceSettings
|
||||
.resolveActiveProfileAsObject()
|
||||
.then(activeProfile => {
|
||||
lastActiveProfile = activeProfile?.id ?? null;
|
||||
onActiveProfileChange(activeProfile);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create element for dropdown.
|
||||
* @param {string} text Base text for the option.
|
||||
* @param {(event: MouseEvent) => void} onClickHandler Click handler. Event will be prevented by default.
|
||||
* @return {HTMLAnchorElement}
|
||||
*/
|
||||
static #createDropdownLink(text, onClickHandler) {
|
||||
/** @type {HTMLAnchorElement} */
|
||||
const dropdownLink = document.createElement('a');
|
||||
dropdownLink.href = '#';
|
||||
dropdownLink.innerText = text;
|
||||
dropdownLink.className = 'tag__dropdown__link';
|
||||
|
||||
dropdownLink.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
onClickHandler(event);
|
||||
});
|
||||
|
||||
return dropdownLink;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
})
|
||||
}
|
||||
81
src/lib/components/TagsForm.js
Normal file
81
src/lib/components/TagsForm.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
|
||||
import {getComponent} from "$lib/components/base/ComponentUtils.js";
|
||||
|
||||
export class TagsForm extends BaseComponent {
|
||||
/**
|
||||
* Collect all the tag categories available on the page and color the tags in the editor according to them.
|
||||
*/
|
||||
refreshTagColors() {
|
||||
const tagCategories = this.#gatherTagCategories();
|
||||
const editableTags = this.container.querySelectorAll('.tag');
|
||||
|
||||
for (let tagElement of editableTags) {
|
||||
// Tag name is stored in the "remove" link and not in the tag itself.
|
||||
const removeLink = tagElement.querySelector('a');
|
||||
|
||||
if (!removeLink) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tagName = removeLink.dataset.tagName;
|
||||
|
||||
if (!tagCategories.has(tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryName = tagCategories.get(tagName);
|
||||
|
||||
tagElement.dataset.tagCategory = categoryName;
|
||||
tagElement.setAttribute('data-tag-category', categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect list of categories from the tags on the page.
|
||||
* @return {Map<string, string>}
|
||||
*/
|
||||
#gatherTagCategories() {
|
||||
/** @type {Map<string, string>} */
|
||||
const tagCategories = new Map();
|
||||
|
||||
for (let tagElement of document.querySelectorAll('.tag[data-tag-name][data-tag-category]')) {
|
||||
tagCategories.set(tagElement.dataset.tagName, tagElement.dataset.tagCategory);
|
||||
}
|
||||
|
||||
return tagCategories;
|
||||
}
|
||||
|
||||
static watchForEditors() {
|
||||
document.body.addEventListener('click', event => {
|
||||
const targetElement = event.target;
|
||||
|
||||
if (!(targetElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagEditorWrapper = targetElement.closest('#image_tags_and_source');
|
||||
|
||||
if (!tagEditorWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshTrigger = targetElement.closest('.js-taginput-show, #edit-tags')
|
||||
|
||||
if (!refreshTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagFormElement = tagEditorWrapper.querySelector('#tags-form');
|
||||
|
||||
/** @type {TagsForm|null} */
|
||||
let tagEditor = getComponent(tagFormElement);
|
||||
|
||||
if (!tagEditor || (!tagEditor instanceof TagsForm)) {
|
||||
tagEditor = new TagsForm(tagFormElement);
|
||||
tagEditor.initialize();
|
||||
}
|
||||
|
||||
tagEditor.refreshTagColors();
|
||||
});
|
||||
}
|
||||
}
|
||||
73
src/lib/extension/EntitiesTransporter.ts
Normal file
73
src/lib/extension/EntitiesTransporter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -5,10 +5,17 @@ export default class MiscSettings extends CacheableSettings {
|
||||
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);
|
||||
}
|
||||
|
||||
26
src/lib/extension/transporting/exporters.js
Normal file
26
src/lib/extension/transporting/exporters.js
Normal 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);
|
||||
}
|
||||
39
src/lib/extension/transporting/validators.js
Normal file
39
src/lib/extension/transporting/validators.js
Normal 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);
|
||||
}
|
||||
23
src/lib/utils.js
Normal file
23
src/lib/utils.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Traverse and find the object using the key path.
|
||||
* @param {Object} targetObject Target object to traverse into.
|
||||
* @param {string[]} path Path of keys to traverse deep into the object.
|
||||
* @return {Object|null} Resulting object or null if nothing found (or target entry is not an object.
|
||||
*/
|
||||
export function findDeepObject(targetObject, path) {
|
||||
let result = targetObject;
|
||||
|
||||
for (let key of path) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
result = result[key];
|
||||
}
|
||||
|
||||
if (!result || typeof result !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,10 +1,27 @@
|
||||
<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>
|
||||
<MenuItem href="/settings/maintenance">Tagging Profiles</MenuItem>
|
||||
{#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>
|
||||
<MenuItem href="/about">About</MenuItem>
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
<Menu>
|
||||
<MenuItem href="/">Back</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/settings/maintenance">Tagging Profiles</MenuItem>
|
||||
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
|
||||
</Menu>
|
||||
@@ -27,12 +27,12 @@
|
||||
|
||||
<Menu>
|
||||
<MenuItem icon="arrow-left" href="/">Back</MenuItem>
|
||||
<MenuItem icon="plus" href="/settings/maintenance/new/edit">Create New</MenuItem>
|
||||
<MenuItem icon="plus" href="/features/maintenance/new/edit">Create New</MenuItem>
|
||||
{#if profiles.length}
|
||||
<hr>
|
||||
{/if}
|
||||
{#each profiles as profile}
|
||||
<MenuRadioItem href="/settings/maintenance/{profile.id}"
|
||||
<MenuRadioItem href="/features/maintenance/{profile.id}"
|
||||
name="active-profile"
|
||||
value="{profile.id}"
|
||||
checked="{$activeProfileStore === profile.id}"
|
||||
@@ -42,5 +42,5 @@
|
||||
{/each}
|
||||
<hr>
|
||||
<MenuItem href="#" on:click={resetActiveProfile}>Reset Active Profile</MenuItem>
|
||||
<MenuItem href="/settings/maintenance/import">Import Profile</MenuItem>
|
||||
<MenuItem href="/features/maintenance/import">Import Profile</MenuItem>
|
||||
</Menu>
|
||||
@@ -22,7 +22,7 @@
|
||||
profile = resolvedProfile;
|
||||
} else {
|
||||
console.warn(`Profile ${profileId} not found.`);
|
||||
goto('/settings/maintenance');
|
||||
goto('/features/maintenance');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/settings/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if profile}
|
||||
@@ -46,7 +46,7 @@
|
||||
{/if}
|
||||
<Menu>
|
||||
<hr>
|
||||
<MenuItem icon="wrench" href="/settings/maintenance/{profileId}/edit">Edit Profile</MenuItem>
|
||||
<MenuItem icon="wrench" href="/features/maintenance/{profileId}/edit">Edit Profile</MenuItem>
|
||||
<MenuItem icon="tag" href="#" on:click={activateProfile}>
|
||||
{#if isActiveProfile}
|
||||
<span>Profile is Active</span>
|
||||
@@ -54,7 +54,7 @@
|
||||
<span>Activate Profile</span>
|
||||
{/if}
|
||||
</MenuItem>
|
||||
<MenuItem icon="file-export" href="/settings/maintenance/{profileId}/export">
|
||||
<MenuItem icon="file-export" href="/features/maintenance/{profileId}/export">
|
||||
Export Profile
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
@@ -30,7 +30,7 @@
|
||||
profileName = targetProfile.settings.name;
|
||||
tagsList = [...targetProfile.settings.tags].sort((a, b) => a.localeCompare(b));
|
||||
} else {
|
||||
goto('/settings/maintenance');
|
||||
goto('/features/maintenance');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
targetProfile.settings.tags = [...tagsList];
|
||||
|
||||
await targetProfile.save();
|
||||
await goto('/settings/maintenance/' + targetProfile.id);
|
||||
await goto('/features/maintenance/' + targetProfile.id);
|
||||
}
|
||||
|
||||
async function deleteProfile() {
|
||||
@@ -54,12 +54,12 @@
|
||||
}
|
||||
|
||||
await targetProfile.delete();
|
||||
await goto('/settings/maintenance');
|
||||
await goto('/features/maintenance');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem icon="arrow-left" href="/settings/maintenance{profileId === 'new' ? '' : '/' + profileId}">
|
||||
<MenuItem icon="arrow-left" href="/features/maintenance{profileId === 'new' ? '' : '/' + profileId}">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -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,23 +16,24 @@
|
||||
*/
|
||||
const profile = $maintenanceProfilesStore.find(profile => profile.id === profileId);
|
||||
|
||||
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
|
||||
/** @type {string} */
|
||||
let exportedProfile = '';
|
||||
/** @type {string} */
|
||||
let compressedProfile = '';
|
||||
|
||||
if (!profile) {
|
||||
goto('/settings/maintenance/');
|
||||
goto('/features/maintenance/');
|
||||
} else {
|
||||
exportedProfile = profile.toJSON();
|
||||
compressedProfile = profile.toCompressedJSON();
|
||||
exportedProfile = profilesTransporter.exportToJSON(profile);
|
||||
compressedProfile = profilesTransporter.exportToCompressedJSON(profile);
|
||||
}
|
||||
|
||||
let isCompressedProfileShown = true;
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/settings/maintenance/{profileId}" icon="arrow-left">
|
||||
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">
|
||||
Back
|
||||
</MenuItem>
|
||||
<hr>
|
||||
@@ -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
|
||||
@@ -53,7 +56,7 @@
|
||||
}
|
||||
|
||||
candidateProfile.save().then(() => {
|
||||
goto(`/settings/maintenance`);
|
||||
goto(`/features/maintenance`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,13 +68,13 @@
|
||||
const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings);
|
||||
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
|
||||
clonedProfile.save().then(() => {
|
||||
goto(`/settings/maintenance`);
|
||||
goto(`/features/maintenance`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem icon="arrow-left" href="/settings/maintenance">Back</MenuItem>
|
||||
<MenuItem icon="arrow-left" href="/features/maintenance">Back</MenuItem>
|
||||
<hr>
|
||||
</Menu>
|
||||
{#if errorMessage}
|
||||
@@ -8,4 +8,6 @@
|
||||
<hr>
|
||||
<MenuItem href="/preferences/search">Search</MenuItem>
|
||||
<MenuItem href="/preferences/misc">Misc & Tools</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/preferences/debug">Debug</MenuItem>
|
||||
</Menu>
|
||||
|
||||
10
src/routes/preferences/debug/+page.svelte
Normal file
10
src/routes/preferences/debug/+page.svelte
Normal 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="/preferences" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
<MenuItem href="/preferences/debug/storage">Inspect Storages</MenuItem>
|
||||
</Menu>
|
||||
13
src/routes/preferences/debug/storage/+page.svelte
Normal file
13
src/routes/preferences/debug/storage/+page.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import MenuItem from "$components/ui/menu/MenuItem.svelte";
|
||||
import Menu from "$components/ui/menu/Menu.svelte";
|
||||
import {storagesCollection} from "$stores/debug.js";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/preferences/debug" icon="arrow-left">Back</MenuItem>
|
||||
<hr>
|
||||
{#each Object.keys($storagesCollection) as storageName}
|
||||
<MenuItem href="/preferences/debug/storage/{storageName}/">Storage: {storageName}</MenuItem>
|
||||
{/each}
|
||||
</Menu>
|
||||
29
src/routes/preferences/debug/storage/[...path]/+page.svelte
Normal file
29
src/routes/preferences/debug/storage/[...path]/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import StorageViewer from "$components/debugging/StorageViewer.svelte";
|
||||
import {page} from "$app/stores";
|
||||
import {goto} from "$app/navigation";
|
||||
|
||||
let pathString = '';
|
||||
/** @type {string[]} */
|
||||
let pathArray = [];
|
||||
/** @type {string|undefined} */
|
||||
let storageName = void 0;
|
||||
|
||||
$: {
|
||||
pathString = $page.params.path;
|
||||
pathArray = pathString.length ? pathString.split("/") : [];
|
||||
storageName = pathArray.shift()
|
||||
|
||||
if (pathArray.length && pathArray[pathArray.length - 1] === '') {
|
||||
pathArray.pop();
|
||||
}
|
||||
|
||||
if (!storageName) {
|
||||
goto("/preferences/debug/storage");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if storageName}
|
||||
<StorageViewer storage="{storageName}" path="{pathArray}"></StorageViewer>
|
||||
{/if}
|
||||
21
src/stores/debug.js
Normal file
21
src/stores/debug.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
/**
|
||||
* This is readable version of storages. Any changes made to these objects will not be sent to the local storage.
|
||||
* @type {Writable<Record<string, Object>>}
|
||||
*/
|
||||
export const storagesCollection = writable({});
|
||||
|
||||
chrome.storage.local.get(storages => {
|
||||
storagesCollection.set(storages);
|
||||
});
|
||||
|
||||
chrome.storage.local.onChanged.addListener(changes => {
|
||||
storagesCollection.update(storages => {
|
||||
for (let updatedStorageName of Object.keys(changes)) {
|
||||
storages[updatedStorageName] = changes[updatedStorageName].newValue;
|
||||
}
|
||||
|
||||
return storages;
|
||||
})
|
||||
});
|
||||
@@ -160,9 +160,9 @@
|
||||
.fullscreen-viewer {
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
opacity: var(--opacity, 0);
|
||||
background-color: black;
|
||||
transition: opacity 0.1s;
|
||||
transition: opacity 0.1s, transform 0.1s;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -171,6 +171,7 @@
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
transform: translateY(var(--offset, 0));
|
||||
|
||||
img, video {
|
||||
object-fit: contain;
|
||||
@@ -179,7 +180,12 @@
|
||||
}
|
||||
|
||||
&.shown {
|
||||
opacity: 1;
|
||||
opacity: var(--opacity, 1);
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
&.swiped {
|
||||
opacity: var(--opacity, 1);
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user