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

Merge pull request #93 from koloml/feature/migrate-svelte-to-v5-syntax

Migration to Svelte 5 syntax
This commit is contained in:
2025-02-17 18:25:54 -05:00
committed by GitHub
41 changed files with 1764 additions and 1301 deletions

View File

@@ -403,6 +403,365 @@ ij_typescript_while_brace_force = never
ij_typescript_while_on_new_line = false
ij_typescript_wrap_comments = false
[*.svelte]
indent_size = 2
tab_width = 2
ij_continuation_indent_size = 2
ij_javascript_align_imports = false
ij_javascript_align_multiline_array_initializer_expression = false
ij_javascript_align_multiline_binary_operation = false
ij_javascript_align_multiline_chained_methods = false
ij_javascript_align_multiline_extends_list = false
ij_javascript_align_multiline_for = true
ij_javascript_align_multiline_parameters = true
ij_javascript_align_multiline_parameters_in_calls = false
ij_javascript_align_multiline_ternary_operation = false
ij_javascript_align_object_properties = 0
ij_javascript_align_union_types = false
ij_javascript_align_var_statements = 0
ij_javascript_array_initializer_new_line_after_left_brace = false
ij_javascript_array_initializer_right_brace_on_new_line = false
ij_javascript_array_initializer_wrap = off
ij_javascript_assignment_wrap = off
ij_javascript_binary_operation_sign_on_next_line = false
ij_javascript_binary_operation_wrap = off
ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/**
ij_javascript_blank_lines_after_imports = 1
ij_javascript_blank_lines_around_class = 1
ij_javascript_blank_lines_around_field = 0
ij_javascript_blank_lines_around_function = 1
ij_javascript_blank_lines_around_method = 1
ij_javascript_block_brace_style = end_of_line
ij_javascript_block_comment_add_space = false
ij_javascript_block_comment_at_first_column = true
ij_javascript_call_parameters_new_line_after_left_paren = false
ij_javascript_call_parameters_right_paren_on_new_line = false
ij_javascript_call_parameters_wrap = off
ij_javascript_catch_on_new_line = false
ij_javascript_chained_call_dot_on_new_line = true
ij_javascript_class_brace_style = end_of_line
ij_javascript_comma_on_new_line = false
ij_javascript_do_while_brace_force = never
ij_javascript_else_on_new_line = false
ij_javascript_enforce_trailing_comma = keep
ij_javascript_extends_keyword_wrap = off
ij_javascript_extends_list_wrap = off
ij_javascript_field_prefix = _
ij_javascript_file_name_style = relaxed
ij_javascript_finally_on_new_line = false
ij_javascript_for_brace_force = never
ij_javascript_for_statement_new_line_after_left_paren = false
ij_javascript_for_statement_right_paren_on_new_line = false
ij_javascript_for_statement_wrap = off
ij_javascript_force_quote_style = false
ij_javascript_force_semicolon_style = false
ij_javascript_function_expression_brace_style = end_of_line
ij_javascript_if_brace_force = never
ij_javascript_import_merge_members = global
ij_javascript_import_prefer_absolute_path = global
ij_javascript_import_sort_members = true
ij_javascript_import_sort_module_name = false
ij_javascript_import_use_node_resolution = true
ij_javascript_imports_wrap = on_every_item
ij_javascript_indent_case_from_switch = true
ij_javascript_indent_chained_calls = true
ij_javascript_indent_package_children = 0
ij_javascript_jsx_attribute_value = braces
ij_javascript_keep_blank_lines_in_code = 2
ij_javascript_keep_first_column_comment = true
ij_javascript_keep_indents_on_empty_lines = false
ij_javascript_keep_line_breaks = true
ij_javascript_keep_simple_blocks_in_one_line = false
ij_javascript_keep_simple_methods_in_one_line = false
ij_javascript_line_comment_add_space = true
ij_javascript_line_comment_at_first_column = false
ij_javascript_method_brace_style = end_of_line
ij_javascript_method_call_chain_wrap = off
ij_javascript_method_parameters_new_line_after_left_paren = false
ij_javascript_method_parameters_right_paren_on_new_line = false
ij_javascript_method_parameters_wrap = off
ij_javascript_object_literal_wrap = on_every_item
ij_javascript_object_types_wrap = on_every_item
ij_javascript_parentheses_expression_new_line_after_left_paren = false
ij_javascript_parentheses_expression_right_paren_on_new_line = false
ij_javascript_place_assignment_sign_on_next_line = false
ij_javascript_prefer_as_type_cast = false
ij_javascript_prefer_explicit_types_function_expression_returns = false
ij_javascript_prefer_explicit_types_function_returns = false
ij_javascript_prefer_explicit_types_vars_fields = false
ij_javascript_prefer_parameters_wrap = false
ij_javascript_property_prefix =
ij_javascript_reformat_c_style_comments = false
ij_javascript_space_after_colon = true
ij_javascript_space_after_comma = true
ij_javascript_space_after_dots_in_rest_parameter = false
ij_javascript_space_after_generator_mult = true
ij_javascript_space_after_property_colon = true
ij_javascript_space_after_quest = true
ij_javascript_space_after_type_colon = true
ij_javascript_space_after_unary_not = false
ij_javascript_space_before_async_arrow_lparen = true
ij_javascript_space_before_catch_keyword = true
ij_javascript_space_before_catch_left_brace = true
ij_javascript_space_before_catch_parentheses = true
ij_javascript_space_before_class_lbrace = true
ij_javascript_space_before_class_left_brace = true
ij_javascript_space_before_colon = true
ij_javascript_space_before_comma = false
ij_javascript_space_before_do_left_brace = true
ij_javascript_space_before_else_keyword = true
ij_javascript_space_before_else_left_brace = true
ij_javascript_space_before_finally_keyword = true
ij_javascript_space_before_finally_left_brace = true
ij_javascript_space_before_for_left_brace = true
ij_javascript_space_before_for_parentheses = true
ij_javascript_space_before_for_semicolon = false
ij_javascript_space_before_function_left_parenth = true
ij_javascript_space_before_generator_mult = false
ij_javascript_space_before_if_left_brace = true
ij_javascript_space_before_if_parentheses = true
ij_javascript_space_before_method_call_parentheses = false
ij_javascript_space_before_method_left_brace = true
ij_javascript_space_before_method_parentheses = false
ij_javascript_space_before_property_colon = false
ij_javascript_space_before_quest = true
ij_javascript_space_before_switch_left_brace = true
ij_javascript_space_before_switch_parentheses = true
ij_javascript_space_before_try_left_brace = true
ij_javascript_space_before_type_colon = false
ij_javascript_space_before_unary_not = false
ij_javascript_space_before_while_keyword = true
ij_javascript_space_before_while_left_brace = true
ij_javascript_space_before_while_parentheses = true
ij_javascript_spaces_around_additive_operators = true
ij_javascript_spaces_around_arrow_function_operator = true
ij_javascript_spaces_around_assignment_operators = true
ij_javascript_spaces_around_bitwise_operators = true
ij_javascript_spaces_around_equality_operators = true
ij_javascript_spaces_around_logical_operators = true
ij_javascript_spaces_around_multiplicative_operators = true
ij_javascript_spaces_around_relational_operators = true
ij_javascript_spaces_around_shift_operators = true
ij_javascript_spaces_around_unary_operator = false
ij_javascript_spaces_within_array_initializer_brackets = false
ij_javascript_spaces_within_brackets = false
ij_javascript_spaces_within_catch_parentheses = false
ij_javascript_spaces_within_for_parentheses = false
ij_javascript_spaces_within_if_parentheses = false
ij_javascript_spaces_within_imports = true
ij_javascript_spaces_within_interpolation_expressions = false
ij_javascript_spaces_within_method_call_parentheses = false
ij_javascript_spaces_within_method_parentheses = false
ij_javascript_spaces_within_object_literal_braces = true
ij_javascript_spaces_within_object_type_braces = true
ij_javascript_spaces_within_parentheses = false
ij_javascript_spaces_within_switch_parentheses = false
ij_javascript_spaces_within_type_assertion = false
ij_javascript_spaces_within_union_types = true
ij_javascript_spaces_within_while_parentheses = false
ij_javascript_special_else_if_treatment = true
ij_javascript_ternary_operation_signs_on_next_line = false
ij_javascript_ternary_operation_wrap = off
ij_javascript_union_types_wrap = on_every_item
ij_javascript_use_chained_calls_group_indents = false
ij_javascript_use_double_quotes = true
ij_javascript_use_explicit_js_extension = never
ij_javascript_use_import_type = auto
ij_javascript_use_path_mapping = always
ij_javascript_use_public_modifier = false
ij_javascript_use_semicolon_after_statement = true
ij_javascript_var_declaration_wrap = normal
ij_javascript_while_brace_force = never
ij_javascript_while_on_new_line = false
ij_javascript_wrap_comments = false
ij_scss_align_closing_brace_with_properties = false
ij_scss_blank_lines_around_nested_selector = 1
ij_scss_blank_lines_between_blocks = 1
ij_scss_block_comment_add_space = false
ij_scss_brace_placement = 0
ij_scss_enforce_quotes_on_format = false
ij_scss_hex_color_long_format = false
ij_scss_hex_color_lower_case = false
ij_scss_hex_color_short_format = false
ij_scss_hex_color_upper_case = false
ij_scss_keep_blank_lines_in_code = 2
ij_scss_keep_indents_on_empty_lines = false
ij_scss_keep_single_line_blocks = false
ij_scss_line_comment_add_space = false
ij_scss_line_comment_at_first_column = false
ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow
ij_scss_space_after_colon = true
ij_scss_space_before_opening_brace = true
ij_scss_use_double_quotes = true
ij_scss_value_alignment = 0
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 = true
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 = true
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 = never
ij_typescript_use_import_type = auto
ij_typescript_use_path_mapping = always
ij_typescript_use_public_modifier = false
ij_typescript_use_semicolon_after_statement = true
ij_typescript_var_declaration_wrap = normal
ij_typescript_while_brace_force = never
ij_typescript_while_on_new_line = false
ij_typescript_wrap_comments = false
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3
ij_html_align_attributes = true

View File

@@ -1,126 +1,123 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { storagesCollection } from "$stores/debug";
import { goto } from "$app/navigation";
import { findDeepObject } from "$lib/utils";
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { storagesCollection } from "$stores/debug";
import { goto } from "$app/navigation";
import { findDeepObject } from "$lib/utils";
/** @type {string} */
export let storage;
interface StorageViewerProps {
storage: string;
path: string[];
}
/** @type {string[]} */
export let path;
type BreadcrumbsArray = [string, string][];
/** @type {Object|null} */
let targetStorage = null;
/** @type {[string, string][]} */
let breadcrumbs = [];
/** @type {Object<string, any>|null} */
let targetObject = null;
let targetPathString = '';
let { storage, path }: StorageViewerProps = $props();
$: {
/** @type {[string, string][]} */
const builtBreadcrumbs = [];
let breadcrumbs = $derived.by<BreadcrumbsArray>(() => {
return path.reduce<BreadcrumbsArray>((resultCrumbs, entry) => {
let entryPath = entry;
breadcrumbs = path.reduce((resultCrumbs, entry) => {
let entryPath = entry;
if (resultCrumbs.length) {
entryPath = resultCrumbs[resultCrumbs.length - 1][1] + "/" + entryPath;
}
if (resultCrumbs.length) {
entryPath = resultCrumbs[resultCrumbs.length - 1][1] + "/" + entryPath;
}
resultCrumbs.push([entry, entryPath]);
resultCrumbs.push([entry, entryPath]);
return resultCrumbs;
}, [])
});
return resultCrumbs;
}, builtBreadcrumbs);
let targetStorage = $derived.by<object|null>(() => {
return $storagesCollection[storage];
});
targetPathString = path.join("/");
let targetObject = $derived.by<Record<string, any> | null>(() => {
return targetStorage
? findDeepObject(targetStorage, path)
: null;
});
if (targetPathString.length) {
targetPathString += "/";
}
let targetPathString = $derived.by<string>(() => {
let pathString = path.join("/");
if (pathString.length) {
pathString += "/";
}
$: {
targetStorage = $storagesCollection[storage];
return pathString;
});
if (!targetStorage) {
goto("/preferences/debug/storage");
}
$effect(() => {
if (!targetStorage) {
goto("/preferences/debug/storage");
}
});
/**
* Helper function to resolve type, including the null.
* @param value Value to resolve type from.
* @return Type of the value, including "null" for null.
*/
function resolveType(value: unknown): string {
let typeName: string = typeof value;
if (typeName === 'object' && value === null) {
typeName = 'null';
}
$: {
targetObject = targetStorage
? findDeepObject(targetStorage, path)
: null;
return typeName;
}
/**
* Helper function to resolve value, including values like null or undefined.
* @param value Value to resolve.
* @return String representation of the value.
*/
function resolveValue(value: unknown): string {
if (value === null) {
return "null";
}
/**
* Helper function to resolve type, including the null.
* @param {*} value Value to resolve type from.
* @return {string} Type of the value, including "null" for null.
*/
function resolveType(value) {
/** @type {string} */
let typeName = typeof value;
if (typeName === 'object' && value === null) {
typeName = 'null';
}
return typeName;
if (value === undefined) {
return "undefined";
}
/**
* Helper function to resolve value, including values like null or undefined.
* @param {*} value Value to resolve.
* @return {string} String representation of the value.
*/
function resolveValue(value) {
if (value === null) {
return "null";
}
if (value === undefined) {
return "undefined";
}
return value?.toString() ?? '';
}
return value?.toString() ?? '';
}
</script>
<Menu>
<MenuItem href="/preferences/debug/storage" icon="arrow-left">Back</MenuItem>
<hr>
<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}
<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}: {resolveType(targetObject[key])} = {resolveValue(targetObject[key])}
</MenuItem>
{/if}
{/each}
</Menu>
<Menu>
<hr>
{#each Object.entries(targetObject) as [key, _]}
{#if targetObject[key] && typeof targetObject[key] === 'object'}
<MenuItem href="/preferences/debug/storage/{storage}/{targetPathString}{key}">
{key}: Object
</MenuItem>
{:else}
<MenuItem>
{key}: {resolveType(targetObject[key])} = {resolveValue(targetObject[key])}
</MenuItem>
{/if}
{/each}
</Menu>
{/if}
<style lang="scss">
.path {
display: flex;
flex-wrap: wrap;
column-gap: .5em;
}
.path {
display: flex;
flex-wrap: wrap;
column-gap: .5em;
}
</style>

View File

@@ -1,59 +1,60 @@
<script>
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
<script lang="ts">
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
import type TagGroup from "$entities/TagGroup";
/**
* @type {import('$entities/TagGroup').default}
*/
export let group;
interface GroupViewProps {
group: TagGroup;
}
let sortedTagsList, sortedPrefixes;
let { group }: GroupViewProps = $props();
let sortedTagsList = $derived<string[]>(group.settings.tags.sort((a, b) => a.localeCompare(b))),
sortedPrefixes = $derived<string[]>(group.settings.prefixes.sort((a, b) => a.localeCompare(b)));
$: sortedTagsList = group.settings.tags.sort((a, b) => a.localeCompare(b));
$: sortedPrefixes = group.settings.prefixes.sort((a, b) => a.localeCompare(b));
</script>
<div class="block">
<strong>Group Name:</strong>
<div>{group.settings.name}</div>
<strong>Group Name:</strong>
<div>{group.settings.name}</div>
</div>
{#if sortedTagsList.length}
<div class="block">
<strong>Tags:</strong>
<TagsColorContainer targetCategory="{group.settings.category}">
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
</TagsColorContainer>
</div>
<div class="block">
<strong>Tags:</strong>
<TagsColorContainer targetCategory={group.settings.category}>
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
</TagsColorContainer>
</div>
{/if}
{#if sortedPrefixes.length}
<div class="block">
<strong>Prefixes:</strong>
<TagsColorContainer targetCategory="{group.settings.category}">
<div class="tags-list">
{#each sortedPrefixes as prefixName}
<span class="tag">{prefixName}*</span>
{/each}
</div>
</TagsColorContainer>
</div>
<div class="block">
<strong>Prefixes:</strong>
<TagsColorContainer targetCategory={group.settings.category}>
<div class="tags-list">
{#each sortedPrefixes as prefixName}
<span class="tag">{prefixName}*</span>
{/each}
</div>
</TagsColorContainer>
</div>
{/if}
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.block + .block {
margin-top: .5em;
.block + .block {
margin-top: .5em;
strong {
display: block;
margin-bottom: .25em;
}
strong {
display: block;
margin-bottom: .25em;
}
}
</style>

View File

@@ -1,36 +1,41 @@
<script>
/** @type {import('$entities/MaintenanceProfile').default} */
export let profile;
<script lang="ts">
import type MaintenanceProfile from "$entities/MaintenanceProfile";
const sortedTagsList = profile.settings.tags.sort((a, b) => a.localeCompare(b));
interface ProfileViewProps {
profile: MaintenanceProfile;
}
let { profile }: ProfileViewProps = $props();
const sortedTagsList = profile.settings.tags.sort((a, b) => a.localeCompare(b));
</script>
<div class="block">
<strong>Profile:</strong>
<div>{profile.settings.name}</div>
<strong>Profile:</strong>
<div>{profile.settings.name}</div>
</div>
<div class="block">
<strong>Tags:</strong>
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
<strong>Tags:</strong>
<div class="tags-list">
{#each sortedTagsList as tagName}
<span class="tag">{tagName}</span>
{/each}
</div>
</div>
<style lang="scss">
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.block + .block {
margin-top: .5em;
.block + .block {
margin-top: .5em;
strong {
display: block;
margin-bottom: .25em;
}
strong {
display: block;
margin-bottom: .25em;
}
}
</style>

View File

@@ -1,32 +1,32 @@
<script>
import { version } from "$app/environment";
import { version } from "$app/environment";
</script>
<footer>
<a href="https://github.com/koloml/furbooru-tagging-assistant/releases/tag/{version}" target="_blank">
v{version}
</a>
<span>, made with ♥ by KoloMl.</span>
<a href="https://github.com/koloml/furbooru-tagging-assistant/releases/tag/{version}" target="_blank">
v{version}
</a>
<span>, made with ♥ by KoloMl.</span>
</footer>
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
footer {
display: flex;
width: 100%;
background: colors.$footer;
color: colors.$footer-text;
padding: 0 24px;
font-size: 12px;
line-height: 36px;
footer {
display: flex;
width: 100%;
background: colors.$footer;
color: colors.$footer-text;
padding: 0 24px;
font-size: 12px;
line-height: 36px;
a {
color: inherit;
a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
&:hover {
text-decoration: underline;
}
}
}
</style>

View File

@@ -1,28 +1,28 @@
<header>
<a href="/">Furbooru Tagging Assistant</a>
<a href="/">Furbooru Tagging Assistant</a>
</header>
<style lang="scss">
@use "$styles/colors";
@use "$styles/colors";
header {
background: colors.$header;
padding: 0 24px;
display: flex;
position: sticky;
top: 0;
left: 0;
right: 0;
header {
background: colors.$header;
padding: 0 24px;
display: flex;
position: sticky;
top: 0;
left: 0;
right: 0;
a {
color: colors.$text;
line-height: 36px;
padding: 0 12px;
margin-left: -12px;
a {
color: colors.$text;
line-height: 36px;
padding: 0 12px;
margin-left: -12px;
&:hover {
background: colors.$header-hover-background;
}
}
&:hover {
background: colors.$header-hover-background;
}
}
}
</style>

View File

@@ -1,62 +1,68 @@
<script>
/** @type {string} */
export let targetCategory = '';
<script lang="ts">
import type { Snippet } from "svelte";
interface TagColorContainerProps {
targetCategory?: string;
children?: Snippet;
}
let { targetCategory = '', children }: TagColorContainerProps = $props();
</script>
<div class="tag-color-container tag-color-container--{targetCategory || 'default'}">
<slot></slot>
{@render children?.()}
</div>
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
.tag-color-container:is(.tag-color-container--rating) :global(.tag) {
background-color: colors.$tag-rating-background;
color: colors.$tag-rating-text;
}
.tag-color-container:is(:global(.tag-color-container--rating)) :global(.tag) {
background-color: colors.$tag-rating-background;
color: colors.$tag-rating-text;
}
.tag-color-container:is(.tag-color-container--spoiler) :global(.tag) {
background-color: colors.$tag-spoiler-background;
color: colors.$tag-spoiler-text;
}
.tag-color-container:is(:global(.tag-color-container--spoiler)) :global(.tag) {
background-color: colors.$tag-spoiler-background;
color: colors.$tag-spoiler-text;
}
.tag-color-container:is(.tag-color-container--origin) :global(.tag) {
background-color: colors.$tag-origin-background;
color: colors.$tag-origin-text;
}
.tag-color-container:is(:global(.tag-color-container--origin)) :global(.tag) {
background-color: colors.$tag-origin-background;
color: colors.$tag-origin-text;
}
.tag-color-container:is(.tag-color-container--oc) :global(.tag) {
background-color: colors.$tag-oc-background;
color: colors.$tag-oc-text;
}
.tag-color-container:is(:global(.tag-color-container--oc)) :global(.tag) {
background-color: colors.$tag-oc-background;
color: colors.$tag-oc-text;
}
.tag-color-container:is(.tag-color-container--error) :global(.tag) {
background-color: colors.$tag-error-background;
color: colors.$tag-error-text;
}
.tag-color-container:is(:global(.tag-color-container--error)) :global(.tag) {
background-color: colors.$tag-error-background;
color: colors.$tag-error-text;
}
.tag-color-container:is(.tag-color-container--character) :global(.tag) {
background-color: colors.$tag-character-background;
color: colors.$tag-character-text;
}
.tag-color-container:is(:global(.tag-color-container--character)) :global(.tag) {
background-color: colors.$tag-character-background;
color: colors.$tag-character-text;
}
.tag-color-container:is(.tag-color-container--content-official) :global(.tag) {
background-color: colors.$tag-content-official-background;
color: colors.$tag-content-official-text;
}
.tag-color-container:is(:global(.tag-color-container--content-official)) :global(.tag) {
background-color: colors.$tag-content-official-background;
color: colors.$tag-content-official-text;
}
.tag-color-container:is(.tag-color-container--content-fanmade) :global(.tag) {
background-color: colors.$tag-content-fanmade-background;
color: colors.$tag-content-fanmade-text;
}
.tag-color-container:is(:global(.tag-color-container--content-fanmade)) :global(.tag) {
background-color: colors.$tag-content-fanmade-background;
color: colors.$tag-content-fanmade-text;
}
.tag-color-container:is(.tag-color-container--species) :global(.tag) {
background-color: colors.$tag-species-background;
color: colors.$tag-species-text;
}
.tag-color-container:is(:global(.tag-color-container--species)) :global(.tag) {
background-color: colors.$tag-species-background;
color: colors.$tag-species-text;
}
.tag-color-container:is(.tag-color-container--body-type) :global(.tag) {
background-color: colors.$tag-body-type-background;
color: colors.$tag-body-type-text;
}
.tag-color-container:is(:global(.tag-color-container--body-type)) :global(.tag) {
background-color: colors.$tag-body-type-background;
color: colors.$tag-body-type-text;
}
</style>

View File

@@ -1,106 +1,113 @@
<script>
/**
* List of tags to edit. Any duplicated tags present in the array will be removed on the first edit.
* @type {string[]}
*/
export let tags = [];
<script lang="ts">
import type { EventHandler } from "svelte/elements";
/** @type {Set<string>} */
let uniqueTags = new Set();
interface TagEditorProps {
// List of tags to edit. Any duplicated tags present in the array will be removed on the first edit.
tags?: string[];
}
$: uniqueTags = new Set(tags);
let {
tags = $bindable([])
}: TagEditorProps = $props();
/** @type {string} */
let addedTagName = '';
let uniqueTags = $state<Set<string>>(new Set());
/**
* Create a callback function to pass into both mouse & keyboard events for tag removal.
* @param {string} tagName
* @return {function(Event)} Callback to pass as event listener.
*/
function createTagRemoveHandler(tagName) {
return event => {
if (event.type === 'click') {
removeTag(tagName);
}
$effect.pre(() => {
uniqueTags = new Set(tags);
});
if (event instanceof KeyboardEvent && (event.code === 'Enter' || event.code === 'Space')) {
// To be more comfortable, automatically focus next available tag's remove button in the list.
if (event.currentTarget instanceof HTMLElement) {
const currenTagElement = event.currentTarget.closest('.tag');
const nextTagElement = currenTagElement?.previousElementSibling ?? currenTagElement?.parentElement?.firstElementChild;
const nextRemoveButton = nextTagElement?.querySelector('.remove');
let addedTagName = $state<string>('');
if (nextRemoveButton instanceof HTMLElement) {
nextRemoveButton.focus();
}
}
/**
* Create a callback function to pass into both mouse & keyboard events for tag removal.
* @param tagName Name to remove when clicked.
* @return Callback to pass as event listener.
*/
function createTagRemoveHandler(tagName: string): EventHandler<Event, HTMLElement> {
return event => {
if (event.type === 'click') {
removeTag(tagName);
}
removeTag(tagName);
}
}
}
if (event instanceof KeyboardEvent && (event.code === 'Enter' || event.code === 'Space')) {
// To be more comfortable, automatically focus next available tag's remove button in the list.
if (event.currentTarget instanceof HTMLElement) {
const currenTagElement = event.currentTarget.closest('.tag');
const nextTagElement = currenTagElement?.previousElementSibling ?? currenTagElement?.parentElement?.firstElementChild;
const nextRemoveButton = nextTagElement?.querySelector('.remove');
/**
* @param {string} tagName
*/
function removeTag(tagName) {
uniqueTags.delete(tagName);
tags = Array.from(uniqueTags);
}
/**
* @param {string} tagName
*/
function addTag(tagName) {
uniqueTags.add(tagName);
tags = Array.from(uniqueTags);
}
/**
* Handle adding new tags to the list or removing them when backspace is pressed.
*
* Additional note: For some reason, mobile Chrome breaks the usual behaviour inside extension. `code` is becoming
* empty, while usually it should contain proper button code.
*
* @param {KeyboardEvent} event
*/
function handleKeyPresses(event) {
if ((event.code === 'Enter' || event.key === 'Enter') && addedTagName.length) {
addTag(addedTagName)
addedTagName = '';
if (nextRemoveButton instanceof HTMLElement) {
nextRemoveButton.focus();
}
}
if ((event.code === 'Backspace' || event.key === 'Backspace') && !addedTagName.length && tags?.length) {
removeTag(tags[tags.length - 1]);
}
removeTag(tagName);
}
}
}
/**
* Remove the tag from the set.
* @param tagName Name of the tag to remove.
*/
function removeTag(tagName: string) {
uniqueTags.delete(tagName);
tags = Array.from(uniqueTags);
}
/**
* Add the tag to the set.
* @param tagName Name of the tag to add.
*/
function addTag(tagName: string) {
uniqueTags.add(tagName);
tags = Array.from(uniqueTags);
}
/**
* Handle adding new tags to the list or removing them when backspace is pressed.
*
* Additional note: For some reason, mobile Chrome breaks the usual behaviour inside extension. `code` is becoming
* empty, while usually it should contain proper button code.
*
* @param event
*/
function handleKeyPresses(event: KeyboardEvent) {
if ((event.code === 'Enter' || event.key === 'Enter') && addedTagName.length) {
addTag(addedTagName)
addedTagName = '';
}
if ((event.code === 'Backspace' || event.key === 'Backspace') && !addedTagName.length && tags?.length) {
removeTag(tags[tags.length - 1]);
}
}
</script>
<div class="tags-editor">
{#each uniqueTags.values() as tagName}
<div class="tag">
{tagName}
<span class="remove" on:click={createTagRemoveHandler(tagName)}
on:keydown={createTagRemoveHandler(tagName)}
role="button" tabindex="0">x</span>
</div>
{/each}
<input type="text"
bind:value={addedTagName}
on:keydown={handleKeyPresses}
autocomplete="off"
autocapitalize="none"/>
{#each uniqueTags.values() as tagName}
<div class="tag">
{tagName}
<span class="remove" onclick={createTagRemoveHandler(tagName)}
onkeydown={createTagRemoveHandler(tagName)}
role="button" tabindex="0">x</span>
</div>
{/each}
<input autocapitalize="none"
autocomplete="off"
bind:value={addedTagName}
onkeydown={handleKeyPresses}
type="text"/>
</div>
<style lang="scss">
.tags-editor {
display: flex;
flex-wrap: wrap;
gap: 6px;
.tags-editor {
display: flex;
flex-wrap: wrap;
gap: 6px;
input {
width: 100%;
}
input {
width: 100%;
}
}
</style>

View File

@@ -1,12 +1,20 @@
<script>
/** @type {string|undefined} */
export let name = undefined;
<script lang="ts">
import type { Snippet } from "svelte";
/** @type {boolean} */
export let checked;
interface CheckboxFieldProps {
name?: string;
checked: boolean;
children?: Snippet;
}
let {
name = undefined,
checked = $bindable(),
children
}: CheckboxFieldProps = $props();
</script>
<input type="checkbox" {name} bind:checked={checked}>
<input bind:checked={checked} {name} type="checkbox">
<span>
<slot></slot>
{@render children?.()}
</span>

View File

@@ -1,11 +1,21 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface FormContainerProps {
children?: Snippet;
}
let { children }: FormContainerProps = $props();
</script>
<form>
<slot></slot>
{@render children?.()}
</form>
<style lang="scss">
form {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
}
form {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
}
</style>

View File

@@ -1,27 +1,35 @@
<script>
<script lang="ts">
import type { Snippet } from "svelte";
/** @type {string|undefined} */
export let label = undefined;
interface FormControlProps {
label?: string;
children?: Snippet;
}
let {
label = undefined,
children
}: FormControlProps = $props();
</script>
<label class="control">
{#if label}
<div class="label">{label}</div>
{/if}
<slot></slot>
{#if label}
<div class="label">{label}</div>
{/if}
{@render children?.()}
</label>
<style lang="scss">
.label {
margin-bottom: .5em;
}
.label {
margin-bottom: .5em;
}
.control {
padding: 5px 0;
.control {
padding: 5px 0;
:global(textarea) {
width: 100%;
resize: vertical;
}
:global(textarea) {
width: 100%;
resize: vertical;
}
}
</style>

View File

@@ -1,40 +1,45 @@
<script>
/**
* @type {string[]|Record<string, string>}
*/
export let options = [];
<script lang="ts">
type SelectFieldOptionsObject = Record<string, string>;
/** @type {string|undefined} */
export let name = undefined;
interface SelectFieldProps {
options?: string[] | SelectFieldOptionsObject;
name?: string;
id?: string;
value?: string;
}
/** @type {string|undefined} */
export let id = undefined;
let {
options = [],
name = undefined,
id = undefined,
value = $bindable(undefined)
}: SelectFieldProps = $props();
/** @type {string|undefined} */
export let value = undefined;
/** @type {Record<string, string>} */
const optionPairs = {};
const optionPairs = $derived.by<SelectFieldOptionsObject>(() => {
const resultPairs: SelectFieldOptionsObject = {};
if (Array.isArray(options)) {
for (let option of options) {
optionPairs[option] = option;
}
for (let optionName of options) {
resultPairs[optionName] = optionName;
}
} else if (options && typeof options === 'object') {
Object.keys(options).forEach((key) => {
optionPairs[key] = options[key];
})
Object.keys(options).forEach(optionKey => {
resultPairs[optionKey] = options[optionKey];
})
}
return resultPairs;
});
</script>
<select {name} {id} bind:value={value}>
{#each Object.entries(optionPairs) as [value, label]}
<option {value}>{label}</option>
{/each}
<select bind:value={value} {id} {name}>
{#each Object.entries(optionPairs) as [value, label]}
<option {value}>{label}</option>
{/each}
</select>
<style lang="scss">
select {
width: 100%;
}
select {
width: 100%;
}
</style>

View File

@@ -1,80 +1,84 @@
<script>
import SelectField from "$components/ui/forms/SelectField.svelte";
import { categories } from "$lib/booru/tag-categories";
<script lang="ts">
import SelectField from "$components/ui/forms/SelectField.svelte";
import { categories } from "$lib/booru/tag-categories";
/** @type {string} */
export let value = '';
interface TagCategorySelectFieldProps {
value?: string;
}
/** @type {Record<string, string>} */
let tagCategoriesOptions = {
'': 'Default'
};
let {
value = $bindable('')
}: TagCategorySelectFieldProps = $props();
tagCategoriesOptions = categories.reduce((options, category) => {
options[category] = category
.replace('-', ' ')
.replace(/(?<=\s|^)\w/g, (matchedCharacter) => matchedCharacter.toUpperCase());
let tagCategoriesOptions = $derived.by<Record<string, string>>(() => {
return categories.reduce<Record<string, string>>((options, category) => {
options[category] = category
.replace('-', ' ')
.replace(/(?<=\s|^)\w/g, (matchedCharacter) => matchedCharacter.toUpperCase());
return options;
}, tagCategoriesOptions);
return options;
}, {
'': 'Default'
})
});
</script>
<SelectField bind:value={value} options={tagCategoriesOptions} name="tag_color"/>
<SelectField bind:value={value} name="tag_color" options={tagCategoriesOptions}/>
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
:global(select[name=tag_color]) {
:global(option) {
&:is(:global([value=rating])) {
background-color: colors.$tag-rating-background;
color: colors.$tag-rating-text;
}
:global(select[name=tag_color]) {
:global(option) {
&:is(:global([value=rating])) {
background-color: colors.$tag-rating-background;
color: colors.$tag-rating-text;
}
&:is(:global([value=spoiler])) {
background-color: colors.$tag-spoiler-background;
color: colors.$tag-spoiler-text;
}
&:is(:global([value=spoiler])) {
background-color: colors.$tag-spoiler-background;
color: colors.$tag-spoiler-text;
}
&:is(:global([value=origin])) {
background-color: colors.$tag-origin-background;
color: colors.$tag-origin-text;
}
&:is(:global([value=origin])) {
background-color: colors.$tag-origin-background;
color: colors.$tag-origin-text;
}
&:is(:global([value=oc])) {
background-color: colors.$tag-oc-background;
color: colors.$tag-oc-text;
}
&:is(:global([value=oc])) {
background-color: colors.$tag-oc-background;
color: colors.$tag-oc-text;
}
&:is(:global([value=error])) {
background-color: colors.$tag-error-background;
color: colors.$tag-error-text;
}
&:is(:global([value=error])) {
background-color: colors.$tag-error-background;
color: colors.$tag-error-text;
}
&:is(:global([value=character])) {
background-color: colors.$tag-character-background;
color: colors.$tag-character-text;
}
&:is(:global([value=character])) {
background-color: colors.$tag-character-background;
color: colors.$tag-character-text;
}
&:is(:global([value=content-official])) {
background-color: colors.$tag-content-official-background;
color: colors.$tag-content-official-text;
}
&:is(:global([value=content-official])) {
background-color: colors.$tag-content-official-background;
color: colors.$tag-content-official-text;
}
&:is(:global([value=content-fanmade])) {
background-color: colors.$tag-content-fanmade-background;
color: colors.$tag-content-fanmade-text;
}
&:is(:global([value=content-fanmade])) {
background-color: colors.$tag-content-fanmade-background;
color: colors.$tag-content-fanmade-text;
}
&:is(:global([value=species])) {
background-color: colors.$tag-species-background;
color: colors.$tag-species-text;
}
&:is(:global([value=species])) {
background-color: colors.$tag-species-background;
color: colors.$tag-species-text;
}
&:is(:global([value=body-type])) {
background-color: colors.$tag-body-type-background;
color: colors.$tag-body-type-text;
}
}
&:is(:global([value=body-type])) {
background-color: colors.$tag-body-type-background;
color: colors.$tag-body-type-text;
}
}
}
</style>

View File

@@ -1,18 +1,21 @@
<script>
/** @type {string|undefined} */
export let name = undefined;
<script lang="ts">
interface TextFieldProps {
name?: string;
placeholder?: string;
value?: string;
}
/** @type {string|undefined} */
export let placeholder = undefined;
/** @type {string} */
export let value = '';
let {
name = undefined,
placeholder = undefined,
value = $bindable('')
}: TextFieldProps = $props();
</script>
<input type="text" {name} {placeholder} bind:value={value}>
<input bind:value={value} {name} {placeholder} type="text">
<style lang="scss">
:global(.control) input {
width: 100%;
}
:global(.control) input {
width: 100%;
}
</style>

View File

@@ -1,38 +1,46 @@
<script lang="ts">
interface MenuProps {
children?: import('svelte').Snippet;
}
let { children }: MenuProps = $props();
</script>
<nav>
<slot></slot>
{@render children?.()}
</nav>
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
nav {
display: flex;
flex-direction: column;
nav {
display: flex;
flex-direction: column;
& > :global(.menu-item) {
padding: 5px 24px;
}
:global(.menu-item) {
color: colors.$text;
&:hover {
background: colors.$header-mobile-link-hover;
}
}
:global(hr) {
background: colors.$block-border;
margin: .5em 24px;
border: 0;
height: 1px;
}
:global(main) > & {
margin: {
left: -24px;
right: -24px;
}
}
& > :global(.menu-item) {
padding: 5px 24px;
}
:global(.menu-item) {
color: colors.$text;
&:hover {
background: colors.$header-mobile-link-hover;
}
}
:global(hr) {
background: colors.$block-border;
margin: .5em 24px;
border: 0;
height: 1px;
}
:global(main) > & {
margin: {
left: -24px;
right: -24px;
}
}
}
</style>

View File

@@ -1,37 +1,44 @@
<script>
import MenuLink from "$components/ui/menu/MenuItem.svelte";
<script lang="ts">
import MenuLink from "$components/ui/menu/MenuItem.svelte";
import type { Snippet } from "svelte";
import type { FormEventHandler, MouseEventHandler } from "svelte/elements";
/**
* @type {boolean}
*/
export let checked;
interface MenuCheckboxItemProps {
checked: boolean;
name?: string;
value?: string;
href?: string;
children?: Snippet;
onclick?: MouseEventHandler<HTMLInputElement>;
oninput?: FormEventHandler<HTMLInputElement>;
}
/**
* @type {string|undefined}
*/
export let name = undefined;
let {
checked = $bindable(),
name = undefined,
value = undefined,
href = undefined,
children,
onclick,
oninput,
}: MenuCheckboxItemProps = $props();
/**
* @type {string|undefined}
*/
export let value = undefined;
/**
* @type {string|null}
*/
export let href = null;
function stopPropagationAndPassCallback(originalEvent: MouseEvent) {
originalEvent.stopPropagation();
onclick?.(originalEvent as MouseEvent & { currentTarget: HTMLInputElement });
}
</script>
<MenuLink {href}>
<input type="checkbox" {name} {value} bind:checked={checked} on:input on:click|stopPropagation>
<slot></slot>
<input bind:checked={checked} {name} onclick={stopPropagationAndPassCallback} {oninput} type="checkbox" {value}>
{@render children?.()}
</MenuLink>
<style lang="scss">
:global(.menu-item) input {
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
:global(.menu-item) input {
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
</style>

View File

@@ -1,41 +1,45 @@
<script>
/**
* @type {string|null}
*/
export let href = null;
<script lang="ts">
import type { Snippet } from "svelte";
import type { MouseEventHandler } from "svelte/elements";
/**
* @type {App.IconName|null}
*/
export let icon = null;
interface MenuItemProps {
href?: string | null;
icon?: App.IconName | null;
target?: App.LinkTarget | undefined;
children?: Snippet;
onclick?: MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>;
}
/**
* @type {App.LinkTarget|undefined}
*/
export let target = undefined;
let {
href = null,
icon = null,
target = undefined,
children,
onclick
}: MenuItemProps = $props();
</script>
<svelte:element this="{href ? 'a': 'span'}" class="menu-item" {href} {target} on:click role="link" tabindex="0">
{#if icon}
<i class="icon icon-{icon}"></i>
{/if}
<slot></slot>
<svelte:element class="menu-item" {href} {onclick} role="link" tabindex="0" {target} this="{href ? 'a': 'span'}">
{#if icon}
<i class="icon icon-{icon}"></i>
{/if}
{@render children?.()}
</svelte:element>
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
.menu-item {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
.menu-item {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
i {
width: 16px;
height: 16px;
background: colors.$text;
margin-right: 6px;
}
i {
width: 16px;
height: 16px;
background: colors.$text;
margin-right: 6px;
}
}
</style>

View File

@@ -1,37 +1,44 @@
<script>
import MenuLink from "$components/ui/menu/MenuItem.svelte";
<script lang="ts">
import MenuLink from "$components/ui/menu/MenuItem.svelte";
import type { Snippet } from "svelte";
import type { FormEventHandler, MouseEventHandler } from "svelte/elements";
/**
* @type {boolean}
*/
export let checked;
interface MenuRadioItemProps {
checked: boolean;
name: string;
value: string;
href?: string | null;
children?: Snippet;
onclick?: MouseEventHandler<HTMLInputElement>;
oninput?: FormEventHandler<HTMLInputElement>;
}
/**
* @type {string}
*/
export let name;
let {
checked,
name,
value,
href = null,
children,
onclick,
oninput,
}: MenuRadioItemProps = $props();
/**
* @type {string}
*/
export let value;
/**
* @type {string|null}
*/
export let href = null;
function stopPropagationAndPassCallback(originalEvent: MouseEvent) {
originalEvent.stopPropagation();
onclick?.(originalEvent as MouseEvent & { currentTarget: HTMLInputElement });
}
</script>
<MenuLink {href}>
<input type="radio" {name} {value} {checked} on:input on:click|stopPropagation>
<slot></slot>
<input {checked} {name} onclick={stopPropagationAndPassCallback} {oninput} type="radio" {value}>
{@render children?.()}
</MenuLink>
<style lang="scss">
:global(.menu-item) input {
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
:global(.menu-item) input {
width: 16px;
height: 16px;
margin-right: 6px;
flex-shrink: 0;
}
</style>

View File

@@ -1,21 +1,27 @@
<script>
import "../styles/popup.scss";
import Header from "$components/layout/Header.svelte";
import Footer from "$components/layout/Footer.svelte";
<script lang="ts">
import "../styles/popup.scss";
import Header from "$components/layout/Header.svelte";
import Footer from "$components/layout/Footer.svelte";
// Sort of a hack, detect if we rendered in the browser tab or in the popup.
// Popup will always should have fixed 320px size, otherwise we consider it opened in the tab.
document.body.classList.toggle('is-in-tab', window.innerWidth > 320);
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
// Sort of a hack, detect if we rendered in the browser tab or in the popup.
// Popup will always should have fixed 320px size, otherwise we consider it opened in the tab.
document.body.classList.toggle('is-in-tab', window.innerWidth > 320);
</script>
<Header/>
<main>
<slot/>
{@render children?.()}
</main>
<Footer/>
<style lang="scss" global>
main {
padding: .5em 24px;
}
<style global lang="scss">
main {
padding: .5em 24px;
}
</style>

View File

@@ -1,29 +1,29 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile";
/** @type {import('$entities/MaintenanceProfile').default|undefined} */
let activeProfile;
let activeProfile = $derived<MaintenanceProfile | null>(
$maintenanceProfiles.find(profile => profile.id === $activeProfileStore) || null
);
$: activeProfile = $maintenanceProfiles.find(profile => profile.id === $activeProfileStore);
function turnOffActiveProfile() {
$activeProfileStore = null;
}
function turnOffActiveProfile() {
$activeProfileStore = null;
}
</script>
<Menu>
{#if activeProfile}
<MenuCheckboxItem checked on:input={turnOffActiveProfile} href="/features/maintenance/{activeProfile.id}">
Active Profile: {activeProfile.settings.name}
</MenuCheckboxItem>
<hr>
{/if}
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
<MenuItem href="/features/groups">Tag Groups</MenuItem>
{#if activeProfile}
<MenuCheckboxItem checked oninput={turnOffActiveProfile} href="/features/maintenance/{activeProfile.id}">
Active Profile: {activeProfile.settings.name}
</MenuCheckboxItem>
<hr>
<MenuItem href="/preferences">Preferences</MenuItem>
<MenuItem href="/about">About</MenuItem>
{/if}
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
<MenuItem href="/features/groups">Tag Groups</MenuItem>
<hr>
<MenuItem href="/preferences">Preferences</MenuItem>
<MenuItem href="/about">About</MenuItem>
</Menu>

View File

@@ -1,25 +1,25 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
</script>
<Menu>
<MenuItem icon="arrow-left" href="/">Back</MenuItem>
<hr>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<h1>
Furbooru Tagging Assistant
Furbooru Tagging Assistant
</h1>
<p>
This is a tool made to help tag images on Furbooru more efficiently. It is currently in development and is not yet
ready for use, but it still can provide some useful functionality.
This is a tool made to help tag images on Furbooru more efficiently. It is currently in development and is not yet
ready for use, but it still can provide some useful functionality.
</p>
<Menu>
<hr>
<MenuItem icon="globe" href="https://furbooru.org" target="_blank">
Visit Furbooru
</MenuItem>
<MenuItem icon="info-circle" href="https://github.com/koloml/furbooru-tagging-assistant" target="_blank">
GitHub Repo
</MenuItem>
<hr>
<MenuItem href="https://furbooru.org" icon="globe" target="_blank">
Visit Furbooru
</MenuItem>
<MenuItem href="https://github.com/koloml/furbooru-tagging-assistant" icon="info-circle" target="_blank">
GitHub Repo
</MenuItem>
</Menu>

View File

@@ -1,10 +1,10 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
</script>
<Menu>
<MenuItem href="/">Back</MenuItem>
<hr>
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
<MenuItem href="/">Back</MenuItem>
<hr>
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
</Menu>

View File

@@ -1,23 +1,21 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import TagGroup from "$entities/TagGroup";
/** @type {import('$entities/TagGroup').default[]} */
let groups = [];
$: groups = $tagGroups.sort((a, b) => a.settings.name.localeCompare(b.settings.name));
let groups = $derived<TagGroup[]>($tagGroups.sort((a, b) => a.settings.name.localeCompare(b.settings.name)));
</script>
<Menu>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<MenuItem href="/features/groups/new/edit" icon="plus">Create New</MenuItem>
{#if groups.length}
<hr>
{#each groups as group}
<MenuItem href="/features/groups/{group.id}">{group.settings.name}</MenuItem>
{/each}
{/if}
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<MenuItem href="/features/groups/new/edit" icon="plus">Create New</MenuItem>
{#if groups.length}
<hr>
<MenuItem href="/features/groups/import">Import Group</MenuItem>
{#each groups as group}
<MenuItem href="/features/groups/{group.id}">{group.settings.name}</MenuItem>
{/each}
{/if}
<hr>
<MenuItem href="/features/groups/import">Import Group</MenuItem>
</Menu>

View File

@@ -1,39 +1,38 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import GroupView from "$components/features/GroupView.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import GroupView from "$components/features/GroupView.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import TagGroup from "$entities/TagGroup";
const groupId = $page.params.id;
/** @type {import('$entities/TagGroup').default|null} */
let group = null;
let groupId = $derived<string>(page.params.id);
let group = $derived<TagGroup | null>($tagGroups.find(group => group.id === groupId) || null);
$effect(() => {
if (groupId === 'new') {
goto('/features/groups/new/edit');
goto('/features/groups/new/edit');
return;
}
$: {
group = $tagGroups.find(group => group.id === groupId) || null;
if (!group) {
console.warn(`Group ${groupId} not found.`);
goto('/features/groups');
}
if (!group) {
console.warn(`Group ${groupId} not found.`);
goto('/features/groups');
}
})
</script>
<Menu>
<MenuItem href="/features/groups" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/features/groups" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if group}
<GroupView {group}/>
<GroupView {group}/>
{/if}
<Menu>
<hr>
<MenuItem href="/features/groups/{groupId}/edit" icon="wrench">Edit Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/export" icon="file-export">Export Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/delete" icon="trash">Delete Group</MenuItem>
<hr>
<MenuItem href="/features/groups/{groupId}/edit" icon="wrench">Edit Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/export" icon="file-export">Export Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/delete" icon="trash">Delete Group</MenuItem>
</Menu>

View File

@@ -1,41 +1,44 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroups } from "$stores/entities/tag-groups";
import type TagGroup from "$entities/TagGroup";
const groupId = $page.params.id;
const targetGroup = $tagGroups.find(group => group.id === groupId);
const groupId = $derived<string>(page.params.id);
const targetGroup = $derived<TagGroup | undefined>($tagGroups.find(group => group.id === groupId));
$effect(() => {
if (!targetGroup) {
void goto('/features/groups');
goto('/features/groups');
}
})
async function deleteGroup() {
if (!targetGroup) {
console.warn('Attempting to delete the group, but the group is not loaded yet.');
return;
}
async function deleteGroup() {
if (!targetGroup) {
console.warn('Attempting to delete the group, but the group is not loaded yet.');
return;
}
await targetGroup.delete();
await goto('/features/groups');
}
await targetGroup.delete();
await goto('/features/groups');
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/groups/{groupId}">Back</MenuItem>
<hr>
<MenuItem href="/features/groups/{groupId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if targetGroup}
<p>
Do you want to remove group "{targetGroup.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem on:click={deleteGroup}>Yes</MenuItem>
<MenuItem href="/features/groups/{groupId}">No</MenuItem>
</Menu>
<p>
Do you want to remove group "{targetGroup.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem onclick={deleteGroup}>Yes</MenuItem>
<MenuItem href="/features/groups/{groupId}">No</MenuItem>
</Menu>
{:else}
<p>Loading...</p>
<p>Loading...</p>
{/if}

View File

@@ -1,82 +1,87 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import TagCategorySelectField from "$components/ui/forms/TagCategorySelectField.svelte";
import TextField from "$components/ui/forms/TextField.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagsEditor from "$components/tags/TagsEditor.svelte";
import TagGroup from "$entities/TagGroup";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import TagCategorySelectField from "$components/ui/forms/TagCategorySelectField.svelte";
import TextField from "$components/ui/forms/TextField.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagsEditor from "$components/tags/TagsEditor.svelte";
import TagGroup from "$entities/TagGroup";
import { tagGroups } from "$stores/entities/tag-groups";
const groupId = $page.params.id;
/** @type {TagGroup|null} */
let targetGroup = null;
let groupName = '';
/** @type {string[]} */
let tagsList = [];
/** @type {string[]} */
let prefixesList = [];
let tagCategory = '';
let groupId = $derived(page.params.id);
let targetGroup = $derived.by<TagGroup | null>(() => {
if (groupId === 'new') {
targetGroup = new TagGroup(crypto.randomUUID(), {});
} else {
targetGroup = $tagGroups.find(group => group.id === groupId) || null;
if (targetGroup) {
groupName = targetGroup.settings.name;
tagsList = [...targetGroup.settings.tags].sort((a, b) => a.localeCompare(b));
prefixesList = [...targetGroup.settings.prefixes].sort((a, b) => a.localeCompare(b));
tagCategory = targetGroup.settings.category;
} else {
goto('/features/groups');
}
return new TagGroup(crypto.randomUUID(), {});
}
async function saveGroup() {
if (!targetGroup) {
console.warn('Attempting to save group, but group is not loaded yet.');
return;
}
return $tagGroups.find(group => group.id === groupId) || null;
});
targetGroup.settings.name = groupName;
targetGroup.settings.tags = [...tagsList];
targetGroup.settings.prefixes = [...prefixesList];
targetGroup.settings.category = tagCategory;
let groupName = $state<string>('');
let tagsList = $state<string[]>([]);
let prefixesList = $state<string[]>([]);
let tagCategory = $state<string>('');
await targetGroup.save();
await goto(`/features/groups/${targetGroup.id}`);
$effect(() => {
if (groupId === 'new') {
return;
}
if (!targetGroup) {
goto('/features/groups');
return;
}
groupName = targetGroup.settings.name;
tagsList = [...targetGroup.settings.tags].sort((a, b) => a.localeCompare(b));
prefixesList = [...targetGroup.settings.prefixes].sort((a, b) => a.localeCompare(b));
tagCategory = targetGroup.settings.category;
});
async function saveGroup() {
if (!targetGroup) {
console.warn('Attempting to save group, but group is not loaded yet.');
return;
}
targetGroup.settings.name = groupName;
targetGroup.settings.tags = [...tagsList];
targetGroup.settings.prefixes = [...prefixesList];
targetGroup.settings.category = tagCategory;
await targetGroup.save();
await goto(`/features/groups/${targetGroup.id}`);
}
</script>
<Menu>
<MenuItem href="/features/groups/${groupId}" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/features/groups/{groupId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Group Name">
<TextField bind:value={groupName} placeholder="Group Name"></TextField>
<FormControl label="Group Name">
<TextField bind:value={groupName} placeholder="Group Name"></TextField>
</FormControl>
<FormControl label="Group Color">
<TagCategorySelectField bind:value={tagCategory}/>
</FormControl>
<TagsColorContainer targetCategory={tagCategory}>
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}/>
</FormControl>
<FormControl label="Group Color">
<TagCategorySelectField bind:value={tagCategory}/>
</TagsColorContainer>
<TagsColorContainer targetCategory={tagCategory}>
<FormControl label="Tag Prefixes">
<TagsEditor bind:tags={prefixesList}/>
</FormControl>
<TagsColorContainer targetCategory="{tagCategory}">
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}/>
</FormControl>
</TagsColorContainer>
<TagsColorContainer targetCategory="{tagCategory}">
<FormControl label="Tag Prefixes">
<TagsEditor bind:tags={prefixesList}/>
</FormControl>
</TagsColorContainer>
</TagsColorContainer>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={saveGroup}>Save Group</MenuItem>
<hr>
<MenuItem onclick={saveGroup}>Save Group</MenuItem>
</Menu>

View File

@@ -1,50 +1,53 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
const groupId = $page.params.id;
const groupTransporter = new EntitiesTransporter(TagGroup);
const group = $tagGroups.find(group => group.id === groupId);
let isEncodedGroupShown = $state(true);
/** @type {string} */
let rawExportedGroup;
/** @type {string} */
let encodedExportedGroup;
const groupId = $derived<string>(page.params.id);
const group = $derived<TagGroup | undefined>($tagGroups.find(group => group.id === groupId));
$effect(() => {
if (!group) {
goto('/features/groups');
} else {
rawExportedGroup = groupTransporter.exportToJSON(group);
encodedExportedGroup = groupTransporter.exportToCompressedJSON(group);
goto('/features/groups');
}
});
let isEncodedGroupShown = true;
const groupTransporter = new EntitiesTransporter(TagGroup);
let rawExportedGroup = $derived<string>(group ? groupTransporter.exportToJSON(group) : '');
let encodedExportedGroup = $derived<string>(group ? groupTransporter.exportToCompressedJSON(group) : '');
let selectedExportString = $derived<string>(isEncodedGroupShown ? encodedExportedGroup : rawExportedGroup);
function toggleEncoding() {
isEncodedGroupShown = !isEncodedGroupShown;
}
</script>
<Menu>
<MenuItem href="/features/groups/{groupId}" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/features/groups/{groupId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Export string">
<textarea readonly rows="6">{isEncodedGroupShown ? encodedExportedGroup : rawExportedGroup}</textarea>
</FormControl>
<FormControl label="Export string">
<textarea readonly rows="6">{selectedExportString}</textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={() => isEncodedGroupShown = !isEncodedGroupShown}>
Switch Format:
{#if isEncodedGroupShown}
Base64-Encoded
{:else}
Raw JSON
{/if}
</MenuItem>
<hr>
<MenuItem onclick={toggleEncoding}>
Switch Format:
{#if isEncodedGroupShown}
Base64-Encoded
{:else}
Raw JSON
{/if}
</MenuItem>
</Menu>

View File

@@ -1,134 +1,130 @@
<script>
import { goto } from "$app/navigation";
import GroupView from "$components/features/GroupView.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
<script lang="ts">
import { goto } from "$app/navigation";
import GroupView from "$components/features/GroupView.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
import { tagGroups } from "$stores/entities/tag-groups";
const groupTransporter = new EntitiesTransporter(TagGroup);
const groupTransporter = new EntitiesTransporter(TagGroup);
/** @type {string} */
let importedString = '';
/** @type {string} */
let errorMessage = '';
let importedString = $state('');
let errorMessage = $state('');
/** @type {TagGroup|null} */
let candidateGroup = null;
/** @type {TagGroup|null} */
let existingGroup = null;
let candidateGroup = $state<TagGroup | null>(null);
let existingGroup = $state<TagGroup | null>(null);
function tryImportingGroup() {
candidateGroup = null;
existingGroup = null;
function tryImportingGroup() {
candidateGroup = null;
existingGroup = null;
errorMessage = '';
importedString = importedString.trim();
errorMessage = '';
importedString = importedString.trim();
if (!importedString) {
errorMessage = 'Nothing to import.';
return;
}
try {
if (importedString.trim().startsWith('{')) {
candidateGroup = groupTransporter.importFromJSON(importedString);
}
candidateGroup = groupTransporter.importFromCompressedJSON(importedString);
} catch (error) {
errorMessage = error instanceof Error
? error.message
: 'Unknown error';
}
if (candidateGroup) {
existingGroup = $tagGroups.find(group => group.id === candidateGroup?.id) ?? null;
}
if (!importedString) {
errorMessage = 'Nothing to import.';
return;
}
function saveGroup() {
if (!candidateGroup) {
return;
}
candidateGroup.save().then(() => {
goto(`/features/groups`);
});
try {
if (importedString.trim().startsWith('{')) {
candidateGroup = groupTransporter.importFromJSON(importedString);
} else {
candidateGroup = groupTransporter.importFromCompressedJSON(importedString);
}
} catch (error) {
errorMessage = error instanceof Error
? error.message
: 'Unknown error';
}
function cloneAndSaveGroup() {
if (!candidateGroup) {
return;
}
const clonedProfile = new TagGroup(crypto.randomUUID(), candidateGroup.settings);
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
clonedProfile.save().then(() => {
goto(`/features/groups`);
});
if (candidateGroup) {
existingGroup = $tagGroups.find(group => group.id === candidateGroup?.id) ?? null;
}
}
function saveGroup() {
if (!candidateGroup) {
return;
}
candidateGroup.save().then(() => {
goto(`/features/groups`);
});
}
function cloneAndSaveGroup() {
if (!candidateGroup) {
return;
}
const clonedProfile = new TagGroup(crypto.randomUUID(), candidateGroup.settings);
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
clonedProfile.save().then(() => {
goto(`/features/groups`);
});
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/groups">Back</MenuItem>
<hr>
<MenuItem href="/features/groups" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if errorMessage}
<p class="error">Failed to import: {errorMessage}</p>
<Menu>
<hr>
</Menu>
<p class="error">Failed to import: {errorMessage}</p>
<Menu>
<hr>
</Menu>
{/if}
{#if !candidateGroup}
<FormContainer>
<FormControl label="Import string">
<textarea bind:value={importedString} rows="6"></textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={tryImportingGroup}>Import</MenuItem>
</Menu>
<FormContainer>
<FormControl label="Import string">
<textarea bind:value={importedString} rows="6"></textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem onclick={tryImportingGroup}>Import</MenuItem>
</Menu>
{:else}
{#if existingGroup}
<p class="warning">
This group will replace the existing "{existingGroup.settings.name}" group, since it have the same ID.
</p>
{/if}
<GroupView group={candidateGroup}></GroupView>
<Menu>
<hr>
{#if existingGroup}
<p class="warning">
This group will replace the existing "{existingGroup.settings.name}" group, since it have the same ID.
</p>
<MenuItem onclick={saveGroup}>Replace Existing Group</MenuItem>
<MenuItem onclick={cloneAndSaveGroup}>Save as New Group</MenuItem>
{:else}
<MenuItem onclick={saveGroup}>Import New Group</MenuItem>
{/if}
<GroupView group="{candidateGroup}"></GroupView>
<Menu>
<hr>
{#if existingGroup}
<MenuItem on:click={saveGroup}>Replace Existing Group</MenuItem>
<MenuItem on:click={cloneAndSaveGroup}>Save as New Group</MenuItem>
{:else}
<MenuItem on:click={saveGroup}>Import New Group</MenuItem>
{/if}
<MenuItem on:click={() => candidateGroup = null}>Cancel</MenuItem>
</Menu>
<MenuItem onclick={() => candidateGroup = null}>Cancel</MenuItem>
</Menu>
{/if}
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
.error, .warning {
padding: 5px 24px;
margin: {
left: -24px;
right: -24px;
}
.error, .warning {
padding: 5px 24px;
margin: {
left: -24px;
right: -24px;
}
}
.error {
background: colors.$error-background;
}
.error {
background: colors.$error-background;
}
.warning {
background: colors.$warning-background;
margin-bottom: .5em;
}
.warning {
background: colors.$warning-background;
margin-bottom: .5em;
}
</style>

View File

@@ -1,46 +1,43 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
/** @type {import('$entities/MaintenanceProfile').default[]} */
let profiles = [];
let profiles = $derived<MaintenanceProfile[]>(
$maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name))
);
$: profiles = $maintenanceProfiles.sort((a, b) => a.settings.name.localeCompare(b.settings.name));
function resetActiveProfile() {
$activeProfileStore = null;
}
function resetActiveProfile() {
$activeProfileStore = null;
}
/**
* @param {Event} event
*/
function enableSelectedProfile(event) {
const target = event.target;
if (target instanceof HTMLInputElement && target.checked) {
activeProfileStore.set(target.value);
}
function enableSelectedProfile(event: Event) {
const target = event.target;
if (target instanceof HTMLInputElement && target.checked) {
activeProfileStore.set(target.value);
}
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/">Back</MenuItem>
<MenuItem icon="plus" href="/features/maintenance/new/edit">Create New</MenuItem>
{#if profiles.length}
<hr>
{/if}
{#each profiles as profile}
<MenuRadioItem href="/features/maintenance/{profile.id}"
name="active-profile"
value="{profile.id}"
checked="{$activeProfileStore === profile.id}"
on:input={enableSelectedProfile}>
{profile.settings.name}
</MenuRadioItem>
{/each}
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<MenuItem href="/features/maintenance/new/edit" icon="plus">Create New</MenuItem>
{#if profiles.length}
<hr>
<MenuItem href="#" on:click={resetActiveProfile}>Reset Active Profile</MenuItem>
<MenuItem href="/features/maintenance/import">Import Profile</MenuItem>
{/if}
{#each profiles as profile}
<MenuRadioItem href="/features/maintenance/{profile.id}"
name="active-profile"
value={profile.id}
checked={$activeProfileStore === profile.id}
oninput={enableSelectedProfile}>
{profile.settings.name}
</MenuRadioItem>
{/each}
<hr>
<MenuItem href="#" onclick={resetActiveProfile}>Reset Active Profile</MenuItem>
<MenuItem href="/features/maintenance/import">Import Profile</MenuItem>
</Menu>

View File

@@ -1,63 +1,68 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import ProfileView from "$components/features/ProfileView.svelte";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
<script lang="ts">
import { run } from 'svelte/legacy';
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { activeProfileStore, maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import ProfileView from "$components/features/ProfileView.svelte";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import { onMount } from "svelte";
const profileId = $page.params.id;
/** @type {import('$entities/MaintenanceProfile').default|null} */
let profile = null;
let profileId = $derived(page.params.id);
let profile = $derived<MaintenanceProfile|null>(
$maintenanceProfiles.find(profile => profile.id === profileId) || null
);
$effect(() => {
if (profileId === 'new') {
goto('/features/maintenance/new/edit');
goto('/features/maintenance/new/edit');
return;
}
$: {
const resolvedProfile = $maintenanceProfiles.find(profile => profile.id === profileId);
if (!profile) {
console.warn(`Profile ${profileId} not found.`);
goto('/features/maintenance');
}
});
if (resolvedProfile) {
profile = resolvedProfile;
} else {
console.warn(`Profile ${profileId} not found.`);
goto('/features/maintenance');
}
let isActiveProfile = $state(false);
$effect.pre(() => {
isActiveProfile = $activeProfileStore === profileId;
});
$effect(() => {
if (isActiveProfile && $activeProfileStore !== profileId) {
$activeProfileStore = profileId;
}
let isActiveProfile = $activeProfileStore === profileId;
$: {
if (isActiveProfile && $activeProfileStore !== profileId) {
$activeProfileStore = profileId;
}
if (!isActiveProfile && $activeProfileStore === profileId) {
$activeProfileStore = null;
}
if (!isActiveProfile && $activeProfileStore === profileId) {
$activeProfileStore = null;
}
});
</script>
<Menu>
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if profile}
<ProfileView {profile}/>
<ProfileView {profile}/>
{/if}
<Menu>
<hr>
<MenuItem icon="wrench" href="/features/maintenance/{profileId}/edit">Edit Profile</MenuItem>
<MenuCheckboxItem bind:checked={isActiveProfile}>
Activate Profile
</MenuCheckboxItem>
<MenuItem icon="file-export" href="/features/maintenance/{profileId}/export">
Export Profile
</MenuItem>
<MenuItem icon="trash" href="/features/maintenance/{profileId}/delete">
Delete Profile
</MenuItem>
<hr>
<MenuItem href="/features/maintenance/{profileId}/edit" icon="wrench">Edit Profile</MenuItem>
<MenuCheckboxItem bind:checked={isActiveProfile}>
Activate Profile
</MenuCheckboxItem>
<MenuItem href="/features/maintenance/{profileId}/export" icon="file-export">
Export Profile
</MenuItem>
<MenuItem href="/features/maintenance/{profileId}/delete" icon="trash">
Delete Profile
</MenuItem>
</Menu>
<style lang="scss">

View File

@@ -1,41 +1,46 @@
<script>
import { goto } from "$app/navigation";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/stores";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
<script lang="ts">
import { goto } from "$app/navigation";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/state";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
const profileId = $page.params.id;
const targetProfile = $maintenanceProfiles.find(profile => profile.id === profileId);
const profileId = $derived(page.params.id);
const targetProfile = $derived<MaintenanceProfile | null>(
$maintenanceProfiles.find(profile => profile.id === profileId) || null
);
$effect(() => {
if (!targetProfile) {
void goto('/features/maintenance');
goto('/features/maintenance');
}
});
async function deleteProfile() {
if (!targetProfile) {
console.warn('Attempting to delete the profile, but the profile is not loaded yet.');
return;
}
async function deleteProfile() {
if (!targetProfile) {
console.warn('Attempting to delete the profile, but the profile is not loaded yet.');
return;
}
await targetProfile.delete();
await goto('/features/maintenance');
}
await targetProfile.delete();
await goto('/features/maintenance');
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/maintenance/{profileId}">Back</MenuItem>
<hr>
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if targetProfile}
<p>
Do you want to remove profile "{targetProfile.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem on:click={deleteProfile}>Yes</MenuItem>
<MenuItem href="/features/maintenance/{profileId}">No</MenuItem>
</Menu>
<p>
Do you want to remove profile "{targetProfile.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem onclick={deleteProfile}>Yes</MenuItem>
<MenuItem href="/features/maintenance/{profileId}">No</MenuItem>
</Menu>
{:else}
<p>Loading...</p>
<p>Loading...</p>
{/if}

View File

@@ -1,69 +1,73 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagsEditor from "$components/tags/TagsEditor.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import TextField from "$components/ui/forms/TextField.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
<script lang="ts">
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagsEditor from "$components/tags/TagsEditor.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import TextField from "$components/ui/forms/TextField.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import MaintenanceProfile from "$entities/MaintenanceProfile";
/** @type {string} */
let profileId = $page.params.id;
/** @type {MaintenanceProfile|null} */
let targetProfile = null;
/** @type {string} */
let profileName = '';
/** @type {string[]} */
let tagsList = [];
let profileId = $derived(page.params.id);
let targetProfile = $derived.by<MaintenanceProfile | null>(() => {
if (profileId === 'new') {
targetProfile = new MaintenanceProfile(crypto.randomUUID(), {});
} else {
const maybeExistingProfile = $maintenanceProfiles.find(profile => profile.id === profileId);
if (maybeExistingProfile) {
targetProfile = maybeExistingProfile;
profileName = targetProfile.settings.name;
tagsList = [...targetProfile.settings.tags].sort((a, b) => a.localeCompare(b));
} else {
goto('/features/maintenance');
}
return new MaintenanceProfile(crypto.randomUUID(), {});
}
async function saveProfile() {
if (!targetProfile) {
console.warn('Attempting to save the profile, but the profile is not loaded yet.');
return;
}
return $maintenanceProfiles.find(profile => profile.id === profileId) || null;
});
targetProfile.settings.name = profileName;
targetProfile.settings.tags = [...tagsList];
targetProfile.settings.temporary = false;
let profileName = $state('');
let tagsList = $state<string[]>([]);
await targetProfile.save();
await goto('/features/maintenance/' + targetProfile.id);
$effect(() => {
if (profileId === 'new') {
return;
}
if (!targetProfile) {
goto('/features/maintenance');
return;
}
profileName = targetProfile.settings.name;
tagsList = [...targetProfile.settings.tags].sort((a, b) => a.localeCompare(b));
});
async function saveProfile() {
if (!targetProfile) {
console.warn('Attempting to save the profile, but the profile is not loaded yet.');
return;
}
targetProfile.settings.name = profileName;
targetProfile.settings.tags = [...tagsList];
targetProfile.settings.temporary = false;
await targetProfile.save();
await goto('/features/maintenance/' + targetProfile.id);
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/maintenance{profileId === 'new' ? '' : '/' + profileId}">
Back
</MenuItem>
<hr>
<MenuItem href="/features/maintenance{profileId === 'new' ? '' : '/' + profileId}" icon="arrow-left">
Back
</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Profile Name">
<TextField bind:value={profileName} placeholder="Profile Name"></TextField>
</FormControl>
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}></TagsEditor>
</FormControl>
<FormControl label="Profile Name">
<TextField bind:value={profileName} placeholder="Profile Name"></TextField>
</FormControl>
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}></TagsEditor>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem href="#" on:click={saveProfile}>Save Profile</MenuItem>
<hr>
<MenuItem href="#" onclick={saveProfile}>Save Profile</MenuItem>
</Menu>

View File

@@ -1,52 +1,53 @@
<script>
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
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";
import MaintenanceProfile from "$entities/MaintenanceProfile";
<script lang="ts">
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
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";
import MaintenanceProfile from "$entities/MaintenanceProfile";
const profileId = $page.params.id;
const profile = $maintenanceProfiles.find(profile => profile.id === profileId);
let isCompressedProfileShown = $state(true);
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
/** @type {string} */
let exportedProfile = '';
/** @type {string} */
let compressedProfile = '';
const profileId = $derived(page.params.id);
const profile = $derived<MaintenanceProfile | null>(
$maintenanceProfiles.find(profile => profile.id === profileId) || null
);
$effect(() => {
if (!profile) {
goto('/features/maintenance/');
} else {
exportedProfile = profilesTransporter.exportToJSON(profile);
compressedProfile = profilesTransporter.exportToCompressedJSON(profile);
goto('/features/maintenance/');
}
});
let isCompressedProfileShown = true;
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
let rawExportedProfile = $derived(profile ? profilesTransporter.exportToJSON(profile) : '');
let compressedExportedProfile = $derived(profile ? profilesTransporter.exportToCompressedJSON(profile) : '');
let selectedExportString = $derived(isCompressedProfileShown ? compressedExportedProfile : rawExportedProfile);
</script>
<Menu>
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">
Back
</MenuItem>
<hr>
<MenuItem href="/features/maintenance/{profileId}" icon="arrow-left">
Back
</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Export string">
<textarea readonly rows="6">{isCompressedProfileShown ? compressedProfile : exportedProfile}</textarea>
</FormControl>
<FormControl label="Export string">
<textarea readonly rows="6">{selectedExportString}</textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={() => isCompressedProfileShown = !isCompressedProfileShown}>
Switch Format:
{#if isCompressedProfileShown}
Base64-Encoded
{:else}
Raw JSON
{/if}
</MenuItem>
<hr>
<MenuItem onclick={() => isCompressedProfileShown = !isCompressedProfileShown}>
Switch Format:
{#if isCompressedProfileShown}
Base64-Encoded
{:else}
Raw JSON
{/if}
</MenuItem>
</Menu>

View File

@@ -1,134 +1,130 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile";
import FormControl from "$components/ui/forms/FormControl.svelte";
import ProfileView from "$components/features/ProfileView.svelte";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import { goto } from "$app/navigation";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
<script lang="ts">
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 MaintenanceProfile from "$entities/MaintenanceProfile";
import FormControl from "$components/ui/forms/FormControl.svelte";
import ProfileView from "$components/features/ProfileView.svelte";
import { maintenanceProfiles } from "$stores/entities/maintenance-profiles";
import { goto } from "$app/navigation";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter";
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);
/** @type {string} */
let importedString = '';
/** @type {string} */
let errorMessage = '';
let importedString = $state('');
let errorMessage = $state('');
/** @type {MaintenanceProfile|null} */
let candidateProfile = null;
/** @type {MaintenanceProfile|null} */
let existingProfile = null;
let candidateProfile = $state<MaintenanceProfile | null>(null);
let existingProfile = $state<MaintenanceProfile | null>(null);
function tryImportingProfile() {
candidateProfile = null;
existingProfile = null;
function tryImportingProfile() {
candidateProfile = null;
existingProfile = null;
errorMessage = '';
importedString = importedString.trim();
errorMessage = '';
importedString = importedString.trim();
if (!importedString) {
errorMessage = 'Nothing to import.';
return;
}
try {
if (importedString.trim().startsWith('{')) {
candidateProfile = profilesTransporter.importFromJSON(importedString);
}
candidateProfile = profilesTransporter.importFromCompressedJSON(importedString);
} catch (error) {
errorMessage = error instanceof Error
? error.message
: 'Unknown error';
}
if (candidateProfile) {
existingProfile = $maintenanceProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
}
if (!importedString) {
errorMessage = 'Nothing to import.';
return;
}
function saveProfile() {
if (!candidateProfile) {
return;
}
candidateProfile.save().then(() => {
goto(`/features/maintenance`);
});
try {
if (importedString.trim().startsWith('{')) {
candidateProfile = profilesTransporter.importFromJSON(importedString);
} else {
candidateProfile = profilesTransporter.importFromCompressedJSON(importedString)
}
} catch (error) {
errorMessage = error instanceof Error
? error.message
: 'Unknown error';
}
function cloneAndSaveProfile() {
if (!candidateProfile) {
return;
}
const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings);
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
clonedProfile.save().then(() => {
goto(`/features/maintenance`);
});
if (candidateProfile) {
existingProfile = $maintenanceProfiles.find(profile => profile.id === candidateProfile?.id) ?? null;
}
}
function saveProfile() {
if (!candidateProfile) {
return;
}
candidateProfile.save().then(() => {
goto(`/features/maintenance`);
});
}
function cloneAndSaveProfile() {
if (!candidateProfile) {
return;
}
const clonedProfile = new MaintenanceProfile(crypto.randomUUID(), candidateProfile.settings);
clonedProfile.settings.name += ` (Clone ${new Date().toISOString()})`;
clonedProfile.save().then(() => {
goto(`/features/maintenance`);
});
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/maintenance">Back</MenuItem>
<hr>
<MenuItem href="/features/maintenance" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if errorMessage}
<p class="error">Failed to import: {errorMessage}</p>
<Menu>
<hr>
</Menu>
<p class="error">Failed to import: {errorMessage}</p>
<Menu>
<hr>
</Menu>
{/if}
{#if !candidateProfile}
<FormContainer>
<FormControl label="Import string">
<textarea bind:value={importedString} rows="6"></textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={tryImportingProfile}>Import</MenuItem>
</Menu>
<FormContainer>
<FormControl label="Import string">
<textarea bind:value={importedString} rows="6"></textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem onclick={tryImportingProfile}>Import</MenuItem>
</Menu>
{:else}
{#if existingProfile}
<p class="warning">
This profile will replace the existing "{existingProfile.settings.name}" profile, since it have the same ID.
</p>
{/if}
<ProfileView profile={candidateProfile}></ProfileView>
<Menu>
<hr>
{#if existingProfile}
<p class="warning">
This profile will replace the existing "{existingProfile.settings.name}" profile, since it have the same ID.
</p>
<MenuItem onclick={saveProfile}>Replace Existing Profile</MenuItem>
<MenuItem onclick={cloneAndSaveProfile}>Save as New Profile</MenuItem>
{:else}
<MenuItem onclick={saveProfile}>Import New Profile</MenuItem>
{/if}
<ProfileView profile="{candidateProfile}"></ProfileView>
<Menu>
<hr>
{#if existingProfile}
<MenuItem on:click={saveProfile}>Replace Existing Profile</MenuItem>
<MenuItem on:click={cloneAndSaveProfile}>Save as New Profile</MenuItem>
{:else}
<MenuItem on:click={saveProfile}>Import New Profile</MenuItem>
{/if}
<MenuItem on:click={() => candidateProfile = null}>Cancel</MenuItem>
</Menu>
<MenuItem onclick={() => candidateProfile = null}>Cancel</MenuItem>
</Menu>
{/if}
<style lang="scss">
@use '$styles/colors';
@use '$styles/colors';
.error, .warning {
padding: 5px 24px;
margin: {
left: -24px;
right: -24px;
}
.error, .warning {
padding: 5px 24px;
margin: {
left: -24px;
right: -24px;
}
}
.error {
background: colors.$error-background;
}
.error {
background: colors.$error-background;
}
.warning {
background: colors.$warning-background;
margin-bottom: .5em;
}
.warning {
background: colors.$warning-background;
margin-bottom: .5em;
}
</style>

View File

@@ -1,14 +1,14 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
</script>
<Menu>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/preferences/tags">Tagging</MenuItem>
<MenuItem href="/preferences/search">Search</MenuItem>
<MenuItem href="/preferences/misc">Misc & Tools</MenuItem>
<hr>
<MenuItem href="/preferences/debug">Debug</MenuItem>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/preferences/tags">Tagging</MenuItem>
<MenuItem href="/preferences/search">Search</MenuItem>
<MenuItem href="/preferences/misc">Misc & Tools</MenuItem>
<hr>
<MenuItem href="/preferences/debug">Debug</MenuItem>
</Menu>

View File

@@ -1,10 +1,10 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
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>
<MenuItem href="/preferences" icon="arrow-left">Back</MenuItem>
<hr>
<MenuItem href="/preferences/debug/storage">Inspect Storages</MenuItem>
</Menu>

View File

@@ -1,13 +1,13 @@
<script>
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import { storagesCollection } from "$stores/debug";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import { storagesCollection } from "$stores/debug";
</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}
<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>

View File

@@ -1,29 +1,38 @@
<script>
import StorageViewer from "$components/debugging/StorageViewer.svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
<script lang="ts">
import StorageViewer from "$components/debugging/StorageViewer.svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
let pathString = '';
/** @type {string[]} */
let pathArray = [];
/** @type {string|undefined} */
let storageName = void 0;
let pathArray = $derived.by<string[]>(() => {
const pathString = page.params.path;
$: {
pathString = $page.params.path;
pathArray = pathString.length ? pathString.split("/") : [];
storageName = pathArray.shift()
return pathString.length ? pathString.split('/') : [];
});
if (pathArray.length && pathArray[pathArray.length - 1] === '') {
pathArray.pop();
}
let storageName = $derived.by<string | undefined>(() => {
return pathArray[0];
});
if (!storageName) {
goto("/preferences/debug/storage");
}
// Copy of the array without the storage or empty string at the end.
let normalizedPathArray = $derived.by<string[]>(() => {
// Excludes storage name
const resultArray = pathArray.slice(1);
// Getting rid of trailing empty entry
if (resultArray.length && resultArray[resultArray.length - 1] === '') {
resultArray.pop();
}
return resultArray;
});
$effect(() => {
if (!storageName) {
goto("/preferences/debug/storage");
}
});
</script>
{#if storageName}
<StorageViewer storage="{storageName}" path="{pathArray}"></StorageViewer>
<StorageViewer storage={storageName} path={normalizedPathArray}></StorageViewer>
{/if}

View File

@@ -1,20 +1,20 @@
<script>
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import { fullScreenViewerEnabled } from "$stores/preferences/misc";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import { fullScreenViewerEnabled } from "$stores/preferences/misc";
</script>
<Menu>
<MenuItem icon="arrow-left" href="/preferences">Back</MenuItem>
<hr>
<MenuItem href="/preferences" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl>
<CheckboxField bind:checked={$fullScreenViewerEnabled}>
Enable fullscreen viewer button
</CheckboxField>
</FormControl>
<FormControl>
<CheckboxField bind:checked={$fullScreenViewerEnabled}>
Enable fullscreen viewer button
</CheckboxField>
</FormControl>
</FormContainer>

View File

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

View File

@@ -1,20 +1,20 @@
<script>
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance";
import CheckboxField from "$components/ui/forms/CheckboxField.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { stripBlacklistedTagsEnabled } from "$stores/preferences/maintenance";
</script>
<Menu>
<MenuItem icon="arrow-left" href="/preferences">Back</MenuItem>
<hr>
<MenuItem href="/preferences" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl>
<CheckboxField bind:checked={$stripBlacklistedTagsEnabled}>
Automatically remove black-listed tags from the images
</CheckboxField>
</FormControl>
<FormControl>
<CheckboxField bind:checked={$stripBlacklistedTagsEnabled}>
Automatically remove black-listed tags from the images
</CheckboxField>
</FormControl>
</FormContainer>