diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cf890289 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,315 @@ +[*] +charset = utf-8 +end_of_line = crlf +indent_size = 3 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 3 +ij_continuation_indent_size = 1 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.css] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_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_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.sass] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_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_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +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_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_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 + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.cjs,*.js}] +indent_size = 3 +tab_width = 3 +ij_continuation_indent_size = 1 +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 = normal +ij_javascript_assignment_wrap = normal +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 = next_line +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 = true +ij_javascript_chained_call_dot_on_new_line = false +ij_javascript_class_brace_style = next_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = true +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = split_into_lines +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = true +ij_javascript_for_brace_force = always +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 = next_line +ij_javascript_if_brace_force = always +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 = true +ij_javascript_keep_simple_methods_in_one_line = true +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = next_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_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_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 = false +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 = false +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 = global +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 = always +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.graphqlconfig,*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal +ij_html_uniform_ident = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..ac9e61df --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +.github/ +docs/ +external/ +lang/ +scripts/ +styles/ +templates/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..613a3c84 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +/** + * Loads https://github.com/typhonjs-node-config/typhonjs-config-eslint/blob/master/3.0/basic/es8/server/node/.eslintrc + * Loads https://github.com/typhonjs-fvtt/eslint-config-foundry.js/blob/main/0.8.0.js + * + * NPM: https://www.npmjs.com/package/typhonjs-config-eslint + * NPM: https://www.npmjs.com/package/@typhonjs-fvtt/eslint-config-foundry.js + */ +{ + // ESLint configs are prone to particular choices, so if the first config below doesn't work for you then replace + // with one that you do prefer. The second config defines globals defined in `foundry.js` for use w/ `no-shadow`. + "extends": [ + "@typhonjs-config/eslint-config/esm/2022/browser", + "@typhonjs-fvtt/eslint-config-foundry.js" + ], + + // Defines / overrides a few more environment parameters not provided in the configs above. + "env": { + "jquery": true + }, + + // Prevents overwriting any built in globals particularly from `@typhonjs-fvtt/eslint-config-foundry.js`, but also + // node & browser environments. `event / window.event` shadowing is allowed due to being a common variable name and + // an uncommonly used browser feature. + // + // Note: if you are using Typescript you must use `@typescript-eslint/no-shadow` + "rules": { + "no-shadow": ["error", { + "builtinGlobals": true, + "hoist": "all", + "allow": [ + "document", + "event", + "name", + "parent", + "status", + "top" + ] + }] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4ee706a5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +packs/** binary diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a7c20698 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,41 @@ +# From https://github.com/League-of-Foundry-Developers/FoundryVTT-Module-Template/blob/master/.github/workflows/main.yml +name: Release Creation + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + # Substitute the Manifest and Download URLs in the module.json + - name: Substitute Manifest and Download Links For Versioned Ones + id: sub_manifest_link_version + uses: microsoft/variable-substitution@v1 + with: + files: 'module.json' + env: + version: ${{github.event.release.tag_name}} + url: https://github.com/${{github.repository}} + manifest: https://github.com/${{github.repository}}/releases/latest/download/module.json + download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/module.zip + + # Create a zip file with all files required by the module to add to the release + - run: zip -r ./module.zip module.json assets/ css/ database/ external/ lang/ packs/ scripts/ src/ styles/ templates/ LICENSE AUTHORS + + # Create a release for this specific version + - name: Update Release with Files + id: create_version_release + uses: ncipollo/release-action@v1 + with: + allowUpdates: true # Set this to false if you want to prevent updating existing releases + name: ${{ github.event.release.name }} + draft: false + prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} + artifacts: './module.json, ./module.zip' + tag: ${{ github.event.release.tag_name }} + body: ${{ github.event.release.body }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml.bak similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release.yml.bak diff --git a/.gitignore b/.gitignore index 0dd20ab4..f2a2f972 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .idea/ -styles/.sass-cache/ -devtool/ +docs/ lang/untranslated.json node_modules/ foundry.js -package.json -/changelog.html +package-lock.json \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..f38e272f --- /dev/null +++ b/AUTHORS @@ -0,0 +1,26 @@ +# This is the official list of foundryvtt-forien-quest-log authors for copyright purposes. +# +# This does not necessarily list everyone who has contributed code, since in +# some cases, their employer may be the copyright holder. To see the full list +# of contributors, see the revision history in source control or +# https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/graphs/contributors. +# +# Authors who wish to be recognized in this file should add themselves (or +# their employer, as appropriate). + +4535992 <5201916+p4535992@users.noreply.github.com> +BrotherSharp <41280723+BrotherSharper@users.noreply.github.com> +Dilomos <49794325+Dilomos@users.noreply.github.com> +Eadorin +eclarke12 <42503461+eclarke12@users.noreply.github.com> +innocenti +JJBocanegra <5797636+JJBocanegra@users.noreply.github.com> +klo +Lyndsey Toft +Michael Leahy +Rughalt <802214+Rughalt@users.noreply.github.com> +Sad <23254376+zeteticl@users.noreply.github.com> +sdenec <8881200+sdenec@users.noreply.github.com> +Wojciech "Forien" Szulc +xdy <246024+xdy@users.noreply.github.com> +Fallayn diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..bf32643b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +## Contribution Etiquette +Open Source projects function most efficiently when everyone communicates well with each other. Here are some suggested practices that will let everyone else work alongside you comfortably: + +### **Did you find a bug? Do you want to suggest an enhancement?** + +* **Ensure your contribution is novel** by searching the [Issues](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/issues) page. Be sure to look through both open and closed issues, especially for enhancement suggestions as rejected suggestions will be closed. + +* If applicable **use the appropriate issue templates** to automatically apply the relevant tag to the created issue. This allows issues to be quickly differentiated as bug reports, enhancement suggestions, or whatever else without a maintainer manually adding the tag to the issue. The bug report template will specify which information to add that will aid in reproducing the bug. + +* Inconsequential fixes regarding typos, whitespace, etc. may not warrant an issue and can skip straight to the Pull Request. + +### **Do you want to fix a currently existing issue?** + +* Before working on an issue please **honor issue assignments** and ask to be assigned to the issue in a comment on the issue page. This allows everyone else to see that someone is working on the issue preventing any duplicate pull requests down the line. If someone is already assigned to the issue feel free to reach out to the assignee and inquire on the progress of that fix. + +* While working on an issue **document your changes** by providing detailed commit messages and providing an overview of planned / current implementation / changes on the issue page. + +* When you feel your fork adequately fixes an issue, **submit a pull request for review**! [Please indicate which issue is to be closed if the PR is merged](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) as well as giving a detailed overview of your changes. + +* Please **limit the content of your pull requests to its linked issues**. If your pull request does anything additional you must first open an issue that your PR may close (see the below section). + +### **Do you intend to add a new feature or change an existing one?** + +* Please **open an issue** to generate feedback on the change first. The maintainers reserve the right to reject changes / additions that they do not want to maintain, so be sure that the maintainers are on the same page as you to avoid wasted work! + +* Now that an issue covers the changes you wanted to make refer to the above section for implementing the changes. + +### **Do you want to submit/update a translation?** + +* **Submit a pull request** with your new / updated JSON file in the [lang folder](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/tree/master/lang). + +* Be sure that the JSON file is either completely flat or completely nested. + +* If your localization does not include translations for any strings please indicate as much in the [lang / missing folder](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/tree/master/lang/missing). + +### **Do you want to contribute documentation?** + +* Please **follow the above guidelines** regarding opening issues and submitting pull requests. Some documentation issues may be broader and ongoing, in which case you may want to contribute to assignee's forks first. Any smaller changes should still open a detailed issue and submit a detailed pull request that [closes that issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). + +### **Do you have questions about the source code?** + +* **Join the [TyphonJS Discord](https://discord.gg/mnbgN8f)** and direct questions to the appropriate channel (#forien-quest-log). + +* Please **do not directly contact maintainers** with questions; if you have a question other people might as well, so discussions should take place in public forums where others can see and learn from it as well. + +Thank you for your interest in contributing to Forien's Quest Log! :heart: diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b552b2c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 Wojciech "Forien" Szulc, AUTHORS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 0fe83685..77be973f 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,107 @@ # FoundryVTT - Forien's Quest Log -![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/forien/foundryvtt-forien-quest-log?style=for-the-badge) -![GitHub Releases](https://img.shields.io/github/downloads/Forien/foundryvtt-forien-quest-log/latest/total?style=for-the-badge) -![GitHub All Releases](https://img.shields.io/github/downloads/Forien/foundryvtt-forien-quest-log/total?style=for-the-badge&label=Downloads+total) -**[Compatibility]**: *FoundryVTT* 0.6.0+ -**[Systems]**: *any* -**[Languages]**: *Chinese, English, French, German, Japanese, Korean, Polish, Portuguese (Brazil), Spanish* -This module provides comprehensive Quest Log system for players and Game Masters to use with Foundry Virtual Table Top +![FQL Version](https://img.shields.io/badge/dynamic/json?url=https://raw.githubusercontent.com/Forien/foundryvtt-forien-quest-log/master/module.json&label=Forien%27s+Quest+Log+version&query=version&style=flat-square&color=success") +![Foundry Core Compatible Version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2FLeague-of-Foundry-Developers%2Ffoundryvtt-forien-quest-log%2Fmaster%2Fmodule.json&label=Foundry%20Version&query=$.compatibility.verified&colorB=orange) +![GitHub release](https://img.shields.io/github/release-date/Forien/foundryvtt-forien-quest-log) +[![GitHub commits](https://img.shields.io/github/commits-since/Forien/foundryvtt-forien-quest-log/latest)](https://github.com/Forien/foundryvtt-forien-quest-log/commits/) +![the latest version zip](https://img.shields.io/github/downloads/Forien/foundryvtt-forien-quest-log/latest/module.zip) +![Forge installs](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%25&url=https%3A%2F%2Fforge-vtt.com%2Fapi%2Fbazaar%2Fpackage%2Fforien-quest-log) +[![Foundry Hub Endorsements](https://img.shields.io/endpoint?logoColor=white&url=https%3A%2F%2Fwww.foundryvtt-hub.com%2Fwp-json%2Fhubapi%2Fv1%2Fpackage%2Fforien-quest-log%2Fshield%2Fendorsements)](https://www.foundryvtt-hub.com/package/forien-quest-log/) + +[![Weblate Translations](https://weblate.foundryvtt-hub.com/widgets/forien-quest-log/-/287x66-grey.png)](https://weblate.foundryvtt-hub.com/engage/forien-quest-log/) + +This module provides comprehensive Quest Log system for players and Game Masters to use with [Foundry VTT](https://foundryvtt.com/). + +**[Compatibility]**: _FoundryVTT_ `v11` / `v12` as of FQL version `0.8.0`. + +**[Game Systems]**: _any_ + +**[Language Translations]**: _Chinese (simplified / traditional), Dutch, English, Finnish, French, German, Italian, Japanese, Korean, Polish, +Portuguese (Brazil), Russian, Spanish, Swedish_ ## Installation -1. Install Forien's Quest Log using manifest URL: https://raw.githubusercontent.com/Forien/foundryvtt-forien-quest-log/master/module.json -2. While loaded in World, enable **_Forien's Quest Log_** module. +1. (Recommended) Install Forien's Quest Log from the Foundry package manager directly. + - _or_ manually using the manifest URL: `https://github.com/Forien/foundryvtt-forien-quest-log/releases/latest/download/module.json` -## Usage -Button to access Quest Log is situated on the bottom of Journal Directory. -I think module is quite user-friendly with intuitive UI, however if you are confused and lost, you might want to [check out Wiki](https://github.com/Forien/foundryvtt-forien-quest-log/wiki) or most recent [Release Video](https://www.patreon.com/forien/posts?filters[tag]=quest%20log&filters[media_types]=video). +2. While loaded in your World, enable **_Forien's Quest Log_** in the `Module Management` configuration. +## Recent Updates -## Features +The major `0.8.0` update to FQL: -* Quest Log windows that lists all quests divided into `In Progress`, `Completed` and `Failed` tabs -* Quest creator with WYSIWYG editors for description and GM notes -* Quest objectives -* Draggable Item rewards -* Fully editable Quest Details window -* Personal Quests -* Quest Branching in the form of Sub Quests +- Foundry v11 / v12 dual support. +- Actors can now be set as rewards allowing [Item Piles](https://foundryvtt.com/packages/item-piles) and various game + system loot functionality to be utilized in rewards distribution for currency and items. +- Custom rewards may use game system text enrichment where available to distribute XP. For example with the `dnd5e` + system use `[[/award 400xp]]`. +- Player Notes - players can now leave notes on quests in a separate section similar to `GM Notes`. +- Editors switched to ProseMirror. -## Future plans (current ideas) +## Usage -Plans for future include: -* a toggle "hide future tasks from players" -* Chapter/Arc system -* draggable EXP/Money rewards (need to wait for FVTT 0.7.0) +A button to access the Quest Log is situated on the bottom of Journal Directory or in the left hand scene controls icon +toolbar under notes / journal entries where two new icons (scroll and list) opens the Quest Log and Quest Tracker. There +also are two `macro compendiums` available for FQL that provide ready to go macros to drop onto your hotbar that allow +convenient access to FQL for players and several GM related options. -You can **always** check current and up-to-date [planned and requested features here](https://github.com/Forien/foundryvtt-forien-quest-log/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) +There is a series of useful [in-depth video tutorials on YouTube](https://www.youtube.com/playlist?list=PLHslnNa8QKdD_M29g_Zs0f9zyAUVJ32Ne) +that cover the `0.7 - 0.8` releases. -*If you have **any** suggestion or idea on new contents, hit me up on Discord!* +FQL is quite user-friendly with an intuitive UI, however you might want to [check out the Wiki](https://github.com/Forien/foundryvtt-forien-quest-log/wiki) for more detailed usage including macros and Quest API details for external developers integrations. -## Translations +## Features -If you are interested in translating my module, simply make a new Pull Request with your changes, or contact me on Discord. +- Quest Log windows that lists all quests divided into `In Progress`, `Completed` and `Failed` tabs. +- Quest creator with WYSIWYG editors for description and GM notes. +- Quest objectives. +- Draggable Item rewards. +- Fully editable Quest Details window. +- Personal Quests. +- Quest Branching in the form of Sub Quests. -#### How to translate +## About -I maintain both English and Polish translation of this module, so you check on those two to see how translation file can look like. It can be either expanded (nested) JSON like English, or flat JSON like Polish. +FQL is being updated for stability across core Foundry updates. This stability and long term maintenance of such is +the _main feature_ of FQL presently. You can rest assured that the quest log experience you know and _love_ will +continue to be available now and into the future. -Order of Localization Strings inside a `.json` file is indifferent. +Moving forward FQL is transitioning back to ownership and maintenance by Forien who is active again with Foundry 3rd +party development. -Localization file **must be** either completely flat, or completely expanded (nested). Not partially both. +## Translations -#### What is `missing` Folder? +FQL uses Weblate to coordinate language translation from community translators. Through this interface you are able to +provide language corrections and translations. I am more than willing to support even more language translations for +FQL, so if your language isn't represented yet please visit the [FQL Weblate Portal](https://weblate.foundryvtt-hub.com/engage/forien-quest-log/) +and get in contact. -The `lang/missing/` folder contains files for all languages showing all Localization Strings that are in the Module, but are not covered by that Language. For example, there are 6 strings not covered by Polish language, but since they are simply `API Error` messages, there is no need. +## Future plans (current ideas) +Rock solid stability through future releases of Foundry VTT and even more language / internationalization support. +At this time a few quality of life features may be added in any given release as well. -## Contact +_If you think you have found a bug or usability issue with FQL itself please file an issue in the +[FQL Issue Tracker](https://github.com/Forien/foundryvtt-forien-quest-log/issues)_. -If you wish to contact me for any reason, reach me out on Discord using my tag: `Forien#2130` +## Contact +TBD ## Acknowledgments -* Great thanks to sdenec for his invaluable help with UI overhaul! -* Thanks to Atropos for his relentless work on developing and improving the Foundry VTT -* Thanks to necxelos, TomChristoffer and Kralug for their massive lists of suggestions -* Thanks to Brother Sharp for providing Japanese translation -* Thanks to Acd-Jake for providing German translation -* Thanks to KLO for providing Korean translation -* Thanks to rectulo and Naoki for providing French translation -* Thanks to JJBocanegra for providing Spanish translation -* Thanks to Ztt1996 for providing Chinese translation -* Thanks to Innocenti for providing Brazilian Portuguese translation +See [Authors](https://github.com/Forien/foundryvtt-forien-quest-log/blob/master/AUTHORS) and +[Contributors](https://github.com/Forien/foundryvtt-forien-quest-log/graphs/contributors) -## Support +## Support (Historical) -If you wish to support module development, please consider [becoming Patron](https://www.patreon.com/foundryworkshop) or donating [through Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6P2RRX7HVEMV2&source=url). Thanks! +Between the summer of '21 and '24 FQL was developed and maintained by Michael Leahy aka [TyphonJS](https://github.com/typhonrt) / +[TyphonJS Discord](https://typhonjs.io/discord/). Michael took FQL from MVP to the questing powerhouse that FQL became +and maintained the package through a challenging series of Foundry core update `v0.8` through `v12`. ## License -Forien's Quest Log is a module for Foundry VTT by Forien and is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). +Forien's Quest Log is a module for Foundry VTT by Forien and is licensed under a [MIT License](https://github.com/Forien/foundryvtt-forien-quest-log/blob/master/LICENSE). -This work is licensed under Foundry Virtual Tabletop [EULA - Limited License Agreement for module development from May 29, 2020](https://foundryvtt.com/article/license/). +This work is licensed under Foundry Virtual Tabletop [EULA - Limited License Agreement for module development from February 17, 2021](https://foundryvtt.com/article/license/). diff --git a/assets-raw/icons/macros/icons-macros.xcf b/assets-raw/icons/macros/icons-macros.xcf new file mode 100644 index 00000000..6a3dbd6d Binary files /dev/null and b/assets-raw/icons/macros/icons-macros.xcf differ diff --git a/assets-raw/icons/macros/svg/arrows-alt-h-solid.svg b/assets-raw/icons/macros/svg/arrows-alt-h-solid.svg new file mode 100644 index 00000000..72419ff8 --- /dev/null +++ b/assets-raw/icons/macros/svg/arrows-alt-h-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/arrows-alt-solid.svg b/assets-raw/icons/macros/svg/arrows-alt-solid.svg new file mode 100644 index 00000000..59ca1366 --- /dev/null +++ b/assets-raw/icons/macros/svg/arrows-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/check-solid.svg b/assets-raw/icons/macros/svg/check-solid.svg new file mode 100644 index 00000000..15d7ab5e --- /dev/null +++ b/assets-raw/icons/macros/svg/check-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/check-square-regular.svg b/assets-raw/icons/macros/svg/check-square-regular.svg new file mode 100644 index 00000000..c7699d23 --- /dev/null +++ b/assets-raw/icons/macros/svg/check-square-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/check-square-solid.svg b/assets-raw/icons/macros/svg/check-square-solid.svg new file mode 100644 index 00000000..5638e19b --- /dev/null +++ b/assets-raw/icons/macros/svg/check-square-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/exclamation-solid.svg b/assets-raw/icons/macros/svg/exclamation-solid.svg new file mode 100644 index 00000000..32de166c --- /dev/null +++ b/assets-raw/icons/macros/svg/exclamation-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/eye-slash-solid.svg b/assets-raw/icons/macros/svg/eye-slash-solid.svg new file mode 100644 index 00000000..e19a3c59 --- /dev/null +++ b/assets-raw/icons/macros/svg/eye-slash-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/hashtag-solid.svg b/assets-raw/icons/macros/svg/hashtag-solid.svg new file mode 100644 index 00000000..f63399a7 --- /dev/null +++ b/assets-raw/icons/macros/svg/hashtag-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/pen-nib-solid.svg b/assets-raw/icons/macros/svg/pen-nib-solid.svg new file mode 100644 index 00000000..408b1a4d --- /dev/null +++ b/assets-raw/icons/macros/svg/pen-nib-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/scroll-solid.svg b/assets-raw/icons/macros/svg/scroll-solid.svg new file mode 100644 index 00000000..82f0f2d3 --- /dev/null +++ b/assets-raw/icons/macros/svg/scroll-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/square-regular.svg b/assets-raw/icons/macros/svg/square-regular.svg new file mode 100644 index 00000000..4e73fc5f --- /dev/null +++ b/assets-raw/icons/macros/svg/square-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/star-solid.svg b/assets-raw/icons/macros/svg/star-solid.svg new file mode 100644 index 00000000..ce744164 --- /dev/null +++ b/assets-raw/icons/macros/svg/star-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/tasks-solid.svg b/assets-raw/icons/macros/svg/tasks-solid.svg new file mode 100644 index 00000000..d283aaa5 --- /dev/null +++ b/assets-raw/icons/macros/svg/tasks-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/trophy-solid.svg b/assets-raw/icons/macros/svg/trophy-solid.svg new file mode 100644 index 00000000..543ed227 --- /dev/null +++ b/assets-raw/icons/macros/svg/trophy-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/user-friends-solid.svg b/assets-raw/icons/macros/svg/user-friends-solid.svg new file mode 100644 index 00000000..1add45ec --- /dev/null +++ b/assets-raw/icons/macros/svg/user-friends-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets-raw/icons/macros/svg/user-solid.svg b/assets-raw/icons/macros/svg/user-solid.svg new file mode 100644 index 00000000..23737bfd --- /dev/null +++ b/assets-raw/icons/macros/svg/user-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/fonts/almendra-v15-latin-regular.woff b/assets/fonts/almendra-v15-latin-regular.woff new file mode 100644 index 00000000..a89343e2 Binary files /dev/null and b/assets/fonts/almendra-v15-latin-regular.woff differ diff --git a/assets/fonts/almendra-v15-latin-regular.woff2 b/assets/fonts/almendra-v15-latin-regular.woff2 new file mode 100644 index 00000000..4921ea52 Binary files /dev/null and b/assets/fonts/almendra-v15-latin-regular.woff2 differ diff --git a/assets/fonts/audiowide-v9-latin-regular.woff b/assets/fonts/audiowide-v9-latin-regular.woff new file mode 100644 index 00000000..daf570b4 Binary files /dev/null and b/assets/fonts/audiowide-v9-latin-regular.woff differ diff --git a/assets/fonts/audiowide-v9-latin-regular.woff2 b/assets/fonts/audiowide-v9-latin-regular.woff2 new file mode 100644 index 00000000..898c7467 Binary files /dev/null and b/assets/fonts/audiowide-v9-latin-regular.woff2 differ diff --git a/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff b/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff new file mode 100644 index 00000000..9475ceff Binary files /dev/null and b/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff differ diff --git a/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff2 b/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff2 new file mode 100644 index 00000000..749b1edc Binary files /dev/null and b/assets/fonts/bilbo-swash-caps-v15-latin-regular.woff2 differ diff --git a/assets/fonts/medievalsharp-v14-latin-regular.woff b/assets/fonts/medievalsharp-v14-latin-regular.woff new file mode 100644 index 00000000..b7f034d6 Binary files /dev/null and b/assets/fonts/medievalsharp-v14-latin-regular.woff differ diff --git a/assets/fonts/medievalsharp-v14-latin-regular.woff2 b/assets/fonts/medievalsharp-v14-latin-regular.woff2 new file mode 100644 index 00000000..b79e0c05 Binary files /dev/null and b/assets/fonts/medievalsharp-v14-latin-regular.woff2 differ diff --git a/assets/fonts/metamorphous-v13-latin-regular.woff b/assets/fonts/metamorphous-v13-latin-regular.woff new file mode 100644 index 00000000..bac16eaf Binary files /dev/null and b/assets/fonts/metamorphous-v13-latin-regular.woff differ diff --git a/assets/fonts/metamorphous-v13-latin-regular.woff2 b/assets/fonts/metamorphous-v13-latin-regular.woff2 new file mode 100644 index 00000000..950132c9 Binary files /dev/null and b/assets/fonts/metamorphous-v13-latin-regular.woff2 differ diff --git a/assets/fonts/nova-square-v15-latin-regular.woff b/assets/fonts/nova-square-v15-latin-regular.woff new file mode 100644 index 00000000..871f00be Binary files /dev/null and b/assets/fonts/nova-square-v15-latin-regular.woff differ diff --git a/assets/fonts/nova-square-v15-latin-regular.woff2 b/assets/fonts/nova-square-v15-latin-regular.woff2 new file mode 100644 index 00000000..66c212b9 Binary files /dev/null and b/assets/fonts/nova-square-v15-latin-regular.woff2 differ diff --git a/assets/icons/macros/DBMigration.png b/assets/icons/macros/DBMigration.png new file mode 100644 index 00000000..086913d7 Binary files /dev/null and b/assets/icons/macros/DBMigration.png differ diff --git a/assets/icons/macros/allowPlayersAcceptOff.png b/assets/icons/macros/allowPlayersAcceptOff.png new file mode 100644 index 00000000..1cb6ffab Binary files /dev/null and b/assets/icons/macros/allowPlayersAcceptOff.png differ diff --git a/assets/icons/macros/allowPlayersAcceptOn.png b/assets/icons/macros/allowPlayersAcceptOn.png new file mode 100644 index 00000000..73c3a2f4 Binary files /dev/null and b/assets/icons/macros/allowPlayersAcceptOn.png differ diff --git a/assets/icons/macros/allowPlayersCreateOff.png b/assets/icons/macros/allowPlayersCreateOff.png new file mode 100644 index 00000000..27837a00 Binary files /dev/null and b/assets/icons/macros/allowPlayersCreateOff.png differ diff --git a/assets/icons/macros/allowPlayersCreateOn.png b/assets/icons/macros/allowPlayersCreateOn.png new file mode 100644 index 00000000..6af59724 Binary files /dev/null and b/assets/icons/macros/allowPlayersCreateOn.png differ diff --git a/assets/icons/macros/allowPlayersDragOff.png b/assets/icons/macros/allowPlayersDragOff.png new file mode 100644 index 00000000..f993c391 Binary files /dev/null and b/assets/icons/macros/allowPlayersDragOff.png differ diff --git a/assets/icons/macros/allowPlayersDragOn.png b/assets/icons/macros/allowPlayersDragOn.png new file mode 100644 index 00000000..97d7ca6e Binary files /dev/null and b/assets/icons/macros/allowPlayersDragOn.png differ diff --git a/assets/icons/macros/countHiddenOff.png b/assets/icons/macros/countHiddenOff.png new file mode 100644 index 00000000..4a4f793a Binary files /dev/null and b/assets/icons/macros/countHiddenOff.png differ diff --git a/assets/icons/macros/countHiddenOn.png b/assets/icons/macros/countHiddenOn.png new file mode 100644 index 00000000..4100b3d2 Binary files /dev/null and b/assets/icons/macros/countHiddenOn.png differ diff --git a/assets/icons/macros/defaultAbstractRewardImage.png b/assets/icons/macros/defaultAbstractRewardImage.png new file mode 100644 index 00000000..44eefecc Binary files /dev/null and b/assets/icons/macros/defaultAbstractRewardImage.png differ diff --git a/assets/icons/macros/hideFQLFromPlayersOff.png b/assets/icons/macros/hideFQLFromPlayersOff.png new file mode 100644 index 00000000..ea1f9960 Binary files /dev/null and b/assets/icons/macros/hideFQLFromPlayersOff.png differ diff --git a/assets/icons/macros/hideFQLFromPlayersOn.png b/assets/icons/macros/hideFQLFromPlayersOn.png new file mode 100644 index 00000000..ed84af43 Binary files /dev/null and b/assets/icons/macros/hideFQLFromPlayersOn.png differ diff --git a/assets/icons/macros/notifyRewardDropOff.png b/assets/icons/macros/notifyRewardDropOff.png new file mode 100644 index 00000000..f1e242b0 Binary files /dev/null and b/assets/icons/macros/notifyRewardDropOff.png differ diff --git a/assets/icons/macros/notifyRewardDropOn.png b/assets/icons/macros/notifyRewardDropOn.png new file mode 100644 index 00000000..09f417b2 Binary files /dev/null and b/assets/icons/macros/notifyRewardDropOn.png differ diff --git a/assets/icons/macros/openQuestLog.png b/assets/icons/macros/openQuestLog.png new file mode 100644 index 00000000..5ab2de33 Binary files /dev/null and b/assets/icons/macros/openQuestLog.png differ diff --git a/assets/icons/macros/questTrackerEnableOff.png b/assets/icons/macros/questTrackerEnableOff.png new file mode 100644 index 00000000..860d4aa9 Binary files /dev/null and b/assets/icons/macros/questTrackerEnableOff.png differ diff --git a/assets/icons/macros/questTrackerEnableOn.png b/assets/icons/macros/questTrackerEnableOn.png new file mode 100644 index 00000000..5dcf02dc Binary files /dev/null and b/assets/icons/macros/questTrackerEnableOn.png differ diff --git a/assets/icons/macros/questTrackerResizableOff.png b/assets/icons/macros/questTrackerResizableOff.png new file mode 100644 index 00000000..c33b9fdb Binary files /dev/null and b/assets/icons/macros/questTrackerResizableOff.png differ diff --git a/assets/icons/macros/questTrackerResizableOn.png b/assets/icons/macros/questTrackerResizableOn.png new file mode 100644 index 00000000..055cef78 Binary files /dev/null and b/assets/icons/macros/questTrackerResizableOn.png differ diff --git a/assets/icons/macros/trustedPlayerEditOff.png b/assets/icons/macros/trustedPlayerEditOff.png new file mode 100644 index 00000000..1be34f07 Binary files /dev/null and b/assets/icons/macros/trustedPlayerEditOff.png differ diff --git a/assets/icons/macros/trustedPlayerEditOn.png b/assets/icons/macros/trustedPlayerEditOn.png new file mode 100644 index 00000000..ea95308b Binary files /dev/null and b/assets/icons/macros/trustedPlayerEditOn.png differ diff --git a/changelog.md b/changelog.md index eb9dd50a..056b69fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,165 @@ # Changelog +## Release 0.8.0 +This major `0.8.0` update brings to FQL: +- Actors can be set as quest rewards (Item Piles / game system loot support). +- Player Notes - players can now leave notes on quests in a separate section similar to `GM Notes` +- ProseMirror editor support / removed TinyMCE. +- Streamlined codebase updating to ES2022. +- Dual v11 / v12 support. +- This is the last release by Michael Leahy (TyphonJS) before FQL returned to Forien. -### v0.5.1 +## Release 0.7.12 +The minor `0.7.12` update brings to FQL: + +- Foundry v11 support. +- Removed support for v9 / v10 to prevent any compatibility warnings. +- Fixed minor TinyMCE configuration for correct font support. +- Updated Spanish translation. + +## Release 0.7.11 +The minor `0.7.11` update brings to FQL: + +- Corrects a new compatibility warning that came up w/ the 10.285 Foundry release. +- Provides workarounds for various misbehaving game systems (Gurps / L5R). +- Refines the "show quest log to players" feature showing the players the specific quest status tab the GM currently + has selected. +- Adds a Dutch language translation. + +## Release 0.7.10 +This is a major release that brings dual compatibility on Foundry `v9` & `v10`. + +Several quality of life improvements including: + +- Quest document linking is enabled again. +- Show quest log to players link in quest log app header (for GM). +- Show quest tracker to players with icon in quest tracker header (for GM). +- Ability to set quest tracker to transparent via fill icon in app header. +- Expanded language / translation support. + +[Weblate setup](https://weblate.foundryvtt-hub.com/engage/forien-quest-log/) for language / translation community +updates. + +## Release 0.7.9 +- Disable synthetic quest type registration for Foundry `v9+`. As it turns out `CONST` was locked down last +minute in the v9 release cycle. The feature affected is "document linking" for quests. There is a replacement module / +continuing the quest log that will enable this functionality again with a different implementation. You can join the +TyphonJS Discord server to get an announcement when the new module is available: https://discord.gg/mnbgN8f + +## Release 0.7.8 +- A few fixes for external module conflicts. + +## Release 0.7.7 +This release is a major quality of life update. + +- Quest Tracker overhaul + - Removed floating quest log. + - Quest tracker is now dockable / resizable. + - UI management + +- Macro Compendiums + - GM & Player macro compendiums. + - Most common macros to control FQL are pre-made and ready to drop in hotbar. + +- TinyMCE Corrections + - Default font in editor is the same when viewing quest. + - New smaller font sizes. + - Correct implementation for line spacing. + +- Theming + - Support for Whetstone. + - Support for Lib Themer (beta) / not officially released yet. + +## Release 0.7.6 +This release is a major quality of life update. There is deeper integration with Foundry allowing quests to be made into Notes on the canvas and many enhancements to the TinyMCE content editing capabilities. + +- FQL journal entry opens quest details. +- Drag FQL journal entry or quest from quest log to canvas as a Note. +- Quest tracker / floating log can interact w/ objectives. +- Content entity linking in objectives / abstract rewards +- TinyMCE + - New fonts + - oEmbed plugin + - style border + - style drop shadow + - Styles + - Blend mode + - Border + - Filters + - Blur + - Drop Shadow + - Grayscale + - Float + - Fonts + - Neon + - Line height + - Margin + - Opacity + - Source Code Editing + - CSS selector + - background gradient + - background before / after + +## Release 0.7.5 +Small quality of life update. + +Fix for Issue #98. +Esc key now cancels all single line input editing. +For all single line input fields cursor is set to end of input. +The default module setting for countHidden is now false instead of true. +Updated French translation / language file. + +## Release 0.7.4 +Brings substantial finishing aspects to FQL + +Improved user experience +Trusted Player Edit +New quest creation workflow +In-memory QuestDB +and much more! + +## Release 0.7.0-0.7.3 +Total rewrite that fixes most known bugs, courtesy of @typhonrt. +Existing quests will be migrated, except that quest rewards will be removed and need to re-added manually. +Remember to back up your world before installing. + +This is the first release from Michael Leahy (TyphonJS). + +## Release 0.6.0 +* Set compatible with Foundry 0.8.6 (and removes compatibility with lower versions) +* Left sidebar buttons moved from it's own menu (the pen icon) to buttons under the Notes menu (the bookmark icon) + +## Release 0.5.8 +* Set compatible with Foundry 0.7.10 +* Updated Chinese localization courtesy of FuyuEnnju +* Localization fix courtesy of FuyuEnnju + +## Release 0.5.7 +* Buttons to delete and change vertical alignment of quest giver and splash image added courtesy of @Dilomos + +## Release 0.5.6 +* Added the QuestTracker from the 'party-unit-frames' project courtesy of @p4535992 +* Added support for Bug Reporter +* Hopefully fixed the broken release flow... + +## Release 0.5.5 +* Failed release, withdrawn + +## Release 0.5.4 +* A floating quest window, by Rughalt +* Translation to zh-tw, by zeteticl +* Set compatible Foundry version to 0.7.9 (as that is what I tested the above with) + +## Release v0.5.3 +* [BUG] Quest not loading when user removed from world (@xdy) +* Add Svenska (Swedish) translation (@xdy) +* Updated release to point to the League fork instead of the death-save one (@eclarke12) + +## v0.5.2 +* [BUG] Quest Preview not saving +* [BUG] Can't close new Quest Window +* Provide compatibility with Foundry VTT 0.7.7 + +## v0.5.1 * [BUG] "_fql_quests" folder not being created on fresh installs ## v0.5.0 diff --git a/css/init.css b/css/init.css new file mode 100644 index 00000000..8328adb1 --- /dev/null +++ b/css/init.css @@ -0,0 +1,2 @@ +.fql-app{background:url(../../../ui/denim075.png) repeat;border-radius:5px;box-shadow:0 0 20px #000;color:#f0f0e0;margin:3px 0;max-height:100%;position:absolute}.fql-window-app{padding:0;z-index:99}.fql-window-app,.fql-window-app .window-content{display:flex;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start}.fql-window-app .window-content{color:#191813;overflow-x:hidden;overflow-y:auto;padding:8px}.fql-window-app .window-header{border-bottom:1px solid #000;flex:0 0 30px;line-height:30px;overflow:hidden;padding:0 8px;pointer-events:auto}.fql-window-app .window-header a{flex:none;margin:0 0 0 8px}.fql-window-app .window-header h4{font-family:Signika,sans-serif}.fql-window-app .window-header i[class^=fa]{margin-right:3px}.fql-window-app .window-header .window-title{margin:0;word-break:break-all}.fql-window-app .window-resizable-handle{background:#444;border:1px solid #111;border-radius:4px 0 0 0;bottom:-1px;height:20px;padding:2px;position:absolute;right:0;width:20px}.fql-window-app .window-resizable-handle i.fas{transform:rotate(45deg)}.fql-window-app.minimized .window-header{border:1px solid #000}.fql-window-app.minimized .window-resizable-handle{display:none}#forien-quest-log .pad-l-4,#quest-tracker .pad-l-4,.window-app.forien-quest-preview .pad-l-4{padding-left:4px}#forien-quest-log .pad-l-8,#quest-tracker .pad-l-8,.window-app.forien-quest-preview .pad-l-8{padding-left:8px}#forien-quest-log i,#quest-tracker i,.window-app.forien-quest-preview i{flex:none}#forien-quest-log i.fas.fa-star,#quest-tracker i.fas.fa-star,.window-app.forien-quest-preview i.fas.fa-star{color:gold;filter:drop-shadow(0 0 2px #000)}#forien-quest-log i.fas.fa-fill,#quest-tracker i.fas.fa-fill,.window-app.forien-quest-preview i.fas.fa-fill{color:#add8e6;filter:drop-shadow(0 0 2px #000)}#forien-quest-log i.fas.fa-fill.off,#quest-tracker i.fas.fa-fill.off,.window-app.forien-quest-preview i.fas.fa-fill.off{color:#fff;filter:drop-shadow(0 0 2px #000)}#forien-quest-log .window-header,.window-app.forien-quest-preview .window-header{margin:0}#forien-quest-log .window-content,.window-app.forien-quest-preview .window-content{border-radius:0 0 5px 5px;margin:0;padding:0}#forien-quest-log .tab,.window-app.forien-quest-preview .tab{display:none;height:100%}#forien-quest-log .tab.active,.window-app.forien-quest-preview .tab.active{display:block}#forien-quest-log h1,.window-app.forien-quest-preview h1{flex:0 0 1px;font-size:22px;font-weight:700;line-height:1;margin:0 0 8px;padding:0 0 4px}#forien-quest-log h2,.window-app.forien-quest-preview h2{border-width:2px;font-size:18px;font-weight:700;margin:0 0 4px;padding:0 0 2px}#forien-quest-log label,.window-app.forien-quest-preview label{display:block;margin-bottom:3px}#forien-quest-log input[type=text],.window-app.forien-quest-preview input[type=text]{background:#ffffff80;border:none;box-shadow:inset 0 0 3px 1px #0000;height:26px;padding:4px 8px;transition:box-shadow .3s ease}#forien-quest-log input[type=text]:hover,.window-app.forien-quest-preview input[type=text]:hover{box-shadow:0 0 0 1px var(--palette-primary,var(--default-primary-accent,#ff6400)) inset}#forien-quest-log button,.window-app.forien-quest-preview button{border-radius:5px;cursor:pointer;height:30px;margin-bottom:2px;margin-right:4px;margin-top:2px;transition:border-color .3s ease,background .3s ease,box-shadow .3s ease}#forien-quest-log button:first-child,.window-app.forien-quest-preview button:first-child{margin-left:0}#forien-quest-log nav.tabs,.window-app.forien-quest-preview nav.tabs{align-items:center;background:#fff3;flex:0 0 40px;font-size:larger;justify-content:flex-start;padding:0 16px}#forien-quest-log nav.tabs .item,.window-app.forien-quest-preview nav.tabs .item{flex:0 0 1px;margin-left:1rem;text-align:left;transition:color .3s ease;white-space:nowrap}#forien-quest-log nav.tabs .item:hover,.window-app.forien-quest-preview nav.tabs .item:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400));text-shadow:none}#forien-quest-log nav.tabs .item:first-child,.window-app.forien-quest-preview nav.tabs .item:first-child{margin-left:0}#forien-quest-log nav.tabs .item.active,#forien-quest-log nav.tabs .item.active:hover,.window-app.forien-quest-preview nav.tabs .item.active,.window-app.forien-quest-preview nav.tabs .item.active:hover{color:inherit;font-weight:700;text-shadow:none}#forien-quest-log .hidden,.window-app.forien-quest-preview .hidden{display:none}#forien-quest-log .icon-button,.window-app.forien-quest-preview .icon-button{flex:none}#forien-quest-log .editor,.window-app.forien-quest-preview .editor{background:#ffffff80;border-radius:5px;height:100%;padding:8px}#forien-quest-log .editor .editor-content,.window-app.forien-quest-preview .editor .editor-content{height:100%;overflow:auto;padding:0}#forien-quest-log .actions,.window-app.forien-quest-preview .actions{align-items:center;border-left:1px solid #00000026;display:flex;flex:0 0 100px;height:100%;justify-content:center}#forien-quest-log .actions.is-player,.window-app.forien-quest-preview .actions.is-player{flex:0 0 60px}#forien-quest-log .actions span.justify-center,.window-app.forien-quest-preview .actions span.justify-center{flex:1;margin:1px 1px 1px auto;visibility:hidden}#forien-quest-log .actions span.spacer,.window-app.forien-quest-preview .actions span.spacer{flex:0 0 4px}#forien-quest-log .actions i,.window-app.forien-quest-preview .actions i{border:none;color:#000000bf;cursor:pointer;flex:0 0 18px;font-size:16px;padding:0;text-align:center;transition:color .3s ease}#forien-quest-log .actions i.fa-sort,.window-app.forien-quest-preview .actions i.fa-sort{border-right:1px solid #00000026;cursor:move;flex:0 0 18px;width:14px}#forien-quest-log .actions i.fa-eye,.window-app.forien-quest-preview .actions i.fa-eye{flex:0 0 20px;padding-left:1px}#forien-quest-log .actions i.fa-eye-slash,.window-app.forien-quest-preview .actions i.fa-eye-slash{flex:0 0 20px}#forien-quest-log .actions i.fa-check-circle,.window-app.forien-quest-preview .actions i.fa-check-circle{color:#00af00cc}#forien-quest-log .actions i.fa-times-circle,.window-app.forien-quest-preview .actions i.fa-times-circle{color:#c80000cc}#forien-quest-log .actions i.fa-trash,.window-app.forien-quest-preview .actions i.fa-trash{color:#f009}#forien-quest-log .actions i.fa-play,.window-app.forien-quest-preview .actions i.fa-play{font-size:14px;padding-top:2px}#forien-quest-log .actions i:hover,.window-app.forien-quest-preview .actions i:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}#forien-quest-log .actions i:hover.is-player,.window-app.forien-quest-preview .actions i:hover.is-player{color:#000000bf;cursor:default}#forien-quest-log .actions i:first-child,.window-app.forien-quest-preview .actions i:first-child{margin:0}#forien-quest-log .actions-single,.window-app.forien-quest-preview .actions-single{flex:0 0 1px;padding:0 8px}#forien-quest-log .actions-single i,.window-app.forien-quest-preview .actions-single i{cursor:pointer;font-size:18px;transition:color .3s ease}#forien-quest-log .actions-single i:hover,.window-app.forien-quest-preview .actions-single i:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}#forien-quest-log section,.window-app.forien-quest-preview section{display:block;flex-direction:row;justify-content:flex-start}#forien-quest-log ::-webkit-scrollbar-corner,.window-app.forien-quest-preview ::-webkit-scrollbar-corner{background:#0000001a}#forien-quest-log{min-height:640px;min-width:500px}#forien-quest-log.window-app .window-content{overflow:visible}#forien-quest-log .quest-log{display:flex;flex-direction:column;padding:0 0 16px}#forien-quest-log .quest-log.bookmarks nav.log-tabs{align-items:flex-end;background:none;border-block-end:none;flex:0;flex-direction:column;left:0;padding:0;position:absolute;transform:translateX(-97%)}#forien-quest-log .quest-log.bookmarks nav.log-tabs .item{background:var(--palette-app-background-image,url(../../../ui/parchment.jpg)) repeat;border-radius:5px 0 0 5px;box-shadow:0 5px 5px -5px #0000004d,0 -5px 5px -5px #0000004d;margin:0 0 4px;padding:8px 16px;position:relative;text-align:right;transition:padding .3s ease,width .3s ease,color .3s ease;width:150px;z-index:1}#forien-quest-log .quest-log.bookmarks nav.log-tabs .item.active,#forien-quest-log .quest-log.bookmarks nav.log-tabs .item:hover{padding-right:32px;width:166px}#forien-quest-log .quest-log.bookmarks nav.log-tabs .item.active:after{border-radius:5px 0 0 5px;content:"";height:100%;left:0;position:absolute;top:0;width:100%;z-index:-1}#forien-quest-log .quest-log .log-body{flex:1;padding:0 16px}#forien-quest-log .quest-log .log-body header{border-block-end:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));border-bottom:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));display:flex;justify-content:space-between;margin-bottom:4px;margin-top:0;padding-top:0}#forien-quest-log .quest-log .log-body header h1{align-self:flex-end;border:none;margin:0;padding-bottom:4px}#forien-quest-log .quest-log .log-body header button{flex:0 0 fit-content}#forien-quest-log .quest-log .tab{flex-direction:column;padding:4px 0 0}#forien-quest-log .quest-log .tab.active{display:flex}#forien-quest-log .quest-log .tab .table{flex:1;overflow-y:auto;scrollbar-width:thin}#forien-quest-log .quest-log .table ul{list-style:none;margin:0;padding:0}#forien-quest-log .quest-log .table ul li.drag-quest{align-items:center;background:#ffffff4d;border:1px solid #0000;border-radius:5px;display:flex;justify-content:flex-start;margin:0 4px 2px 0;min-height:42px;transition:border-color .3s ease,box-shadow .3s ease}#forien-quest-log .quest-log .table ul li.drag-quest:hover{border-color:var(--palette-primary,var(--default-primary-accent,#ff6400));box-shadow:0 0 2px var(--palette-primary,var(--default-primary-accent,#ff6400)) inset}#forien-quest-log .quest-log .table ul .open-quest{cursor:pointer}#forien-quest-log .quest-log .table ul .img{background-size:cover;border-radius:5px;flex:0 0 40px;height:40px;width:40px}#forien-quest-log .quest-log .table ul .title{display:flex;flex:1;flex-direction:column;justify-content:center;min-height:42px;padding-left:2px;padding-right:8px}#forien-quest-log .quest-log .table ul .title h2{border:none;font-size:16px;font-weight:700;line-height:1;margin:0;padding:0}#forien-quest-log .quest-log .table ul .title p{font-size:12px;font-weight:400;margin:0;padding:0}#forien-quest-log .quest-log .table ul .tasks{align-items:center;border-left:1px solid #00000026;display:flex;flex:0 0 50px;font-size:18px;justify-content:center;min-height:42px}#forien-quest-log .quest-log .table ul .actions{min-height:42px}.window-app.forien-quest-preview{min-height:640px;min-width:1000px}.window-app.forien-quest-preview .tab.active{display:flex;flex-direction:column}.window-app.forien-quest-preview .quest-preview{background:#0000001a;display:flex;flex-direction:column;height:100%;overflow-y:hidden;padding:0}.window-app.forien-quest-preview .quest-body{flex:1;height:100%;overflow-x:hidden;overflow-y:auto;padding:6px 14px 14px}.window-app.forien-quest-preview .quest-body .details-header{display:flex;flex:0 0 1px;margin-bottom:6px}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc{background-color:#0000001a;border-radius:5px;flex:0 0 116px;font-size:12px;font-weight:700;height:116px;line-height:1.2;margin-right:8px;position:relative;text-align:center;width:116px}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .drop-info{align-items:center;border:2px dashed #00000080;border-radius:5px;cursor:pointer;display:flex;height:100%;justify-content:center}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .quest-giver-image{background-size:cover;border-radius:5px;height:100%;width:100%}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage{align-items:center;background:var(--default-secondary-accent,#ffffffbf);border-radius:5px;cursor:pointer;display:flex;height:22px;justify-content:center;left:0;position:absolute;top:0;transition:color .3s ease;width:22px}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage i{border-radius:50%;font-size:16px;line-height:1}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .deleteQuestGiver{align-items:center;background:var(--default-secondary-accent,#ffffffbf);border-radius:5px;cursor:pointer;display:flex;height:22px;justify-content:center;position:absolute;right:0;top:0;transition:color .3s ease;width:22px}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .deleteQuestGiver:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .deleteQuestGiver i{border-radius:50%;font-size:16px;line-height:1}.window-app.forien-quest-preview .quest-body .details-header .quest-setup{display:flex;flex:1;flex-direction:column}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-title{align-items:center;display:flex;justify-content:space-between}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .editable-container{flex:1;padding-left:6px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .editable-container input{height:28px;margin-bottom:8px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link{background-size:cover;cursor:pointer;flex:0 0 100px;position:relative}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link span{align-items:center;background:#0000004d;color:#ffffffa6;display:flex;font-size:28px;height:100%;justify-content:center;left:50%;opacity:1;position:absolute;top:50%;transform:translate(-50%,-50%);transition:opacity .3s ease;width:100%}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link span:hover{opacity:0}.window-app.forien-quest-preview .quest-body .details-header .quest-setup section{background:#ffffff26;border-radius:5px;display:flex;flex:1;margin-right:4px;overflow:hidden}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-details{display:flex;flex:1;flex-direction:column;justify-content:center;padding-right:16px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name{display:inline-flex;flex-direction:row;justify-content:left}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name .editable-container{flex:0 0 auto}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name .editable-container input{height:22px;margin-bottom:2px;padding:0}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name h2{border:none;cursor:pointer;display:inline-block;margin:0;transition:color .3s ease}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name h2:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name .action-single{flex:0 0 1px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status{display:flex;padding-left:6px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p{margin:0 8px 0 0}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p:after{content:"|";margin-left:8px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p:last-child{margin:0}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p:last-child:after{content:none}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name-link{cursor:pointer;transition:color .3s ease}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name-link i{font-size:12px}.window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name-link:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .quest-info{display:flex;flex:1;overflow-y:hidden}.window-app.forien-quest-preview .quest-body .quest-info header{display:flex;justify-content:space-between}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right button{flex:0 0 1px;font-size:15px;height:20px;line-height:1;white-space:nowrap}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right button i{font-size:10px}.window-app.forien-quest-preview .quest-body .quest-info .quest-description{flex:0 0 48%;height:100%;margin-right:8px;overflow-y:hidden}.window-app.forien-quest-preview .quest-body .quest-info .quest-description .description{background:#fff6;border-radius:5px;height:calc(100% - 30px);overflow:auto;padding:8px}.window-app.forien-quest-preview .quest-body .quest-info .quest-description .description .description-content{height:100%;overflow:auto;padding:0 4px 0 0}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right{display:flex;flex:0 0 51%;flex-direction:column}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right h2{border:none;margin:0 auto 0 0}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right header{border-block-end:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));border-bottom:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));display:flex;flex:0 0 1px;margin-bottom:4px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right span.spacer-edit{flex:0 0 18px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks{display:flex;flex:0 0 calc(50% - 8px);flex-direction:column;overflow-y:hidden}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .quest-box,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-box{flex:1;overflow-y:hidden}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards ul,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks ul{display:flex;flex-direction:column;height:100%;list-style:none;margin:0;overflow-y:auto;padding:0}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards ul li,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks ul li{align-items:center;background:#fff6;border-radius:5px;display:flex;margin:0 4px 2px 0}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards ul li:last-of-type,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks ul li:last-of-type{margin-bottom:0}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .is-link,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .is-link{cursor:pointer}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container{flex:1;padding:4px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container p,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container p{margin:0;max-width:290px;word-wrap:break-word}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container p.can-edit,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container p.can-edit{max-width:290px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container p.player-edit,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container p.player-edit{max-width:330px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container p.player,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container p.player{max-width:400px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container input,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container input{height:16px;line-height:14px;padding:0 4px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks{margin-bottom:16px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .toggleState{align-items:center;border-right:1px solid #00000026;cursor:pointer;display:flex;flex:0 0 32px;font-size:18px;height:100%;justify-content:center;transition:color .3s ease}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .toggleState:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container{align-items:center;border-right:1px solid #00000026;display:flex;flex:0 0 32px;font-size:18px;height:100%;justify-content:center}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container .state-display{align-items:center;background:#0000000d;border:1px solid #0000004d;border-radius:2px;display:flex;height:16px;justify-content:center;width:16px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container .state-display i{font-size:11px;line-height:16px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link{cursor:pointer;flex:1;margin:0;padding:4px;transition:color .3s ease;word-wrap:break-word}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link .can-edit{max-width:290px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link .player-edit{max-width:330px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link .player{max-width:390px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name-link i{font-size:12px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .task-hidden{background:#fff3}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .task-hidden .task-name{opacity:.5}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards button.hide-all-rewards,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards button.show-all-rewards{flex:0 0 90px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards button.lock-all-rewards,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards button.unlock-all-rewards{flex:0 0 98px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward{flex:0 0 25px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards span.spacer-edit{flex:0 0 18px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .drop-info{background:#0000;border:2px dashed #00000080;border-radius:5px;flex:1 0 25px;justify-content:center;line-height:20px;margin-right:4px;padding:0 16px;text-align:center}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden{background:#fff3}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden .reward-image,.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden .reward-name{opacity:.5}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-image-container{align-items:center;background-color:#222;border-radius:5px 0 0 5px;display:flex;flex:0 0 25px;height:100%;overflow:hidden}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-image-container.can-edit{cursor:pointer}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-image{background-position:50%;background-size:cover;height:25px;width:25px}.window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-name{flex:1;font-size:14px;font-weight:400;margin:0;padding-right:8px}.window-app.forien-quest-preview .quest-body .gmnotes .quest-gmnotes,.window-app.forien-quest-preview .quest-body .playernotes .quest-playernotes{height:100%;overflow-y:hidden}.window-app.forien-quest-preview .quest-body .management .row{display:flex;flex:0 0 1px}.window-app.forien-quest-preview .quest-body .management .quest-settings{display:flex;flex:1;flex-direction:column;height:226px;margin-right:8px}.window-app.forien-quest-preview .quest-body .management .quest-settings .setting-groups{flex:0 0 1px}.window-app.forien-quest-preview .quest-body .management .quest-settings label{margin:0 0 0 4px;overflow:hidden;text-overflow:ellipsis;width:calc(100% - 20px)}.window-app.forien-quest-preview .quest-body .management .quest-splash{flex:0 0 40%;position:relative}.window-app.forien-quest-preview .quest-body .management .quest-splash .splash-image{background-color:#0000001a;background-size:cover;border-radius:5px;height:200px;width:100%}.window-app.forien-quest-preview .quest-body .management .quest-splash .splash-image:hover{background-color:rgba(0,0,0,.075)}.window-app.forien-quest-preview .quest-body .management .quest-splash .state-container{margin-left:12px;position:relative}.window-app.forien-quest-preview .quest-body .management .quest-splash .state-container input[type=checkbox]{cursor:pointer;display:inline;height:18px;margin:0;position:absolute;top:1px;vertical-align:center;width:18px}.window-app.forien-quest-preview .quest-body .management .quest-splash .state-container label{display:inline;font-weight:lighter;left:22px;position:absolute;width:260px}.window-app.forien-quest-preview .quest-body .management .quest-splash .drop-info{align-items:center;border:2px dashed #00000080;border-radius:5px;cursor:pointer;display:flex;height:100%;justify-content:center}.window-app.forien-quest-preview .quest-body .management .quest-splash .splash-border{border:2px dashed #00000080}.window-app.forien-quest-preview .quest-body .management .quest-splash .delete-splash{align-items:center;background:var(--default-secondary-accent,#ffffffbf);border-radius:5px;cursor:pointer;display:flex;height:22px;justify-content:center;left:calc(100% - 22px);position:relative;top:0;transition:color .3s ease;width:22px}.window-app.forien-quest-preview .quest-body .management .quest-splash .delete-splash:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .management .quest-splash .delete-splash i{border-radius:50%;font-size:16px;line-height:1}.window-app.forien-quest-preview .quest-body .management .quest-splash .change-splash-pos{align-items:center;background:var(--default-secondary-accent,#ffffffbf);border-radius:5px;cursor:pointer;display:flex;height:22px;justify-content:center;left:calc(100% - 22px);position:relative;right:0;top:calc(100% - 44px);transition:color .3s ease;width:22px}.window-app.forien-quest-preview .quest-body .management .quest-splash .change-splash-pos:hover{color:var(--palette-primary,var(--default-primary-accent,#ff6400))}.window-app.forien-quest-preview .quest-body .management .quest-splash .change-splash-pos i{border-radius:50%;font-size:16px;line-height:1}.window-app.forien-quest-preview .quest-body .management .subquests{display:flex;flex:1;flex-direction:column;margin-top:16px;overflow:hidden}.window-app.forien-quest-preview .quest-body .management .subquests header{border-block-end:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));border-bottom:2px solid var(--palette-primary,var(--default-primary-accent,#782e22));display:flex;justify-content:space-between;margin-bottom:4px;margin-top:0;padding-top:0}.window-app.forien-quest-preview .quest-body .management .subquests h2{align-self:flex-end;border:none;margin:0;padding-bottom:0}.window-app.forien-quest-preview .quest-body .management .subquests button{flex:0 0 fit-content}.window-app.forien-quest-preview .quest-body .management .subquests .subquests-box{flex:1;list-style:none;margin:0;overflow-y:auto;padding:0}.window-app.forien-quest-preview .quest-body .management .subquests .subquests-box li{align-items:center;background:#ffffff4d;border:1px solid #0000;border-radius:5px;display:flex;height:30px;margin:0 4px 2px 0;transition:border-color .3s ease,box-shadow .3s ease}.window-app.forien-quest-preview .quest-body .management .subquests .subquests-box li:hover{border-color:var(--palette-primary,var(--default-primary-accent,#ff6400));box-shadow:0 0 2px var(--palette-primary,var(--default-primary-accent,#ff6400)) inset}.window-app.forien-quest-preview .quest-body .management .subquests .subquests-box h2{align-self:center;border:none;cursor:pointer;flex:1;font-size:14px;line-height:30px;margin:0 4px;transition:color .3s ease}.window-app.forien-quest-preview .quest-body .editor{height:calc(100% - 30px)}.window-app.forien-quest-preview .quest-body .gmnotes .editor{height:calc(100% - 36px)}#quest-tracker{max-height:750px;max-width:400px;min-height:72px;min-width:275px;pointer-events:none}@keyframes fql-jiggle{0%{animation-timing-function:ease-in;transform:rotate(-.25deg)}50%{animation-timing-function:ease-out;transform:rotate(.5deg)}}#quest-tracker .window-content{scrollbar-width:thin}#quest-tracker a.content-link{background:var(--palette-fql-qt-color-background-entitylink,#ddd);border:none;color:var(--palette-fql-qt-color-background-entitylink-contrast-text,#000)}#quest-tracker.fql-app{background:var(--palette-fql-qt-image-background,url(../../../ui/denim075.png)) repeat;background-blend-mode:var(--palette-fql-qt-image-background-blend-mode,normal);background-color:var(--palette-fql-qt-color-background,#0000);box-shadow:0 0 12px #000}#quest-tracker.fql-app.no-background{background:none;box-shadow:none;scrollbar-color:#505050b3 #3c3c3c80}#quest-tracker.fql-app.no-background ::-webkit-scrollbar-thumb{background:#3c3c3c80;border:#505050b3}#quest-tracker.fql-app.no-background .window-resizable-handle{opacity:.4}#quest-tracker .window-content{font-family:Open Sans,Lato,Signika,sans-serif;overflow-y:auto;padding:0 8px}#quest-tracker .window-content,#quest-tracker .window-header{color:var(--palette-fql-qt-text-color,var(--default-primary-color,#eee))}#quest-tracker .window-header{pointer-events:auto}#quest-tracker .window-header h4{font-family:Signika,sans-serif}#quest-tracker .window-resizable-handle{pointer-events:auto}#quest-tracker *{box-sizing:border-box}#quest-tracker #hidden{color:var(--palette-fql-qt-text-color-shaded-text,#888)}#quest-tracker .quest:not(:last-child){margin-bottom:16px}#quest-tracker .quests{flex:none;padding:8px 0}#quest-tracker .no-quests{filter:drop-shadow(1px 1px 1px #000);flex:none;padding:12px 0 8px}#quest-tracker .quest{display:flex;flex:1;flex-direction:column;height:auto;overflow-x:hidden;overflow-y:hidden}#quest-tracker .quest .title a,#quest-tracker .quest .title i{filter:drop-shadow(1px 1px 1px #000)}#quest-tracker .quest i{cursor:pointer;pointer-events:auto}#quest-tracker .quest i:last-of-type{flex:0 0 26px;text-align:right}#quest-tracker .quest .title{align-items:center;display:flex;flex-direction:row;font-size:18px;margin:0}#quest-tracker .quest .quest-tracker-header{cursor:pointer;height:auto;pointer-events:auto;width:fit-content}#quest-tracker .quest .quest-tracker-span{flex:1}#quest-tracker .quest .quest-tracker-link{pointer-events:auto}#quest-tracker .quest .tasks{list-style:none;margin:3px 0 0;padding-left:4px}#quest-tracker .quest .tasks i:last-of-type{flex:0 0 21px;text-align:right}#quest-tracker .quest .tasks .subquest{filter:drop-shadow(1px 1px 1px #000)}#quest-tracker .quest .tasks .quest-tracker-task{cursor:pointer;filter:drop-shadow(1px 1px 1px #000);width:fit-content}#quest-tracker .quest .tasks .quest-tracker-task span{cursor:pointer;pointer-events:auto}#quest-tracker .quest .tasks .quest-tracker-task span.check-square,#quest-tracker .quest .tasks .quest-tracker-task span.minus-square{-webkit-text-decoration:line-through;text-decoration:line-through}#quest-tracker .quest .tasks .subquest-separator{background-color:#ffffff80;height:1px;margin-bottom:4px;margin-top:3px;width:50px}#quest-tracker .quest .tasks .task{align-items:center;display:flex;margin:2px 0 0}#quest-tracker .quest .tasks .task span{cursor:pointer}#quest-tracker .quest .tasks .task span.check-square,#quest-tracker .quest .tasks .task span.minus-square{-webkit-text-decoration:line-through;text-decoration:line-through}#quest-tracker .quest .tasks .task:before{align-self:flex-start;content:"\f0c8";display:inline-block;font-family:Font Awesome\ 5 Free;font-weight:400;min-width:14px;padding-right:4px;pointer-events:auto;width:fit-content}#quest-tracker .quest .tasks .task.minus-square:before{content:"\f00d";display:inline-block;font-weight:900;min-width:13px;padding-left:1px;width:fit-content}#quest-tracker .quest .tasks .task.check-square:before{content:"\f00c";display:inline-block;font-weight:900;min-width:14px;width:fit-content} +/*# sourceMappingURL=init.css.map */ \ No newline at end of file diff --git a/css/init.css.map b/css/init.css.map new file mode 100644 index 00000000..8c679dd7 --- /dev/null +++ b/css/init.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../styles/basicapp.scss","init.css","../styles/quest-general.scss","../styles/global-variables.scss","../styles/quest-log.scss","../styles/global-mixin.scss","../styles/quest-preview.scss","../styles/quest-tracker.scss"],"names":[],"mappings":"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","file":"init.css"} \ No newline at end of file diff --git a/database/DBMigration.js b/database/DBMigration.js new file mode 100644 index 00000000..7bfac3ba --- /dev/null +++ b/database/DBMigration.js @@ -0,0 +1,156 @@ +/* eslint-disable */ + +import { + Socket, + Utils } from '../src/control/index.js'; + +import { constants } from '../src/model/constants.js'; + +import { + dbSchema_1, + dbSchema_2, + dbSchema_3 } from './schema/index.js'; + +/** + * Defines the callback functions to execute for each schemaVersion level. + * + * @type {Record} + */ +const migrateImpl = { + 0: () => {}, // Schema level 0 is a noop / assume all data is stored in JE content. + // 1: dbSchema_1, // Migrate to schema 1 transferring any old data to JE flags. + // 2: dbSchema_2, // Schema 2 - store quest giver data in Quest data instead of doing a UUID lookup in Enrich. + // 3: dbSchema_3, // V10 image / name refresh - dnd5e system amongst others have significant image path changes for compendiums. +}; + +/** + * Provides a utility module to manage DB migrations when new versions of FQL are installed / loaded for the first time. + * These updates are organized as schema versions with callback functions defined above. Each schema migration function + * stores the version number in module settings for {@link DBMigration.setting}. On startup {@link DBMigration.migrate} + * is invoked in the `ready` Hook callback from `./src/init.js`. If the current schema version already equals + * {@link DBMigration.version} no migration occurs. Also if there are no journal entries in the `_fql_quests` folder + * no migration occurs which is often the case with a new world and the module setting is set to not run migration + * again. + * + * In the case that a GM needs to manually run migration there is a hook defined in {@link FQLHooks.runDBMigration}. + * This is `ForienQuestLog.Run.DBMigration` which can be executed by a macro with + * `Hooks.call('ForienQuestLog.Run.DBMigration', );`. To run all migration manually substitute + * `` with `0`. + * + * @see registerHooks + */ +export class DBMigration +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + + /** + * Defines the current max schema version. + * + * @returns {number} max schema version. + */ + static get version() { return 0; } + + /** + * Defines the module setting key to store current level DB migration level that already has run for schemaVersion. + * + * @returns {string} module setting for schemaVersion. + */ + static get setting() { return 'dbSchema'; } + + /** + * Runs DB migration. If no `schemaVersion` is set the module setting {@link DBMigration.setting} is used to get the + * current schema value which is stored after any migration occurs. There is a hook available + * `ForienQuestLog.Run.DBMigration`. + * + * @param {number} schemaVersion - A valid schema version from 0 to DBMigration.version - 1 + * + * @returns {Promise} + */ + static async migrate(schemaVersion = void 0) + { + // TODO: Note that the DB migration code is disabled. The last DB migration was between v9 and v10 of Foundry + // September '22. FQL has been v11+ for a while. To provide a clean start for any future DB migration the old + // migration code for previous versions of Foundry is outdated and any migrations moving forward can start fresh. + // This code is kept around to provide a guide / possible solution for future DB migration, but otherwise should + // be freshly evaluated. The previous highest schema value for potential DB migration was `3`; this may still be + // set for the `dbSchema` world setting in the off chance of a world being upgraded. It is recommended when + // DB migration is necessary in the future to use a fresh world setting other than `dbSchema` to provide future + // migration tracking. + + // try + // { + // // Registers the DB Schema world setting. By default this is 0. The `0.7.0` release of FQL has a schema of `1`. + // game.settings.register(constants.moduleName, this.setting, { + // scope: 'world', + // config: false, + // default: 0, + // type: Number + // }); + // + // // If no schemaVersion is defined then pull the value from module settings. + // if (schemaVersion === void 0) + // { + // schemaVersion = game.settings.get(constants.moduleName, this.setting); + // } + // else + // { + // // Otherwise make sure that the schemaVersion supplied to migrate is valid. + // if (!Number.isInteger(schemaVersion) || schemaVersion < 0 || schemaVersion > DBMigration.version - 1) + // { + // const err = `ForienQuestLog - DBMigrate.migrate - schemaVersion must be an integer (0 - ${ + // DBMigration.version - 1})`; + // + // ui.notifications.error(err); + // console.error(err); + // } + // } + // + // // The DB schema matches the current version + // if (schemaVersion === this.version) { return; } + // + // // Increment the schema version to run against the proper callback function. + // schemaVersion++; + // + // // Sanity check to make sure there is a schema migration function for the next schema update. + // if (typeof migrateImpl[schemaVersion] !== 'function') { return; } + // + // const folder = await Utils.initializeQuestFolder(); + // + // // Early out if there are no journal entries / quests in the `_fql-quests` folder. + // const folderContentLength = folder?.contents?.length ?? 0; + // if (folderContentLength === 0) + // { + // await game.settings.set(constants.moduleName, DBMigration.setting, DBMigration.version); + // return; + // } + // + // ui.notifications.info(game.i18n.localize('ForienQuestLog.Migration.Notifications.Start')); + // + // // Start at the schema version and stop when the version exceeds the max version. + // for (let version = schemaVersion; version <= this.version; version++) + // { + // if (version !== 0) + // { + // ui.notifications.info(game.i18n.format('ForienQuestLog.Migration.Notifications.Schema', { version })); + // } + // + // await migrateImpl[version](); + // } + // + // ui.notifications.info(game.i18n.localize('ForienQuestLog.Migration.Notifications.Complete')); + // + // Socket.refreshAll(); + // } + // catch (err) + // { + // console.error(err); + // } + } +} \ No newline at end of file diff --git a/database/schema/dbSchema_1.js b/database/schema/dbSchema_1.js new file mode 100644 index 00000000..fad6ba87 --- /dev/null +++ b/database/schema/dbSchema_1.js @@ -0,0 +1,200 @@ +import { DBMigration } from '../DBMigration.js'; + +import { + FVTTCompat, + Utils } from '../../src/control/index.js'; + +import { Quest } from '../../src/model/index.js'; + +import { + constants, + questStatus } from '../../src/model/constants.js'; + +/** + * Performs DB migration from schema 0 to 1. + * + * Moves serialized quest data from journal entry content field to flags stored by the {@link constants.moduleName} + * and {@link constants.flagDB}. In the process perform any reversal of potential corrupted data which can occur on + * Foundry versions `0.7.10` and `0.8.6` which have improperly configured content sanitation filters that affect the + * journal entry content field. + * + * New data fields: + * - location -> {string}; default: null + * - priority -> {number}; default: 0 + * - type -> {string}; default: null + * - date -> {object} + * - {number} create - Date.now(). + * - {number} active - set if quest is in progress to Date.now(). + * - {number} end - set if quest is completed / failed to Date.now(). + * + * @returns {Promise} + */ +export async function dbSchema_1() +{ + const folder = await Utils.initializeQuestFolder(); + if (!folder) { return; } + + // Iterate through all journal entries from `_fql_quests`. + for (const entry of FVTTCompat.folderContents(folder)) + { + try + { + const flagContent = entry.getFlag(constants.moduleName, constants.flagDB); + + // If there is flag content don't migrate the data otherwise execute `migrateData`. + const content = flagContent ? flagContent : await migrateData(entry); + + if (content !== null) + { + // The new DB schema gets picked up in Quest -> initData. + const quest = new Quest(content, entry); + + // Accept the default permission if defined otherwise set to observer. + const defaultPermission = FVTTCompat.ownership(entry)?.default ?? CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER; + + const data = { + name: quest.name, + content: '', + flags: { + [constants.moduleName]: { json: quest.toJSON() } + } + }; + + data.ownership = { default: defaultPermission }; + + await entry.update(data); + } + else + { + // Must delete any no conforming journal entries. This likely never occurs. + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + await entry.delete(); + } + } + catch (err) + { + // Must delete any journal entries / quests that fail the migration process. + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + await entry.delete(); + } + } + + // Set the DBMigration.setting to `1` indicating that migration to schema version `1` is complete. + await game.settings.set(constants.moduleName, DBMigration.setting, 1); +} + +/** + * Attempts to migrate data from FQL 0.6.0 or prior to new 0.7.0 data format. This function also attempts to reverse any + * damage that the initial server side sanitation filters may have caused to the old data format. + * + * @param {JournalEntry} entry - The source journal entry storing quest data. + * + * @returns {object|null} Loaded Quest data or null if loading fails. + */ +async function migrateData(entry) +{ + let content; + + try + { + // Strip leading / trailing HTML tags in case someone attempted to look at / modify the JE. + let entryContent = FVTTCompat.get(entry, 'content'); + entryContent = entryContent.replace(/^

/, ''); + entryContent = entryContent.replace(/<\/p>$/, ''); + + try + { + // If parsing fails here we are then dealing with damaged data likely from 0.7.10 / 0.8.6 + // server side sanitation filters. + content = JSON.parse(entryContent); + } + catch (e1) + { + try + { + // These regex substitutions will attempt to reverse damage to the stored JSON. + entryContent = entryContent.replace(/("\\"|\\"")/gm, '\\"'); + entryContent = entryContent.replace(/:">/gm, '\\">'); + entryContent = entryContent.replace(/:" /gm, '\\" '); + + // Non-printable characters need to be removed; replace the entire range of non-printable characters. + entryContent = entryContent.replace(/[\x00-\x1F]/gm, ''); // eslint-disable-line no-control-regex + + content = JSON.parse(entryContent); + } + catch (e2) + { + console.error(e2); + return null; + } + } + } + catch (e) + { + console.error(e); + return null; + } + + // Convert title to name; all new Quest use `name` instead of `title` to match Foundry document model + content.name = content.title; + delete content.title; + + // As things go the rewards format for the new FQL only stores UUID and basic info. Can't support old + // rewards format. + content.rewards = []; + + // Old FQL will often store the entire actor data as the giver, so if it is not a string then set to null. + // Verify the quest giver exists; look up UUID. + if (typeof content.giver === 'string') + { + try + { + const doc = await fromUuid(content.giver); + if (!doc) { content.giver = null; } + } + catch (err) { content.giver = null; } + } + else // Handle the situation where the giver could be the complete actor data or is already null. + { + content.giver = null; + } + + // Verify that parent quest is valid. + if (content.parent) + { + try + { + const doc = game.journal.get(content.parent); + if (!doc) { content.parent = null; } + } + catch (err) { content.parent = null; } + } + + // Verify that all subquests refer to active journal entries; if not then remove them. + if (Array.isArray(content.subquests)) + { + const subquests = []; + for (const subquest of content.subquests) + { + try + { + const doc = game.journal.get(subquest); + if (doc) { subquests.push(subquest); } + } + catch (err) { /* */ } + } + + content.subquests = subquests; + } + + // Note: in DB schema v2 update status hidden is renamed to 'inactive'; use bare strings here instead of questStatus. + try + { + if (!questStatus[content.status]) { content.status = 'hidden'; } + } + catch (err) { content.status = 'hidden'; } + + return content; +} diff --git a/database/schema/dbSchema_2.js b/database/schema/dbSchema_2.js new file mode 100644 index 00000000..377f4f22 --- /dev/null +++ b/database/schema/dbSchema_2.js @@ -0,0 +1,75 @@ +import { DBMigration } from '../DBMigration.js'; + +import { + FVTTCompat, + Utils } from '../../src/control/index.js'; + +import { Quest } from '../../src/model/index.js'; + +import { + constants, + questStatus } from '../../src/model/constants.js'; + +/** + * Performs DB migration from schema 1 to 2. + * + * New data field: + * {string} giverData - Stores the quest giver data from {@link Quest.giverFromQuest}. + * + * Convert data: + * {string} status - convert 'hidden' to 'inactive' for code clarity. + * + * The purpose of this update is to store the quest giver data in the new `giverData` field. + * Presently the quest giver if non abstract and a Foundry UUID is looked up in the enrich process via `fromUUID` to + * retrieve this data. This is the only asynchronous action during the enrichment process and to provide QuestDB / + * in-memory caching of Quest and enriched data the process needs to be synchronous to make sure that order of + * operations succeeds atomically and as quick as possible. This update will process all quest data and perform that + * lookup and store the quest giver data in `giverData`. In FQL 0.7.4 and above to update the quest giver data the + * user needs to open QuestPreview and simply save the quest. A macro will also be provided to update all quest giver + * data in bulk. + * + * @returns {Promise} + */ +export async function dbSchema_2() +{ + const folder = await Utils.initializeQuestFolder(); + if (!folder) { return; } + + for (const entry of FVTTCompat.folderContents(folder)) + { + try + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + if (content) + { + const quest = new Quest(content, entry); + + // Load quest giver assets and store as 'giverData'. + if (typeof quest.giver === 'string') + { + const data = await Quest.giverFromQuest(quest); + if (data && typeof data.img === 'string' && data.img.length) { quest.giverData = data; } + } + + // Change any status of 'hidden' to 'inactive'. + if (quest.status === 'hidden') { quest.status = questStatus.inactive; } + + await quest.save(); + } + else + { + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + } + } + catch (err) + { + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + } + } + + // Set the DBMigration.setting to `2` indicating that migration to schema version `2` is complete. + await game.settings.set(constants.moduleName, DBMigration.setting, 2); +} diff --git a/database/schema/dbSchema_3.js b/database/schema/dbSchema_3.js new file mode 100644 index 00000000..74aa4934 --- /dev/null +++ b/database/schema/dbSchema_3.js @@ -0,0 +1,296 @@ +import { DBMigration } from '../DBMigration.js'; + +import { + FVTTCompat, + Utils } from '../../src/control/index.js'; + +import { Quest } from '../../src/model/index.js'; + +import { constants } from '../../src/model/constants.js'; + +/** + * Performs DB migration for v10 and all systems updating cached images / names of quest givers and reward items. + * + * The purpose of this update is that image paths for compendium items have changed for dnd5e system. World items will + * have been migrated and compendium items updated. This update does UUID lookups for all quest givers and item rewards + * changing the image path if it is found and differs from the cached value in the quest flag data. + * + * @returns {Promise} + */ +export async function dbSchema_3() +{ + const folder = await Utils.initializeQuestFolder(); + if (!folder) { return; } + + let dnd5eIconMap = void 0; + + const removedData = []; + + // Retrieve DnD5e system icon migration map if applicable. + if (typeof game?.dnd5e?.migrations?.getMigrationData === 'function') + { + const dndData = await game.dnd5e.migrations.getMigrationData(); + if (dndData && typeof dndData?.iconMap === 'object' && dndData?.iconMap !== null) + { + dnd5eIconMap = dndData.iconMap; + } + } + + for (const entry of FVTTCompat.folderContents(folder)) + { + try + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + if (content) + { + const quest = new Quest(content, entry); + + handleSplashImage(quest, dnd5eIconMap); + const removedDataEntry = {}; + + await handleQuestGiver(quest, removedDataEntry, dnd5eIconMap); + await handleRewards(quest, removedDataEntry, dnd5eIconMap); + + await quest.save(); + + if (Object.keys(removedDataEntry).length > 0) + { + removedDataEntry.questName = quest.name; + removedDataEntry.questId = entry.id; + removedData.push(removedDataEntry); + } + } + else + { + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + } + } + catch (err) + { + console.log(game.i18n.format('ForienQuestLog.Migration.Notifications.CouldNotMigrate', + { name: FVTTCompat.get(entry, 'name') })); + } + } + + // Post informational message to notifications and chat message if unlinked document data exists. + if (removedData.length > 0) + { + ui.notifications.warn(game.i18n.localize('ForienQuestLog.Migration.ChatMessage.Notification')); + + let content = game.i18n.localize('ForienQuestLog.Migration.ChatMessage.Header'); + + for (const entry of removedData) + { + // Shorten to fit in sidebar. + const questName = entry.questName.length > 38 ? `${entry.questName.substring(0, 38)}...` : entry.questName; + + content += `@JournalEntry[${entry.questId}]{${questName}}
`; + + if (typeof entry.giverName === 'string') + { + // Shorten to fit in sidebar. + const giverName = entry.giverName.length > 40 ? `${entry.giverName.substring(0, 40)}...` : entry.giverName; + + content += `${game.i18n.localize('ForienQuestLog.Migration.ChatMessage.QuestGiver')}
- ${ + giverName}
`; + } + + if (Array.isArray(entry.rewards)) + { + content += `${game.i18n.localize('ForienQuestLog.Migration.ChatMessage.QuestRewards')} (${ + entry.rewards.length})
`; + + for (const reward of entry.rewards) + { + // Shorten to fit in sidebar. + const rewardName = reward.length > 40 ? `${reward.substring(0, 40)}...` : reward; + content += `- ${rewardName}
`; + } + } + content += `
`; + } + + content += game.i18n.localize('ForienQuestLog.Migration.ChatMessage.Footer'); + + ChatMessage.create({ + user: game.user.id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content + }); + } + + // Set the DBMigration.setting to `3` indicating that migration to schema version `3` is complete. + await game.settings.set(constants.moduleName, DBMigration.setting, 3); +} + +/** + * @param {string} path - + * + * @param {object} dnd5eIconMap - + * + * @returns {string} Converted path. + */ +function swap5eImage(path, dnd5eIconMap) +{ + if (typeof path !== 'string' || !dnd5eIconMap) { return void 0; } + + if (typeof dnd5eIconMap[path] === 'string') + { + return dnd5eIconMap[path]; + } + else if (path.startsWith('systems/dnd5e')) + { + // Hope for the best and swap extensions to `webp`. + return path.replace(/\.[^/.]+$/, '.webp'); + } + + return path; +} + +/** + * Update quest splash image specifically for remapping dnd5e system images. + * + * @param {Quest} quest - + * + * @param {object} dnd5eIconMap - + */ +function handleSplashImage(quest, dnd5eIconMap) +{ + if (typeof quest.splash === 'string' && dnd5eIconMap) + { + const newPath = swap5eImage(quest.splash, dnd5eIconMap); + if (typeof newPath === 'string') + { + quest.splash = newPath; + } + } +} + +/** + * Update quest giver images w/ special handling for dnd5e system. + * + * @param {Quest} quest - + * + * @param {object} removedDataEntry - + * + * @param {object} dnd5eIconMap - + */ +async function handleQuestGiver(quest, removedDataEntry, dnd5eIconMap) +{ + // Load quest giver assets and store as 'giverData'. + if (typeof quest.giver === 'string') + { + // Handle remapping any dnd5e images used for abstract quest givers. + if (quest.giver === 'abstract' && dnd5eIconMap) + { + const newPath = swap5eImage(quest.image, dnd5eIconMap); + if (typeof newPath === 'string') + { + quest.image = newPath; + if (quest.giverData && typeof quest.giverData?.img === 'string') { quest.giverData.img = newPath; } + } + } + else + { + try + { + // Do a lookup and if it fails then reset giver / giverData below. + const doc = await globalThis.fromUuid(quest.giver); + + if (doc) + { + const data = await Quest.giverFromQuest(quest); + if (data && typeof data.img === 'string' && data.img.length) { quest.giverData = data; } + } + else + { + const giverName = quest?.giverData?.name ?? 'Unknown'; + console.warn(`Forien's Quest Log warning; removed quest giver "${giverName}" from quest: ${quest.name}`); + + // Document is not found, so remove quest giver and giverData. + quest.giver = null; + quest.giverData = null; + + removedDataEntry.giverName = giverName; + } + } + catch (err) + { + const giverName = quest?.giverData?.name ?? 'Unknown'; + console.warn(`Forien's Quest Log warning; removed quest giver "${giverName}" from quest: ${quest.name}`); + + // An error occurred / remove quest giver. + quest.giver = null; + quest.giverData = null; + removedDataEntry.giverName = giverName; + } + } + } +} + +/** + * Update all abstract and item quest rewards. Verify that items still exist otherwise remove them. For dnd5e system + * attempt to remap abstract reward images. For items with valid documents update the name and image otherwise remove + * them. + * + * @param {Quest} quest - + * + * @param {object} removedDataEntry - + * + * @param {object} dnd5eIconMap - + */ +async function handleRewards(quest, removedDataEntry, dnd5eIconMap) +{ + if (!Array.isArray(quest.rewards)) { return; } + + for (let cntr = quest.rewards.length; --cntr >= 0;) + { + const reward = quest.rewards[cntr]; + + if (dnd5eIconMap && reward?.type === 'Abstract' && typeof reward?.data?.img === 'string') + { + const newPath = swap5eImage(reward.data.img, dnd5eIconMap); + if (typeof newPath === 'string') + { + reward.data.img = newPath; + } + } + else if (reward?.type === 'Item' && typeof reward?.data?.uuid === 'string') + { + try + { + const doc = await globalThis.fromUuid(reward.data.uuid); + + // Remove reward as no document found. + if (!doc) + { + const rewardName = reward.data?.name ?? 'Unknown'; + console.warn(`ForienQuestLog warning; removed item reward "${rewardName}" from quest: ${quest.name}`); + + quest.rewards.splice(cntr, 1); + + if (!Array.isArray(removedDataEntry.rewards)) { removedDataEntry.rewards = []; } + removedDataEntry.rewards.push(rewardName); + } + else + { + reward.data.img = doc.img; + reward.data.name = doc.name; + } + } + catch (err) + { + const rewardName = reward.data?.name ?? 'Unknown'; + console.warn(`ForienQuestLog warning; removed item reward "${rewardName}" from quest: ${quest.name}`); + + // Remove reward on any error. + quest.rewards.splice(cntr, 1); + + if (!Array.isArray(removedDataEntry.rewards)) { removedDataEntry.rewards = []; } + removedDataEntry.rewards.push(rewardName); + } + } + } +} \ No newline at end of file diff --git a/database/schema/index.js b/database/schema/index.js new file mode 100644 index 00000000..ada6a6e3 --- /dev/null +++ b/database/schema/index.js @@ -0,0 +1,3 @@ +export * from './dbSchema_1.js'; +export * from './dbSchema_2.js'; +export * from './dbSchema_3.js'; \ No newline at end of file diff --git a/external/DOMPurify.js b/external/DOMPurify.js new file mode 100644 index 00000000..e561869e --- /dev/null +++ b/external/DOMPurify.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/LICENSE */ +const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:a,seal:i,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;a||(a=function(e){return e}),i||(i=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=unapply(Array.prototype.forEach),d=unapply(Array.prototype.pop),p=unapply(Array.prototype.push),m=unapply(String.prototype.toLowerCase),f=unapply(String.prototype.toString),h=unapply(String.prototype.match),T=unapply(String.prototype.replace),g=unapply(String.prototype.indexOf),y=unapply(String.prototype.trim),E=unapply(Object.prototype.hasOwnProperty),S=unapply(RegExp.prototype.test),_=(A=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:m;t&&t(e,null);let a=o.length;for(;a--;){let t=o[a];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[a]=e),t=e)}e[t]=!0}return e}function cleanArray(e){for(let t=0;t/gm),U=i(/\${[\w\W]*}/gm),P=i(/^data-[\-\w.\u00B7-\uFFFF]/),F=i(/^aria-[\-\w]+$/),H=i(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),z=i(/^(?:\w+script|data):/i),G=i(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),B=i(/^html$/i),W=i(/^[a-z][.\w]*(-[.\w]+)+$/i);var Y=Object.freeze({__proto__:null,MUSTACHE_EXPR:M,ERB_EXPR:I,TMPLIT_EXPR:U,DATA_ATTR:P,ARIA_ATTR:F,IS_ALLOWED_URI:H,IS_SCRIPT_OR_DATA:z,ATTR_WHITESPACE:G,DOCTYPE_NAME:B,CUSTOM_ELEMENT:W});const getGlobal=function(){return"undefined"==typeof window?null:window};var j=function createDOMPurify(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:getGlobal();const DOMPurify=e=>createDOMPurify(e);if(DOMPurify.version="3.1.5",DOMPurify.removed=[],!t||!t.document||9!==t.document.nodeType)return DOMPurify.isSupported=!1,DOMPurify;let{document:n}=t;const o=n,r=o.currentScript,{DocumentFragment:i,HTMLTemplateElement:c,Node:s,Element:A,NodeFilter:M,NamedNodeMap:I=t.NamedNodeMap||t.MozNamedAttrMap,HTMLFormElement:U,DOMParser:P,trustedTypes:F}=t,z=A.prototype,G=lookupGetter(z,"cloneNode"),W=lookupGetter(z,"nextSibling"),j=lookupGetter(z,"childNodes"),X=lookupGetter(z,"parentNode");if("function"==typeof c){const e=n.createElement("template");e.content&&e.content.ownerDocument&&(n=e.content.ownerDocument)}let q,$="";const{implementation:K,createNodeIterator:V,createDocumentFragment:Z,getElementsByTagName:J}=n,{importNode:Q}=o;let ee={};DOMPurify.isSupported="function"==typeof e&&"function"==typeof X&&K&&void 0!==K.createHTMLDocument;const{MUSTACHE_EXPR:te,ERB_EXPR:ne,TMPLIT_EXPR:oe,DATA_ATTR:re,ARIA_ATTR:ae,IS_SCRIPT_OR_DATA:ie,ATTR_WHITESPACE:le,CUSTOM_ELEMENT:ce}=Y;let{IS_ALLOWED_URI:se}=Y,ue=null;const de=addToSet({},[...N,...b,...R,...D,...L]);let pe=null;const me=addToSet({},[...O,...v,...k,...x]);let fe=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),he=null,Te=null,ge=!0,ye=!0,Ee=!1,Se=!0,_e=!1,Ae=!0,Ne=!1,be=!1,Re=!1,we=!1,De=!1,Ce=!1,Le=!0,Oe=!1,ve=!0,ke=!1,xe={},Me=null;const Ie=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ue=null;const Pe=addToSet({},["audio","video","img","source","image","track"]);let Fe=null;const He=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ze="http://www.w3.org/1998/Math/MathML",Ge="http://www.w3.org/2000/svg",Be="http://www.w3.org/1999/xhtml";let We=Be,Ye=!1,je=null;const Xe=addToSet({},[ze,Ge,Be],f);let qe=null;const $e=["application/xhtml+xml","text/html"];let Ke=null,Ve=null;const Ze=n.createElement("form"),isRegexOrFunction=function(e){return e instanceof RegExp||e instanceof Function},_parseConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!Ve||Ve!==e){if(e&&"object"==typeof e||(e={}),e=clone(e),qe=-1===$e.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Ke="application/xhtml+xml"===qe?f:m,ue=E(e,"ALLOWED_TAGS")?addToSet({},e.ALLOWED_TAGS,Ke):de,pe=E(e,"ALLOWED_ATTR")?addToSet({},e.ALLOWED_ATTR,Ke):me,je=E(e,"ALLOWED_NAMESPACES")?addToSet({},e.ALLOWED_NAMESPACES,f):Xe,Fe=E(e,"ADD_URI_SAFE_ATTR")?addToSet(clone(He),e.ADD_URI_SAFE_ATTR,Ke):He,Ue=E(e,"ADD_DATA_URI_TAGS")?addToSet(clone(Pe),e.ADD_DATA_URI_TAGS,Ke):Pe,Me=E(e,"FORBID_CONTENTS")?addToSet({},e.FORBID_CONTENTS,Ke):Ie,he=E(e,"FORBID_TAGS")?addToSet({},e.FORBID_TAGS,Ke):{},Te=E(e,"FORBID_ATTR")?addToSet({},e.FORBID_ATTR,Ke):{},xe=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,ge=!1!==e.ALLOW_ARIA_ATTR,ye=!1!==e.ALLOW_DATA_ATTR,Ee=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Se=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,_e=e.SAFE_FOR_TEMPLATES||!1,Ae=!1!==e.SAFE_FOR_XML,Ne=e.WHOLE_DOCUMENT||!1,we=e.RETURN_DOM||!1,De=e.RETURN_DOM_FRAGMENT||!1,Ce=e.RETURN_TRUSTED_TYPE||!1,Re=e.FORCE_BODY||!1,Le=!1!==e.SANITIZE_DOM,Oe=e.SANITIZE_NAMED_PROPS||!1,ve=!1!==e.KEEP_CONTENT,ke=e.IN_PLACE||!1,se=e.ALLOWED_URI_REGEXP||H,We=e.NAMESPACE||Be,fe=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&isRegexOrFunction(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(fe.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&isRegexOrFunction(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(fe.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(fe.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),_e&&(ye=!1),De&&(we=!0),xe&&(ue=addToSet({},L),pe=[],!0===xe.html&&(addToSet(ue,N),addToSet(pe,O)),!0===xe.svg&&(addToSet(ue,b),addToSet(pe,v),addToSet(pe,x)),!0===xe.svgFilters&&(addToSet(ue,R),addToSet(pe,v),addToSet(pe,x)),!0===xe.mathMl&&(addToSet(ue,D),addToSet(pe,k),addToSet(pe,x))),e.ADD_TAGS&&(ue===de&&(ue=clone(ue)),addToSet(ue,e.ADD_TAGS,Ke)),e.ADD_ATTR&&(pe===me&&(pe=clone(pe)),addToSet(pe,e.ADD_ATTR,Ke)),e.ADD_URI_SAFE_ATTR&&addToSet(Fe,e.ADD_URI_SAFE_ATTR,Ke),e.FORBID_CONTENTS&&(Me===Ie&&(Me=clone(Me)),addToSet(Me,e.FORBID_CONTENTS,Ke)),ve&&(ue["#text"]=!0),Ne&&addToSet(ue,["html","head","body"]),ue.table&&(addToSet(ue,["tbody"]),delete he.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');q=e.TRUSTED_TYPES_POLICY,$=q.createHTML("")}else void 0===q&&(q=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(F,r)),null!==q&&"string"==typeof $&&($=q.createHTML(""));a&&a(e),Ve=e}},Je=addToSet({},["mi","mo","mn","ms","mtext"]),Qe=addToSet({},["foreignobject","annotation-xml"]),et=addToSet({},["title","style","font","a","script"]),tt=addToSet({},[...b,...R,...w]),nt=addToSet({},[...D,...C]),_forceRemove=function(e){p(DOMPurify.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},_removeAttribute=function(e,t){try{p(DOMPurify.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(DOMPurify.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!pe[e])if(we||De)try{_forceRemove(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_initDocument=function(e){let t=null,o=null;if(Re)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);o=t&&t[0]}"application/xhtml+xml"===qe&&We===Be&&(e=''+e+"");const r=q?q.createHTML(e):e;if(We===Be)try{t=(new P).parseFromString(r,qe)}catch(e){}if(!t||!t.documentElement){t=K.createDocument(We,"template",null);try{t.documentElement.innerHTML=Ye?$:r}catch(e){}}const a=t.body||t.documentElement;return e&&o&&a.insertBefore(n.createTextNode(o),a.childNodes[0]||null),We===Be?J.call(t,Ne?"html":"body")[0]:Ne?t.documentElement:a},_createNodeIterator=function(e){return V.call(e.ownerDocument||e,e,M.SHOW_ELEMENT|M.SHOW_COMMENT|M.SHOW_TEXT|M.SHOW_PROCESSING_INSTRUCTION|M.SHOW_CDATA_SECTION,null)},_isClobbered=function(e){return e instanceof U&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof I)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},_isNode=function(e){return"function"==typeof s&&e instanceof s},_executeHook=function(e,t,n){ee[e]&&u(ee[e],(e=>{e.call(DOMPurify,t,n,Ve)}))},_sanitizeElements=function(e){let t=null;if(_executeHook("beforeSanitizeElements",e,null),_isClobbered(e))return _forceRemove(e),!0;const n=Ke(e.nodeName);if(_executeHook("uponSanitizeElement",e,{tagName:n,allowedTags:ue}),e.hasChildNodes()&&!_isNode(e.firstElementChild)&&S(/<[/\w]/g,e.innerHTML)&&S(/<[/\w]/g,e.textContent))return _forceRemove(e),!0;if(7===e.nodeType)return _forceRemove(e),!0;if(Ae&&8===e.nodeType&&S(/<[/\w]/g,e.data))return _forceRemove(e),!0;if(!ue[n]||he[n]){if(!he[n]&&_isBasicCustomElement(n)){if(fe.tagNameCheck instanceof RegExp&&S(fe.tagNameCheck,n))return!1;if(fe.tagNameCheck instanceof Function&&fe.tagNameCheck(n))return!1}if(ve&&!Me[n]){const t=X(e)||e.parentNode,n=j(e)||e.childNodes;if(n&&t)for(let o=n.length-1;o>=0;--o){const r=G(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,W(e))}}return _forceRemove(e),!0}return e instanceof A&&!function(e){let t=X(e);t&&t.tagName||(t={namespaceURI:We,tagName:"template"});const n=m(e.tagName),o=m(t.tagName);return!!je[e.namespaceURI]&&(e.namespaceURI===Ge?t.namespaceURI===Be?"svg"===n:t.namespaceURI===ze?"svg"===n&&("annotation-xml"===o||Je[o]):Boolean(tt[n]):e.namespaceURI===ze?t.namespaceURI===Be?"math"===n:t.namespaceURI===Ge?"math"===n&&Qe[o]:Boolean(nt[n]):e.namespaceURI===Be?!(t.namespaceURI===Ge&&!Qe[o])&&!(t.namespaceURI===ze&&!Je[o])&&!nt[n]&&(et[n]||!tt[n]):!("application/xhtml+xml"!==qe||!je[e.namespaceURI]))}(e)?(_forceRemove(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(_e&&3===e.nodeType&&(t=e.textContent,u([te,ne,oe],(e=>{t=T(t,e," ")})),e.textContent!==t&&(p(DOMPurify.removed,{element:e.cloneNode()}),e.textContent=t)),_executeHook("afterSanitizeElements",e,null),!1):(_forceRemove(e),!0)},_isValidAttribute=function(e,t,o){if(Le&&("id"===t||"name"===t)&&(o in n||o in Ze))return!1;if(ye&&!Te[t]&&S(re,t));else if(ge&&S(ae,t));else if(!pe[t]||Te[t]){if(!(_isBasicCustomElement(e)&&(fe.tagNameCheck instanceof RegExp&&S(fe.tagNameCheck,e)||fe.tagNameCheck instanceof Function&&fe.tagNameCheck(e))&&(fe.attributeNameCheck instanceof RegExp&&S(fe.attributeNameCheck,t)||fe.attributeNameCheck instanceof Function&&fe.attributeNameCheck(t))||"is"===t&&fe.allowCustomizedBuiltInElements&&(fe.tagNameCheck instanceof RegExp&&S(fe.tagNameCheck,o)||fe.tagNameCheck instanceof Function&&fe.tagNameCheck(o))))return!1}else if(Fe[t]);else if(S(se,T(o,le,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==g(o,"data:")||!Ue[e])if(Ee&&!S(ie,T(o,le,"")));else if(o)return!1;return!0},_isBasicCustomElement=function(e){return"annotation-xml"!==e&&h(e,ce)},_sanitizeAttributes=function(e){_executeHook("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:pe};let o=t.length;for(;o--;){const r=t[o],{name:a,namespaceURI:i,value:l}=r,c=Ke(a);let s="value"===a?l:y(l);if(n.attrName=c,n.attrValue=s,n.keepAttr=!0,n.forceKeepAttr=void 0,_executeHook("uponSanitizeAttribute",e,n),s=n.attrValue,n.forceKeepAttr)continue;if(_removeAttribute(a,e),!n.keepAttr)continue;if(!Se&&S(/\/>/i,s)){_removeAttribute(a,e);continue}if(Ae&&S(/((--!?|])>)|<\/(style|title)/i,s)){_removeAttribute(a,e);continue}_e&&u([te,ne,oe],(e=>{s=T(s,e," ")}));const p=Ke(e.nodeName);if(_isValidAttribute(p,c,s)){if(!Oe||"id"!==c&&"name"!==c||(_removeAttribute(a,e),s="user-content-"+s),q&&"object"==typeof F&&"function"==typeof F.getAttributeType)if(i);else switch(F.getAttributeType(p,c)){case"TrustedHTML":s=q.createHTML(s);break;case"TrustedScriptURL":s=q.createScriptURL(s)}try{i?e.setAttributeNS(i,a,s):e.setAttribute(a,s),_isClobbered(e)?_forceRemove(e):d(DOMPurify.removed)}catch(e){}}}_executeHook("afterSanitizeAttributes",e,null)},ot=function _sanitizeShadowDOM(e){let t=null;const n=_createNodeIterator(e);for(_executeHook("beforeSanitizeShadowDOM",e,null);t=n.nextNode();)_executeHook("uponSanitizeShadowNode",t,null),_sanitizeElements(t)||(t.content instanceof i&&_sanitizeShadowDOM(t.content),_sanitizeAttributes(t));_executeHook("afterSanitizeShadowDOM",e,null)};return DOMPurify.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,a=null,l=null;if(Ye=!e,Ye&&(e="\x3c!--\x3e"),"string"!=typeof e&&!_isNode(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!DOMPurify.isSupported)return e;if(be||_parseConfig(t),DOMPurify.removed=[],"string"==typeof e&&(ke=!1),ke){if(e.nodeName){const t=Ke(e.nodeName);if(!ue[t]||he[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof s)n=_initDocument("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!we&&!_e&&!Ne&&-1===e.indexOf("<"))return q&&Ce?q.createHTML(e):e;if(n=_initDocument(e),!n)return we?null:Ce?$:""}n&&Re&&_forceRemove(n.firstChild);const c=_createNodeIterator(ke?e:n);for(;a=c.nextNode();)_sanitizeElements(a)||(a.content instanceof i&&ot(a.content),_sanitizeAttributes(a));if(ke)return e;if(we){if(De)for(l=Z.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(pe.shadowroot||pe.shadowrootmode)&&(l=Q.call(o,l,!0)),l}let d=Ne?n.outerHTML:n.innerHTML;return Ne&&ue["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(B,n.ownerDocument.doctype.name)&&(d="\n"+d),_e&&u([te,ne,oe],(e=>{d=T(d,e," ")})),q&&Ce?q.createHTML(d):d},DOMPurify.setConfig=function(){_parseConfig(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),be=!0},DOMPurify.clearConfig=function(){Ve=null,be=!1},DOMPurify.isValidAttribute=function(e,t,n){Ve||_parseConfig({});const o=Ke(e),r=Ke(t);return _isValidAttribute(o,r,n)},DOMPurify.addHook=function(e,t){"function"==typeof t&&(ee[e]=ee[e]||[],p(ee[e],t))},DOMPurify.removeHook=function(e){if(ee[e])return d(ee[e])},DOMPurify.removeHooks=function(e){ee[e]&&(ee[e]=[])},DOMPurify.removeAllHooks=function(){ee={}},DOMPurify}();export{j as default}; +//# sourceMappingURL=DOMPurify.js.map diff --git a/external/DOMPurify.js.map b/external/DOMPurify.js.map new file mode 100644 index 00000000..f817c305 --- /dev/null +++ b/external/DOMPurify.js.map @@ -0,0 +1 @@ +{"version":3,"file":"DOMPurify.js","sources":["../node_modules/dompurify/dist/purify.es.mjs"],"sourcesContent":["/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/LICENSE */\n\nconst {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor\n} = Object;\nlet {\n freeze,\n seal,\n create\n} = Object; // eslint-disable-line import/no-mutable-exports\nlet {\n apply,\n construct\n} = typeof Reflect !== 'undefined' && Reflect;\nif (!freeze) {\n freeze = function freeze(x) {\n return x;\n };\n}\nif (!seal) {\n seal = function seal(x) {\n return x;\n };\n}\nif (!apply) {\n apply = function apply(fun, thisValue, args) {\n return fun.apply(thisValue, args);\n };\n}\nif (!construct) {\n construct = function construct(Func, args) {\n return new Func(...args);\n };\n}\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\nconst regExpTest = unapply(RegExp.prototype.test);\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param {Function} func - The function to be wrapped and called.\n * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return function (thisArg) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n return apply(func, thisArg, args);\n };\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param {Function} func - The constructor function to be wrapped and called.\n * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(func) {\n return function () {\n for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n args[_key2] = arguments[_key2];\n }\n return construct(func, args);\n };\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param {Object} set - The set to which elements will be added.\n * @param {Array} array - The array containing elements to be added to the set.\n * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns {Object} The modified set with added elements.\n */\nfunction addToSet(set, array) {\n let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n element = lcElement;\n }\n }\n set[element] = true;\n }\n return set;\n}\n\n/**\n * Clean up an array to harden against CSPP\n *\n * @param {Array} array - The array to be cleaned.\n * @returns {Array} The cleaned version of the array\n */\nfunction cleanArray(array) {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n return array;\n}\n\n/**\n * Shallow clone an object\n *\n * @param {Object} object - The object to be cloned.\n * @returns {Object} A new object that copies the original.\n */\nfunction clone(object) {\n const newObject = create(null);\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n if (isPropertyExist) {\n if (Array.isArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (value && typeof value === 'object' && value.constructor === Object) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n return newObject;\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param {Object} object - The object to look up the getter function in its prototype chain.\n * @param {String} prop - The property name for which to find the getter function.\n * @returns {Function} The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n object = getPrototypeOf(object);\n }\n function fallbackValue() {\n return null;\n }\n return fallbackValue;\n}\n\nconst html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);\n\n// SVG\nconst svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);\nconst svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nconst svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);\nconst mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nconst mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\nconst text = freeze(['#text']);\n\nconst html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);\nconst svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);\nconst mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);\nconst xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n// eslint-disable-next-line unicorn/better-regex\nconst MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nconst ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nconst TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\nconst DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\nconst ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nconst IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\n\nconst IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nconst ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\n\nconst DOCTYPE_NAME = seal(/^html$/i);\nconst CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\nvar EXPRESSIONS = /*#__PURE__*/Object.freeze({\n __proto__: null,\n MUSTACHE_EXPR: MUSTACHE_EXPR,\n ERB_EXPR: ERB_EXPR,\n TMPLIT_EXPR: TMPLIT_EXPR,\n DATA_ATTR: DATA_ATTR,\n ARIA_ATTR: ARIA_ATTR,\n IS_ALLOWED_URI: IS_ALLOWED_URI,\n IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE: ATTR_WHITESPACE,\n DOCTYPE_NAME: DOCTYPE_NAME,\n CUSTOM_ELEMENT: CUSTOM_ELEMENT\n});\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nconst NODE_TYPE = {\n element: 1,\n attribute: 2,\n text: 3,\n cdataSection: 4,\n entityReference: 5,\n // Deprecated\n entityNode: 6,\n // Deprecated\n progressingInstruction: 7,\n comment: 8,\n document: 9,\n documentType: 10,\n documentFragment: 11,\n notation: 12 // Deprecated\n};\n\nconst getGlobal = function getGlobal() {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.\n * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {\n if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n }\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn('TrustedTypes policy ' + policyName + ' could not be created.');\n return null;\n }\n};\nfunction createDOMPurify() {\n let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n const DOMPurify = root => createDOMPurify(root);\n\n /**\n * Version label, exposed for easier checks\n * if DOMPurify is up to date or not\n */\n DOMPurify.version = '3.1.5';\n\n /**\n * Array of elements that DOMPurify removed during sanitation.\n * Empty if nothing was removed.\n */\n DOMPurify.removed = [];\n if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n return DOMPurify;\n }\n let {\n document\n } = window;\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes\n } = window;\n const ElementPrototype = Element.prototype;\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n let trustedTypesPolicy;\n let emptyHTML = '';\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName\n } = document;\n const {\n importNode\n } = originalDocument;\n let hooks = {};\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n CUSTOM_ELEMENT\n } = EXPRESSIONS;\n let {\n IS_ALLOWED_URI: IS_ALLOWED_URI$1\n } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);\n\n /*\n * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false\n }\n }));\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Output should be safe even for XML used within HTML and alike.\n * This means, DOMPurify removes comments when containing risky content.\n */\n let SAFE_FOR_XML = true;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);\n\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);\n\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n\n const formElement = document.createElement('form');\n const isRegexOrFunction = function isRegexOrFunction(testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n\n /**\n * _parseConfig\n *\n * @param {Object} cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function _parseConfig() {\n let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;\n\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;\n\n /* Set configuration parameters */\n ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES),\n // eslint-disable-line indent\n cfg.ADD_URI_SAFE_ATTR,\n // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS),\n // eslint-disable-line indent\n cfg.ADD_DATA_URI_TAGS,\n // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {};\n FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {};\n USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, text);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, html$1);\n addToSet(ALLOWED_ATTR, html);\n }\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, svg$1);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, svgFilters);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, mathMl$1);\n addToSet(ALLOWED_ATTR, mathMl);\n addToSet(ALLOWED_ATTR, xml);\n }\n }\n\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n if (cfg.ADD_ATTR) {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');\n }\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');\n }\n\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);\n }\n\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n CONFIG = cfg;\n };\n const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'annotation-xml']);\n\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\n\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);\n const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);\n\n /**\n * @param {Element} element a DOM element whose namespace is being checked\n * @returns {boolean} Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function _checkValidNamespace(element) {\n let parent = getParentNode(element);\n\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template'\n };\n }\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'svg';\n }\n\n // The only way to switch from MathML to SVG is via`\n // svg if parent is either or MathML\n // text integration points.\n if (parent.namespaceURI === MATHML_NAMESPACE) {\n return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);\n }\n\n // We only allow elements that are defined in SVG\n // spec. All others are disallowed in SVG namespace.\n return Boolean(ALL_SVG_TAGS[tagName]);\n }\n if (element.namespaceURI === MATHML_NAMESPACE) {\n // The only way to switch from HTML namespace to MathML\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'math';\n }\n\n // The only way to switch from SVG to MathML is via\n // and HTML integration points\n if (parent.namespaceURI === SVG_NAMESPACE) {\n return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n }\n\n // We only allow elements that are defined in MathML\n // spec. All others are disallowed in MathML namespace.\n return Boolean(ALL_MATHML_TAGS[tagName]);\n }\n if (element.namespaceURI === HTML_NAMESPACE) {\n // The only way to switch from SVG to HTML is via\n // HTML integration points, and from MathML to HTML\n // is via MathML text integration points\n if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {\n return false;\n }\n if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {\n return false;\n }\n\n // We disallow tags that are specific for MathML\n // or SVG and should never appear in HTML namespace\n return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);\n }\n\n // For XHTML and XML documents that support custom namespaces\n if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {\n return true;\n }\n\n // The code should never reach this place (this means\n // that the element somehow got namespace that is not\n // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n // Return false just in case.\n return false;\n };\n\n /**\n * _forceRemove\n *\n * @param {Node} node a DOM node\n */\n const _forceRemove = function _forceRemove(node) {\n arrayPush(DOMPurify.removed, {\n element: node\n });\n try {\n // eslint-disable-next-line unicorn/prefer-dom-node-remove\n node.parentNode.removeChild(node);\n } catch (_) {\n node.remove();\n }\n };\n\n /**\n * _removeAttribute\n *\n * @param {String} name an Attribute name\n * @param {Node} node a DOM node\n */\n const _removeAttribute = function _removeAttribute(name, node) {\n try {\n arrayPush(DOMPurify.removed, {\n attribute: node.getAttributeNode(name),\n from: node\n });\n } catch (_) {\n arrayPush(DOMPurify.removed, {\n attribute: null,\n from: node\n });\n }\n node.removeAttribute(name);\n\n // We void attribute values for unremovable \"is\"\" attributes\n if (name === 'is' && !ALLOWED_ATTR[name]) {\n if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n try {\n _forceRemove(node);\n } catch (_) {}\n } else {\n try {\n node.setAttribute(name, '');\n } catch (_) {}\n }\n }\n };\n\n /**\n * _initDocument\n *\n * @param {String} dirty a string of dirty markup\n * @return {Document} a DOM, filled with the dirty markup\n */\n const _initDocument = function _initDocument(dirty) {\n /* Create a HTML document */\n let doc = null;\n let leadingWhitespace = null;\n if (FORCE_BODY) {\n dirty = '' + dirty;\n } else {\n /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n leadingWhitespace = matches && matches[0];\n }\n if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {\n // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n dirty = '' + dirty + '';\n }\n const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;\n /*\n * Use the DOMParser API by default, fallback later if needs be\n * DOMParser not work for svg when has multiple root element.\n */\n if (NAMESPACE === HTML_NAMESPACE) {\n try {\n doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n } catch (_) {}\n }\n\n /* Use createHTMLDocument in case DOMParser is not available */\n if (!doc || !doc.documentElement) {\n doc = implementation.createDocument(NAMESPACE, 'template', null);\n try {\n doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;\n } catch (_) {\n // Syntax error if dirtyPayload is invalid xml\n }\n }\n const body = doc.body || doc.documentElement;\n if (dirty && leadingWhitespace) {\n body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);\n }\n\n /* Work on whole document or just its body */\n if (NAMESPACE === HTML_NAMESPACE) {\n return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];\n }\n return WHOLE_DOCUMENT ? doc.documentElement : body;\n };\n\n /**\n * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n *\n * @param {Node} root The root element or node to start traversing on.\n * @return {NodeIterator} The created NodeIterator\n */\n const _createNodeIterator = function _createNodeIterator(root) {\n return createNodeIterator.call(root.ownerDocument || root, root,\n // eslint-disable-next-line no-bitwise\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);\n };\n\n /**\n * _isClobbered\n *\n * @param {Node} elm element to check for clobbering attacks\n * @return {Boolean} true if clobbered, false if safe\n */\n const _isClobbered = function _isClobbered(elm) {\n return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');\n };\n\n /**\n * Checks whether the given object is a DOM node.\n *\n * @param {Node} object object to check whether it's a DOM node\n * @return {Boolean} true is object is a DOM node\n */\n const _isNode = function _isNode(object) {\n return typeof Node === 'function' && object instanceof Node;\n };\n\n /**\n * _executeHook\n * Execute user configurable hooks\n *\n * @param {String} entryPoint Name of the hook's entry point\n * @param {Node} currentNode node to work on with the hook\n * @param {Object} data additional hook parameters\n */\n const _executeHook = function _executeHook(entryPoint, currentNode, data) {\n if (!hooks[entryPoint]) {\n return;\n }\n arrayForEach(hooks[entryPoint], hook => {\n hook.call(DOMPurify, currentNode, data, CONFIG);\n });\n };\n\n /**\n * _sanitizeElements\n *\n * @protect nodeName\n * @protect textContent\n * @protect removeChild\n *\n * @param {Node} currentNode to check for permission to exist\n * @return {Boolean} true if node was killed, false if left alive\n */\n const _sanitizeElements = function _sanitizeElements(currentNode) {\n let content = null;\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeElements', currentNode, null);\n\n /* Check if element is clobbered or can clobber */\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Now let's check the element's type and name */\n const tagName = transformCaseFunc(currentNode.nodeName);\n\n /* Execute a hook if present */\n _executeHook('uponSanitizeElement', currentNode, {\n tagName,\n allowedTags: ALLOWED_TAGS\n });\n\n /* Detect mXSS attempts abusing namespace confusion */\n if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\\w]/g, currentNode.innerHTML) && regExpTest(/<[/\\w]/g, currentNode.textContent)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove any ocurrence of processing instructions */\n if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove any kind of possibly harmful comments */\n if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\\w]/g, currentNode.data)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove element if anything forbids its presence */\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {\n return false;\n }\n if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {\n return false;\n }\n }\n\n /* Keep content except for bad-listed elements */\n if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n if (childNodes && parentNode) {\n const childCount = childNodes.length;\n for (let i = childCount - 1; i >= 0; --i) {\n const childClone = cloneNode(childNodes[i], true);\n childClone.__removalCount = (currentNode.__removalCount || 0) + 1;\n parentNode.insertBefore(childClone, getNextSibling(currentNode));\n }\n }\n }\n _forceRemove(currentNode);\n return true;\n }\n\n /* Check whether element has a valid namespace */\n if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Make sure that older browsers don't get fallback-tag mXSS */\n if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Sanitize element content to be template-safe */\n if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {\n /* Get the element's text content */\n content = currentNode.textContent;\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n content = stringReplace(content, expr, ' ');\n });\n if (currentNode.textContent !== content) {\n arrayPush(DOMPurify.removed, {\n element: currentNode.cloneNode()\n });\n currentNode.textContent = content;\n }\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeElements', currentNode, null);\n return false;\n };\n\n /**\n * _isValidAttribute\n *\n * @param {string} lcTag Lowercase tag name of containing element.\n * @param {string} lcName Lowercase attribute name.\n * @param {string} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid, otherwise false.\n */\n // eslint-disable-next-line complexity\n const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {\n /* Make sure attribute cannot clobber */\n if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {\n return false;\n }\n\n /* Allow valid data-* attributes: At least one character after \"-\"\n (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n We don't need to check the value; it's always URI safe. */\n if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n if (\n // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||\n // Alternative, second condition checks if it's an `is`-attribute, AND\n // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {\n return false;\n }\n /* Check value is safe. First, is attr inert? If so, is safe */\n } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {\n return false;\n } else ;\n return true;\n };\n\n /**\n * _isBasicCustomElement\n * checks if at least one dash is included in tagName, and it's not the first char\n * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n *\n * @param {string} tagName name of the tag of the node to sanitize\n * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n */\n const _isBasicCustomElement = function _isBasicCustomElement(tagName) {\n return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);\n };\n\n /**\n * _sanitizeAttributes\n *\n * @protect attributes\n * @protect nodeName\n * @protect removeAttribute\n * @protect setAttribute\n *\n * @param {Node} currentNode to sanitize\n */\n const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {\n /* Execute a hook if present */\n _executeHook('beforeSanitizeAttributes', currentNode, null);\n const {\n attributes\n } = currentNode;\n\n /* Check if we have attributes; if not we might have a text node */\n if (!attributes) {\n return;\n }\n const hookEvent = {\n attrName: '',\n attrValue: '',\n keepAttr: true,\n allowedAttributes: ALLOWED_ATTR\n };\n let l = attributes.length;\n\n /* Go backwards over all attributes; safely remove bad ones */\n while (l--) {\n const attr = attributes[l];\n const {\n name,\n namespaceURI,\n value: attrValue\n } = attr;\n const lcName = transformCaseFunc(name);\n let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n /* Execute a hook if present */\n hookEvent.attrName = lcName;\n hookEvent.attrValue = value;\n hookEvent.keepAttr = true;\n hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n value = hookEvent.attrValue;\n /* Did the hooks approve of the attribute? */\n if (hookEvent.forceKeepAttr) {\n continue;\n }\n\n /* Remove attribute */\n _removeAttribute(name, currentNode);\n\n /* Did the hooks approve of the attribute? */\n if (!hookEvent.keepAttr) {\n continue;\n }\n\n /* Work around a security issue in jQuery 3.0 */\n if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n\n /* Work around a security issue with comments inside attributes */\n if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\\/(style|title)/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n\n /* Sanitize attribute content to be template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n value = stringReplace(value, expr, ' ');\n });\n }\n\n /* Is `value` valid for this attribute? */\n const lcTag = transformCaseFunc(currentNode.nodeName);\n if (!_isValidAttribute(lcTag, lcName, value)) {\n continue;\n }\n\n /* Full DOM Clobbering protection via namespace isolation,\n * Prefix id and name attributes with `user-content-`\n */\n if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n // Remove the attribute with this value\n _removeAttribute(name, currentNode);\n\n // Prefix the value and later re-create the attribute with the sanitized value\n value = SANITIZE_NAMED_PROPS_PREFIX + value;\n }\n\n /* Handle attributes that require Trusted Types */\n if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {\n if (namespaceURI) ; else {\n switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n case 'TrustedHTML':\n {\n value = trustedTypesPolicy.createHTML(value);\n break;\n }\n case 'TrustedScriptURL':\n {\n value = trustedTypesPolicy.createScriptURL(value);\n break;\n }\n }\n }\n }\n\n /* Handle invalid data-* attribute set by try-catching it */\n try {\n if (namespaceURI) {\n currentNode.setAttributeNS(namespaceURI, name, value);\n } else {\n /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n currentNode.setAttribute(name, value);\n }\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n } else {\n arrayPop(DOMPurify.removed);\n }\n } catch (_) {}\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeAttributes', currentNode, null);\n };\n\n /**\n * _sanitizeShadowDOM\n *\n * @param {DocumentFragment} fragment to iterate over recursively\n */\n const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {\n let shadowNode = null;\n const shadowIterator = _createNodeIterator(fragment);\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeShadowDOM', fragment, null);\n while (shadowNode = shadowIterator.nextNode()) {\n /* Execute a hook if present */\n _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n /* Sanitize tags and elements */\n if (_sanitizeElements(shadowNode)) {\n continue;\n }\n\n /* Deep shadow DOM detected */\n if (shadowNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(shadowNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(shadowNode);\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeShadowDOM', fragment, null);\n };\n\n /**\n * Sanitize\n * Public method providing core sanitation functionality\n *\n * @param {String|Node} dirty string or DOM node\n * @param {Object} cfg object\n */\n // eslint-disable-next-line complexity\n DOMPurify.sanitize = function (dirty) {\n let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n let body = null;\n let importedNode = null;\n let currentNode = null;\n let returnNode = null;\n /* Make sure we have a string to sanitize.\n DO NOT return early, as this will return the wrong type if\n the user has requested a DOM object rather than a string */\n IS_EMPTY_INPUT = !dirty;\n if (IS_EMPTY_INPUT) {\n dirty = '';\n }\n\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n if (typeof dirty.toString === 'function') {\n dirty = dirty.toString();\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n } else {\n throw typeErrorCreate('toString is not a function');\n }\n }\n\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n\n /* Clean up removed elements */\n DOMPurify.removed = [];\n\n /* Check if dirty is correctly typed for IN_PLACE */\n if (typeof dirty === 'string') {\n IN_PLACE = false;\n }\n if (IN_PLACE) {\n /* Do some early pre-sanitization to avoid unsafe root nodes */\n if (dirty.nodeName) {\n const tagName = transformCaseFunc(dirty.nodeName);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');\n }\n }\n } else if (dirty instanceof Node) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n } else {\n /* Exit directly if we have nothing to do */\n if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;\n }\n\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n /* Now start iterating over the created document */\n while (currentNode = nodeIterator.nextNode()) {\n /* Sanitize tags and elements */\n if (_sanitizeElements(currentNode)) {\n continue;\n }\n\n /* Shadow DOM detected, sanitize it */\n if (currentNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(currentNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(currentNode);\n }\n\n /* If we sanitized `dirty` in-place, return it. */\n if (IN_PLACE) {\n return dirty;\n }\n\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n return returnNode;\n }\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n /* Serialize doctype if allowed */\n if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {\n serializedHTML = '\\n' + serializedHTML;\n }\n\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n serializedHTML = stringReplace(serializedHTML, expr, ' ');\n });\n }\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;\n };\n\n /**\n * Public method to set the configuration once\n * setConfig\n *\n * @param {Object} cfg configuration object\n */\n DOMPurify.setConfig = function () {\n let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n\n /**\n * Public method to remove the configuration\n * clearConfig\n *\n */\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n };\n\n /**\n * Public method to check if an attribute value is valid.\n * Uses last set config, if any. Otherwise, uses config defaults.\n * isValidAttribute\n *\n * @param {String} tag Tag name of containing element.\n * @param {String} attr Attribute name.\n * @param {String} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n */\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n\n /**\n * AddHook\n * Public method to add DOMPurify hooks\n *\n * @param {String} entryPoint entry point for the hook to add\n * @param {Function} hookFunction function to execute\n */\n DOMPurify.addHook = function (entryPoint, hookFunction) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n hooks[entryPoint] = hooks[entryPoint] || [];\n arrayPush(hooks[entryPoint], hookFunction);\n };\n\n /**\n * RemoveHook\n * Public method to remove a DOMPurify hook at a given entryPoint\n * (pops it from the stack of hooks if more are present)\n *\n * @param {String} entryPoint entry point for the hook to remove\n * @return {Function} removed(popped) hook\n */\n DOMPurify.removeHook = function (entryPoint) {\n if (hooks[entryPoint]) {\n return arrayPop(hooks[entryPoint]);\n }\n };\n\n /**\n * RemoveHooks\n * Public method to remove all DOMPurify hooks at a given entryPoint\n *\n * @param {String} entryPoint entry point for the hooks to remove\n */\n DOMPurify.removeHooks = function (entryPoint) {\n if (hooks[entryPoint]) {\n hooks[entryPoint] = [];\n }\n };\n\n /**\n * RemoveAllHooks\n * Public method to remove all DOMPurify hooks\n */\n DOMPurify.removeAllHooks = function () {\n hooks = {};\n };\n return DOMPurify;\n}\nvar purify = createDOMPurify();\n\nexport { purify as default };\n//# sourceMappingURL=purify.es.mjs.map\n"],"names":["entries","setPrototypeOf","isFrozen","getPrototypeOf","getOwnPropertyDescriptor","Object","freeze","seal","create","apply","construct","Reflect","x","fun","thisValue","args","Func","arrayForEach","unapply","Array","prototype","forEach","arrayPop","pop","arrayPush","push","stringToLowerCase","String","toLowerCase","stringToString","toString","stringMatch","match","stringReplace","replace","stringIndexOf","indexOf","stringTrim","trim","objectHasOwnProperty","hasOwnProperty","regExpTest","RegExp","test","typeErrorCreate","func","TypeError","_len2","arguments","length","_key2","thisArg","_len","_key","addToSet","set","array","transformCaseFunc","undefined","l","element","lcElement","cleanArray","index","clone","object","newObject","property","value","isArray","constructor","lookupGetter","prop","desc","get","html$1","svg$1","svgFilters","svgDisallowed","mathMl$1","mathMlDisallowed","text","html","svg","mathMl","xml","MUSTACHE_EXPR","ERB_EXPR","TMPLIT_EXPR","DATA_ATTR","ARIA_ATTR","IS_ALLOWED_URI","IS_SCRIPT_OR_DATA","ATTR_WHITESPACE","DOCTYPE_NAME","CUSTOM_ELEMENT","EXPRESSIONS","__proto__","getGlobal","window","purify","createDOMPurify","DOMPurify","root","version","removed","document","nodeType","isSupported","originalDocument","currentScript","DocumentFragment","HTMLTemplateElement","Node","Element","NodeFilter","NamedNodeMap","MozNamedAttrMap","HTMLFormElement","DOMParser","trustedTypes","ElementPrototype","cloneNode","getNextSibling","getChildNodes","getParentNode","template","createElement","content","ownerDocument","trustedTypesPolicy","emptyHTML","implementation","createNodeIterator","createDocumentFragment","getElementsByTagName","importNode","hooks","createHTMLDocument","IS_ALLOWED_URI$1","ALLOWED_TAGS","DEFAULT_ALLOWED_TAGS","ALLOWED_ATTR","DEFAULT_ALLOWED_ATTR","CUSTOM_ELEMENT_HANDLING","tagNameCheck","writable","configurable","enumerable","attributeNameCheck","allowCustomizedBuiltInElements","FORBID_TAGS","FORBID_ATTR","ALLOW_ARIA_ATTR","ALLOW_DATA_ATTR","ALLOW_UNKNOWN_PROTOCOLS","ALLOW_SELF_CLOSE_IN_ATTR","SAFE_FOR_TEMPLATES","SAFE_FOR_XML","WHOLE_DOCUMENT","SET_CONFIG","FORCE_BODY","RETURN_DOM","RETURN_DOM_FRAGMENT","RETURN_TRUSTED_TYPE","SANITIZE_DOM","SANITIZE_NAMED_PROPS","KEEP_CONTENT","IN_PLACE","USE_PROFILES","FORBID_CONTENTS","DEFAULT_FORBID_CONTENTS","DATA_URI_TAGS","DEFAULT_DATA_URI_TAGS","URI_SAFE_ATTRIBUTES","DEFAULT_URI_SAFE_ATTRIBUTES","MATHML_NAMESPACE","SVG_NAMESPACE","HTML_NAMESPACE","NAMESPACE","IS_EMPTY_INPUT","ALLOWED_NAMESPACES","DEFAULT_ALLOWED_NAMESPACES","PARSER_MEDIA_TYPE","SUPPORTED_PARSER_MEDIA_TYPES","CONFIG","formElement","isRegexOrFunction","testValue","Function","_parseConfig","cfg","ADD_URI_SAFE_ATTR","ADD_DATA_URI_TAGS","ALLOWED_URI_REGEXP","ADD_TAGS","ADD_ATTR","table","tbody","TRUSTED_TYPES_POLICY","createHTML","createScriptURL","purifyHostElement","createPolicy","suffix","ATTR_NAME","hasAttribute","getAttribute","policyName","scriptUrl","_","console","warn","_createTrustedTypesPolicy","MATHML_TEXT_INTEGRATION_POINTS","HTML_INTEGRATION_POINTS","COMMON_SVG_AND_HTML_ELEMENTS","ALL_SVG_TAGS","ALL_MATHML_TAGS","_forceRemove","node","parentNode","removeChild","remove","_removeAttribute","name","attribute","getAttributeNode","from","removeAttribute","setAttribute","_initDocument","dirty","doc","leadingWhitespace","matches","dirtyPayload","parseFromString","documentElement","createDocument","innerHTML","body","insertBefore","createTextNode","childNodes","call","_createNodeIterator","SHOW_ELEMENT","SHOW_COMMENT","SHOW_TEXT","SHOW_PROCESSING_INSTRUCTION","SHOW_CDATA_SECTION","_isClobbered","elm","nodeName","textContent","attributes","namespaceURI","hasChildNodes","_isNode","_executeHook","entryPoint","currentNode","data","hook","_sanitizeElements","tagName","allowedTags","firstElementChild","_isBasicCustomElement","i","childClone","__removalCount","parent","parentTagName","Boolean","_checkValidNamespace","expr","_isValidAttribute","lcTag","lcName","_sanitizeAttributes","hookEvent","attrName","attrValue","keepAttr","allowedAttributes","attr","forceKeepAttr","getAttributeType","setAttributeNS","_sanitizeShadowDOM","fragment","shadowNode","shadowIterator","nextNode","sanitize","importedNode","returnNode","appendChild","firstChild","nodeIterator","shadowroot","shadowrootmode","serializedHTML","outerHTML","doctype","setConfig","clearConfig","isValidAttribute","tag","addHook","hookFunction","removeHook","removeHooks","removeAllHooks"],"mappings":";AAEA,MAAMA,QACJA,EAAOC,eACPA,EAAcC,SACdA,EAAQC,eACRA,EAAcC,yBACdA,GACEC,OACJ,IAAIC,OACFA,EAAMC,KACNA,EAAIC,OACJA,GACEH,QACAI,MACFA,EAAKC,UACLA,GACqB,oBAAZC,SAA2BA,QACjCL,IACHA,EAAS,SAAgBM,GACvB,OAAOA,CACX,GAEKL,IACHA,EAAO,SAAcK,GACnB,OAAOA,CACX,GAEKH,IACHA,EAAQ,SAAeI,EAAKC,EAAWC,GACrC,OAAOF,EAAIJ,MAAMK,EAAWC,EAChC,GAEKL,IACHA,EAAY,SAAmBM,EAAMD,GACnC,OAAO,IAAIC,KAAQD,EACvB,GAEA,MAAME,EAAeC,QAAQC,MAAMC,UAAUC,SACvCC,EAAWJ,QAAQC,MAAMC,UAAUG,KACnCC,EAAYN,QAAQC,MAAMC,UAAUK,MACpCC,EAAoBR,QAAQS,OAAOP,UAAUQ,aAC7CC,EAAiBX,QAAQS,OAAOP,UAAUU,UAC1CC,EAAcb,QAAQS,OAAOP,UAAUY,OACvCC,EAAgBf,QAAQS,OAAOP,UAAUc,SACzCC,EAAgBjB,QAAQS,OAAOP,UAAUgB,SACzCC,EAAanB,QAAQS,OAAOP,UAAUkB,MACtCC,EAAuBrB,QAAQb,OAAOe,UAAUoB,gBAChDC,EAAavB,QAAQwB,OAAOtB,UAAUuB,MACtCC,GAuBeC,EAvBeC,UAwB3B,WACL,IAAK,IAAIC,EAAQC,UAAUC,OAAQlC,EAAO,IAAII,MAAM4B,GAAQG,EAAQ,EAAGA,EAAQH,EAAOG,IACpFnC,EAAKmC,GAASF,UAAUE,GAE1B,OAAOxC,EAAUmC,EAAM9B,EAC3B,GANA,IAAqB8B,EAfrB,SAAS3B,QAAQ2B,GACf,OAAO,SAAUM,GACf,IAAK,IAAIC,EAAOJ,UAAUC,OAAQlC,EAAO,IAAII,MAAMiC,EAAO,EAAIA,EAAO,EAAI,GAAIC,EAAO,EAAGA,EAAOD,EAAMC,IAClGtC,EAAKsC,EAAO,GAAKL,UAAUK,GAE7B,OAAO5C,EAAMoC,EAAMM,EAASpC,EAChC,CACA,CAyBA,SAASuC,SAASC,EAAKC,GACrB,IAAIC,EAAoBT,UAAUC,OAAS,QAAsBS,IAAjBV,UAAU,GAAmBA,UAAU,GAAKtB,EACxFzB,GAIFA,EAAesD,EAAK,MAEtB,IAAII,EAAIH,EAAMP,OACd,KAAOU,KAAK,CACV,IAAIC,EAAUJ,EAAMG,GACpB,GAAuB,iBAAZC,EAAsB,CAC/B,MAAMC,EAAYJ,EAAkBG,GAChCC,IAAcD,IAEX1D,EAASsD,KACZA,EAAMG,GAAKE,GAEbD,EAAUC,EAEb,CACDN,EAAIK,IAAW,CAChB,CACD,OAAOL,CACT,CAQA,SAASO,WAAWN,GAClB,IAAK,IAAIO,EAAQ,EAAGA,EAAQP,EAAMP,OAAQc,IAChBxB,EAAqBiB,EAAOO,KAElDP,EAAMO,GAAS,MAGnB,OAAOP,CACT,CAQA,SAASQ,MAAMC,GACb,MAAMC,EAAY1D,EAAO,MACzB,IAAK,MAAO2D,EAAUC,KAAUpE,EAAQiE,GACd1B,EAAqB0B,EAAQE,KAE/ChD,MAAMkD,QAAQD,GAChBF,EAAUC,GAAYL,WAAWM,GACxBA,GAA0B,iBAAVA,GAAsBA,EAAME,cAAgBjE,OACrE6D,EAAUC,GAAYH,MAAMI,GAE5BF,EAAUC,GAAYC,GAI5B,OAAOF,CACT,CASA,SAASK,aAAaN,EAAQO,GAC5B,KAAkB,OAAXP,GAAiB,CACtB,MAAMQ,EAAOrE,EAAyB6D,EAAQO,GAC9C,GAAIC,EAAM,CACR,GAAIA,EAAKC,IACP,OAAOxD,QAAQuD,EAAKC,KAEtB,GAA0B,mBAAfD,EAAKL,MACd,OAAOlD,QAAQuD,EAAKL,MAEvB,CACDH,EAAS9D,EAAe8D,EACzB,CAID,OAHA,WACE,OAAO,IACR,CAEH,CAEA,MAAMU,EAASrE,EAAO,CAAC,IAAK,OAAQ,UAAW,UAAW,OAAQ,UAAW,QAAS,QAAS,IAAK,MAAO,MAAO,MAAO,QAAS,aAAc,OAAQ,KAAM,SAAU,SAAU,UAAW,SAAU,OAAQ,OAAQ,MAAO,WAAY,UAAW,OAAQ,WAAY,KAAM,YAAa,MAAO,UAAW,MAAO,SAAU,MAAO,MAAO,KAAM,KAAM,UAAW,KAAM,WAAY,aAAc,SAAU,OAAQ,SAAU,OAAQ,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,OAAQ,SAAU,SAAU,KAAM,OAAQ,IAAK,MAAO,QAAS,MAAO,MAAO,QAAS,SAAU,KAAM,OAAQ,MAAO,OAAQ,UAAW,OAAQ,WAAY,QAAS,MAAO,OAAQ,KAAM,WAAY,SAAU,SAAU,IAAK,UAAW,MAAO,WAAY,IAAK,KAAM,KAAM,OAAQ,IAAK,OAAQ,UAAW,SAAU,SAAU,QAAS,SAAU,SAAU,OAAQ,SAAU,SAAU,QAAS,MAAO,UAAW,MAAO,QAAS,QAAS,KAAM,WAAY,WAAY,QAAS,KAAM,QAAS,OAAQ,KAAM,QAAS,KAAM,IAAK,KAAM,MAAO,QAAS,QAGn+BsE,EAAQtE,EAAO,CAAC,MAAO,IAAK,WAAY,cAAe,eAAgB,eAAgB,gBAAiB,mBAAoB,SAAU,WAAY,OAAQ,OAAQ,UAAW,SAAU,OAAQ,IAAK,QAAS,WAAY,QAAS,QAAS,OAAQ,iBAAkB,SAAU,OAAQ,WAAY,QAAS,OAAQ,UAAW,UAAW,WAAY,iBAAkB,OAAQ,OAAQ,QAAS,SAAU,SAAU,OAAQ,WAAY,QAAS,OAAQ,QAAS,OAAQ,UAC3cuE,EAAavE,EAAO,CAAC,UAAW,gBAAiB,sBAAuB,cAAe,mBAAoB,oBAAqB,oBAAqB,iBAAkB,eAAgB,UAAW,UAAW,UAAW,UAAW,UAAW,iBAAkB,UAAW,UAAW,cAAe,eAAgB,WAAY,eAAgB,qBAAsB,cAAe,SAAU,iBAMhYwE,EAAgBxE,EAAO,CAAC,UAAW,gBAAiB,SAAU,UAAW,YAAa,mBAAoB,iBAAkB,gBAAiB,gBAAiB,gBAAiB,QAAS,YAAa,OAAQ,eAAgB,YAAa,UAAW,gBAAiB,SAAU,MAAO,aAAc,UAAW,QAChTyE,EAAWzE,EAAO,CAAC,OAAQ,WAAY,SAAU,UAAW,QAAS,SAAU,KAAM,aAAc,gBAAiB,KAAM,KAAM,QAAS,UAAW,WAAY,QAAS,OAAQ,KAAM,SAAU,QAAS,SAAU,OAAQ,OAAQ,UAAW,SAAU,MAAO,QAAS,MAAO,SAAU,aAAc,gBAIxS0E,EAAmB1E,EAAO,CAAC,UAAW,cAAe,aAAc,WAAY,YAAa,UAAW,UAAW,SAAU,SAAU,QAAS,YAAa,aAAc,iBAAkB,cAAe,SAC3M2E,EAAO3E,EAAO,CAAC,UAEf4E,EAAO5E,EAAO,CAAC,SAAU,SAAU,QAAS,MAAO,iBAAkB,eAAgB,uBAAwB,WAAY,aAAc,UAAW,SAAU,UAAW,cAAe,cAAe,UAAW,OAAQ,QAAS,QAAS,QAAS,OAAQ,UAAW,WAAY,eAAgB,SAAU,cAAe,WAAY,WAAY,UAAW,MAAO,WAAY,0BAA2B,wBAAyB,WAAY,YAAa,UAAW,eAAgB,OAAQ,MAAO,UAAW,SAAU,SAAU,OAAQ,OAAQ,WAAY,KAAM,YAAa,YAAa,QAAS,OAAQ,QAAS,OAAQ,OAAQ,UAAW,OAAQ,MAAO,MAAO,YAAa,QAAS,SAAU,MAAO,YAAa,WAAY,QAAS,OAAQ,QAAS,UAAW,aAAc,SAAU,OAAQ,UAAW,UAAW,cAAe,cAAe,UAAW,gBAAiB,sBAAuB,SAAU,UAAW,UAAW,aAAc,WAAY,MAAO,WAAY,MAAO,WAAY,OAAQ,OAAQ,UAAW,aAAc,QAAS,WAAY,QAAS,OAAQ,QAAS,OAAQ,UAAW,QAAS,MAAO,SAAU,OAAQ,QAAS,UAAW,WAAY,QAAS,YAAa,OAAQ,SAAU,SAAU,QAAS,QAAS,OAAQ,QAAS,SAC5tC6E,EAAM7E,EAAO,CAAC,gBAAiB,aAAc,WAAY,qBAAsB,SAAU,gBAAiB,gBAAiB,UAAW,gBAAiB,iBAAkB,QAAS,OAAQ,KAAM,QAAS,OAAQ,gBAAiB,YAAa,YAAa,QAAS,sBAAuB,8BAA+B,gBAAiB,kBAAmB,KAAM,KAAM,IAAK,KAAM,KAAM,kBAAmB,YAAa,UAAW,UAAW,MAAO,WAAY,YAAa,MAAO,OAAQ,eAAgB,YAAa,SAAU,cAAe,cAAe,gBAAiB,cAAe,YAAa,mBAAoB,eAAgB,aAAc,eAAgB,cAAe,KAAM,KAAM,KAAM,KAAM,aAAc,WAAY,gBAAiB,oBAAqB,SAAU,OAAQ,KAAM,kBAAmB,KAAM,MAAO,IAAK,KAAM,KAAM,KAAM,KAAM,UAAW,YAAa,aAAc,WAAY,OAAQ,eAAgB,iBAAkB,eAAgB,mBAAoB,iBAAkB,QAAS,aAAc,aAAc,eAAgB,eAAgB,cAAe,cAAe,mBAAoB,YAAa,MAAO,OAAQ,QAAS,SAAU,OAAQ,MAAO,OAAQ,aAAc,SAAU,WAAY,UAAW,QAAS,SAAU,cAAe,SAAU,WAAY,cAAe,OAAQ,aAAc,sBAAuB,mBAAoB,eAAgB,SAAU,gBAAiB,sBAAuB,iBAAkB,IAAK,KAAM,KAAM,SAAU,OAAQ,OAAQ,cAAe,YAAa,UAAW,SAAU,SAAU,QAAS,OAAQ,kBAAmB,mBAAoB,mBAAoB,eAAgB,cAAe,eAAgB,cAAe,aAAc,eAAgB,mBAAoB,oBAAqB,iBAAkB,kBAAmB,oBAAqB,iBAAkB,SAAU,eAAgB,QAAS,eAAgB,iBAAkB,WAAY,UAAW,UAAW,YAAa,mBAAoB,cAAe,kBAAmB,iBAAkB,aAAc,OAAQ,KAAM,KAAM,UAAW,SAAU,UAAW,aAAc,UAAW,aAAc,gBAAiB,gBAAiB,QAAS,eAAgB,OAAQ,eAAgB,mBAAoB,mBAAoB,IAAK,KAAM,KAAM,QAAS,IAAK,KAAM,KAAM,IAAK,eAC9vE8E,EAAS9E,EAAO,CAAC,SAAU,cAAe,QAAS,WAAY,QAAS,eAAgB,cAAe,aAAc,aAAc,QAAS,MAAO,UAAW,eAAgB,WAAY,QAAS,QAAS,SAAU,OAAQ,KAAM,UAAW,SAAU,gBAAiB,SAAU,SAAU,iBAAkB,YAAa,WAAY,cAAe,UAAW,UAAW,gBAAiB,WAAY,WAAY,OAAQ,WAAY,WAAY,aAAc,UAAW,SAAU,SAAU,cAAe,gBAAiB,uBAAwB,YAAa,YAAa,aAAc,WAAY,iBAAkB,iBAAkB,YAAa,UAAW,QAAS,UACrpB+E,EAAM/E,EAAO,CAAC,aAAc,SAAU,cAAe,YAAa,gBAGlEgF,EAAgB/E,EAAK,6BACrBgF,EAAWhF,EAAK,yBAChBiF,EAAcjF,EAAK,iBACnBkF,EAAYlF,EAAK,8BACjBmF,EAAYnF,EAAK,kBACjBoF,EAAiBpF,EAAK,6FAGtBqF,EAAoBrF,EAAK,yBACzBsF,EAAkBtF,EAAK,+DAGvBuF,EAAevF,EAAK,WACpBwF,EAAiBxF,EAAK,4BAE5B,IAAIyF,EAA2B3F,OAAOC,OAAO,CAC3C2F,UAAW,KACXX,cAAeA,EACfC,SAAUA,EACVC,YAAaA,EACbC,UAAWA,EACXC,UAAWA,EACXC,eAAgBA,EAChBC,kBAAmBA,EACnBC,gBAAiBA,EACjBC,aAAcA,EACdC,eAAgBA,IAIlB,MAiBMG,UAAY,WAChB,MAAyB,oBAAXC,OAAyB,KAAOA,MAChD,EAkxCG,IAACC,EAzuCJ,SAASC,kBACP,IAAIF,EAASnD,UAAUC,OAAS,QAAsBS,IAAjBV,UAAU,GAAmBA,UAAU,GAAKkD,YACjF,MAAMI,UAAYC,GAAQF,gBAAgBE,GAa1C,GAPAD,UAAUE,QAAU,QAMpBF,UAAUG,QAAU,IACfN,IAAWA,EAAOO,UAhEb,IAgEyBP,EAAOO,SAASC,SAIjD,OADAL,UAAUM,aAAc,EACjBN,UAET,IAAII,SACFA,GACEP,EACJ,MAAMU,EAAmBH,EACnBI,EAAgBD,EAAiBC,eACjCC,iBACJA,EAAgBC,oBAChBA,EAAmBC,KACnBA,EAAIC,QACJA,EAAOC,WACPA,EAAUC,aACVA,EAAejB,EAAOiB,cAAgBjB,EAAOkB,gBAAeC,gBAC5DA,EAAeC,UACfA,EAASC,aACTA,GACErB,EACEsB,EAAmBP,EAAQ9F,UAC3BsG,EAAYnD,aAAakD,EAAkB,aAC3CE,EAAiBpD,aAAakD,EAAkB,eAChDG,EAAgBrD,aAAakD,EAAkB,cAC/CI,EAAgBtD,aAAakD,EAAkB,cAQrD,GAAmC,mBAAxBT,EAAoC,CAC7C,MAAMc,EAAWpB,EAASqB,cAAc,YACpCD,EAASE,SAAWF,EAASE,QAAQC,gBACvCvB,EAAWoB,EAASE,QAAQC,cAE/B,CACD,IAAIC,EACAC,EAAY,GAChB,MAAMC,eACJA,EAAcC,mBACdA,EAAkBC,uBAClBA,EAAsBC,qBACtBA,GACE7B,GACE8B,WACJA,GACE3B,EACJ,IAAI4B,GAAQ,CAAA,EAKZnC,UAAUM,YAAiC,mBAAZ5G,GAAmD,mBAAlB6H,GAAgCO,QAAwD1E,IAAtC0E,EAAeM,mBACjI,MAAMpD,cACJA,GAAaC,SACbA,GAAQC,YACRA,GAAWC,UACXA,GAASC,UACTA,GAASE,kBACTA,GAAiBC,gBACjBA,GAAeE,eACfA,IACEC,EACJ,IACEL,eAAgBgD,IACd3C,EAQA4C,GAAe,KACnB,MAAMC,GAAuBvF,SAAS,GAAI,IAAIqB,KAAWC,KAAUC,KAAeE,KAAaE,IAG/F,IAAI6D,GAAe,KACnB,MAAMC,GAAuBzF,SAAS,CAAE,EAAE,IAAI4B,KAASC,KAAQC,KAAWC,IAQ1E,IAAI2D,GAA0B3I,OAAOE,KAAKC,EAAO,KAAM,CACrDyI,aAAc,CACZC,UAAU,EACVC,cAAc,EACdC,YAAY,EACZhF,MAAO,MAETiF,mBAAoB,CAClBH,UAAU,EACVC,cAAc,EACdC,YAAY,EACZhF,MAAO,MAETkF,+BAAgC,CAC9BJ,UAAU,EACVC,cAAc,EACdC,YAAY,EACZhF,OAAO,MAKPmF,GAAc,KAGdC,GAAc,KAGdC,IAAkB,EAGlBC,IAAkB,EAGlBC,IAA0B,EAI1BC,IAA2B,EAK3BC,IAAqB,EAKrBC,IAAe,EAGfC,IAAiB,EAGjBC,IAAa,EAIbC,IAAa,EAMbC,IAAa,EAIbC,IAAsB,EAItBC,IAAsB,EAKtBC,IAAe,EAefC,IAAuB,EAIvBC,IAAe,EAIfC,IAAW,EAGXC,GAAe,CAAA,EAGfC,GAAkB,KACtB,MAAMC,GAA0BrH,SAAS,CAAE,EAAE,CAAC,iBAAkB,QAAS,WAAY,OAAQ,gBAAiB,OAAQ,SAAU,OAAQ,KAAM,KAAM,KAAM,KAAM,QAAS,UAAW,WAAY,WAAY,YAAa,SAAU,QAAS,MAAO,WAAY,QAAS,QAAS,QAAS,QAG1R,IAAIsH,GAAgB,KACpB,MAAMC,GAAwBvH,SAAS,CAAE,EAAE,CAAC,QAAS,QAAS,MAAO,SAAU,QAAS,UAGxF,IAAIwH,GAAsB,KAC1B,MAAMC,GAA8BzH,SAAS,GAAI,CAAC,MAAO,QAAS,MAAO,KAAM,QAAS,OAAQ,UAAW,cAAe,OAAQ,UAAW,QAAS,QAAS,QAAS,UAClK0H,GAAmB,qCACnBC,GAAgB,6BAChBC,GAAiB,+BAEvB,IAAIC,GAAYD,GACZE,IAAiB,EAGjBC,GAAqB,KACzB,MAAMC,GAA6BhI,SAAS,GAAI,CAAC0H,GAAkBC,GAAeC,IAAiBrJ,GAGnG,IAAI0J,GAAoB,KACxB,MAAMC,GAA+B,CAAC,wBAAyB,aAE/D,IAAI/H,GAAoB,KAGpBgI,GAAS,KAKb,MAAMC,GAAchF,EAASqB,cAAc,QACrC4D,kBAAoB,SAA2BC,GACnD,OAAOA,aAAqBlJ,QAAUkJ,aAAqBC,QAC/D,EAQQC,aAAe,WACnB,IAAIC,EAAM/I,UAAUC,OAAS,QAAsBS,IAAjBV,UAAU,GAAmBA,UAAU,GAAK,CAAA,EAC9E,IAAIyI,IAAUA,KAAWM,EAAzB,CAyIA,GApIKA,GAAsB,iBAARA,IACjBA,EAAM,CAAA,GAIRA,EAAM/H,MAAM+H,GACZR,IAEiE,IAAjEC,GAA6BpJ,QAAQ2J,EAAIR,mBAnCT,YAmCiEQ,EAAIR,kBAGrG9H,GAA0C,0BAAtB8H,GAAgD1J,EAAiBH,EAGrFkH,GAAerG,EAAqBwJ,EAAK,gBAAkBzI,SAAS,CAAE,EAAEyI,EAAInD,aAAcnF,IAAqBoF,GAC/GC,GAAevG,EAAqBwJ,EAAK,gBAAkBzI,SAAS,CAAE,EAAEyI,EAAIjD,aAAcrF,IAAqBsF,GAC/GsC,GAAqB9I,EAAqBwJ,EAAK,sBAAwBzI,SAAS,CAAE,EAAEyI,EAAIV,mBAAoBxJ,GAAkByJ,GAC9HR,GAAsBvI,EAAqBwJ,EAAK,qBAAuBzI,SAASU,MAAM+G,IAEtFgB,EAAIC,kBAEJvI,IAEEsH,GACFH,GAAgBrI,EAAqBwJ,EAAK,qBAAuBzI,SAASU,MAAM6G,IAEhFkB,EAAIE,kBAEJxI,IAEEoH,GACFH,GAAkBnI,EAAqBwJ,EAAK,mBAAqBzI,SAAS,CAAE,EAAEyI,EAAIrB,gBAAiBjH,IAAqBkH,GACxHpB,GAAchH,EAAqBwJ,EAAK,eAAiBzI,SAAS,CAAE,EAAEyI,EAAIxC,YAAa9F,IAAqB,CAAA,EAC5G+F,GAAcjH,EAAqBwJ,EAAK,eAAiBzI,SAAS,CAAE,EAAEyI,EAAIvC,YAAa/F,IAAqB,CAAA,EAC5GgH,KAAelI,EAAqBwJ,EAAK,iBAAkBA,EAAItB,aAC/DhB,IAA0C,IAAxBsC,EAAItC,gBACtBC,IAA0C,IAAxBqC,EAAIrC,gBACtBC,GAA0BoC,EAAIpC,0BAA2B,EACzDC,IAA4D,IAAjCmC,EAAInC,yBAC/BC,GAAqBkC,EAAIlC,qBAAsB,EAC/CC,IAAoC,IAArBiC,EAAIjC,aACnBC,GAAiBgC,EAAIhC,iBAAkB,EACvCG,GAAa6B,EAAI7B,aAAc,EAC/BC,GAAsB4B,EAAI5B,sBAAuB,EACjDC,GAAsB2B,EAAI3B,sBAAuB,EACjDH,GAAa8B,EAAI9B,aAAc,EAC/BI,IAAoC,IAArB0B,EAAI1B,aACnBC,GAAuByB,EAAIzB,uBAAwB,EACnDC,IAAoC,IAArBwB,EAAIxB,aACnBC,GAAWuB,EAAIvB,WAAY,EAC3B7B,GAAmBoD,EAAIG,oBAAsBvG,EAC7CwF,GAAYY,EAAIZ,WAAaD,GAC7BlC,GAA0B+C,EAAI/C,yBAA2B,GACrD+C,EAAI/C,yBAA2B2C,kBAAkBI,EAAI/C,wBAAwBC,gBAC/ED,GAAwBC,aAAe8C,EAAI/C,wBAAwBC,cAEjE8C,EAAI/C,yBAA2B2C,kBAAkBI,EAAI/C,wBAAwBK,sBAC/EL,GAAwBK,mBAAqB0C,EAAI/C,wBAAwBK,oBAEvE0C,EAAI/C,yBAAiG,kBAA/D+C,EAAI/C,wBAAwBM,iCACpEN,GAAwBM,+BAAiCyC,EAAI/C,wBAAwBM,gCAEnFO,KACFH,IAAkB,GAEhBS,KACFD,IAAa,GAIXO,KACF7B,GAAetF,SAAS,GAAI2B,GAC5B6D,GAAe,IACW,IAAtB2B,GAAavF,OACf5B,SAASsF,GAAcjE,GACvBrB,SAASwF,GAAc5D,KAEA,IAArBuF,GAAatF,MACf7B,SAASsF,GAAchE,GACvBtB,SAASwF,GAAc3D,GACvB7B,SAASwF,GAAczD,KAEO,IAA5BoF,GAAa5F,aACfvB,SAASsF,GAAc/D,GACvBvB,SAASwF,GAAc3D,GACvB7B,SAASwF,GAAczD,KAEG,IAAxBoF,GAAarF,SACf9B,SAASsF,GAAc7D,GACvBzB,SAASwF,GAAc1D,GACvB9B,SAASwF,GAAczD,KAKvB0G,EAAII,WACFvD,KAAiBC,KACnBD,GAAe5E,MAAM4E,KAEvBtF,SAASsF,GAAcmD,EAAII,SAAU1I,KAEnCsI,EAAIK,WACFtD,KAAiBC,KACnBD,GAAe9E,MAAM8E,KAEvBxF,SAASwF,GAAciD,EAAIK,SAAU3I,KAEnCsI,EAAIC,mBACN1I,SAASwH,GAAqBiB,EAAIC,kBAAmBvI,IAEnDsI,EAAIrB,kBACFA,KAAoBC,KACtBD,GAAkB1G,MAAM0G,KAE1BpH,SAASoH,GAAiBqB,EAAIrB,gBAAiBjH,KAI7C8G,KACF3B,GAAa,UAAW,GAItBmB,IACFzG,SAASsF,GAAc,CAAC,OAAQ,OAAQ,SAItCA,GAAayD,QACf/I,SAASsF,GAAc,CAAC,iBACjBW,GAAY+C,OAEjBP,EAAIQ,qBAAsB,CAC5B,GAAmD,mBAAxCR,EAAIQ,qBAAqBC,WAClC,MAAM5J,EAAgB,+EAExB,GAAwD,mBAA7CmJ,EAAIQ,qBAAqBE,gBAClC,MAAM7J,EAAgB,oFAIxBsF,EAAqB6D,EAAIQ,qBAGzBpE,EAAYD,EAAmBsE,WAAW,GAChD,WAEiC9I,IAAvBwE,IACFA,EAzb0B,SAAmCV,EAAckF,GACjF,GAA4B,iBAAjBlF,GAAkE,mBAA9BA,EAAamF,aAC1D,OAAO,KAMT,IAAIC,EAAS,KACb,MAAMC,EAAY,wBACdH,GAAqBA,EAAkBI,aAAaD,KACtDD,EAASF,EAAkBK,aAAaF,IAE1C,MAAMG,EAAa,aAAeJ,EAAS,IAAMA,EAAS,IAC1D,IACE,OAAOpF,EAAamF,aAAaK,EAAY,CAC3CR,WAAWtH,GACFA,EAETuH,gBAAgBQ,GACPA,GAGZ,CAAC,MAAOC,GAKP,OADAC,QAAQC,KAAK,uBAAyBJ,EAAa,0BAC5C,IACR,CACH,CA2Z6BK,CAA0B7F,EAAcV,IAIpC,OAAvBoB,GAAoD,iBAAdC,IACxCA,EAAYD,EAAmBsE,WAAW,KAM1ClM,GACFA,EAAOyL,GAETN,GAASM,CArKR,CAsKL,EACQuB,GAAiChK,SAAS,CAAA,EAAI,CAAC,KAAM,KAAM,KAAM,KAAM,UACvEiK,GAA0BjK,SAAS,CAAA,EAAI,CAAC,gBAAiB,mBAMzDkK,GAA+BlK,SAAS,CAAA,EAAI,CAAC,QAAS,QAAS,OAAQ,IAAK,WAK5EmK,GAAenK,SAAS,CAAA,EAAI,IAAIsB,KAAUC,KAAeC,IACzD4I,GAAkBpK,SAAS,CAAE,EAAE,IAAIyB,KAAaC,IA8FhD2I,aAAe,SAAsBC,GACzCpM,EAAU8E,UAAUG,QAAS,CAC3B7C,QAASgK,IAEX,IAEEA,EAAKC,WAAWC,YAAYF,EAC7B,CAAC,MAAOV,GACPU,EAAKG,QACN,CACL,EAQQC,iBAAmB,SAA0BC,EAAML,GACvD,IACEpM,EAAU8E,UAAUG,QAAS,CAC3ByH,UAAWN,EAAKO,iBAAiBF,GACjCG,KAAMR,GAET,CAAC,MAAOV,GACP1L,EAAU8E,UAAUG,QAAS,CAC3ByH,UAAW,KACXE,KAAMR,GAET,CAID,GAHAA,EAAKS,gBAAgBJ,GAGR,OAATA,IAAkBnF,GAAamF,GACjC,GAAI/D,IAAcC,GAChB,IACEwD,aAAaC,EACvB,CAAU,MAAOV,GAAK,MAEd,IACEU,EAAKU,aAAaL,EAAM,GAClC,CAAU,MAAOf,GAAK,CAGtB,EAQQqB,cAAgB,SAAuBC,GAE3C,IAAIC,EAAM,KACNC,EAAoB,KACxB,GAAIzE,GACFuE,EAAQ,oBAAsBA,MACzB,CAEL,MAAMG,EAAU5M,EAAYyM,EAAO,eACnCE,EAAoBC,GAAWA,EAAQ,EACxC,CACyB,0BAAtBpD,IAAiDJ,KAAcD,KAEjEsD,EAAQ,iEAAmEA,EAAQ,kBAErF,MAAMI,EAAe1G,EAAqBA,EAAmBsE,WAAWgC,GAASA,EAKjF,GAAIrD,KAAcD,GAChB,IACEuD,GAAM,IAAIlH,GAAYsH,gBAAgBD,EAAcrD,GAC5D,CAAQ,MAAO2B,GAAK,CAIhB,IAAKuB,IAAQA,EAAIK,gBAAiB,CAChCL,EAAMrG,EAAe2G,eAAe5D,GAAW,WAAY,MAC3D,IACEsD,EAAIK,gBAAgBE,UAAY5D,GAAiBjD,EAAYyG,CAC9D,CAAC,MAAO1B,GAER,CACF,CACD,MAAM+B,EAAOR,EAAIQ,MAAQR,EAAIK,gBAM7B,OALIN,GAASE,GACXO,EAAKC,aAAaxI,EAASyI,eAAeT,GAAoBO,EAAKG,WAAW,IAAM,MAIlFjE,KAAcD,GACT3C,EAAqB8G,KAAKZ,EAAK1E,GAAiB,OAAS,QAAQ,GAEnEA,GAAiB0E,EAAIK,gBAAkBG,CAClD,EAQQK,oBAAsB,SAA6B/I,GACvD,OAAO8B,EAAmBgH,KAAK9I,EAAK0B,eAAiB1B,EAAMA,EAE3DY,EAAWoI,aAAepI,EAAWqI,aAAerI,EAAWsI,UAAYtI,EAAWuI,4BAA8BvI,EAAWwI,mBAAoB,KACvJ,EAQQC,aAAe,SAAsBC,GACzC,OAAOA,aAAevI,IAA4C,iBAAjBuI,EAAIC,UAAoD,iBAApBD,EAAIE,aAAuD,mBAApBF,EAAI/B,eAAgC+B,EAAIG,sBAAsB5I,IAAgD,mBAAxByI,EAAIxB,iBAA8D,mBAArBwB,EAAIvB,cAA2D,iBAArBuB,EAAII,cAAyD,mBAArBJ,EAAIX,cAA4D,mBAAtBW,EAAIK,cACnY,EAQQC,QAAU,SAAiBlM,GAC/B,MAAuB,mBAATgD,GAAuBhD,aAAkBgD,CAC3D,EAUQmJ,aAAe,SAAsBC,EAAYC,EAAaC,GAC7D9H,GAAM4H,IAGXpP,EAAawH,GAAM4H,IAAaG,IAC9BA,EAAKnB,KAAK/I,UAAWgK,EAAaC,EAAM9E,GAAO,GAErD,EAYQgF,kBAAoB,SAA2BH,GACnD,IAAItI,EAAU,KAMd,GAHAoI,aAAa,yBAA0BE,EAAa,MAGhDV,aAAaU,GAEf,OADA3C,aAAa2C,IACN,EAIT,MAAMI,EAAUjN,GAAkB6M,EAAYR,UAS9C,GANAM,aAAa,sBAAuBE,EAAa,CAC/CI,UACAC,YAAa/H,KAIX0H,EAAYJ,kBAAoBC,QAAQG,EAAYM,oBAAsBnO,EAAW,UAAW6N,EAAYtB,YAAcvM,EAAW,UAAW6N,EAAYP,aAE9J,OADApC,aAAa2C,IACN,EAIT,GAlwBsB,IAkwBlBA,EAAY3J,SAEd,OADAgH,aAAa2C,IACN,EAIT,GAAIxG,IAvwBG,IAuwBawG,EAAY3J,UAAkClE,EAAW,UAAW6N,EAAYC,MAElG,OADA5C,aAAa2C,IACN,EAIT,IAAK1H,GAAa8H,IAAYnH,GAAYmH,GAAU,CAElD,IAAKnH,GAAYmH,IAAYG,sBAAsBH,GAAU,CAC3D,GAAI1H,GAAwBC,wBAAwBvG,QAAUD,EAAWuG,GAAwBC,aAAcyH,GAC7G,OAAO,EAET,GAAI1H,GAAwBC,wBAAwB4C,UAAY7C,GAAwBC,aAAayH,GACnG,OAAO,CAEV,CAGD,GAAInG,KAAiBG,GAAgBgG,GAAU,CAC7C,MAAM7C,EAAahG,EAAcyI,IAAgBA,EAAYzC,WACvDuB,EAAaxH,EAAc0I,IAAgBA,EAAYlB,WAC7D,GAAIA,GAAcvB,EAEhB,IAAK,IAAIiD,EADU1B,EAAWnM,OACJ,EAAG6N,GAAK,IAAKA,EAAG,CACxC,MAAMC,EAAarJ,EAAU0H,EAAW0B,IAAI,GAC5CC,EAAWC,gBAAkBV,EAAYU,gBAAkB,GAAK,EAChEnD,EAAWqB,aAAa6B,EAAYpJ,EAAe2I,GACpD,CAEJ,CAED,OADA3C,aAAa2C,IACN,CACR,CAGD,OAAIA,aAAuBpJ,IAzTA,SAA8BtD,GACzD,IAAIqN,EAASpJ,EAAcjE,GAItBqN,GAAWA,EAAOP,UACrBO,EAAS,CACPhB,aAAc9E,GACduF,QAAS,aAGb,MAAMA,EAAUhP,EAAkBkC,EAAQ8M,SACpCQ,EAAgBxP,EAAkBuP,EAAOP,SAC/C,QAAKrF,GAAmBzH,EAAQqM,gBAG5BrM,EAAQqM,eAAiBhF,GAIvBgG,EAAOhB,eAAiB/E,GACP,QAAZwF,EAMLO,EAAOhB,eAAiBjF,GACP,QAAZ0F,IAAwC,mBAAlBQ,GAAsC5D,GAA+B4D,IAK7FC,QAAQ1D,GAAaiD,IAE1B9M,EAAQqM,eAAiBjF,GAIvBiG,EAAOhB,eAAiB/E,GACP,SAAZwF,EAKLO,EAAOhB,eAAiBhF,GACP,SAAZyF,GAAsBnD,GAAwB2D,GAKhDC,QAAQzD,GAAgBgD,IAE7B9M,EAAQqM,eAAiB/E,KAIvB+F,EAAOhB,eAAiBhF,KAAkBsC,GAAwB2D,OAGlED,EAAOhB,eAAiBjF,KAAqBsC,GAA+B4D,MAMxExD,GAAgBgD,KAAalD,GAA6BkD,KAAajD,GAAaiD,MAIpE,0BAAtBnF,KAAiDF,GAAmBzH,EAAQqM,eASpF,CA0O2CmB,CAAqBd,IAC1D3C,aAAa2C,IACN,GAIQ,aAAZI,GAAsC,YAAZA,GAAqC,aAAZA,IAA2BjO,EAAW,8BAA+B6N,EAAYtB,YAMrInF,IA7zBA,IA6zBsByG,EAAY3J,WAEpCqB,EAAUsI,EAAYP,YACtB9O,EAAa,CAACqE,GAAeC,GAAUC,KAAc6L,IACnDrJ,EAAU/F,EAAc+F,EAASqJ,EAAM,IAAI,IAEzCf,EAAYP,cAAgB/H,IAC9BxG,EAAU8E,UAAUG,QAAS,CAC3B7C,QAAS0M,EAAY5I,cAEvB4I,EAAYP,YAAc/H,IAK9BoI,aAAa,wBAAyBE,EAAa,OAC5C,IArBL3C,aAAa2C,IACN,EAqBb,EAWQgB,kBAAoB,SAA2BC,EAAOC,EAAQpN,GAElE,GAAIiG,KAA4B,OAAXmH,GAA8B,SAAXA,KAAuBpN,KAASsC,GAAYtC,KAASsH,IAC3F,OAAO,EAOT,GAAIhC,KAAoBF,GAAYgI,IAAW/O,EAAWgD,GAAW+L,SAAgB,GAAI/H,IAAmBhH,EAAWiD,GAAW8L,SAAgB,IAAK1I,GAAa0I,IAAWhI,GAAYgI,IACzL,KAIAX,sBAAsBU,KAAWvI,GAAwBC,wBAAwBvG,QAAUD,EAAWuG,GAAwBC,aAAcsI,IAAUvI,GAAwBC,wBAAwB4C,UAAY7C,GAAwBC,aAAasI,MAAYvI,GAAwBK,8BAA8B3G,QAAUD,EAAWuG,GAAwBK,mBAAoBmI,IAAWxI,GAAwBK,8BAA8BwC,UAAY7C,GAAwBK,mBAAmBmI,KAGve,OAAXA,GAAmBxI,GAAwBM,iCAAmCN,GAAwBC,wBAAwBvG,QAAUD,EAAWuG,GAAwBC,aAAc7E,IAAU4E,GAAwBC,wBAAwB4C,UAAY7C,GAAwBC,aAAa7E,KAClS,OAAO,OAGJ,GAAI0G,GAAoB0G,SAAgB,GAAI/O,EAAWkG,GAAkB1G,EAAcmC,EAAOyB,GAAiB,WAAa,GAAgB,QAAX2L,GAA+B,eAAXA,GAAsC,SAAXA,GAAgC,WAAVD,GAAwD,IAAlCpP,EAAciC,EAAO,WAAkBwG,GAAc2G,GAAe,GAAI5H,KAA4BlH,EAAWmD,GAAmB3D,EAAcmC,EAAOyB,GAAiB,WAAa,GAAIzB,EAC1Z,OAAO,EAET,OAAO,CACX,EAUQyM,sBAAwB,SAA+BH,GAC3D,MAAmB,mBAAZA,GAAgC3O,EAAY2O,EAAS3K,GAChE,EAYQ0L,oBAAsB,SAA6BnB,GAEvDF,aAAa,2BAA4BE,EAAa,MACtD,MAAMN,WACJA,GACEM,EAGJ,IAAKN,EACH,OAEF,MAAM0B,EAAY,CAChBC,SAAU,GACVC,UAAW,GACXC,UAAU,EACVC,kBAAmBhJ,IAErB,IAAInF,EAAIqM,EAAW/M,OAGnB,KAAOU,KAAK,CACV,MAAMoO,EAAO/B,EAAWrM,IAClBsK,KACJA,EAAIgC,aACJA,EACA7L,MAAOwN,GACLG,EACEP,EAAS/N,GAAkBwK,GACjC,IAAI7J,EAAiB,UAAT6J,EAAmB2D,EAAYvP,EAAWuP,GAUtD,GAPAF,EAAUC,SAAWH,EACrBE,EAAUE,UAAYxN,EACtBsN,EAAUG,UAAW,EACrBH,EAAUM,mBAAgBtO,EAC1B0M,aAAa,wBAAyBE,EAAaoB,GACnDtN,EAAQsN,EAAUE,UAEdF,EAAUM,cACZ,SAOF,GAHAhE,iBAAiBC,EAAMqC,IAGlBoB,EAAUG,SACb,SAIF,IAAKjI,IAA4BnH,EAAW,OAAQ2B,GAAQ,CAC1D4J,iBAAiBC,EAAMqC,GACvB,QACD,CAGD,GAAIxG,IAAgBrH,EAAW,gCAAiC2B,GAAQ,CACtE4J,iBAAiBC,EAAMqC,GACvB,QACD,CAGGzG,IACF5I,EAAa,CAACqE,GAAeC,GAAUC,KAAc6L,IACnDjN,EAAQnC,EAAcmC,EAAOiN,EAAM,IAAI,IAK3C,MAAME,EAAQ9N,GAAkB6M,EAAYR,UAC5C,GAAKwB,kBAAkBC,EAAOC,EAAQpN,GAAtC,CAgBA,IATIkG,IAAoC,OAAXkH,GAA8B,SAAXA,IAE9CxD,iBAAiBC,EAAMqC,GAGvBlM,EA/tB8B,gBA+tBQA,GAIpC8D,GAA8C,iBAAjBV,GAAsE,mBAAlCA,EAAayK,iBAChF,GAAIhC,QACF,OAAQzI,EAAayK,iBAAiBV,EAAOC,IAC3C,IAAK,cAEDpN,EAAQ8D,EAAmBsE,WAAWpI,GACtC,MAEJ,IAAK,mBAEDA,EAAQ8D,EAAmBuE,gBAAgBrI,GAQrD,IACM6L,EACFK,EAAY4B,eAAejC,EAAchC,EAAM7J,GAG/CkM,EAAYhC,aAAaL,EAAM7J,GAE7BwL,aAAaU,GACf3C,aAAa2C,GAEbhP,EAASgF,UAAUG,QAE7B,CAAQ,MAAOyG,GAAK,CA5Cb,CA6CF,CAGDkD,aAAa,0BAA2BE,EAAa,KACzD,EAOQ6B,GAAqB,SAASA,mBAAmBC,GACrD,IAAIC,EAAa,KACjB,MAAMC,EAAiBhD,oBAAoB8C,GAI3C,IADAhC,aAAa,0BAA2BgC,EAAU,MAC3CC,EAAaC,EAAeC,YAEjCnC,aAAa,yBAA0BiC,EAAY,MAG/C5B,kBAAkB4B,KAKlBA,EAAWrK,mBAAmBjB,GAChCoL,mBAAmBE,EAAWrK,SAIhCyJ,oBAAoBY,IAItBjC,aAAa,yBAA0BgC,EAAU,KACrD,EA0PE,OAhPA9L,UAAUkM,SAAW,SAAUhE,GAC7B,IAAIzC,EAAM/I,UAAUC,OAAS,QAAsBS,IAAjBV,UAAU,GAAmBA,UAAU,GAAK,CAAA,EAC1EiM,EAAO,KACPwD,EAAe,KACfnC,EAAc,KACdoC,EAAa,KAUjB,GANAtH,IAAkBoD,EACdpD,KACFoD,EAAQ,eAIW,iBAAVA,IAAuB2B,QAAQ3B,GAAQ,CAChD,GAA8B,mBAAnBA,EAAM1M,SAMf,MAAMc,EAAgB,8BAJtB,GAAqB,iBADrB4L,EAAQA,EAAM1M,YAEZ,MAAMc,EAAgB,kCAK3B,CAGD,IAAK0D,UAAUM,YACb,OAAO4H,EAeT,GAXKxE,IACH8B,aAAaC,GAIfzF,UAAUG,QAAU,GAGC,iBAAV+H,IACThE,IAAW,GAETA,IAEF,GAAIgE,EAAMsB,SAAU,CAClB,MAAMY,EAAUjN,GAAkB+K,EAAMsB,UACxC,IAAKlH,GAAa8H,IAAYnH,GAAYmH,GACxC,MAAM9N,EAAgB,0DAEzB,OACI,GAAI4L,aAAiBvH,EAG1BgI,EAAOV,cAAc,iBACrBkE,EAAexD,EAAKhH,cAAcO,WAAWgG,GAAO,GAzmC/C,IA0mCDiE,EAAa9L,UAA4D,SAA1B8L,EAAa3C,UAG3B,SAA1B2C,EAAa3C,SADtBb,EAAOwD,EAKPxD,EAAK0D,YAAYF,OAEd,CAEL,IAAKvI,KAAeL,KAAuBE,KAEnB,IAAxByE,EAAMpM,QAAQ,KACZ,OAAO8F,GAAsBkC,GAAsBlC,EAAmBsE,WAAWgC,GAASA,EAO5F,GAHAS,EAAOV,cAAcC,IAGhBS,EACH,OAAO/E,GAAa,KAAOE,GAAsBjC,EAAY,EAEhE,CAGG8G,GAAQhF,IACV0D,aAAasB,EAAK2D,YAIpB,MAAMC,EAAevD,oBAAoB9E,GAAWgE,EAAQS,GAG5D,KAAOqB,EAAcuC,EAAaN,YAE5B9B,kBAAkBH,KAKlBA,EAAYtI,mBAAmBjB,GACjCoL,GAAmB7B,EAAYtI,SAIjCyJ,oBAAoBnB,IAItB,GAAI9F,GACF,OAAOgE,EAIT,GAAItE,GAAY,CACd,GAAIC,GAEF,IADAuI,EAAapK,EAAuB+G,KAAKJ,EAAKhH,eACvCgH,EAAK2D,YAEVF,EAAWC,YAAY1D,EAAK2D,iBAG9BF,EAAazD,EAYf,OAVInG,GAAagK,YAAchK,GAAaiK,kBAQ1CL,EAAalK,EAAW6G,KAAKxI,EAAkB6L,GAAY,IAEtDA,CACR,CACD,IAAIM,EAAiBjJ,GAAiBkF,EAAKgE,UAAYhE,EAAKD,UAa5D,OAVIjF,IAAkBnB,GAAa,aAAeqG,EAAKhH,eAAiBgH,EAAKhH,cAAciL,SAAWjE,EAAKhH,cAAciL,QAAQjF,MAAQxL,EAAWqD,EAAcmJ,EAAKhH,cAAciL,QAAQjF,QAC3L+E,EAAiB,aAAe/D,EAAKhH,cAAciL,QAAQjF,KAAO,MAAQ+E,GAIxEnJ,IACF5I,EAAa,CAACqE,GAAeC,GAAUC,KAAc6L,IACnD2B,EAAiB/Q,EAAc+Q,EAAgB3B,EAAM,IAAI,IAGtDnJ,GAAsBkC,GAAsBlC,EAAmBsE,WAAWwG,GAAkBA,CACvG,EAQE1M,UAAU6M,UAAY,WAEpBrH,aADU9I,UAAUC,OAAS,QAAsBS,IAAjBV,UAAU,GAAmBA,UAAU,GAAK,CAAA,GAE9EgH,IAAa,CACjB,EAOE1D,UAAU8M,YAAc,WACtB3H,GAAS,KACTzB,IAAa,CACjB,EAYE1D,UAAU+M,iBAAmB,SAAUC,EAAKvB,EAAM3N,GAE3CqH,IACHK,aAAa,CAAE,GAEjB,MAAMyF,EAAQ9N,GAAkB6P,GAC1B9B,EAAS/N,GAAkBsO,GACjC,OAAOT,kBAAkBC,EAAOC,EAAQpN,EAC5C,EASEkC,UAAUiN,QAAU,SAAUlD,EAAYmD,GACZ,mBAAjBA,IAGX/K,GAAM4H,GAAc5H,GAAM4H,IAAe,GACzC7O,EAAUiH,GAAM4H,GAAamD,GACjC,EAUElN,UAAUmN,WAAa,SAAUpD,GAC/B,GAAI5H,GAAM4H,GACR,OAAO/O,EAASmH,GAAM4H,GAE5B,EAQE/J,UAAUoN,YAAc,SAAUrD,GAC5B5H,GAAM4H,KACR5H,GAAM4H,GAAc,GAE1B,EAME/J,UAAUqN,eAAiB,WACzBlL,GAAQ,CAAA,CACZ,EACSnC,SACT,CACaD","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/external/collect.js b/external/collect.js new file mode 100644 index 00000000..da1f73cf --- /dev/null +++ b/external/collect.js @@ -0,0 +1,2 @@ +var t={exports:{}},e={isArray:t=>Array.isArray(t),isObject:t=>"object"==typeof t&&!1===Array.isArray(t)&&null!==t,isFunction:t=>"function"==typeof t};const{isFunction:i}=e;var average$1=function(t){return void 0===t?this.sum()/this.items.length:i(t)?new this.constructor(this.items).sum(t)/this.items.length:new this.constructor(this.items).pluck(t).sum()/this.items.length},s=average$1,clone$2=function(t){let e;return Array.isArray(t)?(e=[],e.push(...t)):(e={},Object.keys(t).forEach((i=>{e[i]=t[i]}))),e};const o=clone$2;var values$8=function(t){const e=[];return Array.isArray(t)?e.push(...t):"Collection"===t.constructor.name?e.push(...t.all()):Object.keys(t).forEach((i=>e.push(t[i]))),e};const r=values$8,{isFunction:n}=e;var contains$1=function(t,e){if(void 0!==e)return Array.isArray(this.items)?this.items.filter((i=>void 0!==i[t]&&i[t]===e)).length>0:void 0!==this.items[t]&&this.items[t]===e;if(n(t))return this.items.filter(((e,i)=>t(e,i))).length>0;if(Array.isArray(this.items))return-1!==this.items.indexOf(t);const i=r(this.items);return i.push(...Object.keys(this.items)),-1!==i.indexOf(t)};const c=values$8;var variadic$4=function(t){return Array.isArray(t[0])?t[0]:t};const h=variadic$4;function falsyValue(t){if(Array.isArray(t)){if(t.length)return!1}else if(null!=t&&"object"==typeof t){if(Object.keys(t).length)return!1}else if(t)return!1;return!0}const{isFunction:l}=e,{isFunction:u}=e,{isArray:f,isObject:p}=e,{isFunction:y}=e;var nestedValue$8=function(t,e){try{return e.split(".").reduce(((t,e)=>t[e]),t)}catch(e){return t}};const m=nestedValue$8,{isFunction:a}=e,C=variadic$4,w=nestedValue$8,{isFunction:d}=e,{isFunction:A}=e,O=values$8,b=variadic$4,j=clone$2,{isArray:g,isObject:k}=e,v=nestedValue$8,E=variadic$4;var deleteKeys$2=function(t,...e){E(e).forEach((e=>{delete t[e]}))};const{isArray:x,isObject:F}=e,N=deleteKeys$2,{isFunction:S}=e,M=values$8,{isArray:J,isObject:B,isFunction:I}=e,{isArray:K,isObject:$}=e,P=deleteKeys$2,D=values$8,{isObject:R}=e,{isArray:W,isObject:U,isFunction:V}=e,{isArray:T,isObject:q,isFunction:z}=e,{isFunction:G}=e;var H=contains$1;const L=nestedValue$8,{isFunction:Q}=e,X=values$8,{isFunction:Y}=e,{isArray:Z,isObject:_,isFunction:tt}=e,{isArray:et,isObject:it,isFunction:st}=e;var whenNotEmpty=function(t,e){if(Array.isArray(this.items)&&this.items.length)return t(this);if(Object.keys(this.items).length)return t(this);if(void 0!==e){if(Array.isArray(this.items)&&!this.items.length)return e(this);if(!Object.keys(this.items).length)return e(this)}return this},whenEmpty=function(t,e){if(Array.isArray(this.items)&&!this.items.length)return t(this);if(!Object.keys(this.items).length)return t(this);if(void 0!==e){if(Array.isArray(this.items)&&this.items.length)return e(this);if(Object.keys(this.items).length)return e(this)}return this};const{isFunction:ot}=e,rt=values$8,nt=values$8,ct=nestedValue$8,ht=values$8,lt=nestedValue$8,ut=nestedValue$8,ft=values$8,pt=nestedValue$8;function Collection(t){void 0===t||Array.isArray(t)||"object"==typeof t?t instanceof this.constructor?this.items=t.all():this.items=t||[]:this.items=[t]}"undefined"!=typeof Symbol&&(Collection.prototype[Symbol.iterator]=function(){let t=-1;return{next:()=>(t+=1,{value:this.items[t],done:t>=this.items.length})}}),Collection.prototype.toJSON=function(){return this.items},Collection.prototype.all=function(){return this.items},Collection.prototype.average=average$1,Collection.prototype.avg=s,Collection.prototype.chunk=function(t){const e=[];let i=0;if(Array.isArray(this.items))do{const s=this.items.slice(i,i+t),o=new this.constructor(s);e.push(o),i+=t}while(ir.put(t,this.items[t]))),e.push(r),i+=t}while(i{i[t]=e[s]})):"object"==typeof this.items&&"object"==typeof e?Object.keys(this.items).forEach(((t,s)=>{i[this.items[t]]=e[Object.keys(e)[s]]})):Array.isArray(this.items)?i[this.items[0]]=e:"string"==typeof this.items&&Array.isArray(e)?[i[this.items]]=e:"string"==typeof this.items&&(i[this.items]=e),new this.constructor(i)},Collection.prototype.concat=function(t){let e=t;t instanceof this.constructor?e=t.all():"object"==typeof t&&(e=[],Object.keys(t).forEach((i=>{e.push(t[i])})));const i=o(this.items);return e.forEach((t=>{"object"==typeof t?Object.keys(t).forEach((e=>i.push(t[e]))):i.push(t)})),new this.constructor(i)},Collection.prototype.contains=contains$1,Collection.prototype.containsOneItem=function(){return 1===this.count()},Collection.prototype.count=function(){let t=0;return Array.isArray(this.items)&&(t=this.items.length),Math.max(Object.keys(this.items).length,t)},Collection.prototype.countBy=function(t=(t=>t)){return new this.constructor(this.items).groupBy(t).map((t=>t.count()))},Collection.prototype.crossJoin=function(...t){return new this.constructor(function join(t,e,i){let s=i[0];s instanceof e&&(s=s.all());const o=i.slice(1),r=!o.length;let n=[];for(let i=0;i-1===e.indexOf(t)));return new this.constructor(i)},Collection.prototype.diffAssoc=function(t){let e=t;t instanceof this.constructor&&(e=t.all());const i={};return Object.keys(this.items).forEach((t=>{void 0!==e[t]&&e[t]===this.items[t]||(i[t]=this.items[t])})),new this.constructor(i)},Collection.prototype.diffKeys=function(t){let e;e=t instanceof this.constructor?t.all():t;const i=Object.keys(e),s=Object.keys(this.items).filter((t=>-1===i.indexOf(t)));return new this.constructor(this.items).only(s)},Collection.prototype.diffUsing=function(t,e){const i=this.items.filter((i=>!(t&&t.some((t=>0===e(i,t))))));return new this.constructor(i)},Collection.prototype.doesntContain=function(t,e){return!this.contains(t,e)},Collection.prototype.dump=function(){return console.log(this),this},Collection.prototype.duplicates=function(){const t=[],e={},stringifiedValue=t=>Array.isArray(t)||"object"==typeof t?JSON.stringify(t):t;return Array.isArray(this.items)?this.items.forEach(((i,s)=>{const o=stringifiedValue(i);-1===t.indexOf(o)?t.push(o):e[s]=i})):"object"==typeof this.items&&Object.keys(this.items).forEach((i=>{const s=stringifiedValue(this.items[i]);-1===t.indexOf(s)?t.push(s):e[i]=this.items[i]})),new this.constructor(e)},Collection.prototype.each=function(t){let e=!1;if(Array.isArray(this.items)){const{length:i}=this.items;for(let s=0;s{t(...e,i)})),this},Collection.prototype.every=function(t){return c(this.items).every(t)},Collection.prototype.except=function(...t){const e=h(t);if(Array.isArray(this.items)){const t=this.items.filter((t=>-1===e.indexOf(t)));return new this.constructor(t)}const i={};return Object.keys(this.items).forEach((t=>{-1===e.indexOf(t)&&(i[t]=this.items[t])})),new this.constructor(i)},Collection.prototype.filter=function(t){const e=t||!1;let i=null;return i=Array.isArray(this.items)?function(t,e){if(t)return e.filter(t);const i=[];for(let t=0;t{t?t(e[s],s)&&(i[s]=e[s]):falsyValue(e[s])||(i[s]=e[s])})),i}(e,this.items),new this.constructor(i)},Collection.prototype.first=function(t,e){if(l(t)){const i=Object.keys(this.items);for(let e=0;e{throw new Error("Item not found.")}));const s=this.where(t,e,i);if(s.isEmpty())throw new Error("Item not found.");return s.first()},Collection.prototype.firstWhere=function(t,e,i){return this.where(t,e,i).first()||null},Collection.prototype.flatMap=function(t){return this.map(t).collapse()},Collection.prototype.flatten=function(t){let e=t||1/0,i=!1,s=[];const flat=function(t){s=[],f(t)?t.forEach((t=>{f(t)?s=s.concat(t):p(t)?Object.keys(t).forEach((e=>{s=s.concat(t[e])})):s.push(t)})):Object.keys(t).forEach((e=>{f(t[e])?s=s.concat(t[e]):p(t[e])?Object.keys(t[e]).forEach((i=>{s=s.concat(t[e][i])})):s.push(t[e])})),i=s.filter((t=>p(t))),i=0===i.length,e-=1};for(flat(this.items);!i&&e>0;)flat(s);return new this.constructor(s)},Collection.prototype.flip=function(){const t={};return Array.isArray(this.items)?Object.keys(this.items).forEach((e=>{t[this.items[e]]=Number(e)})):Object.keys(this.items).forEach((e=>{t[this.items[e]]=e})),new this.constructor(t)},Collection.prototype.forPage=function(t,e){let i={};return Array.isArray(this.items)?i=this.items.slice(t*e-e,t*e):Object.keys(this.items).slice(t*e-e,t*e).forEach((t=>{i[t]=this.items[t]})),new this.constructor(i)},Collection.prototype.forget=function(t){return Array.isArray(this.items)?this.items.splice(t,1):delete this.items[t],this},Collection.prototype.get=function(t,e=null){return void 0!==this.items[t]?this.items[t]:y(e)?e():null!==e?e:null},Collection.prototype.groupBy=function(t){const e={};return this.items.forEach(((i,s)=>{let o;o=a(t)?t(i,s):m(i,t)||0===m(i,t)?m(i,t):"",void 0===e[o]&&(e[o]=new this.constructor([])),e[o].push(i)})),new this.constructor(e)},Collection.prototype.has=function(...t){const e=C(t);return e.filter((t=>Object.hasOwnProperty.call(this.items,t))).length===e.length},Collection.prototype.implode=function(t,e){return void 0===e?this.items.join(t):new this.constructor(this.items).pluck(t).all().join(e)},Collection.prototype.intersect=function(t){let e=t;t instanceof this.constructor&&(e=t.all());const i=this.items.filter((t=>-1!==e.indexOf(t)));return new this.constructor(i)},Collection.prototype.intersectByKeys=function(t){let e=Object.keys(t);t instanceof this.constructor&&(e=Object.keys(t.all()));const i={};return Object.keys(this.items).forEach((t=>{-1!==e.indexOf(t)&&(i[t]=this.items[t])})),new this.constructor(i)},Collection.prototype.isEmpty=function(){return Array.isArray(this.items)?!this.items.length:!Object.keys(this.items).length},Collection.prototype.isNotEmpty=function(){return!this.isEmpty()},Collection.prototype.join=function(t,e){const i=this.values();if(void 0===e)return i.implode(t);const s=i.count();if(0===s)return"";if(1===s)return i.last();const o=i.pop();return i.implode(t)+e+o},Collection.prototype.keyBy=function(t){const e={};return d(t)?this.items.forEach((i=>{e[t(i)]=i})):this.items.forEach((i=>{const s=w(i,t);e[s||""]=i})),new this.constructor(e)},Collection.prototype.keys=function(){let t=Object.keys(this.items);return Array.isArray(this.items)&&(t=t.map(Number)),new this.constructor(t)},Collection.prototype.last=function(t,e){let{items:i}=this;if(A(t)&&(i=this.filter(t).all()),Array.isArray(i)&&!i.length||!Object.keys(i).length)return A(e)?e():e;if(Array.isArray(i))return i[i.length-1];const s=Object.keys(i);return i[s[s.length-1]]},Collection.prototype.macro=function(t,e){this.constructor.prototype[t]=e},Collection.prototype.make=function(t=[]){return new this.constructor(t)},Collection.prototype.map=function(t){if(Array.isArray(this.items))return new this.constructor(this.items.map(t));const e={};return Object.keys(this.items).forEach((i=>{e[i]=t(this.items[i],i)})),new this.constructor(e)},Collection.prototype.mapSpread=function(t){return this.map(((e,i)=>t(...e,i)))},Collection.prototype.mapToDictionary=function(t){const e={};return this.items.forEach(((i,s)=>{const[o,r]=t(i,s);void 0===e[o]?e[o]=[r]:e[o].push(r)})),new this.constructor(e)},Collection.prototype.mapInto=function(t){return this.map(((e,i)=>new t(e,i)))},Collection.prototype.mapToGroups=function(t){const e={};return this.items.forEach(((i,s)=>{const[o,r]=t(i,s);void 0===e[o]?e[o]=[r]:e[o].push(r)})),new this.constructor(e)},Collection.prototype.mapWithKeys=function(t){const e={};return Array.isArray(this.items)?this.items.forEach(((i,s)=>{const[o,r]=t(i,s);e[o]=r})):Object.keys(this.items).forEach((i=>{const[s,o]=t(this.items[i],i);e[s]=o})),new this.constructor(e)},Collection.prototype.max=function(t){if("string"==typeof t){const e=this.items.filter((e=>void 0!==e[t]));return Math.max(...e.map((e=>e[t])))}return Math.max(...this.items)},Collection.prototype.median=function(t){const{length:e}=this.items;return void 0===t?e%2==0?(this.items[e/2-1]+this.items[e/2])/2:this.items[Math.floor(e/2)]:e%2==0?(this.items[e/2-1][t]+this.items[e/2][t])/2:this.items[Math.floor(e/2)][t]},Collection.prototype.merge=function(t){let e=t;if("string"==typeof e&&(e=[e]),Array.isArray(this.items)&&Array.isArray(e))return new this.constructor(this.items.concat(e));const i=JSON.parse(JSON.stringify(this.items));return Object.keys(e).forEach((t=>{i[t]=e[t]})),new this.constructor(i)},Collection.prototype.mergeRecursive=function(t){const merge=(t,e)=>{const i={};return Object.keys({...t,...e}).forEach((s=>{void 0===t[s]&&void 0!==e[s]?i[s]=e[s]:void 0!==t[s]&&void 0===e[s]?i[s]=t[s]:void 0!==t[s]&&void 0!==e[s]&&(t[s]===e[s]?i[s]=t[s]:Array.isArray(t[s])||"object"!=typeof t[s]||Array.isArray(e[s])||"object"!=typeof e[s]?i[s]=[].concat(t[s],e[s]):i[s]=merge(t[s],e[s]))})),i};return t?"Collection"===t.constructor.name?new this.constructor(merge(this.items,t.all())):new this.constructor(merge(this.items,t)):this},Collection.prototype.min=function(t){if(void 0!==t){const e=this.items.filter((e=>void 0!==e[t]));return Math.min(...e.map((e=>e[t])))}return Math.min(...this.items)},Collection.prototype.mode=function(t){const e=[];let i=1;return this.items.length?(this.items.forEach((s=>{const o=e.filter((e=>void 0!==t?e.key===s[t]:e.key===s));if(o.length){o[0].count+=1;const{count:t}=o[0];t>i&&(i=t)}else void 0!==t?e.push({key:s[t],count:1}):e.push({key:s,count:1})})),e.filter((t=>t.count===i)).map((t=>t.key))):null},Collection.prototype.nth=function(t,e=0){const i=O(this.items).slice(e).filter(((e,i)=>i%t==0));return new this.constructor(i)},Collection.prototype.only=function(...t){const e=b(t);if(Array.isArray(this.items)){const t=this.items.filter((t=>-1!==e.indexOf(t)));return new this.constructor(t)}const i={};return Object.keys(this.items).forEach((t=>{-1!==e.indexOf(t)&&(i[t]=this.items[t])})),new this.constructor(i)},Collection.prototype.pad=function(t,e){const i=Math.abs(t),s=this.count();if(i<=s)return this;let o=i-s;const r=j(this.items),n=Array.isArray(this.items),c=t<0;for(let t=0;t{!0===t(i)?e[0].push(i):e[1].push(i)}))):(e=[new this.constructor({}),new this.constructor({})],Object.keys(this.items).forEach((i=>{const s=this.items[i];!0===t(s)?e[0].put(i,s):e[1].put(i,s)}))),new this.constructor(e)},Collection.prototype.pipe=function(t){return t(this)},Collection.prototype.pluck=function(t,e){if(-1!==t.indexOf("*")){const i=function(t){const e={};return t.forEach(((t,i)=>{!function buildKeyPath(t,i){k(t)?Object.keys(t).forEach((e=>{buildKeyPath(t[e],`${i}.${e}`)})):g(t)&&t.forEach(((t,e)=>{buildKeyPath(t,`${i}.${e}`)})),e[i]=t}(t,i)})),e}(this.items),s=[];if(void 0!==e){const t=new RegExp(`0.${e}`,"g"),o=`0.${e}`.split(".").length;Object.keys(i).forEach((e=>{const r=e.match(t);if(r){const t=r[0];t.split(".").length===o&&s.push(i[t])}}))}const o=[],r=new RegExp(`0.${t}`,"g"),n=`0.${t}`.split(".").length;if(Object.keys(i).forEach((t=>{const e=t.match(r);if(e){const t=e[0];t.split(".").length===n&&o.push(i[t])}})),void 0!==e){const t={};return this.items.forEach(((e,i)=>{t[s[i]||""]=o})),new this.constructor(t)}return new this.constructor([o])}if(void 0!==e){const i={};return this.items.forEach((s=>{void 0!==v(s,t)?i[s[e]||""]=v(s,t):i[s[e]||""]=null})),new this.constructor(i)}return this.map((e=>void 0!==v(e,t)?v(e,t):null))},Collection.prototype.pop=function(t=1){if(this.isEmpty())return null;if(x(this.items))return 1===t?this.items.pop():new this.constructor(this.items.splice(-t));if(F(this.items)){const e=Object.keys(this.items);if(1===t){const t=e[e.length-1],i=this.items[t];return N(this.items,t),i}const i=e.slice(-t),s=i.reduce(((t,e)=>(t[e]=this.items[e],t)),{});return N(this.items,i),new this.constructor(s)}return null},Collection.prototype.prepend=function(t,e){return void 0!==e?this.put(e,t):(this.items.unshift(t),this)},Collection.prototype.pull=function(t,e){let i=this.items[t]||null;return i||void 0===e||(i=S(e)?e():e),delete this.items[t],i},Collection.prototype.push=function(...t){return this.items.push(...t),this},Collection.prototype.put=function(t,e){return this.items[t]=e,this},Collection.prototype.random=function(t=null){const e=M(this.items),i=new this.constructor(e).shuffle();return t!==parseInt(t,10)?i.first():i.take(t)},Collection.prototype.reduce=function(t,e){let i=null;return void 0!==e&&(i=e),Array.isArray(this.items)?this.items.forEach((e=>{i=t(i,e)})):Object.keys(this.items).forEach((e=>{i=t(i,this.items[e],e)})),i},Collection.prototype.reject=function(t){return new this.constructor(this.items).filter((e=>!t(e)))},Collection.prototype.replace=function(t){if(!t)return this;if(Array.isArray(t)){const e=this.items.map(((e,i)=>t[i]||e));return new this.constructor(e)}if("Collection"===t.constructor.name){const e={...this.items,...t.all()};return new this.constructor(e)}const e={...this.items,...t};return new this.constructor(e)},Collection.prototype.replaceRecursive=function(t){const replace=(t,e)=>{const i={...t};return Object.keys({...t,...e}).forEach((s=>{Array.isArray(e[s])||"object"!=typeof e[s]?void 0===t[s]&&void 0!==e[s]?"object"==typeof t[s]?i[s]={...e[s]}:i[s]=e[s]:void 0!==t[s]&&void 0===e[s]?"object"==typeof t[s]?i[s]={...t[s]}:i[s]=t[s]:void 0!==t[s]&&void 0!==e[s]&&("object"==typeof e[s]?i[s]={...e[s]}:i[s]=e[s]):i[s]=replace(t[s],e[s])})),i};return t?Array.isArray(t)||"object"==typeof t?"Collection"===t.constructor.name?new this.constructor(replace(this.items,t.all())):new this.constructor(replace(this.items,t)):new this.constructor(replace(this.items,[t])):this},Collection.prototype.reverse=function(){const t=[].concat(this.items).reverse();return new this.constructor(t)},Collection.prototype.search=function(t,e){let i;const find=(i,s)=>I(t)?t(this.items[s],s):e?this.items[s]===t:this.items[s]==t;return J(this.items)?i=this.items.findIndex(find):B(this.items)&&(i=Object.keys(this.items).find((t=>find(this.items[t],t)))),!(void 0===i||i<0)&&i},Collection.prototype.shift=function(t=1){if(this.isEmpty())return null;if(K(this.items))return 1===t?this.items.shift():new this.constructor(this.items.splice(0,t));if($(this.items)){if(1===t){const t=Object.keys(this.items)[0],e=this.items[t];return delete this.items[t],e}const e=Object.keys(this.items).slice(0,t),i=e.reduce(((t,e)=>(t[e]=this.items[e],t)),{});return P(this.items,e),new this.constructor(i)}return null},Collection.prototype.shuffle=function(){const t=D(this.items);let e,i,s;for(s=t.length;s;s-=1)e=Math.floor(Math.random()*s),i=t[s-1],t[s-1]=t[e],t[e]=i;return this.items=t,this},Collection.prototype.skip=function(t){return R(this.items)?new this.constructor(Object.keys(this.items).reduce(((e,i,s)=>(s+1>t&&(e[i]=this.items[i]),e)),{})):new this.constructor(this.items.slice(t))},Collection.prototype.skipUntil=function(t){let e,i=null,callback=e=>e===t;return V(t)&&(callback=t),W(this.items)&&(e=this.items.filter((t=>(!0!==i&&(i=callback(t)),i)))),U(this.items)&&(e=Object.keys(this.items).reduce(((t,e)=>(!0!==i&&(i=callback(this.items[e])),!1!==i&&(t[e]=this.items[e]),t)),{})),new this.constructor(e)},Collection.prototype.skipWhile=function(t){let e,i=null,callback=e=>e===t;return z(t)&&(callback=t),T(this.items)&&(e=this.items.filter((t=>(!0!==i&&(i=!callback(t)),i)))),q(this.items)&&(e=Object.keys(this.items).reduce(((t,e)=>(!0!==i&&(i=!callback(this.items[e])),!1!==i&&(t[e]=this.items[e]),t)),{})),new this.constructor(e)},Collection.prototype.slice=function(t,e){let i=this.items.slice(t);return void 0!==e&&(i=i.slice(0,e)),new this.constructor(i)},Collection.prototype.sole=function(t,e,i){let s;if(s=G(t)?this.filter(t):this.where(t,e,i),s.isEmpty())throw new Error("Item not found.");if(s.count()>1)throw new Error("Multiple items found.");return s.first()},Collection.prototype.some=H,Collection.prototype.sort=function(t){const e=[].concat(this.items);return void 0===t?this.every((t=>"number"==typeof t))?e.sort(((t,e)=>t-e)):e.sort():e.sort(t),new this.constructor(e)},Collection.prototype.sortDesc=function(){return this.sort().reverse()},Collection.prototype.sortBy=function(t){const e=[].concat(this.items),getValue=e=>Q(t)?t(e):L(e,t);return e.sort(((t,e)=>{const i=getValue(t),s=getValue(e);return null==i?1:null==s||is?1:0})),new this.constructor(e)},Collection.prototype.sortByDesc=function(t){return this.sortBy(t).reverse()},Collection.prototype.sortKeys=function(){const t={};return Object.keys(this.items).sort().forEach((e=>{t[e]=this.items[e]})),new this.constructor(t)},Collection.prototype.sortKeysDesc=function(){const t={};return Object.keys(this.items).sort().reverse().forEach((e=>{t[e]=this.items[e]})),new this.constructor(t)},Collection.prototype.splice=function(t,e,i){const s=this.slice(t,e);if(this.items=this.diff(s.all()).all(),Array.isArray(i))for(let e=0,{length:s}=i;e{-1!==i.indexOf(t)&&(s[t]=this.items[t])})),new this.constructor(s)}return t<0?new this.constructor(this.items.slice(t)):new this.constructor(this.items.slice(0,t))},Collection.prototype.takeUntil=function(t){let e,i=null,callback=e=>e===t;return tt(t)&&(callback=t),Z(this.items)&&(e=this.items.filter((t=>(!1!==i&&(i=!callback(t)),i)))),_(this.items)&&(e=Object.keys(this.items).reduce(((t,e)=>(!1!==i&&(i=!callback(this.items[e])),!1!==i&&(t[e]=this.items[e]),t)),{})),new this.constructor(e)},Collection.prototype.takeWhile=function(t){let e,i=null,callback=e=>e===t;return st(t)&&(callback=t),et(this.items)&&(e=this.items.filter((t=>(!1!==i&&(i=callback(t)),i)))),it(this.items)&&(e=Object.keys(this.items).reduce(((t,e)=>(!1!==i&&(i=callback(this.items[e])),!1!==i&&(t[e]=this.items[e]),t)),{})),new this.constructor(e)},Collection.prototype.tap=function(t){return t(this),this},Collection.prototype.times=function(t,e){for(let i=1;i<=t;i+=1)this.items.push(e(i));return this},Collection.prototype.toArray=function(){const t=this.constructor;function iterate(e,i){const s=[];e instanceof t?(e.items.forEach((t=>iterate(t,s))),i.push(s)):Array.isArray(e)?(e.forEach((t=>iterate(t,s))),i.push(s)):i.push(e)}if(Array.isArray(this.items)){const t=[];return this.items.forEach((e=>{iterate(e,t)})),t}return this.values().all()},Collection.prototype.toJson=function(){return"object"!=typeof this.items||Array.isArray(this.items)?JSON.stringify(this.toArray()):JSON.stringify(this.all())},Collection.prototype.transform=function(t){if(Array.isArray(this.items))this.items=this.items.map(t);else{const e={};Object.keys(this.items).forEach((i=>{e[i]=t(this.items[i],i)})),this.items=e}return this},Collection.prototype.undot=function(){if(Array.isArray(this.items))return this;let t={};return Object.keys(this.items).forEach((e=>{if(-1!==e.indexOf(".")){const i=t;e.split(".").reduce(((t,i,s,o)=>(t[i]||(t[i]={}),s===o.length-1&&(t[i]=this.items[e]),t[i])),i),t={...t,...i}}else t[e]=this.items[e]})),new this.constructor(t)},Collection.prototype.unless=function(t,e,i){t?i(this):e(this)},Collection.prototype.unlessEmpty=whenNotEmpty,Collection.prototype.unlessNotEmpty=whenEmpty,Collection.prototype.union=function(t){const e=JSON.parse(JSON.stringify(this.items));return Object.keys(t).forEach((i=>{void 0===this.items[i]&&(e[i]=t[i])})),new this.constructor(e)},Collection.prototype.unique=function(t){let e;if(void 0===t)e=this.items.filter(((t,e,i)=>i.indexOf(t)===e));else{e=[];const i=[];for(let s=0,{length:o}=this.items;sct(e,t))));if(!1===e)return new this.constructor(r.filter((e=>!ct(e,t))));void 0===i&&(o=e,s="===");const n=r.filter((e=>{switch(s){case"==":return ct(e,t)===Number(o)||ct(e,t)===o.toString();default:case"===":return ct(e,t)===o;case"!=":case"<>":return ct(e,t)!==Number(o)&&ct(e,t)!==o.toString();case"!==":return ct(e,t)!==o;case"<":return ct(e,t)":return ct(e,t)>o;case">=":return ct(e,t)>=o}}));return new this.constructor(n)},Collection.prototype.whereBetween=function(t,e){return this.where(t,">=",e[0]).where(t,"<=",e[e.length-1])},Collection.prototype.whereIn=function(t,e){const i=ht(e),s=this.items.filter((e=>-1!==i.indexOf(lt(e,t))));return new this.constructor(s)},Collection.prototype.whereInstanceOf=function(t){return this.filter((e=>e instanceof t))},Collection.prototype.whereNotBetween=function(t,e){return this.filter((i=>ut(i,t)e[e.length-1]))},Collection.prototype.whereNotIn=function(t,e){const i=ft(e),s=this.items.filter((e=>-1===i.indexOf(pt(e,t))));return new this.constructor(s)},Collection.prototype.whereNull=function(t=null){return this.where(t,"===",null)},Collection.prototype.whereNotNull=function(t=null){return this.where(t,"!==",null)},Collection.prototype.wrap=function(t){return t instanceof this.constructor?t:"object"==typeof t?new this.constructor(t):new this.constructor([t])},Collection.prototype.zip=function(t){let e=t;e instanceof this.constructor&&(e=e.all());const i=this.items.map(((t,i)=>new this.constructor([t,e[i]])));return new this.constructor(i)};const collect=t=>new Collection(t);t.exports=collect;var yt=t.exports.collect=collect;t.exports.default=collect,t.exports.Collection=Collection;export{yt as default}; +//# sourceMappingURL=collect.js.map diff --git a/external/collect.js.map b/external/collect.js.map new file mode 100644 index 00000000..57628839 --- /dev/null +++ b/external/collect.js.map @@ -0,0 +1 @@ +{"version":3,"file":"collect.js","sources":["../node_modules/collect.js/src/helpers/is.js","../node_modules/collect.js/src/methods/average.js","../node_modules/collect.js/src/methods/avg.js","../node_modules/collect.js/src/helpers/clone.js","../node_modules/collect.js/src/methods/concat.js","../node_modules/collect.js/src/helpers/values.js","../node_modules/collect.js/src/methods/contains.js","../node_modules/collect.js/src/methods/every.js","../node_modules/collect.js/src/helpers/variadic.js","../node_modules/collect.js/src/methods/except.js","../node_modules/collect.js/src/methods/filter.js","../node_modules/collect.js/src/methods/first.js","../node_modules/collect.js/src/methods/firstOrFail.js","../node_modules/collect.js/src/methods/flatten.js","../node_modules/collect.js/src/methods/get.js","../node_modules/collect.js/src/helpers/nestedValue.js","../node_modules/collect.js/src/methods/groupBy.js","../node_modules/collect.js/src/methods/has.js","../node_modules/collect.js/src/methods/keyBy.js","../node_modules/collect.js/src/methods/last.js","../node_modules/collect.js/src/methods/nth.js","../node_modules/collect.js/src/methods/only.js","../node_modules/collect.js/src/methods/pad.js","../node_modules/collect.js/src/methods/pluck.js","../node_modules/collect.js/src/helpers/deleteKeys.js","../node_modules/collect.js/src/methods/pop.js","../node_modules/collect.js/src/methods/pull.js","../node_modules/collect.js/src/methods/random.js","../node_modules/collect.js/src/methods/search.js","../node_modules/collect.js/src/methods/shift.js","../node_modules/collect.js/src/methods/shuffle.js","../node_modules/collect.js/src/methods/skip.js","../node_modules/collect.js/src/methods/skipUntil.js","../node_modules/collect.js/src/methods/skipWhile.js","../node_modules/collect.js/src/methods/sole.js","../node_modules/collect.js/src/methods/some.js","../node_modules/collect.js/src/methods/sortBy.js","../node_modules/collect.js/src/methods/sum.js","../node_modules/collect.js/src/methods/takeUntil.js","../node_modules/collect.js/src/methods/takeWhile.js","../node_modules/collect.js/src/methods/whenNotEmpty.js","../node_modules/collect.js/src/methods/whenEmpty.js","../node_modules/collect.js/src/methods/unique.js","../node_modules/collect.js/src/methods/values.js","../node_modules/collect.js/src/methods/where.js","../node_modules/collect.js/src/methods/whereIn.js","../node_modules/collect.js/src/methods/whereNotBetween.js","../node_modules/collect.js/src/methods/whereNotIn.js","../node_modules/collect.js/src/index.js","../node_modules/collect.js/src/methods/symbol.iterator.js","../node_modules/collect.js/src/methods/all.js","../node_modules/collect.js/src/methods/chunk.js","../node_modules/collect.js/src/methods/collapse.js","../node_modules/collect.js/src/methods/combine.js","../node_modules/collect.js/src/methods/containsOneItem.js","../node_modules/collect.js/src/methods/count.js","../node_modules/collect.js/src/methods/countBy.js","../node_modules/collect.js/src/methods/crossJoin.js","../node_modules/collect.js/src/methods/dd.js","../node_modules/collect.js/src/methods/diff.js","../node_modules/collect.js/src/methods/diffAssoc.js","../node_modules/collect.js/src/methods/diffKeys.js","../node_modules/collect.js/src/methods/diffUsing.js","../node_modules/collect.js/src/methods/doesntContain.js","../node_modules/collect.js/src/methods/dump.js","../node_modules/collect.js/src/methods/duplicates.js","../node_modules/collect.js/src/methods/each.js","../node_modules/collect.js/src/methods/eachSpread.js","../node_modules/collect.js/src/methods/firstWhere.js","../node_modules/collect.js/src/methods/flatMap.js","../node_modules/collect.js/src/methods/flip.js","../node_modules/collect.js/src/methods/forPage.js","../node_modules/collect.js/src/methods/forget.js","../node_modules/collect.js/src/methods/implode.js","../node_modules/collect.js/src/methods/intersect.js","../node_modules/collect.js/src/methods/intersectByKeys.js","../node_modules/collect.js/src/methods/isEmpty.js","../node_modules/collect.js/src/methods/isNotEmpty.js","../node_modules/collect.js/src/methods/join.js","../node_modules/collect.js/src/methods/keys.js","../node_modules/collect.js/src/methods/macro.js","../node_modules/collect.js/src/methods/make.js","../node_modules/collect.js/src/methods/map.js","../node_modules/collect.js/src/methods/mapSpread.js","../node_modules/collect.js/src/methods/mapToDictionary.js","../node_modules/collect.js/src/methods/mapInto.js","../node_modules/collect.js/src/methods/mapToGroups.js","../node_modules/collect.js/src/methods/mapWithKeys.js","../node_modules/collect.js/src/methods/max.js","../node_modules/collect.js/src/methods/median.js","../node_modules/collect.js/src/methods/merge.js","../node_modules/collect.js/src/methods/mergeRecursive.js","../node_modules/collect.js/src/methods/min.js","../node_modules/collect.js/src/methods/mode.js","../node_modules/collect.js/src/methods/partition.js","../node_modules/collect.js/src/methods/pipe.js","../node_modules/collect.js/src/methods/prepend.js","../node_modules/collect.js/src/methods/push.js","../node_modules/collect.js/src/methods/put.js","../node_modules/collect.js/src/methods/reduce.js","../node_modules/collect.js/src/methods/reject.js","../node_modules/collect.js/src/methods/replace.js","../node_modules/collect.js/src/methods/replaceRecursive.js","../node_modules/collect.js/src/methods/reverse.js","../node_modules/collect.js/src/methods/slice.js","../node_modules/collect.js/src/methods/sort.js","../node_modules/collect.js/src/methods/sortDesc.js","../node_modules/collect.js/src/methods/sortByDesc.js","../node_modules/collect.js/src/methods/sortKeys.js","../node_modules/collect.js/src/methods/sortKeysDesc.js","../node_modules/collect.js/src/methods/splice.js","../node_modules/collect.js/src/methods/split.js","../node_modules/collect.js/src/methods/take.js","../node_modules/collect.js/src/methods/tap.js","../node_modules/collect.js/src/methods/times.js","../node_modules/collect.js/src/methods/toArray.js","../node_modules/collect.js/src/methods/toJson.js","../node_modules/collect.js/src/methods/transform.js","../node_modules/collect.js/src/methods/undot.js","../node_modules/collect.js/src/methods/unless.js","../node_modules/collect.js/src/methods/union.js","../node_modules/collect.js/src/methods/unwrap.js","../node_modules/collect.js/src/methods/when.js","../node_modules/collect.js/src/methods/whereBetween.js","../node_modules/collect.js/src/methods/whereInstanceOf.js","../node_modules/collect.js/src/methods/whereNull.js","../node_modules/collect.js/src/methods/whereNotNull.js","../node_modules/collect.js/src/methods/wrap.js","../node_modules/collect.js/src/methods/zip.js"],"sourcesContent":["'use strict';\n\nmodule.exports = {\n /**\n * @returns {boolean}\n */\n isArray: item => Array.isArray(item),\n\n /**\n * @returns {boolean}\n */\n isObject: item => typeof item === 'object' && Array.isArray(item) === false && item !== null,\n\n /**\n * @returns {boolean}\n */\n isFunction: item => typeof item === 'function',\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function average(key) {\n if (key === undefined) {\n return this.sum() / this.items.length;\n }\n\n if (isFunction(key)) {\n return new this.constructor(this.items).sum(key) / this.items.length;\n }\n\n return new this.constructor(this.items).pluck(key).sum() / this.items.length;\n};\n","'use strict';\n\nconst average = require('./average');\n\nmodule.exports = average;\n","'use strict';\n\n/**\n * Clone helper\n *\n * Clone an array or object\n *\n * @param items\n * @returns {*}\n */\nmodule.exports = function clone(items) {\n let cloned;\n\n if (Array.isArray(items)) {\n cloned = [];\n\n cloned.push(...items);\n } else {\n cloned = {};\n\n Object.keys(items).forEach((prop) => {\n cloned[prop] = items[prop];\n });\n }\n\n return cloned;\n};\n","'use strict';\n\nconst clone = require('../helpers/clone');\n\nmodule.exports = function concat(collectionOrArrayOrObject) {\n let list = collectionOrArrayOrObject;\n\n if (collectionOrArrayOrObject instanceof this.constructor) {\n list = collectionOrArrayOrObject.all();\n } else if (typeof collectionOrArrayOrObject === 'object') {\n list = [];\n Object.keys(collectionOrArrayOrObject).forEach((property) => {\n list.push(collectionOrArrayOrObject[property]);\n });\n }\n\n const collection = clone(this.items);\n\n list.forEach((item) => {\n if (typeof item === 'object') {\n Object.keys(item).forEach(key => collection.push(item[key]));\n } else {\n collection.push(item);\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\n/**\n * Values helper\n *\n * Retrieve values from [this.items] when it is an array, object or Collection\n *\n * @param items\n * @returns {*}\n */\nmodule.exports = function values(items) {\n const valuesArray = [];\n\n if (Array.isArray(items)) {\n valuesArray.push(...items);\n } else if (items.constructor.name === 'Collection') {\n valuesArray.push(...items.all());\n } else {\n Object.keys(items).forEach(prop => valuesArray.push(items[prop]));\n }\n\n return valuesArray;\n};\n","'use strict';\n\nconst values = require('../helpers/values');\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function contains(key, value) {\n if (value !== undefined) {\n if (Array.isArray(this.items)) {\n return this.items\n .filter(items => items[key] !== undefined && items[key] === value)\n .length > 0;\n }\n\n return this.items[key] !== undefined && this.items[key] === value;\n }\n\n if (isFunction(key)) {\n return (this.items.filter((item, index) => key(item, index)).length > 0);\n }\n\n if (Array.isArray(this.items)) {\n return this.items.indexOf(key) !== -1;\n }\n\n const keysAndValues = values(this.items);\n keysAndValues.push(...Object.keys(this.items));\n\n return keysAndValues.indexOf(key) !== -1;\n};\n","'use strict';\n\nconst values = require('../helpers/values');\n\nmodule.exports = function every(fn) {\n const items = values(this.items);\n\n return items.every(fn);\n};\n","'use strict';\n\n/**\n * Variadic helper function\n *\n * @param args\n * @returns {Array}\n */\nmodule.exports = function variadic(args) {\n if (Array.isArray(args[0])) {\n return args[0];\n }\n\n return args;\n};\n","'use strict';\n\nconst variadic = require('../helpers/variadic');\n\nmodule.exports = function except(...args) {\n const properties = variadic(args);\n\n if (Array.isArray(this.items)) {\n const collection = this.items\n .filter(item => properties.indexOf(item) === -1);\n\n return new this.constructor(collection);\n }\n\n const collection = {};\n\n Object.keys(this.items).forEach((property) => {\n if (properties.indexOf(property) === -1) {\n collection[property] = this.items[property];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nfunction falsyValue(item) {\n if (Array.isArray(item)) {\n if (item.length) {\n return false;\n }\n } else if (item !== undefined && item !== null\n && typeof item === 'object') {\n if (Object.keys(item).length) {\n return false;\n }\n } else if (item) {\n return false;\n }\n\n return true;\n}\n\nfunction filterObject(func, items) {\n const result = {};\n Object.keys(items).forEach((key) => {\n if (func) {\n if (func(items[key], key)) {\n result[key] = items[key];\n }\n } else if (!falsyValue(items[key])) {\n result[key] = items[key];\n }\n });\n\n return result;\n}\n\nfunction filterArray(func, items) {\n if (func) {\n return items.filter(func);\n }\n const result = [];\n for (let i = 0; i < items.length; i += 1) {\n const item = items[i];\n if (!falsyValue(item)) {\n result.push(item);\n }\n }\n\n return result;\n}\n\nmodule.exports = function filter(fn) {\n const func = fn || false;\n let filteredItems = null;\n if (Array.isArray(this.items)) {\n filteredItems = filterArray(func, this.items);\n } else {\n filteredItems = filterObject(func, this.items);\n }\n\n return new this.constructor(filteredItems);\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function first(fn, defaultValue) {\n if (isFunction(fn)) {\n const keys = Object.keys(this.items);\n\n for (let i = 0; i < keys.length; i += 1) {\n const key = keys[i];\n const item = this.items[key];\n\n if (fn(item, key)) {\n return item;\n }\n }\n\n if (isFunction(defaultValue)) {\n return defaultValue();\n }\n\n return defaultValue;\n }\n\n if ((Array.isArray(this.items) && this.items.length) || (Object.keys(this.items).length)) {\n if (Array.isArray(this.items)) {\n return this.items[0];\n }\n\n const firstKey = Object.keys(this.items)[0];\n\n return this.items[firstKey];\n }\n\n if (isFunction(defaultValue)) {\n return defaultValue();\n }\n\n return defaultValue;\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function firstOrFail(key, operator, value) {\n if (isFunction(key)) {\n return this.first(key, () => {\n throw new Error('Item not found.');\n });\n }\n\n const collection = this.where(key, operator, value);\n\n if (collection.isEmpty()) {\n throw new Error('Item not found.');\n }\n\n return collection.first();\n};\n","'use strict';\n\nconst { isArray, isObject } = require('../helpers/is');\n\nmodule.exports = function flatten(depth) {\n let flattenDepth = depth || Infinity;\n\n let fullyFlattened = false;\n let collection = [];\n\n const flat = function flat(items) {\n collection = [];\n\n if (isArray(items)) {\n items.forEach((item) => {\n if (isArray(item)) {\n collection = collection.concat(item);\n } else if (isObject(item)) {\n Object.keys(item).forEach((property) => {\n collection = collection.concat(item[property]);\n });\n } else {\n collection.push(item);\n }\n });\n } else {\n Object.keys(items).forEach((property) => {\n if (isArray(items[property])) {\n collection = collection.concat(items[property]);\n } else if (isObject(items[property])) {\n Object.keys(items[property]).forEach((prop) => {\n collection = collection.concat(items[property][prop]);\n });\n } else {\n collection.push(items[property]);\n }\n });\n }\n\n fullyFlattened = collection.filter(item => isObject(item));\n fullyFlattened = fullyFlattened.length === 0;\n\n flattenDepth -= 1;\n };\n\n flat(this.items);\n\n while (!fullyFlattened && flattenDepth > 0) {\n flat(collection);\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function get(key, defaultValue = null) {\n if (this.items[key] !== undefined) {\n return this.items[key];\n }\n\n if (isFunction(defaultValue)) {\n return defaultValue();\n }\n\n if (defaultValue !== null) {\n return defaultValue;\n }\n\n return null;\n};\n","'use strict';\n\n/**\n * Get value of a nested property\n *\n * @param mainObject\n * @param key\n * @returns {*}\n */\nmodule.exports = function nestedValue(mainObject, key) {\n try {\n return key.split('.').reduce((obj, property) => obj[property], mainObject);\n } catch (err) {\n // If we end up here, we're not working with an object, and @var mainObject is the value itself\n return mainObject;\n }\n};\n","'use strict';\n\nconst nestedValue = require('../helpers/nestedValue');\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function groupBy(key) {\n const collection = {};\n\n this.items.forEach((item, index) => {\n let resolvedKey;\n\n if (isFunction(key)) {\n resolvedKey = key(item, index);\n } else if (nestedValue(item, key) || nestedValue(item, key) === 0) {\n resolvedKey = nestedValue(item, key);\n } else {\n resolvedKey = '';\n }\n\n if (collection[resolvedKey] === undefined) {\n collection[resolvedKey] = new this.constructor([]);\n }\n\n collection[resolvedKey].push(item);\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst variadic = require('../helpers/variadic');\n\nmodule.exports = function has(...args) {\n const properties = variadic(args);\n\n return properties.filter(key => Object.hasOwnProperty.call(this.items, key)).length\n === properties.length;\n};\n","'use strict';\n\nconst nestedValue = require('../helpers/nestedValue');\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function keyBy(key) {\n const collection = {};\n\n if (isFunction(key)) {\n this.items.forEach((item) => {\n collection[key(item)] = item;\n });\n } else {\n this.items.forEach((item) => {\n const keyValue = nestedValue(item, key);\n\n collection[keyValue || ''] = item;\n });\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function last(fn, defaultValue) {\n let { items } = this;\n\n if (isFunction(fn)) {\n items = this.filter(fn).all();\n }\n\n if ((Array.isArray(items) && !items.length) || (!Object.keys(items).length)) {\n if (isFunction(defaultValue)) {\n return defaultValue();\n }\n\n return defaultValue;\n }\n\n if (Array.isArray(items)) {\n return items[items.length - 1];\n }\n const keys = Object.keys(items);\n\n return items[keys[keys.length - 1]];\n};\n","'use strict';\n\nconst values = require('../helpers/values');\n\nmodule.exports = function nth(n, offset = 0) {\n const items = values(this.items);\n\n const collection = items\n .slice(offset)\n .filter((item, index) => index % n === 0);\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst variadic = require('../helpers/variadic');\n\nmodule.exports = function only(...args) {\n const properties = variadic(args);\n\n if (Array.isArray(this.items)) {\n const collection = this.items\n .filter(item => properties.indexOf(item) !== -1);\n\n return new this.constructor(collection);\n }\n\n const collection = {};\n\n Object.keys(this.items).forEach((prop) => {\n if (properties.indexOf(prop) !== -1) {\n collection[prop] = this.items[prop];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst clone = require('../helpers/clone');\n\nmodule.exports = function pad(size, value) {\n const abs = Math.abs(size);\n const count = this.count();\n\n if (abs <= count) {\n return this;\n }\n\n let diff = abs - count;\n const items = clone(this.items);\n const isArray = Array.isArray(this.items);\n const prepend = size < 0;\n\n for (let iterator = 0; iterator < diff;) {\n if (!isArray) {\n if (items[iterator] !== undefined) {\n diff += 1;\n } else {\n items[iterator] = value;\n }\n } else if (prepend) {\n items.unshift(value);\n } else {\n items.push(value);\n }\n\n iterator += 1;\n }\n\n return new this.constructor(items);\n};\n","'use strict';\n\nconst { isArray, isObject } = require('../helpers/is');\nconst nestedValue = require('../helpers/nestedValue');\n\nconst buildKeyPathMap = function buildKeyPathMap(items) {\n const keyPaths = {};\n\n items.forEach((item, index) => {\n function buildKeyPath(val, keyPath) {\n if (isObject(val)) {\n Object.keys(val).forEach((prop) => {\n buildKeyPath(val[prop], `${keyPath}.${prop}`);\n });\n } else if (isArray(val)) {\n val.forEach((v, i) => {\n buildKeyPath(v, `${keyPath}.${i}`);\n });\n }\n\n keyPaths[keyPath] = val;\n }\n\n buildKeyPath(item, index);\n });\n\n return keyPaths;\n};\n\nmodule.exports = function pluck(value, key) {\n if (value.indexOf('*') !== -1) {\n const keyPathMap = buildKeyPathMap(this.items);\n\n const keyMatches = [];\n\n if (key !== undefined) {\n const keyRegex = new RegExp(`0.${key}`, 'g');\n const keyNumberOfLevels = `0.${key}`.split('.').length;\n\n Object.keys(keyPathMap).forEach((k) => {\n const matchingKey = k.match(keyRegex);\n\n if (matchingKey) {\n const match = matchingKey[0];\n\n if (match.split('.').length === keyNumberOfLevels) {\n keyMatches.push(keyPathMap[match]);\n }\n }\n });\n }\n\n const valueMatches = [];\n const valueRegex = new RegExp(`0.${value}`, 'g');\n const valueNumberOfLevels = `0.${value}`.split('.').length;\n\n\n Object.keys(keyPathMap).forEach((k) => {\n const matchingValue = k.match(valueRegex);\n\n if (matchingValue) {\n const match = matchingValue[0];\n\n if (match.split('.').length === valueNumberOfLevels) {\n valueMatches.push(keyPathMap[match]);\n }\n }\n });\n\n if (key !== undefined) {\n const collection = {};\n\n this.items.forEach((item, index) => {\n collection[keyMatches[index] || ''] = valueMatches;\n });\n\n return new this.constructor(collection);\n }\n\n return new this.constructor([valueMatches]);\n }\n\n if (key !== undefined) {\n const collection = {};\n\n this.items.forEach((item) => {\n if (nestedValue(item, value) !== undefined) {\n collection[item[key] || ''] = nestedValue(item, value);\n } else {\n collection[item[key] || ''] = null;\n }\n });\n\n return new this.constructor(collection);\n }\n\n return this.map((item) => {\n if (nestedValue(item, value) !== undefined) {\n return nestedValue(item, value);\n }\n\n return null;\n });\n};\n","'use strict';\n\nconst variadic = require('./variadic');\n\n/**\n * Delete keys helper\n *\n * Delete one or multiple keys from an object\n *\n * @param obj\n * @param keys\n * @returns {void}\n */\nmodule.exports = function deleteKeys(obj, ...keys) {\n variadic(keys).forEach((key) => {\n // eslint-disable-next-line\n delete obj[key];\n });\n};\n","'use strict';\n\nconst { isArray, isObject } = require('../helpers/is');\nconst deleteKeys = require('../helpers/deleteKeys');\n\nmodule.exports = function pop(count = 1) {\n if (this.isEmpty()) {\n return null;\n }\n\n if (isArray(this.items)) {\n if (count === 1) {\n return this.items.pop();\n }\n\n return new this.constructor(this.items.splice(-count));\n }\n\n if (isObject(this.items)) {\n const keys = Object.keys(this.items);\n\n if (count === 1) {\n const key = keys[keys.length - 1];\n const last = this.items[key];\n\n deleteKeys(this.items, key);\n\n return last;\n }\n\n const poppedKeys = keys.slice(-count);\n\n const newObject = poppedKeys.reduce((acc, current) => {\n acc[current] = this.items[current];\n\n return acc;\n }, {});\n\n deleteKeys(this.items, poppedKeys);\n\n return new this.constructor(newObject);\n }\n\n return null;\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function pull(key, defaultValue) {\n let returnValue = this.items[key] || null;\n\n if (!returnValue && defaultValue !== undefined) {\n if (isFunction(defaultValue)) {\n returnValue = defaultValue();\n } else {\n returnValue = defaultValue;\n }\n }\n\n delete this.items[key];\n\n return returnValue;\n};\n","'use strict';\n\nconst values = require('../helpers/values');\n\nmodule.exports = function random(length = null) {\n const items = values(this.items);\n\n const collection = new this.constructor(items).shuffle();\n\n // If not a length was specified\n if (length !== parseInt(length, 10)) {\n return collection.first();\n }\n\n return collection.take(length);\n};\n","'use strict';\n\n/* eslint-disable eqeqeq */\n\nconst { isArray, isObject, isFunction } = require('../helpers/is');\n\nmodule.exports = function search(valueOrFunction, strict) {\n let result;\n\n const find = (item, key) => {\n if (isFunction(valueOrFunction)) {\n return valueOrFunction(this.items[key], key);\n }\n\n if (strict) {\n return this.items[key] === valueOrFunction;\n }\n\n return this.items[key] == valueOrFunction;\n };\n\n if (isArray(this.items)) {\n result = this.items.findIndex(find);\n } else if (isObject(this.items)) {\n result = Object.keys(this.items).find(key => find(this.items[key], key));\n }\n\n if (result === undefined || result < 0) {\n return false;\n }\n\n return result;\n};\n","'use strict';\n\nconst { isArray, isObject } = require('../helpers/is');\nconst deleteKeys = require('../helpers/deleteKeys');\n\nmodule.exports = function shift(count = 1) {\n if (this.isEmpty()) {\n return null;\n }\n\n if (isArray(this.items)) {\n if (count === 1) {\n return this.items.shift();\n }\n\n return new this.constructor(this.items.splice(0, count));\n }\n\n if (isObject(this.items)) {\n if (count === 1) {\n const key = Object.keys(this.items)[0];\n const value = this.items[key];\n delete this.items[key];\n\n return value;\n }\n\n const keys = Object.keys(this.items);\n const poppedKeys = keys.slice(0, count);\n\n const newObject = poppedKeys.reduce((acc, current) => {\n acc[current] = this.items[current];\n\n return acc;\n }, {});\n\n deleteKeys(this.items, poppedKeys);\n\n return new this.constructor(newObject);\n }\n\n return null;\n};\n","'use strict';\n\nconst values = require('../helpers/values');\n\nmodule.exports = function shuffle() {\n const items = values(this.items);\n\n let j;\n let x;\n let i;\n\n for (i = items.length; i; i -= 1) {\n j = Math.floor(Math.random() * i);\n x = items[i - 1];\n items[i - 1] = items[j];\n items[j] = x;\n }\n\n this.items = items;\n\n return this;\n};\n","'use strict';\n\nconst { isObject } = require('../helpers/is');\n\nmodule.exports = function skip(number) {\n if (isObject(this.items)) {\n return new this.constructor(\n Object.keys(this.items)\n .reduce((accumulator, key, index) => {\n if ((index + 1) > number) {\n accumulator[key] = this.items[key];\n }\n\n return accumulator;\n }, {}),\n );\n }\n\n return new this.constructor(this.items.slice(number));\n};\n","'use strict';\n\nconst { isArray, isObject, isFunction } = require('../helpers/is');\n\nmodule.exports = function skipUntil(valueOrFunction) {\n let previous = null;\n let items;\n\n let callback = value => value === valueOrFunction;\n if (isFunction(valueOrFunction)) {\n callback = valueOrFunction;\n }\n\n if (isArray(this.items)) {\n items = this.items.filter((item) => {\n if (previous !== true) {\n previous = callback(item);\n }\n\n return previous;\n });\n }\n\n if (isObject(this.items)) {\n items = Object.keys(this.items).reduce((acc, key) => {\n if (previous !== true) {\n previous = callback(this.items[key]);\n }\n\n if (previous !== false) {\n acc[key] = this.items[key];\n }\n\n return acc;\n }, {});\n }\n\n return new this.constructor(items);\n};\n","'use strict';\n\nconst { isArray, isObject, isFunction } = require('../helpers/is');\n\nmodule.exports = function skipWhile(valueOrFunction) {\n let previous = null;\n let items;\n\n let callback = value => value === valueOrFunction;\n if (isFunction(valueOrFunction)) {\n callback = valueOrFunction;\n }\n\n if (isArray(this.items)) {\n items = this.items.filter((item) => {\n if (previous !== true) {\n previous = !callback(item);\n }\n\n return previous;\n });\n }\n\n if (isObject(this.items)) {\n items = Object.keys(this.items).reduce((acc, key) => {\n if (previous !== true) {\n previous = !callback(this.items[key]);\n }\n\n if (previous !== false) {\n acc[key] = this.items[key];\n }\n\n return acc;\n }, {});\n }\n\n return new this.constructor(items);\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function sole(key, operator, value) {\n let collection;\n\n if (isFunction(key)) {\n collection = this.filter(key);\n } else {\n collection = this.where(key, operator, value);\n }\n\n if (collection.isEmpty()) {\n throw new Error('Item not found.');\n }\n\n if (collection.count() > 1) {\n throw new Error('Multiple items found.');\n }\n\n return collection.first();\n};\n","'use strict';\n\nconst contains = require('./contains');\n\nmodule.exports = contains;\n","'use strict';\n\nconst nestedValue = require('../helpers/nestedValue');\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function sortBy(valueOrFunction) {\n const collection = [].concat(this.items);\n const getValue = (item) => {\n if (isFunction(valueOrFunction)) {\n return valueOrFunction(item);\n }\n\n return nestedValue(item, valueOrFunction);\n };\n\n collection.sort((a, b) => {\n const valueA = getValue(a);\n const valueB = getValue(b);\n\n if (valueA === null || valueA === undefined) {\n return 1;\n }\n if (valueB === null || valueB === undefined) {\n return -1;\n }\n\n if (valueA < valueB) {\n return -1;\n }\n if (valueA > valueB) {\n return 1;\n }\n\n return 0;\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst values = require('../helpers/values');\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function sum(key) {\n const items = values(this.items);\n\n let total = 0;\n\n if (key === undefined) {\n for (let i = 0, { length } = items; i < length; i += 1) {\n total += parseFloat(items[i]);\n }\n } else if (isFunction(key)) {\n for (let i = 0, { length } = items; i < length; i += 1) {\n total += parseFloat(key(items[i]));\n }\n } else {\n for (let i = 0, { length } = items; i < length; i += 1) {\n total += parseFloat(items[i][key]);\n }\n }\n\n\n return parseFloat(total.toPrecision(12));\n};\n","'use strict';\n\nconst { isArray, isObject, isFunction } = require('../helpers/is');\n\nmodule.exports = function takeUntil(valueOrFunction) {\n let previous = null;\n let items;\n\n let callback = value => value === valueOrFunction;\n if (isFunction(valueOrFunction)) {\n callback = valueOrFunction;\n }\n\n if (isArray(this.items)) {\n items = this.items.filter((item) => {\n if (previous !== false) {\n previous = !callback(item);\n }\n\n return previous;\n });\n }\n\n if (isObject(this.items)) {\n items = Object.keys(this.items).reduce((acc, key) => {\n if (previous !== false) {\n previous = !callback(this.items[key]);\n }\n\n if (previous !== false) {\n acc[key] = this.items[key];\n }\n\n return acc;\n }, {});\n }\n\n return new this.constructor(items);\n};\n","'use strict';\n\nconst { isArray, isObject, isFunction } = require('../helpers/is');\n\nmodule.exports = function takeWhile(valueOrFunction) {\n let previous = null;\n let items;\n\n let callback = value => value === valueOrFunction;\n if (isFunction(valueOrFunction)) {\n callback = valueOrFunction;\n }\n\n if (isArray(this.items)) {\n items = this.items.filter((item) => {\n if (previous !== false) {\n previous = callback(item);\n }\n\n return previous;\n });\n }\n\n if (isObject(this.items)) {\n items = Object.keys(this.items).reduce((acc, key) => {\n if (previous !== false) {\n previous = callback(this.items[key]);\n }\n\n if (previous !== false) {\n acc[key] = this.items[key];\n }\n\n return acc;\n }, {});\n }\n\n return new this.constructor(items);\n};\n","'use strict';\n\nmodule.exports = function whenNotEmpty(fn, defaultFn) {\n if (Array.isArray(this.items) && this.items.length) {\n return fn(this);\n } if (Object.keys(this.items).length) {\n return fn(this);\n }\n\n if (defaultFn !== undefined) {\n if (Array.isArray(this.items) && !this.items.length) {\n return defaultFn(this);\n } if (!Object.keys(this.items).length) {\n return defaultFn(this);\n }\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function whenEmpty(fn, defaultFn) {\n if (Array.isArray(this.items) && !this.items.length) {\n return fn(this);\n } if (!Object.keys(this.items).length) {\n return fn(this);\n }\n\n if (defaultFn !== undefined) {\n if (Array.isArray(this.items) && this.items.length) {\n return defaultFn(this);\n } if (Object.keys(this.items).length) {\n return defaultFn(this);\n }\n }\n\n return this;\n};\n","'use strict';\n\nconst { isFunction } = require('../helpers/is');\n\nmodule.exports = function unique(key) {\n let collection;\n\n if (key === undefined) {\n collection = this.items\n .filter((element, index, self) => self.indexOf(element) === index);\n } else {\n collection = [];\n\n const usedKeys = [];\n\n for (let iterator = 0, { length } = this.items;\n iterator < length; iterator += 1) {\n let uniqueKey;\n if (isFunction(key)) {\n uniqueKey = key(this.items[iterator]);\n } else {\n uniqueKey = this.items[iterator][key];\n }\n\n if (usedKeys.indexOf(uniqueKey) === -1) {\n collection.push(this.items[iterator]);\n usedKeys.push(uniqueKey);\n }\n }\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst getValues = require('../helpers/values');\n\nmodule.exports = function values() {\n return new this.constructor(getValues(this.items));\n};\n","'use strict';\n\nconst values = require('../helpers/values');\nconst nestedValue = require('../helpers/nestedValue');\n\nmodule.exports = function where(key, operator, value) {\n let comparisonOperator = operator;\n let comparisonValue = value;\n\n const items = values(this.items);\n\n if (operator === undefined || operator === true) {\n return new this.constructor(items.filter(item => nestedValue(item, key)));\n }\n\n if (operator === false) {\n return new this.constructor(items.filter(item => !nestedValue(item, key)));\n }\n\n if (value === undefined) {\n comparisonValue = operator;\n comparisonOperator = '===';\n }\n\n const collection = items.filter((item) => {\n switch (comparisonOperator) {\n case '==':\n return nestedValue(item, key) === Number(comparisonValue)\n || nestedValue(item, key) === comparisonValue.toString();\n\n default:\n case '===':\n return nestedValue(item, key) === comparisonValue;\n\n case '!=':\n case '<>':\n return nestedValue(item, key) !== Number(comparisonValue)\n && nestedValue(item, key) !== comparisonValue.toString();\n\n case '!==':\n return nestedValue(item, key) !== comparisonValue;\n\n case '<':\n return nestedValue(item, key) < comparisonValue;\n\n case '<=':\n return nestedValue(item, key) <= comparisonValue;\n\n case '>':\n return nestedValue(item, key) > comparisonValue;\n\n case '>=':\n return nestedValue(item, key) >= comparisonValue;\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst extractValues = require('../helpers/values');\nconst nestedValue = require('../helpers/nestedValue');\n\nmodule.exports = function whereIn(key, values) {\n const items = extractValues(values);\n\n const collection = this.items\n .filter(item => items.indexOf(nestedValue(item, key)) !== -1);\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nconst nestedValue = require('../helpers/nestedValue');\n\nmodule.exports = function whereNotBetween(key, values) {\n return this.filter(item => (\n nestedValue(item, key) < values[0] || nestedValue(item, key) > values[values.length - 1]\n ));\n};\n","'use strict';\n\nconst extractValues = require('../helpers/values');\nconst nestedValue = require('../helpers/nestedValue');\n\nmodule.exports = function whereNotIn(key, values) {\n const items = extractValues(values);\n\n const collection = this.items\n .filter(item => items.indexOf(nestedValue(item, key)) === -1);\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nfunction Collection(collection) {\n if (collection !== undefined && !Array.isArray(collection) && typeof collection !== 'object') {\n this.items = [collection];\n } else if (collection instanceof this.constructor) {\n this.items = collection.all();\n } else {\n this.items = collection || [];\n }\n}\n\n/**\n * Symbol.iterator\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator\n */\nconst SymbolIterator = require('./methods/symbol.iterator');\n\nif (typeof Symbol !== 'undefined') {\n Collection.prototype[Symbol.iterator] = SymbolIterator;\n}\n\n/**\n * Support JSON.stringify\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify\n */\nCollection.prototype.toJSON = function toJSON() {\n return this.items;\n};\n\nCollection.prototype.all = require('./methods/all');\nCollection.prototype.average = require('./methods/average');\nCollection.prototype.avg = require('./methods/avg');\nCollection.prototype.chunk = require('./methods/chunk');\nCollection.prototype.collapse = require('./methods/collapse');\nCollection.prototype.combine = require('./methods/combine');\nCollection.prototype.concat = require('./methods/concat');\nCollection.prototype.contains = require('./methods/contains');\nCollection.prototype.containsOneItem = require('./methods/containsOneItem');\nCollection.prototype.count = require('./methods/count');\nCollection.prototype.countBy = require('./methods/countBy');\nCollection.prototype.crossJoin = require('./methods/crossJoin');\nCollection.prototype.dd = require('./methods/dd');\nCollection.prototype.diff = require('./methods/diff');\nCollection.prototype.diffAssoc = require('./methods/diffAssoc');\nCollection.prototype.diffKeys = require('./methods/diffKeys');\nCollection.prototype.diffUsing = require('./methods/diffUsing');\nCollection.prototype.doesntContain = require('./methods/doesntContain');\nCollection.prototype.dump = require('./methods/dump');\nCollection.prototype.duplicates = require('./methods/duplicates');\nCollection.prototype.each = require('./methods/each');\nCollection.prototype.eachSpread = require('./methods/eachSpread');\nCollection.prototype.every = require('./methods/every');\nCollection.prototype.except = require('./methods/except');\nCollection.prototype.filter = require('./methods/filter');\nCollection.prototype.first = require('./methods/first');\nCollection.prototype.firstOrFail = require('./methods/firstOrFail');\nCollection.prototype.firstWhere = require('./methods/firstWhere');\nCollection.prototype.flatMap = require('./methods/flatMap');\nCollection.prototype.flatten = require('./methods/flatten');\nCollection.prototype.flip = require('./methods/flip');\nCollection.prototype.forPage = require('./methods/forPage');\nCollection.prototype.forget = require('./methods/forget');\nCollection.prototype.get = require('./methods/get');\nCollection.prototype.groupBy = require('./methods/groupBy');\nCollection.prototype.has = require('./methods/has');\nCollection.prototype.implode = require('./methods/implode');\nCollection.prototype.intersect = require('./methods/intersect');\nCollection.prototype.intersectByKeys = require('./methods/intersectByKeys');\nCollection.prototype.isEmpty = require('./methods/isEmpty');\nCollection.prototype.isNotEmpty = require('./methods/isNotEmpty');\nCollection.prototype.join = require('./methods/join');\nCollection.prototype.keyBy = require('./methods/keyBy');\nCollection.prototype.keys = require('./methods/keys');\nCollection.prototype.last = require('./methods/last');\nCollection.prototype.macro = require('./methods/macro');\nCollection.prototype.make = require('./methods/make');\nCollection.prototype.map = require('./methods/map');\nCollection.prototype.mapSpread = require('./methods/mapSpread');\nCollection.prototype.mapToDictionary = require('./methods/mapToDictionary');\nCollection.prototype.mapInto = require('./methods/mapInto');\nCollection.prototype.mapToGroups = require('./methods/mapToGroups');\nCollection.prototype.mapWithKeys = require('./methods/mapWithKeys');\nCollection.prototype.max = require('./methods/max');\nCollection.prototype.median = require('./methods/median');\nCollection.prototype.merge = require('./methods/merge');\nCollection.prototype.mergeRecursive = require('./methods/mergeRecursive');\nCollection.prototype.min = require('./methods/min');\nCollection.prototype.mode = require('./methods/mode');\nCollection.prototype.nth = require('./methods/nth');\nCollection.prototype.only = require('./methods/only');\nCollection.prototype.pad = require('./methods/pad');\nCollection.prototype.partition = require('./methods/partition');\nCollection.prototype.pipe = require('./methods/pipe');\nCollection.prototype.pluck = require('./methods/pluck');\nCollection.prototype.pop = require('./methods/pop');\nCollection.prototype.prepend = require('./methods/prepend');\nCollection.prototype.pull = require('./methods/pull');\nCollection.prototype.push = require('./methods/push');\nCollection.prototype.put = require('./methods/put');\nCollection.prototype.random = require('./methods/random');\nCollection.prototype.reduce = require('./methods/reduce');\nCollection.prototype.reject = require('./methods/reject');\nCollection.prototype.replace = require('./methods/replace');\nCollection.prototype.replaceRecursive = require('./methods/replaceRecursive');\nCollection.prototype.reverse = require('./methods/reverse');\nCollection.prototype.search = require('./methods/search');\nCollection.prototype.shift = require('./methods/shift');\nCollection.prototype.shuffle = require('./methods/shuffle');\nCollection.prototype.skip = require('./methods/skip');\nCollection.prototype.skipUntil = require('./methods/skipUntil');\nCollection.prototype.skipWhile = require('./methods/skipWhile');\nCollection.prototype.slice = require('./methods/slice');\nCollection.prototype.sole = require('./methods/sole');\nCollection.prototype.some = require('./methods/some');\nCollection.prototype.sort = require('./methods/sort');\nCollection.prototype.sortDesc = require('./methods/sortDesc');\nCollection.prototype.sortBy = require('./methods/sortBy');\nCollection.prototype.sortByDesc = require('./methods/sortByDesc');\nCollection.prototype.sortKeys = require('./methods/sortKeys');\nCollection.prototype.sortKeysDesc = require('./methods/sortKeysDesc');\nCollection.prototype.splice = require('./methods/splice');\nCollection.prototype.split = require('./methods/split');\nCollection.prototype.sum = require('./methods/sum');\nCollection.prototype.take = require('./methods/take');\nCollection.prototype.takeUntil = require('./methods/takeUntil');\nCollection.prototype.takeWhile = require('./methods/takeWhile');\nCollection.prototype.tap = require('./methods/tap');\nCollection.prototype.times = require('./methods/times');\nCollection.prototype.toArray = require('./methods/toArray');\nCollection.prototype.toJson = require('./methods/toJson');\nCollection.prototype.transform = require('./methods/transform');\nCollection.prototype.undot = require('./methods/undot');\nCollection.prototype.unless = require('./methods/unless');\nCollection.prototype.unlessEmpty = require('./methods/whenNotEmpty');\nCollection.prototype.unlessNotEmpty = require('./methods/whenEmpty');\nCollection.prototype.union = require('./methods/union');\nCollection.prototype.unique = require('./methods/unique');\nCollection.prototype.unwrap = require('./methods/unwrap');\nCollection.prototype.values = require('./methods/values');\nCollection.prototype.when = require('./methods/when');\nCollection.prototype.whenEmpty = require('./methods/whenEmpty');\nCollection.prototype.whenNotEmpty = require('./methods/whenNotEmpty');\nCollection.prototype.where = require('./methods/where');\nCollection.prototype.whereBetween = require('./methods/whereBetween');\nCollection.prototype.whereIn = require('./methods/whereIn');\nCollection.prototype.whereInstanceOf = require('./methods/whereInstanceOf');\nCollection.prototype.whereNotBetween = require('./methods/whereNotBetween');\nCollection.prototype.whereNotIn = require('./methods/whereNotIn');\nCollection.prototype.whereNull = require('./methods/whereNull');\nCollection.prototype.whereNotNull = require('./methods/whereNotNull');\nCollection.prototype.wrap = require('./methods/wrap');\nCollection.prototype.zip = require('./methods/zip');\n\nconst collect = collection => new Collection(collection);\n\nmodule.exports = collect;\nmodule.exports.collect = collect;\nmodule.exports.default = collect;\nmodule.exports.Collection = Collection;\n","'use strict';\n\nmodule.exports = function SymbolIterator() {\n let index = -1;\n\n return {\n next: () => {\n index += 1;\n\n return {\n value: this.items[index],\n done: index >= this.items.length,\n };\n },\n };\n};\n","'use strict';\n\nmodule.exports = function all() {\n return this.items;\n};\n","'use strict';\n\nmodule.exports = function chunk(size) {\n const chunks = [];\n let index = 0;\n\n if (Array.isArray(this.items)) {\n do {\n const items = this.items.slice(index, index + size);\n const collection = new this.constructor(items);\n\n chunks.push(collection);\n index += size;\n } while (index < this.items.length);\n } else if (typeof this.items === 'object') {\n const keys = Object.keys(this.items);\n\n do {\n const keysOfChunk = keys.slice(index, index + size);\n const collection = new this.constructor({});\n\n keysOfChunk.forEach(key => collection.put(key, this.items[key]));\n\n chunks.push(collection);\n index += size;\n } while (index < keys.length);\n } else {\n chunks.push(new this.constructor([this.items]));\n }\n\n return new this.constructor(chunks);\n};\n","'use strict';\n\nmodule.exports = function collapse() {\n return new this.constructor([].concat(...this.items));\n};\n","'use strict';\n\nmodule.exports = function combine(array) {\n let values = array;\n\n if (values instanceof this.constructor) {\n values = array.all();\n }\n\n const collection = {};\n\n if (Array.isArray(this.items) && Array.isArray(values)) {\n this.items.forEach((key, iterator) => {\n collection[key] = values[iterator];\n });\n } else if (typeof this.items === 'object' && typeof values === 'object') {\n Object.keys(this.items).forEach((key, index) => {\n collection[this.items[key]] = values[Object.keys(values)[index]];\n });\n } else if (Array.isArray(this.items)) {\n collection[this.items[0]] = values;\n } else if (typeof this.items === 'string' && Array.isArray(values)) {\n [collection[this.items]] = values;\n } else if (typeof this.items === 'string') {\n collection[this.items] = values;\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function containsOneItem() {\n return this.count() === 1;\n};\n","'use strict';\n\nmodule.exports = function count() {\n let arrayLength = 0;\n\n if (Array.isArray(this.items)) {\n arrayLength = this.items.length;\n }\n\n return Math.max(Object.keys(this.items).length, arrayLength);\n};\n","'use strict';\n\nmodule.exports = function countBy(fn = value => value) {\n return new this.constructor(this.items)\n .groupBy(fn)\n .map(value => value.count());\n};\n","'use strict';\n\nmodule.exports = function crossJoin(...values) {\n function join(collection, constructor, args) {\n let current = args[0];\n\n if (current instanceof constructor) {\n current = current.all();\n }\n\n const rest = args.slice(1);\n const last = !rest.length;\n let result = [];\n\n for (let i = 0; i < current.length; i += 1) {\n const collectionCopy = collection.slice();\n collectionCopy.push(current[i]);\n\n if (last) {\n result.push(collectionCopy);\n } else {\n result = result.concat(join(collectionCopy, constructor, rest));\n }\n }\n\n return result;\n }\n\n return new this.constructor(join([], this.constructor, [].concat([this.items], values)));\n};\n","'use strict';\n\nmodule.exports = function dd() {\n this.dump();\n\n if (typeof process !== 'undefined') {\n process.exit(1);\n }\n};\n","'use strict';\n\nmodule.exports = function diff(values) {\n let valuesToDiff;\n\n if (values instanceof this.constructor) {\n valuesToDiff = values.all();\n } else {\n valuesToDiff = values;\n }\n\n const collection = this.items.filter(item => valuesToDiff.indexOf(item) === -1);\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function diffAssoc(values) {\n let diffValues = values;\n\n if (values instanceof this.constructor) {\n diffValues = values.all();\n }\n\n const collection = {};\n\n Object.keys(this.items).forEach((key) => {\n if (diffValues[key] === undefined || diffValues[key] !== this.items[key]) {\n collection[key] = this.items[key];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function diffKeys(object) {\n let objectToDiff;\n\n if (object instanceof this.constructor) {\n objectToDiff = object.all();\n } else {\n objectToDiff = object;\n }\n\n const objectKeys = Object.keys(objectToDiff);\n\n const remainingKeys = Object.keys(this.items)\n .filter(item => objectKeys.indexOf(item) === -1);\n\n return new this.constructor(this.items).only(\n remainingKeys,\n );\n};\n","'use strict';\n\nmodule.exports = function diffUsing(values, callback) {\n const collection = this.items.filter(item => (\n !(values && values.some(otherItem => callback(item, otherItem) === 0))\n ));\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function contains(key, value) {\n return !this.contains(key, value);\n};\n","'use strict';\n\nmodule.exports = function dump() {\n // eslint-disable-next-line\n console.log(this);\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function duplicates() {\n const occuredValues = [];\n const duplicateValues = {};\n\n const stringifiedValue = (value) => {\n if (Array.isArray(value) || typeof value === 'object') {\n return JSON.stringify(value);\n }\n\n return value;\n };\n\n if (Array.isArray(this.items)) {\n this.items.forEach((value, index) => {\n const valueAsString = stringifiedValue(value);\n\n if (occuredValues.indexOf(valueAsString) === -1) {\n occuredValues.push(valueAsString);\n } else {\n duplicateValues[index] = value;\n }\n });\n } else if (typeof this.items === 'object') {\n Object.keys(this.items).forEach((key) => {\n const valueAsString = stringifiedValue(this.items[key]);\n\n if (occuredValues.indexOf(valueAsString) === -1) {\n occuredValues.push(valueAsString);\n } else {\n duplicateValues[key] = this.items[key];\n }\n });\n }\n\n return new this.constructor(duplicateValues);\n};\n","'use strict';\n\nmodule.exports = function each(fn) {\n let stop = false;\n\n if (Array.isArray(this.items)) {\n const { length } = this.items;\n\n for (let index = 0; index < length && !stop; index += 1) {\n stop = fn(this.items[index], index, this.items) === false;\n }\n } else {\n const keys = Object.keys(this.items);\n const { length } = keys;\n\n for (let index = 0; index < length && !stop; index += 1) {\n const key = keys[index];\n\n stop = fn(this.items[key], key, this.items) === false;\n }\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function eachSpread(fn) {\n this.each((values, key) => {\n fn(...values, key);\n });\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function firstWhere(key, operator, value) {\n return this.where(key, operator, value).first() || null;\n};\n","'use strict';\n\nmodule.exports = function flatMap(fn) {\n return this.map(fn).collapse();\n};\n","'use strict';\n\nmodule.exports = function flip() {\n const collection = {};\n\n if (Array.isArray(this.items)) {\n Object.keys(this.items).forEach((key) => {\n collection[this.items[key]] = Number(key);\n });\n } else {\n Object.keys(this.items).forEach((key) => {\n collection[this.items[key]] = key;\n });\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function forPage(page, chunk) {\n let collection = {};\n\n if (Array.isArray(this.items)) {\n collection = this.items.slice((page * chunk) - chunk, page * chunk);\n } else {\n Object\n .keys(this.items)\n .slice((page * chunk) - chunk, page * chunk)\n .forEach((key) => {\n collection[key] = this.items[key];\n });\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function forget(key) {\n if (Array.isArray(this.items)) {\n this.items.splice(key, 1);\n } else {\n delete this.items[key];\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function implode(key, glue) {\n if (glue === undefined) {\n return this.items.join(key);\n }\n\n return new this.constructor(this.items).pluck(key).all().join(glue);\n};\n","'use strict';\n\nmodule.exports = function intersect(values) {\n let intersectValues = values;\n\n if (values instanceof this.constructor) {\n intersectValues = values.all();\n }\n\n const collection = this.items\n .filter(item => intersectValues.indexOf(item) !== -1);\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function intersectByKeys(values) {\n let intersectKeys = Object.keys(values);\n\n if (values instanceof this.constructor) {\n intersectKeys = Object.keys(values.all());\n }\n\n const collection = {};\n\n Object.keys(this.items).forEach((key) => {\n if (intersectKeys.indexOf(key) !== -1) {\n collection[key] = this.items[key];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function isEmpty() {\n if (Array.isArray(this.items)) {\n return !this.items.length;\n }\n\n return !Object.keys(this.items).length;\n};\n","'use strict';\n\nmodule.exports = function isNotEmpty() {\n return !this.isEmpty();\n};\n","'use strict';\n\nmodule.exports = function join(glue, finalGlue) {\n const collection = this.values();\n\n if (finalGlue === undefined) {\n return collection.implode(glue);\n }\n\n const count = collection.count();\n\n if (count === 0) {\n return '';\n }\n\n if (count === 1) {\n return collection.last();\n }\n\n const finalItem = collection.pop();\n\n return collection.implode(glue) + finalGlue + finalItem;\n};\n","'use strict';\n\nmodule.exports = function keys() {\n let collection = Object.keys(this.items);\n\n if (Array.isArray(this.items)) {\n collection = collection.map(Number);\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function macro(name, fn) {\n this.constructor.prototype[name] = fn;\n};\n","'use strict';\n\nmodule.exports = function make(items = []) {\n return new this.constructor(items);\n};\n","'use strict';\n\nmodule.exports = function map(fn) {\n if (Array.isArray(this.items)) {\n return new this.constructor(this.items.map(fn));\n }\n\n const collection = {};\n\n Object.keys(this.items).forEach((key) => {\n collection[key] = fn(this.items[key], key);\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function mapSpread(fn) {\n return this.map((values, key) => fn(...values, key));\n};\n","'use strict';\n\nmodule.exports = function mapToDictionary(fn) {\n const collection = {};\n\n this.items.forEach((item, k) => {\n const [key, value] = fn(item, k);\n\n if (collection[key] === undefined) {\n collection[key] = [value];\n } else {\n collection[key].push(value);\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function mapInto(ClassName) {\n return this.map((value, key) => new ClassName(value, key));\n};\n","'use strict';\n\nmodule.exports = function mapToGroups(fn) {\n const collection = {};\n\n this.items.forEach((item, key) => {\n const [keyed, value] = fn(item, key);\n\n if (collection[keyed] === undefined) {\n collection[keyed] = [value];\n } else {\n collection[keyed].push(value);\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function mapWithKeys(fn) {\n const collection = {};\n\n if (Array.isArray(this.items)) {\n this.items.forEach((item, index) => {\n const [keyed, value] = fn(item, index);\n collection[keyed] = value;\n });\n } else {\n Object.keys(this.items).forEach((key) => {\n const [keyed, value] = fn(this.items[key], key);\n collection[keyed] = value;\n });\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function max(key) {\n if (typeof key === 'string') {\n const filtered = this.items.filter(item => item[key] !== undefined);\n\n return Math.max(...filtered.map(item => item[key]));\n }\n\n return Math.max(...this.items);\n};\n","'use strict';\n\nmodule.exports = function median(key) {\n const { length } = this.items;\n\n if (key === undefined) {\n if (length % 2 === 0) {\n return (this.items[(length / 2) - 1] + this.items[length / 2]) / 2;\n }\n\n return this.items[Math.floor(length / 2)];\n }\n\n if (length % 2 === 0) {\n return (this.items[(length / 2) - 1][key]\n + this.items[length / 2][key]) / 2;\n }\n\n return this.items[Math.floor(length / 2)][key];\n};\n","'use strict';\n\nmodule.exports = function merge(value) {\n let arrayOrObject = value;\n\n if (typeof arrayOrObject === 'string') {\n arrayOrObject = [arrayOrObject];\n }\n\n if (Array.isArray(this.items) && Array.isArray(arrayOrObject)) {\n return new this.constructor(this.items.concat(arrayOrObject));\n }\n\n const collection = JSON.parse(JSON.stringify(this.items));\n\n Object.keys(arrayOrObject).forEach((key) => {\n collection[key] = arrayOrObject[key];\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function mergeRecursive(items) {\n const merge = (target, source) => {\n const merged = {};\n\n const mergedKeys = Object.keys({ ...target, ...source });\n\n mergedKeys.forEach((key) => {\n if (target[key] === undefined && source[key] !== undefined) {\n merged[key] = source[key];\n } else if (target[key] !== undefined && source[key] === undefined) {\n merged[key] = target[key];\n } else if (target[key] !== undefined && source[key] !== undefined) {\n if (target[key] === source[key]) {\n merged[key] = target[key];\n } else if (\n (!Array.isArray(target[key]) && typeof target[key] === 'object')\n && (!Array.isArray(source[key]) && typeof source[key] === 'object')\n ) {\n merged[key] = merge(target[key], source[key]);\n } else {\n merged[key] = [].concat(target[key], source[key]);\n }\n }\n });\n\n return merged;\n };\n\n if (!items) {\n return this;\n }\n\n if (items.constructor.name === 'Collection') {\n return new this.constructor(merge(this.items, items.all()));\n }\n\n return new this.constructor(merge(this.items, items));\n};\n","'use strict';\n\nmodule.exports = function min(key) {\n if (key !== undefined) {\n const filtered = this.items.filter(item => item[key] !== undefined);\n\n return Math.min(...filtered.map(item => item[key]));\n }\n\n return Math.min(...this.items);\n};\n","'use strict';\n\nmodule.exports = function mode(key) {\n const values = [];\n let highestCount = 1;\n\n if (!this.items.length) {\n return null;\n }\n\n this.items.forEach((item) => {\n const tempValues = values.filter((value) => {\n if (key !== undefined) {\n return value.key === item[key];\n }\n\n return value.key === item;\n });\n\n if (!tempValues.length) {\n if (key !== undefined) {\n values.push({ key: item[key], count: 1 });\n } else {\n values.push({ key: item, count: 1 });\n }\n } else {\n tempValues[0].count += 1;\n const { count } = tempValues[0];\n\n if (count > highestCount) {\n highestCount = count;\n }\n }\n });\n\n return values\n .filter(value => value.count === highestCount)\n .map(value => value.key);\n};\n","'use strict';\n\nmodule.exports = function partition(fn) {\n let arrays;\n\n if (Array.isArray(this.items)) {\n arrays = [new this.constructor([]), new this.constructor([])];\n\n this.items.forEach((item) => {\n if (fn(item) === true) {\n arrays[0].push(item);\n } else {\n arrays[1].push(item);\n }\n });\n } else {\n arrays = [new this.constructor({}), new this.constructor({})];\n\n Object.keys(this.items).forEach((prop) => {\n const value = this.items[prop];\n\n if (fn(value) === true) {\n arrays[0].put(prop, value);\n } else {\n arrays[1].put(prop, value);\n }\n });\n }\n\n return new this.constructor(arrays);\n};\n","'use strict';\n\nmodule.exports = function pipe(fn) {\n return fn(this);\n};\n","'use strict';\n\nmodule.exports = function prepend(value, key) {\n if (key !== undefined) {\n return this.put(key, value);\n }\n\n this.items.unshift(value);\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function push(...items) {\n this.items.push(...items);\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function put(key, value) {\n this.items[key] = value;\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function reduce(fn, carry) {\n let reduceCarry = null;\n\n if (carry !== undefined) {\n reduceCarry = carry;\n }\n\n if (Array.isArray(this.items)) {\n this.items.forEach((item) => {\n reduceCarry = fn(reduceCarry, item);\n });\n } else {\n Object.keys(this.items).forEach((key) => {\n reduceCarry = fn(reduceCarry, this.items[key], key);\n });\n }\n\n return reduceCarry;\n};\n","'use strict';\n\nmodule.exports = function reject(fn) {\n return new this.constructor(this.items).filter(item => !fn(item));\n};\n","'use strict';\n\nmodule.exports = function replace(items) {\n if (!items) {\n return this;\n }\n\n if (Array.isArray(items)) {\n const replaced = this.items.map((value, index) => items[index] || value);\n\n return new this.constructor(replaced);\n }\n\n if (items.constructor.name === 'Collection') {\n const replaced = { ...this.items, ...items.all() };\n\n return new this.constructor(replaced);\n }\n\n const replaced = { ...this.items, ...items };\n\n return new this.constructor(replaced);\n};\n","'use strict';\n\nmodule.exports = function replaceRecursive(items) {\n const replace = (target, source) => {\n const replaced = { ...target };\n\n const mergedKeys = Object.keys({ ...target, ...source });\n\n mergedKeys.forEach((key) => {\n if (!Array.isArray(source[key]) && typeof source[key] === 'object') {\n replaced[key] = replace(target[key], source[key]);\n } else if (target[key] === undefined && source[key] !== undefined) {\n if (typeof target[key] === 'object') {\n replaced[key] = { ...source[key] };\n } else {\n replaced[key] = source[key];\n }\n } else if (target[key] !== undefined && source[key] === undefined) {\n if (typeof target[key] === 'object') {\n replaced[key] = { ...target[key] };\n } else {\n replaced[key] = target[key];\n }\n } else if (target[key] !== undefined && source[key] !== undefined) {\n if (typeof source[key] === 'object') {\n replaced[key] = { ...source[key] };\n } else {\n replaced[key] = source[key];\n }\n }\n });\n\n return replaced;\n };\n\n if (!items) {\n return this;\n }\n\n if (!Array.isArray(items) && typeof items !== 'object') {\n return new this.constructor(replace(this.items, [items]));\n }\n\n if (items.constructor.name === 'Collection') {\n return new this.constructor(replace(this.items, items.all()));\n }\n\n return new this.constructor(replace(this.items, items));\n};\n","'use strict';\n\nmodule.exports = function reverse() {\n const collection = [].concat(this.items).reverse();\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function slice(remove, limit) {\n let collection = this.items.slice(remove);\n\n if (limit !== undefined) {\n collection = collection.slice(0, limit);\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function sort(fn) {\n const collection = [].concat(this.items);\n\n if (fn === undefined) {\n if (this.every(item => typeof item === 'number')) {\n collection.sort((a, b) => a - b);\n } else {\n collection.sort();\n }\n } else {\n collection.sort(fn);\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function sortDesc() {\n return this.sort().reverse();\n};\n","'use strict';\n\nmodule.exports = function sortByDesc(valueOrFunction) {\n return this.sortBy(valueOrFunction).reverse();\n};\n","'use strict';\n\nmodule.exports = function sortKeys() {\n const ordered = {};\n\n Object.keys(this.items).sort().forEach((key) => {\n ordered[key] = this.items[key];\n });\n\n return new this.constructor(ordered);\n};\n","'use strict';\n\nmodule.exports = function sortKeysDesc() {\n const ordered = {};\n\n Object.keys(this.items).sort().reverse().forEach((key) => {\n ordered[key] = this.items[key];\n });\n\n return new this.constructor(ordered);\n};\n","'use strict';\n\nmodule.exports = function splice(index, limit, replace) {\n const slicedCollection = this.slice(index, limit);\n\n this.items = this.diff(slicedCollection.all()).all();\n\n if (Array.isArray(replace)) {\n for (let iterator = 0, { length } = replace;\n iterator < length; iterator += 1) {\n this.items.splice(index + iterator, 0, replace[iterator]);\n }\n }\n\n return slicedCollection;\n};\n","'use strict';\n\nmodule.exports = function split(numberOfGroups) {\n const itemsPerGroup = Math.round(this.items.length / numberOfGroups);\n\n const items = JSON.parse(JSON.stringify(this.items));\n const collection = [];\n\n for (let iterator = 0; iterator < numberOfGroups; iterator += 1) {\n collection.push(new this.constructor(items.splice(0, itemsPerGroup)));\n }\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function take(length) {\n if (!Array.isArray(this.items) && typeof this.items === 'object') {\n const keys = Object.keys(this.items);\n let slicedKeys;\n\n if (length < 0) {\n slicedKeys = keys.slice(length);\n } else {\n slicedKeys = keys.slice(0, length);\n }\n\n const collection = {};\n\n keys.forEach((prop) => {\n if (slicedKeys.indexOf(prop) !== -1) {\n collection[prop] = this.items[prop];\n }\n });\n\n return new this.constructor(collection);\n }\n\n if (length < 0) {\n return new this.constructor(this.items.slice(length));\n }\n\n return new this.constructor(this.items.slice(0, length));\n};\n","'use strict';\n\nmodule.exports = function tap(fn) {\n fn(this);\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function times(n, fn) {\n for (let iterator = 1; iterator <= n; iterator += 1) {\n this.items.push(fn(iterator));\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function toArray() {\n const collectionInstance = this.constructor;\n\n function iterate(list, collection) {\n const childCollection = [];\n\n if (list instanceof collectionInstance) {\n list.items.forEach(i => iterate(i, childCollection));\n collection.push(childCollection);\n } else if (Array.isArray(list)) {\n list.forEach(i => iterate(i, childCollection));\n collection.push(childCollection);\n } else {\n collection.push(list);\n }\n }\n\n if (Array.isArray(this.items)) {\n const collection = [];\n\n this.items.forEach((items) => {\n iterate(items, collection);\n });\n\n return collection;\n }\n\n return this.values().all();\n};\n","'use strict';\n\nmodule.exports = function toJson() {\n if (typeof this.items === 'object' && !Array.isArray(this.items)) {\n return JSON.stringify(this.all());\n }\n\n return JSON.stringify(this.toArray());\n};\n","'use strict';\n\nmodule.exports = function transform(fn) {\n if (Array.isArray(this.items)) {\n this.items = this.items.map(fn);\n } else {\n const collection = {};\n\n Object.keys(this.items).forEach((key) => {\n collection[key] = fn(this.items[key], key);\n });\n\n this.items = collection;\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function undot() {\n if (Array.isArray(this.items)) {\n return this;\n }\n\n let collection = {};\n\n Object.keys(this.items).forEach((key) => {\n if (key.indexOf('.') !== -1) {\n const obj = collection;\n\n key.split('.').reduce((acc, current, index, array) => {\n if (!acc[current]) {\n acc[current] = {};\n }\n\n if ((index === array.length - 1)) {\n acc[current] = this.items[key];\n }\n\n return acc[current];\n }, obj);\n\n collection = { ...collection, ...obj };\n } else {\n collection[key] = this.items[key];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function when(value, fn, defaultFn) {\n if (!value) {\n fn(this);\n } else {\n defaultFn(this);\n }\n};\n","'use strict';\n\nmodule.exports = function union(object) {\n const collection = JSON.parse(JSON.stringify(this.items));\n\n Object.keys(object).forEach((prop) => {\n if (this.items[prop] === undefined) {\n collection[prop] = object[prop];\n }\n });\n\n return new this.constructor(collection);\n};\n","'use strict';\n\nmodule.exports = function unwrap(value) {\n if (value instanceof this.constructor) {\n return value.all();\n }\n\n return value;\n};\n","'use strict';\n\nmodule.exports = function when(value, fn, defaultFn) {\n if (value) {\n return fn(this, value);\n }\n\n if (defaultFn) {\n return defaultFn(this, value);\n }\n\n return this;\n};\n","'use strict';\n\nmodule.exports = function whereBetween(key, values) {\n return this.where(key, '>=', values[0]).where(key, '<=', values[values.length - 1]);\n};\n","'use strict';\n\nmodule.exports = function whereInstanceOf(type) {\n return this.filter(item => item instanceof type);\n};\n","'use strict';\n\nmodule.exports = function whereNull(key = null) {\n return this.where(key, '===', null);\n};\n","'use strict';\n\nmodule.exports = function whereNotNull(key = null) {\n return this.where(key, '!==', null);\n};\n","'use strict';\n\nmodule.exports = function wrap(value) {\n if (value instanceof this.constructor) {\n return value;\n }\n\n if (typeof value === 'object') {\n return new this.constructor(value);\n }\n\n return new this.constructor([value]);\n};\n","'use strict';\n\nmodule.exports = function zip(array) {\n let values = array;\n\n if (values instanceof this.constructor) {\n values = values.all();\n }\n\n const collection = this.items.map((item, index) => new this.constructor([item, values[index]]));\n\n return new this.constructor(collection);\n};\n"],"names":["is","isArray","item","Array","isObject","isFunction","require$$0","average","key","undefined","this","sum","items","length","constructor","pluck","avg","clone","cloned","push","Object","keys","forEach","prop","values","valuesArray","name","all","require$$1","contains","value","filter","index","indexOf","keysAndValues","variadic","args","falsyValue","nestedValue","mainObject","split","reduce","obj","property","err","deleteKeys","some","whenNotEmpty","fn","defaultFn","whenEmpty","getValues","extractValues","Collection","collection","Symbol","prototype","iterator","next","done","toJSON","require$$2","require$$3","chunk","size","chunks","slice","keysOfChunk","put","collapse","concat","combine","array","collectionOrArrayOrObject","list","require$$8","containsOneItem","count","arrayLength","Math","max","countBy","groupBy","map","crossJoin","join","current","rest","last","result","i","collectionCopy","dd","dump","process","exit","diff","valuesToDiff","diffAssoc","diffValues","diffKeys","object","objectToDiff","objectKeys","remainingKeys","only","diffUsing","callback","otherItem","doesntContain","console","log","duplicates","occuredValues","duplicateValues","stringifiedValue","JSON","stringify","valueAsString","each","stop","eachSpread","every","except","properties","func","filteredItems","filterArray","filterObject","first","defaultValue","firstKey","firstOrFail","operator","Error","where","isEmpty","firstWhere","flatMap","flatten","depth","flattenDepth","Infinity","fullyFlattened","flat","flip","Number","forPage","page","forget","splice","get","resolvedKey","has","hasOwnProperty","call","implode","glue","intersect","intersectValues","intersectByKeys","intersectKeys","isNotEmpty","finalGlue","finalItem","pop","keyBy","keyValue","macro","make","mapSpread","mapToDictionary","k","mapInto","ClassName","mapToGroups","keyed","mapWithKeys","filtered","median","floor","merge","arrayOrObject","parse","mergeRecursive","target","source","merged","min","mode","highestCount","tempValues","nth","n","offset","pad","abs","prepend","unshift","partition","arrays","pipe","keyPathMap","keyPaths","buildKeyPath","val","keyPath","v","buildKeyPathMap","keyMatches","keyRegex","RegExp","keyNumberOfLevels","matchingKey","match","valueMatches","valueRegex","valueNumberOfLevels","matchingValue","poppedKeys","newObject","acc","pull","returnValue","random","shuffle","parseInt","take","carry","reduceCarry","reject","replace","replaced","replaceRecursive","reverse","search","valueOrFunction","strict","find","findIndex","shift","j","x","skip","number","accumulator","skipUntil","previous","skipWhile","remove","limit","sole","require$$85","sort","a","b","sortDesc","sortBy","getValue","valueA","valueB","sortByDesc","sortKeys","ordered","sortKeysDesc","slicedCollection","numberOfGroups","itemsPerGroup","round","total","parseFloat","toPrecision","slicedKeys","takeUntil","takeWhile","tap","times","toArray","collectionInstance","iterate","childCollection","toJson","transform","undot","unless","unlessEmpty","require$$105","unlessNotEmpty","require$$106","union","unique","element","self","usedKeys","uniqueKey","unwrap","when","comparisonOperator","comparisonValue","toString","whereBetween","whereIn","whereInstanceOf","type","whereNotBetween","whereNotIn","whereNull","whereNotNull","wrap","zip","collect","srcModule","exports","collect_1","default"],"mappings":"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","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128]} \ No newline at end of file diff --git a/external/index.js b/external/index.js new file mode 100644 index 00000000..e63634f3 --- /dev/null +++ b/external/index.js @@ -0,0 +1,2 @@ +export { default as collect } from './collect.js'; +export { default as DOMPurify } from './DOMPurify.js'; \ No newline at end of file diff --git a/forien-quest-log.lock b/forien-quest-log.lock new file mode 100644 index 00000000..82ef623b --- /dev/null +++ b/forien-quest-log.lock @@ -0,0 +1 @@ +🔒 \ No newline at end of file diff --git a/lang/cn.json b/lang/cn.json index 08e107aa..be36f05a 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -1,201 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "新任务", - "QuestLogButton": "任务栏", - "Quests": "{0}任务", - "SampleReward": "例如:300点经验值", - "SampleTask": "例如:消灭鸦阁里所有的不死生物", - - "QuestTypes": { - "InProgress": "进行中", - "Completed": "已完成", - "Failed": "已失败", - "Hidden": "已隐藏", - "Labels": { - "available": "可选", - "active": "进行中", - "completed": "已完成", - "failed": "已失败", - "hidden": "已隐藏" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "打开 ”{name}” 任务" + }, + "Notifications": { + "NoQuest": "API 错误:任务ID无效,无法创建宏。" + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "新任务" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (任务奖励):{userName} 将 '{itemName}' 放到 {actorName} 上。" + } + }, + "Utils": { + "Notifications": { + "NoDocument": "无法加载实体 UUID:'{uuid}'。", + "NoPermission": "您没有足够的权限查看此实体表。" + } } }, - - "Buttons": { - "AddNewQuest": "加入新任务", - "AddNewTask": "加入新目标" + "DeleteDialog": { + "BodyObjective": "该目标及其数据将被永久删除。", + "BodyQuest": "此任务及其数据将被永久删除。", + "BodyReward": "此奖励及其数据将被永久删除。", + "Cancel": "取消", + "Delete": "删除", + "HeaderDel": "你确定你要删除:

'{name}'

", + "TitleDel": "删除 {title}" }, - - "QuestForm": { - "Title": "添加新任务", - "QuestGiver": "委托人", - "QuestGiverPlaceholder": "角色的名字或ID", - "QuestTitle": "任务标题", - "DragDropActor": "将人物拖到这里以将其设定为委托人", - "QuestDescription": "任务描述", - "QuestGMNotes": "GM笔记", - "Submit": "提交", - "SubquestOf": "是 {name} 的支线任务" + "Labels": { + "AppHeader": { + "ShowPlayers": "展示给玩家" + }, + "Quest": "寻求" + }, + "Migration": { + "ChatMessage": { + "Footer": "您必须使用来自纲要或您的世界的有效文档数据手动更新上述任务。
", + "Header": "Forien 的任务日志 - (DB 洄游)
从以下任务中删除了未链接的任务给予者或奖励物品:

", + "Notification": "Forien 的任务日志 - 从一个或多个任务中删除未链接的任务给予者或奖励物品。请查看聊天消息以获取更多信息。", + "QuestGiver": "任务给予者", + "QuestRewards": "任务奖励" + }, + "Notifications": { + "Complete": "Forien 的任务日志 - 迁移数据完成。", + "CouldNotMigrate": "Forien 的任务日志 - 无法迁移任务:'{name}'。", + "Schema": "Forien 的任务日志 - 将数据迁移到架构版本 '{version}'。", + "Start": "Forien 的任务日志 - 正在迁移数据,请不要重新加载。" + } + }, + "Notifications": { + "CannotOpen": "无法查看任务详情。你可能缺少访问权限,或者该任务ID错误,或者该任务已被删除。", + "FinishQuestAdded": "请在添加另一个任务之前完成编辑并关闭当前的新任务。", + "LinkCopied": "该任务的链接已复制到粘贴板中。", + "QuestAdded": "添加了 '{name}' 作为新任务,状态为:'{status}'。", + "QuestIDCopied": "此任务的任务 ID 已复制到剪贴板。", + "QuestMoved": "移动了 '{name}'并将新状态设置为:'{target}'。", + "QuestPrimary": "'{name}' 是新的主要任务。", + "QuestTrackerNoActive": "任务追踪器已启用,但目前没有正在进行的任务。", + "UserCantOpen": "玩家 '{user}' 没有查看该任务的权限。" }, - "QuestLog": { - "Title": "任务栏", - "SubTitle": "part of: {0}", - "Table": { - "QuestGiver": "委托人", - "QuestTitle": "标题", - "Tasks": "目标", - "Actions": "行动" + "Buttons": { + "AddQuest": "加入新任务" }, - "Tabs": { - "Available": "可选", - "InProgress": "进行中", - "Completed": "已完成", - "Failed": "已失败", - "Hidden": "已隐藏" + "ContextMenu": { + "CopyEntityLink": "复制实体内容链接", + "CopyQuestID": "复制任务 ID", + "PrimaryQuest": "设置/取消设置为主要任务" + }, + "Labels": { + "TableHeader": "{0}任务", + "SubTitle": "的子任务 {0}" + }, + "Title": "任务栏", + "Tooltips": { + "Objectives": "目標" } }, - "QuestPreview": { - "Title": "任务详情", - "Objectives": "目标", - "Rewards": "奖励", - "DragDropRewards": "将物品拖拽到此处以将其设为奖励", - "InvalidQuestId": "无法预览任务:无效的任务ID。", - "HeaderButtons": { - "Show": "展示给玩家" - }, - + "Buttons": { + "RewardCustom": "添加用户定义", + "RewardHide": "全部藏起来", + "RewardLock": "锁定所有", + "RewardShow": "显示所有", + "RewardUnlock": "全部解锁" + }, + "Labels": { + "CustomSource": "自定义来源", + "Description": "描述:", + "DragDropActor": "拖放演员、项目或日记条目或左键单击以设置自定义来源。", + "DragDropActorPlayer": "拖放演员、项目或日记条目。", + "DragDropRewards": "将物品拖拽到此处以将其设为奖励。", + "GMNotes": "GM 笔记:", + "Objective": "客观的", + "Objectives": "目标:", + "PlayerNotes": "玩家 笔记:", + "Reward": "报酬", + "Rewards": "奖励:" + }, "Management": { - "IsPersonalQuest": "个人任务?", - "IsPersonalQuestDescription": "勾选以设置任务为个人任务。该任务将仅对以下被标记的玩家显示。反勾选此选项将移除所有任务权限并将该任务移到“隐藏”标签下。", - "SplashArt": "Splash Art", - "QuestBranching": "支线任务", "AddSubquest": "新建支线任务", - "CanPlayerEdit": "允许玩家修改任务详情" + "ConfigurePermissions": "配置权限", + "QuestBranching": "支线任务:", + "QuestSettings": "任务设置:", + "SplashArt": "插画:", + "SplashInfo": "单击以设置图像。", + "SplashQuestIcon": "设置为任务图标" + }, + "Notifications": { + "BadUUID": "无法检索文档 UUID:'{uuid}'。", + "WrongDocType": "Forien 的任务日志仅接受世界/纲要演员、物品和日记条目作为任务提供者。", + "WrongItemType": "Forien 的任务日志只接受世界和纲要物品作为奖励。" }, - "Tabs": { "Details": "详情", - "QuestManagement": "管理任务", - "GMNotes": "GM笔记" + "GMNotes": "GM笔记", + "PlayerNotes": "玩家 笔记", + "QuestManagement": "管理任务" + }, + "Title": "任务详情", + "Tooltips": { + "AddCustom": "添加用户定义", + "AddObjective": "加入新目标", + "ChangeSplashPos": "更改插画对齐。", + "DeleteQuestGiver": "删除发布者。", + "DeleteSplash": "删除插画。", + "HideAll": "全部藏起来", + "LockAll": "锁定所有", + "PrimaryQuestSet": "单击以进行主要任务。", + "PrimaryQuestUnset": "单击以取消设置主要任务。", + "RewardHidden": "奖励已隐藏,点击查看。", + "RewardLocked": "奖励已锁定。点击解锁。", + "RewardLockedPlayer": "奖励已锁定。", + "RewardUnlocked": "奖励已解锁。点击锁定。", + "RewardUnlockedPlayer": "奖励已解锁。", + "RewardVisible": "奖励已可见,点击隐藏。", + "ShowAll": "显示所有", + "TaskHidden": "目标已隐藏,点击查看。", + "TaskVisible": "目标已可见,点击隐藏。", + "ToggleImage": "打开或关闭token/角色图片。", + "UnlockAll": "全部解锁", + "ViewSplashArt": "查看插图。" } - - }, - - "DeleteDialog": { - "Title": "删除 {name}", - "Header": "确定吗?", - "Body": "该任务及其数据将被永久删除。", - "Delete": "删除", - "Cancel": "取消" }, - - "CloseDialog": { - "Title": "Leave Form", - "Header": "确定吗?", - "Body": "确定你要关闭该表格吗?任何未保存的数据将丢失。", - "Discard": "放弃更改", - "Cancel": "取消" + "QuestTracker": { + "NoPrimary": "没有可用的主要任务。", + "Title": "任务追踪器", + "Tooltips": { + "BackgroundShow": "點擊顯示背景。", + "BackgroundUnshow": "單擊以使背景透明。", + "PrimaryQuestShow": "单击以显示主要任务。", + "PrimaryQuestUnshow": "点击显示所有任务。" + } }, - - "Notifications": { - "CannotOpen": "无法查看任务详情。你可能缺少访问权限,或者该任务ID错误,或者该任务已被删除。", - "UserCantOpen": "玩家 {user} 没有查看该任务的权限。", - "LinkCopied": "该任务的链接已复制到粘贴板中", - "QuestMoved": "移动该任务至新文件夹并更改其状态为:{target}" + "QuestTypes": { + "Labels": { + "Active": "进行中", + "active": "进行中", + "Available": "可用的", + "available": "可用的", + "Completed": "已完成", + "completed": "已完成", + "Failed": "已失败", + "failed": "已失败", + "InActive": "不活跃", + "inactive": "不活跃", + "Status": "任务是 {statusLabel}。" + }, + "Tooltips": { + "SetActive": "设定为进行中", + "SetAvailable": "设定为可选", + "SetCompleted": "设定为已完成", + "SetFailed": "设定为已失败", + "SetInactive": "设置为非活动", + "Status": "地位:{statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "玩家可以接受任务", + "EnableHint": "勾选以允许玩家从可选任务中接受任务。" + }, + "allowPlayersCreate": { + "Enable": "玩家可以新建任务", + "EnableHint": "勾选以允许玩家新建任务。玩家新建的任务将出现在可选任务栏中并且可被玩家编辑。需要给予玩家“新建日志”权限。" + }, "allowPlayersDrag": { - "Enable": "允许玩家拖拽任务奖励。", + "Enable": "允许玩家拖拽任务奖励", "EnableHint": "勾选以允许玩家将奖励物品从任务详情中拖拽到自己的角色卡上。" }, - "availableQuests": { - "Enable": "显示可用标签", - "EnableHint": "勾选以展示任务日志中所有的 \"可选\" 标签。玩家们可以在接下任务前查看所有的非隐藏任务。" - }, "countHidden": { "Enable": "包括隐藏任务数量", "EnableHint": "勾选以将隐藏任务的数量算入已完成任务/所有任务数量中。" }, + "defaultPermissionLevel": { + "Enable": "默认任务权限级别", + "EnableHint": "创建新任务时设置默认权限级别。", + "NONE": "没有任何", + "OBSERVER": "观察者", + "OWNER": "所有者" + }, + "dynamicBookmarkBackground": { + "Enable": "动态书签背景", + "EnableHint": "如果选中,书签选项卡背景将动态设置为窗口内容背景。" + }, + "hideFQLFromPlayers": { + "Enable": "对玩家隐藏任务日志", + "EnableHint": "启用此选项后,将对所有玩家隐藏任务日志。只有 GM 级别的用户才能访问任务日志。" + }, "navStyle": { - "Enable": "导航方式", - "EnableHint": "选择如何展示任务日志的导航。", "bookmarks": "书签", - "classic": "经典标签页" + "classic": "经典标签页", + "Enable": "导航方式", + "EnableHint": "选择如何展示任务日志的导航。" + }, + "notifyRewardDrop": { + "Enable": "显示奖励掉落通知", + "EnableHint": "检查以查看任务奖励被放到玩家表上时的通知。" + }, + "questTrackerResizable": { + "Enable": "任务追踪器可调整大小", + "EnableHint": "选中以允许手动调整 Quest Tracker 的大小控制。" }, "showFolder": { "Enable": "显示任务文件夹", "EnableHint": "勾选以在日志栏中显示目标文件夹。仅用于DEBUG。" }, "showTasks": { + "default": "展示目标:已完成/全部", "Enable": "显示任务栏中的目标", "EnableHint": "选择是否或如何在任务日志栏中展示任务的目标数量。", - "default": "展示目标:已完成/全部", - "onlyCurrent": "展示目标:已完成", - "no": "隐藏 \"目标\" 列表" - }, - "titleAlign": { - "Enable": "任务标题对齐", - "EnableHint": "选择任务栏中任务标题的对齐方式。", - "left": "左对齐", - "center": "居中对齐" - }, - "playersWelcomeScreen": { - "Enable": "向玩家们展示欢迎界面", - "EnableHint": "反勾选以停止在玩家们登陆时向他们展示Mod更新后的欢迎界面。他们仍可以点击任务栏中的\"帮助\"图标来查看更新信息。" - }, - "allowPlayersAccept": { - "Enable": "玩家可以接受任务", - "EnableHint": "勾选以允许玩家从可选任务中接受任务。" + "no": "隐藏 \"目标\" 列表", + "onlyCurrent": "展示目标:已完成" }, - "allowPlayersCreate": { - "Enable": "玩家可以新建任务", - "EnableHint": "勾选以允许玩家新建任务。玩家新建的任务将出现在可选任务栏中并且可被玩家编辑。需要给予玩家“新建日志”权限。" + "trustedPlayerEdit": { + "Enable": "允许可信玩家任务编辑", + "EnableHint": "检查以允许受信任的玩家对他们拥有的任务拥有扩展的任务编辑和状态控制功能。" } }, - "Tooltips": { - "SetAvailable": "设定为可选", - "SetActive": "设定为进行中", - "SetCompleted": "设定为已完成", - "SetFailed": "设定为已失败", - "Hide": "隐藏", "Delete": "删除", - "AddAbstractReward": "加入非物质奖励", - "PersonalQuestButNoPlayers": "这是一个个人任务,但是当前没有被分配给任何玩家", - "PersonalQuestVisibleFor": "该个人任务属于", - "RewardHidden": "奖励已隐藏,点击查看。", - "RewardVisible": "奖励已可见,点击隐藏。", - "TaskHidden": "目标已隐藏,点击查看。", - "TaskVisible": "目标已可见,点击隐藏。", - "ToggleImage": "打开或关闭token/角色图片" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "打开 „{name}” Quest", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Edit": "编辑", + "HiddenQuestNoPlayers": "这个任务对所有玩家都是隐藏的。", + "PrimaryQuest": "主要任务" } } -} +} \ No newline at end of file diff --git a/lang/de.json b/lang/de.json index 472f34b6..4795a935 100644 --- a/lang/de.json +++ b/lang/de.json @@ -1,71 +1,256 @@ { - "ForienQuestLog.QuestLog.Tabs.InProgress": "Laufend", - "ForienQuestLog.QuestLog.Tabs.Completed": "Abgeschlossen", - "ForienQuestLog.QuestLog.Tabs.Failed": "Fehlgeschlagen", - "ForienQuestLog.QuestLog.Tabs.Hidden": "Verborgen", - - "ForienQuestLog.QuestPreview.Tabs.Details": "Details", - "ForienQuestLog.QuestPreview.Tabs.GMNotes": "GM Notes", - "ForienQuestLog.NewQuest": "Neue Queste", - - "ForienQuestLog.Notifications.QuestMoved": "Die Queste wurde in einen neuen Ordner verschoben und hat nun den Status: {target}", - - "ForienQuestLog.Buttons.AddNewQuest": "Neue Queste", - "ForienQuestLog.Buttons.AddNewTask": "Hinzufügen", - - "ForienQuestLog.QuestTypes.InProgress": "Laufende", - "ForienQuestLog.QuestTypes.Completed": "Abgeschlossene", - "ForienQuestLog.QuestTypes.Failed": "Fehlgeschlagene", - "ForienQuestLog.QuestTypes.Hidden": "Verborgene", - "ForienQuestLog.Quests": "Questen", - "ForienQuestLog.QuestLog.Table.QuestGiver": "Auftraggeber", - "ForienQuestLog.QuestLog.Table.QuestTitle": "Titel", - "ForienQuestLog.QuestLog.Table.Tasks": "Aufgaben", - "ForienQuestLog.QuestLog.Table.Actions": "Aktionen", - "ForienQuestLog.QuestPreview.Objectives": "Ziele", - "ForienQuestLog.QuestPreview.Rewards": "Belohnungen", - - "ForienQuestLog.Settings.showTasks.Enable": "Zeige Aufgaben im Questenlog", - "ForienQuestLog.Settings.showTasks.EnableHint": "Lege fest ob und wie die Menge von Aufgaben (Zielen) neben dem Titel der Queste im Questenlog angezeigt wird. Dies hat keine Auswirkung auf die Vorschau einer individuellen Queste.", - "ForienQuestLog.Settings.showTasks.default": "Zeige Aufgaben: Beendet/Gesamt", - "ForienQuestLog.Settings.showTasks.onlyCurrent": "Zeige Aufgaben: Beendet", - "ForienQuestLog.Settings.showTasks.no": "Verberge \"Aufgaben\"-Spalte", - - "ForienQuestLog.Settings.navStyle.Enable": "Navigationsstil", - "ForienQuestLog.Settings.navStyle.EnableHint": "Lege fest, wie die Navigation im Questenlog angezeigt werden soll.", - "ForienQuestLog.Settings.navStyle.bookmarks": "Lesezeichen", - "ForienQuestLog.Settings.navStyle.classic": "Klassische Reiter", - - "ForienQuestLog.Settings.titleAlign.Enable": "Titelausrichtung der Queste", - "ForienQuestLog.Settings.titleAlign.EnableHint": "Lege fest, wie die Titel der Questen in der Tabelle im Questenlog positioniert werden sollen.", - "ForienQuestLog.Settings.titleAlign.left": "Linksbündig", - "ForienQuestLog.Settings.titleAlign.center": "Zentriert", - - "ForienQuestLog.QuestLogButton": "Questenlog", - "ForienQuestLog.QuestForm.Title": "Neue Queste anlegen", - "ForienQuestLog.QuestLog.Title": "Questenlog", - "ForienQuestLog.QuestPreview.Title": "Details zur Queste", - - "ForienQuestLog.QuestForm.QuestGiver": "Auftraggeber", - "ForienQuestLog.QuestForm.QuestGiverPlaceholder": "Name oder ID des Actors", - "ForienQuestLog.QuestForm.QuestTitle": "Titel der Queste", - "ForienQuestLog.QuestForm.QuestDescription": "Beschreibung der Queste", - "ForienQuestLog.QuestForm.QuestGMNotes": "GM notes", - "ForienQuestLog.QuestForm.Submit": "Übermitteln", - - "ForienQuestLog.SampleTask": "z.B. Töte alle Ratten in der Schänke „Zum verstauchten Knöchel”", - "ForienQuestLog.QuestPreview.DragDropRewards": "Ziehe Gegenstände hier hinein, um sie als Belohnung hinzuzufügen", - - "ForienQuestLog.DeleteDialog.Title": "Lösche {name}", - "ForienQuestLog.DeleteDialog.Header": "Bist du sicher?", - "ForienQuestLog.DeleteDialog.Body": "Diese Queste und ihre Daten werden permanent gelöscht.", - "ForienQuestLog.DeleteDialog.Delete": "Löschen", - "ForienQuestLog.DeleteDialog.Cancel": "Abbrechen", - - "ForienQuestLog.Tooltips.ToggleImage": "Bild des Token/Actor umschalten", - "ForienQuestLog.Tooltips.SetActive": "Stelle auf Laufend", - "ForienQuestLog.Tooltips.SetCompleted": "Stelle auf Beendet", - "ForienQuestLog.Tooltips.SetFailed": "Stelle auf Fehlgeschlagen", - "ForienQuestLog.Tooltips.Hide": "Verbergen", - "ForienQuestLog.Tooltips.Delete": "Löschen" -} + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Offen ”{name}”" + }, + "Notifications": { + "NoQuest": "API-Fehler: Makro kann mit ungültiger Quest-ID nicht erstellt werden." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Neue Quest" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (Questbelohnung): {userName} hat '{itemName}' auf {actorName} gezogen." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Entität mit UUID '{uuid}' konnte nicht geladen werden.", + "NoPermission": "Du verfügst nicht über die nötigen Berechtigungen, um den Bogen dieser Entität einzusehen." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Dieses Ziel und die zugehörigen Daten werden permanent gelöscht.", + "BodyQuest": "Diese Quest und die zugehörigen Daten werden permanent gelöscht.", + "BodyReward": "Diese Belohnung und die zugehörigen Daten werden permanent gelöscht.", + "Cancel": "Abbrechen", + "Delete": "Löschen", + "HeaderDel": "Bist du sicher, dass du

'{name}'

löschen willst", + "TitleDel": "{title} löschen" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Spielern zeigen" + }, + "Quest": "Quest" + }, + "Migration": { + "ChatMessage": { + "Footer": "Du musst die obigen Quests manuell mit gültigen Dokumentdaten aus Kompendien oder deiner Welt aktualisieren.
", + "Header": "Forien's Questlog (DB-Migration)
Unverlinkte(r) Questgeber oder Questbelohnung von unten stehenden Quests entfernt:

", + "Notification": "Forien's Questlog - Unverlinkte(r) Questgeber oder Questbelohnung von mindestens einer Quest entfernt. Bitte Chatnachrichten für mehr Informationen nachlesen.", + "QuestGiver": "Questgeber", + "QuestRewards": "Questbelohnungen" + }, + "Notifications": { + "Complete": "Foriens Questlog - Migration der Daten abgeschlossen.", + "CouldNotMigrate": "Foriens Questlog - Quest konnte nicht migriert werden: '{name}'.", + "Schema": "Foriens Questlog - Migriere Daten zu Schema-Version '{version}'.", + "Start": "Foriens Questlog - Daten werden migriert, bitte nicht neu laden." + } + }, + "Notifications": { + "CannotOpen": "Questdetails können nicht geöffnet werden. Möglicherweise kann die Quest nicht betrachtet werden oder existiert nicht mehr.", + "FinishQuestAdded": "Bitte schließe das Bearbeiten der Quest ab und schließe sie, bevor du eine neue hinzufügst.", + "LinkCopied": "Entitätslink für diese Quest wurde in die Zwischenablage kopiert.", + "QuestAdded": "'{name}' als neue Quest mit Status '{status}' hinzugefügt.", + "QuestIDCopied": "Quest-ID für diese Quest wurde in die Zwischenablage kopiert.", + "QuestMoved": "'{name}' verschoben und Ziel gesetzt auf: '{target}'.", + "QuestPrimary": "'{name}' ist die neue primäre Quest.", + "QuestTrackerNoActive": "Questtracker ist aktiviert, aber aktuell gibt es keine aktiven Quests.", + "UserCantOpen": "Benutzer '{user}' hat nicht die Berechtigungen, um diese Quest zu öffnen." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Quest hinzufügen" + }, + "ContextMenu": { + "CopyEntityLink": "Entität-Link kopieren", + "CopyQuestID": "Kopieren Sie Quest-ID", + "PrimaryQuest": "Als primäre Quest (de)aktivieren" + }, + "Labels": { + "TableHeader": "{0} Quests", + "SubTitle": "Unterquest von {0}" + }, + "Title": "Questlog", + "Tooltips": { + "Objectives": "Ziele" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Benutzerdefinierten", + "RewardHide": "Verbergen", + "RewardLock": "Sperren", + "RewardShow": "Anzeigen", + "RewardUnlock": "Entsperren" + }, + "Labels": { + "CustomSource": "Eigene Quelle", + "Description": "Beschreibung:", + "DragDropActor": "Akteur, Gegenstand oder Notizbucheintrag hierher ziehen oder linksklicken, um sie als eigene Quelle festzulegen.", + "DragDropActorPlayer": "Akteur, Gegenstand oder Notizbucheintrag hierher ziehen.", + "DragDropRewards": "Gegenstände hierher ziehen, um sie als Belohnung festzulegen.", + "GMNotes": "GM-Notizen:", + "Objective": "Ziel", + "Objectives": "Ziele:", + "PlayerNotes": "Spielernotizen:", + "Reward": "Belohnung", + "Rewards": "Belohnungen:" + }, + "Management": { + "AddSubquest": "Unterquest hinzufügen", + "ConfigurePermissions": "Berechtigungen Konfigurieren", + "QuestBranching": "Unterquests:", + "QuestSettings": "Questeinstellungen:", + "SplashArt": "Titelbild:", + "SplashInfo": "Klicken, um Titelbild zu setzen.", + "SplashQuestIcon": "Als Questicon setzen" + }, + "Notifications": { + "BadUUID": "Konnte Dokument mit UUID {uuid} nicht abrufen.", + "WrongDocType": "Foriens Questlog akzeptnur Welt-/Kompendium-Akteure, -Items und -Notizbucheinträge als Questgeber.", + "WrongItemType": "Foriens Questlog akzeptiert nur Welt- und Kompendium-Items als Belohnungen." + }, + "Tabs": { + "Details": "Details", + "GMNotes": "GM-Notizen", + "PlayerNotes": "Spielernotizen", + "QuestManagement": "Quest verwalten" + }, + "Title": "Questdetails - {name}", + "Tooltips": { + "AddCustom": "Benutzerdefiniert Hinzufügen", + "AddObjective": "Ziel hinzufügen", + "ChangeSplashPos": "Splash-Bildausrichtung ändern.", + "DeleteQuestGiver": "Questgeber löschen.", + "DeleteSplash": "Splash Art löschen.", + "HideAll": "Alle verbergen", + "LockAll": "Alle sperren", + "PrimaryQuestSet": "Anklicken, um als primäre Quest zu setzen.", + "PrimaryQuestUnset": "Anklicken, um primäre Quest aufzuheben.", + "RewardHidden": "Belohnung verborgen. Anklicken zum Anzeigen.", + "RewardLocked": "Belohnung ist gesperrt. Klicken zum Entsperren.", + "RewardLockedPlayer": "Belohnung ist gesperrt.", + "RewardUnlocked": "Belohnung ist entsperrt. Klicken zum Sperren.", + "RewardUnlockedPlayer": "Belohung ist entsperrt.", + "RewardVisible": "Belohnung ist sichtbar. Anklicken zum Verbergen.", + "ShowAll": "Alle anzeigen", + "TaskHidden": "Ziel ist verborgen. Anklicken zum Anzeigen.", + "TaskVisible": "Ziel ist sichtbar. Anklicken zum Verbergen.", + "ToggleImage": "Figur/Akteur-Bild umschalten.", + "UnlockAll": "Alle entsperren", + "ViewSplashArt": "Titelfoto ansehen." + } + }, + "QuestTracker": { + "NoPrimary": "Keine primäre Quest verfügbar.", + "Title": "Questtracker", + "Tooltips": { + "BackgroundShow": "Anklicken, um Hintergrund anzuzeigen.", + "BackgroundUnshow": "Für transparenten Hintergrund klicken.", + "PrimaryQuestShow": "Anklicken, um primäre Quest anzuzeigen.", + "PrimaryQuestUnshow": "Anklicken, um alle Quests anzuzeigen." + } + }, + "QuestTypes": { + "Labels": { + "Active": "Aktive", + "active": "aktiv", + "Available": "Verfügbare", + "available": "verfügbar", + "Completed": "Abgeschlossene", + "completed": "abgeschlossen", + "Failed": "Fehlgeschlagene", + "failed": "fehlgeschlagen", + "InActive": "Inaktive", + "inactive": "inaktiv", + "Status": "Quest ist {statusLabel}." + }, + "Tooltips": { + "SetActive": "Als Aktiv setzen", + "SetAvailable": "Als Verfügbar setzen", + "SetCompleted": "Als abgeschlossen setzen", + "SetFailed": "Als Fehlgeschlagen setzen", + "SetInactive": "Als Inaktiv setzen", + "Status": "Status: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Spieler können Quests annehmen", + "EnableHint": "Aktivieren, um Spielern zu erlauben, Quests aus dem Verfügbar-Reiter anzunehmen." + }, + "allowPlayersCreate": { + "Enable": "Spieler können Quests erstellen", + "EnableHint": "Aktivieren, um Spielern zu erlauben, Quests zu erstellen. Von Spielern erstellte Quests erscheinen im Verfügbar-Reiter mit Bearbeitungsrechten für Spieler. BENÖTIGT Kernberechtigung 'Notizbucheinträge erstellen'." + }, + "allowPlayersDrag": { + "Enable": "Spielern das Ziehen von Belohnungen erlauben", + "EnableHint": "Aktivieren, um Spielern zu erlauben, Belohnungen aus dem Questdetailfenster auf Akteure in ihrem Besitz zu ziehen." + }, + "countHidden": { + "Enable": "Verborgene Ziele mitzählen", + "EnableHint": "Aktivieren, um verborgene Ziele bei abgeschlossenen/gesamten Zielen mitzuzählen." + }, + "defaultPermissionLevel": { + "Enable": "Standard-Berechtigungen für Quests", + "EnableHint": "Setzt das Standard-Berechtigungslevel für neu erstellte Quests.", + "NONE": "Nichts", + "OBSERVER": "Beobachter", + "OWNER": "Besitzer" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynamischer Lesezeichenhintergrund", + "EnableHint": "Aktivieren, um den Fensterinhalt dynamisch als den Hintergrund für den Lesezeichenreiter zu setzen." + }, + "hideFQLFromPlayers": { + "Enable": "Questlog für Spieler verbergen", + "EnableHint": "Aktivieren, um das Questlog für alle Spieler zu verbergen. Nur GM-Benutzer können auf das Questlog zugreifen." + }, + "navStyle": { + "bookmarks": "Lesezeichen", + "classic": "Klassische Reiter", + "Enable": "Navigationsstil", + "EnableHint": "Festlegen, wie die Navigation in Foriens Questlog angezeigt wird." + }, + "notifyRewardDrop": { + "Enable": "Bei Ziehen von Belohnungen benachrichtigen", + "EnableHint": "Aktivieren, um eine UI-Benachrichtigung zu erhalten, wenn Questbelohnungen auf Spielerbögen gezogen werden." + }, + "questTrackerResizable": { + "Enable": "Questtracker skalierbar", + "EnableHint": "Aktivieren, um zu erlauben, dass manuell die Größe des Questtrackers verändert werden kann." + }, + "showFolder": { + "Enable": "Questordner anzeigen", + "EnableHint": "Aktivieren, um den Questdaten-Ordner im Notizbuchreiter anzuzeigen. Nur für DEBUG-Zwecke." + }, + "showTasks": { + "default": "Ziele zeigen: abgeschlossen/gesamt", + "Enable": "Ziele im Questlog anzeigen", + "EnableHint": "Lege fest, ob und wie die Zahl der Ziele neben dem Questtitel im Questlog angezeigt wird. Dies betrifft nicht die Quest-Vorschau.", + "no": "Verberge \"Ziele\" Spalte", + "onlyCurrent": "Ziele zeigen: abgeschlossen" + }, + "trustedPlayerEdit": { + "Enable": "Erlaube Seriösen Spielern das Bearbeiten von Quests", + "EnableHint": "Aktivieren, um Seriösen Spielern (Trusted Players) erweiterte Bearbeitungsmöglichkeiten und Statusänderungen für Quests, die sie kontrollieren einzuräumen." + } + }, + "Tooltips": { + "Delete": "Löschen", + "Edit": "Bearbeiten", + "HiddenQuestNoPlayers": "Diese Quest ist vor allen Spielern verborgen.", + "PrimaryQuest": "Primäre Quest" + } + } +} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 0fa3c444..a4d5ef19 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,205 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "New Quest", - "QuestLogButton": "Quest Log", - "Quests": "{0} Quests", - "SampleReward": "e.g. 300 Experience Points", - "SampleTask": "e.g. Kill all rats in „The Twisted Ankle” inn", - - "QuestTypes": { - "InProgress": "Active", - "Completed": "Completed", - "Failed": "Failed", - "Hidden": "Inactive", - "Labels": { - "available": "available", - "active": "in progress", - "completed": "completed", - "failed": "failed", - "hidden": "inactive" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Open ”{name}”" + }, + "Notifications": { + "NoQuest": "API Error: Can't create macro with invalid Quest ID." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "New Quest" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (quest reward): {userName} dropped '{itemName}' onto {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "An entity could not be loaded for UUID: '{uuid}'.", + "NoPermission": "You do not have sufficient permissions to view this entity sheet." + } } }, - - "Buttons": { - "AddNewQuest": "Add new Quest", - "AddNewTask": "Add new Objective", - "AddNewFolder": "Add new Folder" + "DeleteDialog": { + "BodyObjective": "This objective and its data will be permanently deleted.", + "BodyQuest": "This quest and its data will be permanently deleted.", + "BodyReward": "This reward and its data will be permanently deleted.", + "Cancel": "Cancel", + "Delete": "Delete", + "HeaderDel": "Are you sure you want to delete:

'{name}'

", + "TitleDel": "Delete {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Show to Players" + }, + "Quest": "Quest" }, - - "QuestForm": { - "Title": "Add new Quest", - "QuestGiver": "Quest Source", - "QuestGiverPlaceholder": "Enter Actor's name or entity's UUID to set a Quest Source", - "QuestGiverNamePlaceholder": "You selected custom image, please provide name for Quest Source", - "QuestTitle": "Quest Title", - "DragDropActor": "Drag & Drop Actor, Item or Journal Entry here to set a Quest Source", - "QuestDescription": "Quest Description", - "QuestGMNotes": "GM Notes", - "Submit": "Add to Quest Log", - "SubquestOf": "Subquest of {name}" + "Migration": { + "ChatMessage": { + "Footer": "You must manually update the above quests with valid document data from compendiums or your world.
", + "Header": "Forien's Quest Log (DB migration)
Removed unlinked quest giver or reward items from the quests below:

", + "Notification": "Forien's Quest Log - Removed unlinked quest giver or reward items from one or more quests. Please review the chat message for more info.", + "QuestGiver": "Quest Giver", + "QuestRewards": "Quest Rewards" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migrating data complete.", + "CouldNotMigrate": "Forien's Quest Log - Could not migrate quest: '{name}'.", + "Schema": "Forien's Quest Log - Migrating data to schema version '{version}'.", + "Start": "Forien's Quest Log - Migrating data, please do not reload." + } + }, + "Notifications": { + "CannotOpen": "Cannot open Quest Details. This quest may not be observable or may no longer exist.", + "FinishQuestAdded": "Please finish editing and close current new quest before adding another.", + "LinkCopied": "Entity Link for this quest has been copied to clipboard.", + "QuestAdded": "Added '{name}' as a new quest with status: '{status}'.", + "QuestIDCopied": "Quest ID for this quest has been copied to clipboard.", + "QuestMoved": "Moved '{name}' and set new status to: '{target}'.", + "QuestPrimary": "'{name}' is the new primary quest.", + "QuestTrackerNoActive": "Quest Tracker is enabled, but there are currently no in progress quests.", + "UserCantOpen": "User '{user}' doesn't have permission to open this quest." }, - "QuestLog": { - "Title": "Quest Log", - "SubTitle": "Subquest of {0}", - "Table": { - "QuestGiver": "Quest Source", - "QuestTitle": "Title", - "Tasks": "Objectives", - "Actions": "Actions" + "Buttons": { + "AddQuest": "Add Quest" }, - "Tabs": { - "Available": "Available", - "InProgress": "In progress", - "Completed": "Completed", - "Failed": "Failed", - "Hidden": "Inactive" + "ContextMenu": { + "CopyEntityLink": "Copy entity content link", + "CopyQuestID": "Copy quest ID", + "PrimaryQuest": "Set / Unset as primary quest" + }, + "Labels": { + "TableHeader": "{0} Quests", + "SubTitle": "Subquest of {0}" + }, + "Title": "Quest Log", + "Tooltips": { + "Objectives": "Objectives" } }, - "QuestPreview": { - "Title": "Quest Details", - "SubTitle": "Subquest of {0}", - "Objectives": "Objectives", - "Rewards": "Rewards", - "DragDropRewards": "Drag & drop items here to add them as rewards", - "InvalidQuestId": "Cannot open Quest Preview due to invalid Quest ID.", - "HeaderButtons": { - "Show": "Show to Players" - }, - + "Buttons": { + "RewardCustom": "Custom", + "RewardHide": "Hide", + "RewardLock": "Lock", + "RewardShow": "Show", + "RewardUnlock": "Unlock" + }, + "Labels": { + "CustomSource": "Custom Source", + "Description": "Description:", + "DragDropActor": "Drag & Drop Actor, Item or Journal Entry or left click to set a custom source.", + "DragDropActorPlayer": "Drag & Drop Actor, Item or Journal Entry.", + "DragDropRewards": "Drag & drop items here to add them as rewards.", + "GMNotes": "GM Notes:", + "Objective": "Objective", + "Objectives": "Objectives:", + "PlayerNotes": "Player Notes:", + "Reward": "Reward", + "Rewards": "Rewards:" + }, "Management": { - "IsPersonalQuest": "Mark Quest as Personal", - "IsPersonalQuestDescription": "This Quest will only be visible to the selected players. Unchecking this option will remove all permissions and set the quest as inactive.", - "SplashArt": "Splash Art", - "QuestBranching": "Sub Quests", - "AddSubquest": "Create new Sub Quest", - "CanPlayerEdit": "Allow Players to edit Quest Details" - }, - + "AddSubquest": "Add Subquest", + "ConfigurePermissions": "Configure Permissions", + "QuestBranching": "Subquests:", + "QuestSettings": "Quest Settings:", + "SplashArt": "Splash Art:", + "SplashInfo": "Click to set image.", + "SplashQuestIcon": "Set as quest icon" + }, + "Notifications": { + "BadUUID": "Could not retrieve the document for UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log only accepts world / compendium actors, items, and journal entries as quest givers.", + "WrongItemType": "Forien's Quest Log only accepts world and compendium items as rewards." + }, "Tabs": { "Details": "Details", - "QuestManagement": "Manage Quest", - "GMNotes": "GM Notes" + "GMNotes": "GM Notes", + "PlayerNotes": "Player Notes", + "QuestManagement": "Manage Quest" + }, + "Title": "Quest Details - {name}", + "Tooltips": { + "AddCustom": "Add Custom", + "AddObjective": "Add Objective", + "ChangeSplashPos": "Change splash art alignment.", + "DeleteQuestGiver": "Delete quest giver.", + "DeleteSplash": "Delete splash art.", + "HideAll": "Hide All", + "LockAll": "Lock All", + "PrimaryQuestSet": "Click to make primary quest.", + "PrimaryQuestUnset": "Click to unset primary quest.", + "RewardHidden": "Reward is hidden. Click to show.", + "RewardLocked": "Reward is locked. Click to unlock.", + "RewardLockedPlayer": "Reward is locked.", + "RewardUnlocked": "Reward is unlocked. Click to lock.", + "RewardUnlockedPlayer": "Reward is unlocked.", + "RewardVisible": "Reward is visible. Click to hide.", + "ShowAll": "Show All", + "TaskHidden": "Objective is hidden. Click to show.", + "TaskVisible": "Objective is visible. Click to hide.", + "ToggleImage": "Toggle Token/Actor image.", + "UnlockAll": "Unlock All", + "ViewSplashArt": "View splash art." } - - }, - - "DeleteDialog": { - "Title": "Delete {name}", - "Header": "Are you sure?", - "Body": "This quest and its data will be permanently deleted.", - "Delete": "Delete", - "Cancel": "Cancel" }, - - "CloseDialog": { - "Title": "Leave Form", - "Header": "Are you sure?", - "Body": "Are you sure you want to close the form? Unsaved data will be lost.", - "Discard": "Discard changes", - "Cancel": "Cancel" + "QuestTracker": { + "NoPrimary": "No primary quest available.", + "Title": "Quest Tracker", + "Tooltips": { + "BackgroundShow": "Click to show background.", + "BackgroundUnshow": "Click to make background transparent.", + "PrimaryQuestShow": "Click to show primary quest.", + "PrimaryQuestUnshow": "Click to show all quests." + } }, - - "Notifications": { - "CannotOpen": "Cannot open Quest Details. You may lack permissions, Quest might not exist anymore, or provided ID was invalid.", - "UserCantOpen": "User {user} doesn't have permission to open this quest.", - "LinkCopied": "Entity Link for this quest has been copied to clipboard.", - "QuestMoved": "Moved quest to new folder and gave it new status: {target}." + "QuestTypes": { + "Labels": { + "Active": "In Progress", + "active": "in progress", + "Available": "Available", + "available": "available", + "Completed": "Completed", + "completed": "completed", + "Failed": "Failed", + "failed": "failed", + "InActive": "Inactive", + "inactive": "inactive", + "Status": "Quest is {statusLabel}." + }, + "Tooltips": { + "SetActive": "Set as In Progress", + "SetAvailable": "Set as Available", + "SetCompleted": "Set as Completed", + "SetFailed": "Set as Failed", + "SetInactive": "Set as Inactive", + "Status": "Status: {statusI18n}" + } }, - "Settings": { - "allowPlayersDrag": { - "Enable": "Allow Players to drag Rewards to their Inventory", - "EnableHint": "Check to allow Players to drag Rewards from a Quest Details window to their owned Actors." + "allowPlayersAccept": { + "Enable": "Players can accept Quests", + "EnableHint": "Check to allow players to accept Quests from Available tab." }, - "availableQuests": { - "Enable": "Show Available Tab", - "EnableHint": "Check to show the \"Available\" tab in the Quest Log Menu where players can see all non-inactive Quests before they are accepted." + "allowPlayersCreate": { + "Enable": "Players can create Quests", + "EnableHint": "Check to allow players to create Quests. Player created Quests will appear in Available tab with Player Edit permissions. REQUIRES 'create journal' core permission." + }, + "allowPlayersDrag": { + "Enable": "Allow Player Reward Dragging", + "EnableHint": "Check to allow Players to drag Rewards from the Quest Details window to their owned Actors." }, "countHidden": { - "Enable": "Count hidden Tasks", - "EnableHint": "If checked, the number of completed/total tasks will include hidden tasks." + "Enable": "Count hidden objectives", + "EnableHint": "If checked, the number of completed / total objectives will include hidden objectives." + }, + "defaultPermissionLevel": { + "Enable": "Default quest permission level", + "EnableHint": "Sets the default permission level when new quests are created.", + "NONE": "None", + "OBSERVER": "Observer", + "OWNER": "Owner" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynamic Bookmark Background", + "EnableHint": "If checked, the bookmark tab background is dynamically set to the window content background." + }, + "hideFQLFromPlayers": { + "Enable": "Hide Quest Log from players", + "EnableHint": "When enabled this option hides the Quest Log from all players. Only GM level users will be able to access the Quest Log." }, "navStyle": { - "Enable": "Navigation Style", - "EnableHint": "Decide how Quest Log's navigation should be displayed.", "bookmarks": "Bookmarks", - "classic": "Classic tabs" + "classic": "Classic tabs", + "Enable": "Navigation Style", + "EnableHint": "Decide how Quest Log's navigation should be displayed." + }, + "notifyRewardDrop": { + "Enable": "Show Reward Drop Notifications", + "EnableHint": "Check to see UI notifications when quest rewards are dropped onto player sheets." + }, + "questTrackerResizable": { + "Enable": "Quest Tracker Resizable", + "EnableHint": "Check to allow manual resizing control of the Quest Tracker." }, "showFolder": { "Enable": "Show Quest Folder", "EnableHint": "Check to show quest data folder in Journal tab. For DEBUG purposes only." }, "showTasks": { - "Enable": "Show tasks in Quest Log", - "EnableHint": "Decide if or how to show the amount of Objectives next to the Quest Title in Quest Log. This has no effect on the Quest Preview.", "default": "Show Objectives: done/total", - "onlyCurrent": "Show Objectives: done", - "no": "Hide \"Objectives\" column" - }, - "titleAlign": { - "Enable": "Quest Title Alignment", - "EnableHint": "Decide how to position Quest Titles in the Quest Log Table.", - "left": "Aligned to left", - "center": "Centered" - }, - "playersWelcomeScreen": { - "Enable": "Display Welcome Screen to Players", - "EnableHint": "Uncheck to prevent players from seeing Welcome Screen on log in after an update. They might still see it by clicking 'help' icon in Quest Log." - }, - "allowPlayersAccept": { - "Enable": "Players can accept Quests", - "EnableHint": "Check to allow players to accept Quests from Available tab." + "Enable": "Show objectives in Quest Log", + "EnableHint": "Decide if or how to show the amount of Objectives next to the Quest Title in Quest Log. This has no effect on the Quest Preview.", + "no": "Hide \"Objectives\" column", + "onlyCurrent": "Show Objectives: done" }, - "allowPlayersCreate": { - "Enable": "Players can create", - "EnableHint": "Check to allow players to create Quests. Player created Quests will appear in Available tab with Player Edit permissions. REQUIRES 'create journal' core permission." + "trustedPlayerEdit": { + "Enable": "Allow Trusted Player Quest Editing", + "EnableHint": "Check to allow trusted players to have expanded quest editing and status control capabilities over quests they have ownership." } }, - "Tooltips": { - "SetAvailable": "Set as Available", - "SetActive": "Set as In Progress", - "SetCompleted": "Set as Completed", - "SetFailed": "Set as Failed", - "Hide": "Set as Inactive", "Delete": "Delete", "Edit": "Edit", - "AddAbstractReward": "Add Abstract Reward", - "PersonalQuestButNoPlayers": "This is a Personal Quest, but no Players are assigned", - "PersonalQuestVisibleFor": "This is a Personal Quest for", - "RewardHidden": "Reward is hidden. Click to show.", - "RewardVisible": "Reward is visible. Click to hide.", - "TaskHidden": "Objective is hidden. Click to show.", - "TaskVisible": "Objective is visible. Click to hide.", - "ToggleImage": "Toggle Token/Actor image" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "Open „{name}” Quest", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Objective" - } - } + "HiddenQuestNoPlayers": "This Quest is hidden from all players.", + "PrimaryQuest": "Primary Quest" } } -} +} \ No newline at end of file diff --git a/lang/es.json b/lang/es.json index e0407e5d..a7288cd1 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1,195 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "Nueva misión", - "QuestLogButton": "Misiones", - "Quests": "{0} Misiones", - "SampleReward": "p.ej. 300 puntos de experiencia", - "SampleTask": "p.ej. Exterminar las ratas de la posada \"El tobillo torcido\"", - - "QuestTypes": { - "InProgress": "En curso", - "Completed": "Completadas", - "Failed": "Fallidas", - "Hidden": "Ocultas" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Abrir ”{name}”" + }, + "Notifications": { + "NoQuest": "API Error: No se puede crear la macro sin un Quest ID valido." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nueva misión" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (recompensa de misión): {userName} entregó '{itemName}' a {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "No se pudo cargar una entidad para UUID: '{uuid}'.", + "NoPermission": "No tiene suficientes permisos para ver esta hoja de entidad." + } + } }, - - "Buttons": { - "AddNewQuest": "Añadir nueva misión", - "AddNewTask": "Añadir" + "DeleteDialog": { + "BodyObjective": "Este objetivo y sus datos serán eliminados de forma permanente.", + "BodyQuest": "Esta misión y sus datos se eliminarán de forma permanente.", + "BodyReward": "Esta recompensa y sus datos se eliminarán de forma permanente.", + "Cancel": "Cancelar", + "Delete": "Eliminar", + "HeaderDel": "¿Estás seguro de que quieres eliminar?

'{name}'

", + "TitleDel": "Eliminar {title}" }, - - "QuestForm": { - "Title": "Añadir nueva misión", - "QuestGiver": "Dador de la misión", - "QuestGiverPlaceholder": "Nombre del Actor o UUID de la entidad", - "QuestTitle": "Nombre de la misión", - "DragDropActor": "Arrastra y suelta el Actor, Objeto o Apunte aquí para establecerlo como dador de la misión", - "QuestDescription": "Descripción de la misión", - "QuestGMNotes": "Notas del GM", - "Submit": "Crear", - "SubquestOf": "Submisión de {name}" + "Labels": { + "AppHeader": { + "ShowPlayers": "Mostrar a jugadores" + }, + "Quest": "Misión" + }, + "Migration": { + "ChatMessage": { + "Footer": "Debes actualizar manualmente las misiones anteriores con datos de documentos válidos de compendios o de tu mundo.
", + "Header": "Forien's Quest Log (DB migración)
Se eliminaron los elementos de recompensa o no se encuentra al dador de las misiones a continuación:

", + "Notification": "Forien's Quest Log - Se eliminó al dador de misiones o los elementos de recompensa de una o más misiones. Revise el mensaje de chat para obtener más información.", + "QuestGiver": "Dador de Misión", + "QuestRewards": "Recompensas de Misión" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migración de datos completa.", + "CouldNotMigrate": "Forien's Quest Log - No se pudo migrar la misión: '{name}'.", + "Schema": "Forien's Quest Log - Migración de datos a la versión del esquema '{version}'.", + "Start": "Forien's Quest Log - Migrando datos, por favor no recargar." + } + }, + "Notifications": { + "CannotOpen": "No puedes abrir los detalles de la misión. Puede que te falten permisos, que la misión ya no exista o que el ID sea inválido.", + "FinishQuestAdded": "Termina de editar y cierra la nueva misión actual antes de agregar otra.", + "LinkCopied": "El enlace a la entidad de esta misión ha sido copiado al portapapeles.", + "QuestAdded": "Se agregó '{name}' como una nueva misión con estado: '{status}'.", + "QuestIDCopied": "El ID de misión para esta misión se ha copiado al portapapeles.", + "QuestMoved": "Se movió '{name}' y se estableció el nuevo estado en: '{target}'.", + "QuestPrimary": "'{name}' es la nueva misión principal.", + "QuestTrackerNoActive": "Seguimiento de Misiones está habilitado, pero no hay misiones en curso actualmente.", + "UserCantOpen": "El usuario '{user}' no tiene permisos para abrir esta misión." }, - "QuestLog": { - "Title": "Misiones", - "SubTitle": "parte de: {0}", - "Table": { - "QuestGiver": "Dador de la misión", - "QuestTitle": "Nombre", - "Tasks": "Tareas", - "Actions": "Acciones" + "Buttons": { + "AddQuest": "Añadir misión" }, - "Tabs": { - "Available": "Disponibles", - "InProgress": "En curso", - "Completed": "Completadas", - "Failed": "Fallidas", - "Hidden": "Ocultas" + "ContextMenu": { + "CopyEntityLink": "Copiar enlace de contenido de entidad", + "CopyQuestID": "Copiar ID de misión", + "PrimaryQuest": "Establecer / Desestablecer como misión principal" + }, + "Labels": { + "TableHeader": "Misiones {0}", + "SubTitle": "Sub-Misión de: {0}" + }, + "Title": "Registro de Misiones", + "Tooltips": { + "Objectives": "Objetivos" } }, - "QuestPreview": { - "Title": "Detalles de la misión", - "SubTitle": "parte de: {0}", - "Objectives": "Objetivos", - "Rewards": "Recompensas", - "DragDropRewards": "Arrastra y suelta elementos aquí para añadirlos como recompensas", - "InvalidQuestId": "No se puede abrir la vista previa de la misión debido a que el ID de la misión no es válido", - "HeaderButtons": { - "Show": "Mostrar a jugadores" - }, - + "Buttons": { + "RewardCustom": "Personalizado", + "RewardHide": "Ocultar", + "RewardLock": "Bloquear", + "RewardShow": "Mostrar", + "RewardUnlock": "Desbloquear" + }, + "Labels": { + "CustomSource": "Fuente Personalizada", + "Description": "Descripción:", + "DragDropActor": "Arrastre y suelte al actor, elemento o entrada de diario o haga clic con el botón izquierdo para establecer una fuente personalizada.", + "DragDropActorPlayer": "Arrastrar y Soltar actor, elemento o entrada de diario.", + "DragDropRewards": "Arrastra y Suelta elementos aquí para añadirlos como recompensas.", + "GMNotes": "Notas del GM:", + "Objective": "Objetivo", + "Objectives": "Objetivos:", + "PlayerNotes": "Notas del jugador:", + "Reward": "Recompensa", + "Rewards": "Recompensas:" + }, "Management": { - "IsPersonalQuest": "¿Es una misión personal?", - "IsPersonalQuestDescription": "Marcar la misión como personal. Será invisible para todos los jugadores, excepto para los que están marcados específicamente abajo. Desmarcando esta opción se eliminarán todos los permisos y se moverá la misión a la pestaña \"Ocultas\".", - "SplashArt": "Imagen de acompañamiento", - "QuestBranching": "Submisiones", - "AddSubquest": "Crear submisión", - "CanPlayerEdit": "Permitir a los jugadores editar los detalles" - }, - + "AddSubquest": "Crear Sub-Misión", + "ConfigurePermissions": "Configurar Permisos", + "QuestBranching": "Sub-Misiones:", + "QuestSettings": "Configuración de la Misión:", + "SplashArt": "Imagen Portada:", + "SplashInfo": "Haga clic para configurar la imagen.", + "SplashQuestIcon": "Establecer como icono de misión" + }, + "Notifications": { + "BadUUID": "No se pudo recuperar el documento para UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log solo acepta actores del mundo/compendio, elementos y entradas de diario como dador de misiones.", + "WrongItemType": "Forien's Quest Log solo acepta artículos del mundo y del compendio como recompensa." + }, "Tabs": { "Details": "Detalles", - "QuestManagement": "Administrar misión", - "GMNotes": "Notas del GM" + "GMNotes": "Notas del GM", + "PlayerNotes": "Notas del jugador", + "QuestManagement": "Administrar Misión" + }, + "Title": "Detalles de la misión - {name}", + "Tooltips": { + "AddCustom": "Agregar personalizada", + "AddObjective": "Añadir Objetivo", + "ChangeSplashPos": "Cambiar la alineación de la imagen de misión.", + "DeleteQuestGiver": "Eliminar dador de misiones.", + "DeleteSplash": "Eliminar imagen de misión.", + "HideAll": "Ocultar Todo", + "LockAll": "Bloquear Todo", + "PrimaryQuestSet": "Haz clic para seleccionar la misión principal.", + "PrimaryQuestUnset": "Haz clic para anular la misión principal.", + "RewardHidden": "La recompensa está oculta. Haz clic para mostrar.", + "RewardLocked": "La recompensa está bloqueada. Haz clic para desbloquear.", + "RewardLockedPlayer": "La recompensa está bloqueada.", + "RewardUnlocked": "La recompensa está desbloqueada. Haz clic para bloquear.", + "RewardUnlockedPlayer": "La recompensa está desbloqueada.", + "RewardVisible": "La recompensa está visible. Haz clic para ocultar.", + "ShowAll": "Mostrar Todo", + "TaskHidden": "El objetivo está oculto. Haz clic para mostrar.", + "TaskVisible": "La tarea está visible. Haz clic para ocultar.", + "ToggleImage": "Alternar entre imagen de token o actor.", + "UnlockAll": "Desbloquear Todo", + "ViewSplashArt": "Ver imagen de portada." } - - }, - - "DeleteDialog": { - "Title": "Eliminar {name}", - "Header": "¿Estás seguro?", - "Body": "Esta misión y todos sus datos serán permanentemente eliminados.", - "Delete": "Eliminar", - "Cancel": "Cancelar" }, - - "CloseDialog": { - "Title": "Descartar creación de misión", - "Header": "¿Estás seguro?", - "Body": "Los datos no guardados se perderán.", - "Discard": "Descartar cambios", - "Cancel": "Cancelar" + "QuestTracker": { + "NoPrimary": "No hay misión principal disponible.", + "Title": "Seguimiento de Misiones", + "Tooltips": { + "BackgroundShow": "Haz clic para mostrar el fondo.", + "BackgroundUnshow": "Haz clic para hacer que el fondo sea transparente.", + "PrimaryQuestShow": "Haz clic para mostrar la misión principal.", + "PrimaryQuestUnshow": "Haz clic para mostrar todas las misiones." + } }, - - "Notifications": { - "CannotOpen": "No puedes abrir los detalles de la misión. Puede que te falten permisos, que la misión ya no exista o que el ID sea inválido.", - "UserCantOpen": "El usuario {user} no tiene permisos para abrir esta misión.", - "LinkCopied": "El link a la entidad de esta misión ha sido copiado al portapapeles", - "QuestMoved": "Se movió la misión a una nueva carpeta y se le dio el estado: {target}" + "QuestTypes": { + "Labels": { + "Active": "En curso", + "active": "en curso", + "Available": "Disponibles", + "available": "disponible", + "Completed": "Completadas", + "completed": "completada", + "Failed": "Fallidas", + "failed": "fallida", + "InActive": "Inactivas", + "inactive": "inactiva", + "Status": "La Misión está {statusLabel}." + }, + "Tooltips": { + "SetActive": "Establecer como En Curso", + "SetAvailable": "Establecer como Disponible", + "SetCompleted": "Establecer como Completada", + "SetFailed": "Establecer como Fallida", + "SetInactive": "Establecer como Inactiva", + "Status": "Estado: {statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "Los jugadores pueden aceptar misiones", + "EnableHint": "Marcar para permitir a los jugadores aceptar misiones desde la pestaña 'Disponibles'." + }, + "allowPlayersCreate": { + "Enable": "Los jugadores pueden crear misiones", + "EnableHint": "Marcar para permitir a los jugadores crear misiones. Las misiones creadas por ellos aparecerán en la pestaña 'Disponibles'. Requiere tener permisos globales de jugador 'Crear entradas en el Diario'." + }, "allowPlayersDrag": { "Enable": "Permitir a los jugadores arrastrar las recompensas a sus fichas", "EnableHint": "Marcar para permitir a los jugadores arrastrar las recompensas de la ventana de detalles de la misión a su propia ficha de personaje." }, - "availableQuests": { - "Enable": "Mostrar pestaña \"Disponibles\"", - "EnableHint": "Marcar para mostrar la pestaña \"Disponibles\" en el registro de misiones, donde los jugadores podrán ver todas las misiones no ocultas que aún no han sido aceptadas." - }, "countHidden": { "Enable": "Contar las tareas ocultas", "EnableHint": "Marcar para incluir las tareas ocultas en el número de tareas completadas / totales." }, + "defaultPermissionLevel": { + "Enable": "Nivel de permiso de misión predeterminado", + "EnableHint": "Establece el nivel de permiso predeterminado cuando se crean nuevas misiones.", + "NONE": "Ninguno", + "OBSERVER": "Observador", + "OWNER": "Dueño" + }, + "dynamicBookmarkBackground": { + "Enable": "Fondo de Marcador Dinámico", + "EnableHint": "Si está marcado, el fondo de la pestaña de marcadores se establece dinámicamente en el fondo del contenido de la ventana." + }, + "hideFQLFromPlayers": { + "Enable": "Ocultar registro de misiones de los jugadores", + "EnableHint": "Cuando está habilitada, esta opción oculta el registro de misiones de todos los jugadores. Solo los usuarios de nivel GM podrán acceder al registro de misiones." + }, "navStyle": { - "Enable": "Estilo de navegación", - "EnableHint": "Decide como se visualizará el registro de misiones.", "bookmarks": "Marcadores", - "classic": "Pestañas clásicas" + "classic": "Pestañas clásicas", + "Enable": "Estilo de navegación", + "EnableHint": "Decide cómo debe mostrarse la navegación del Registro de Misiones." + }, + "notifyRewardDrop": { + "Enable": "Mostrar notificaciones de entrega de recompensas", + "EnableHint": "Verifique para ver notificaciones cuando las recompensas de misiones se colocan en las hojas de los jugadores." + }, + "questTrackerResizable": { + "Enable": "Seguimiento de misiones redimensionable", + "EnableHint": "Marque para permitir el control de cambio de tamaño manual de la ventana de Seguimiento de Misiones." }, "showFolder": { "Enable": "Mostrar carpeta de misiones", - "EnableHint": "Marcar para mostrar la carpeta de misiones en la pestaña de \"Apuntes\" Solo para modo DEBUG." + "EnableHint": "Marcar para mostrar la carpeta de misiones en la pestaña de \"Diario\" Solo para modo DEBUG." }, "showTasks": { - "Enable": "Mostrar tareas en el registro de misiones", - "EnableHint": "Decide si y como se muestra la cantidad de tareas (objetivos) junto al nombre de la misión en el registro de misiones. Esto no tiene efecto en la visualización de la pantalla de detalle de misión.", "default": "Mostrar tareas: completadas / totales", - "onlyCurrent": "Mostrar tareas: completadas", - "no": "Ocultar columna \"tareas\"" - }, - "titleAlign": { - "Enable": "Alineación del nombre de la misión", - "EnableHint": "Decide como alinear los nombres de misiones en la tabla de registro de misiones.", - "left": "Alineado a la izquierda", - "center": "Centrado" - }, - "playersWelcomeScreen": { - "Enable": "Mostrar mensaje de bienvenida a los jugadores", - "EnableHint": "Desmarcar para prevenir que los jugadores vean la pantalla de bienvenida al entrar a la partida después de una actualización. Aún podrán acceder a ella pulsando en el icono de \"Ayuda\" en la cabecera del registro de misiones." - }, - "allowPlayersAccept": { - "Enable": "Los jugadores pueden aceptar misiones", - "EnableHint": "Marcar para permitir a los jugadores aceptar misiones desde la pestaña \"Disponibles\"." + "Enable": "Mostrar tareas en el registro de misiones", + "EnableHint": "Decide si y cómo se muestra la cantidad de tareas (objetivos) junto al nombre de la misión en el registro de misiones. Esto no tiene efecto en la visualización de la pantalla de detalle de misión.", + "no": "Ocultar columna \"Objetivos\"", + "onlyCurrent": "Mostrar objetivos: completados" }, - "allowPlayersCreate": { - "Enable": "Los jugadores pueden crear misiones", - "EnableHint": "Marcar para permitir a los jugadores crear misiones. Las misiones creadas por ellos aparecerán en la pestaña \"Disponibles\". Requiere tener permisos globales de jugador \"Crear apuntes\"." + "trustedPlayerEdit": { + "Enable": "Permitir la edición de misiones de jugadores de confianza", + "EnableHint": "Marcar para permitir que los jugadores de confianza tengan capacidades ampliadas de edición de misiones y control de estado sobre las misiones de las que son propietarios." } }, - "Tooltips": { - "SetAvailable": "Mover a disponibles", - "SetActive": "Poner en curso", - "SetCompleted": "Completar misión", - "SetFailed": "Mover a fallidas", - "Hide": "Ocultar", "Delete": "Eliminar", - "AddAbstractReward": "Añadir recompensa abstracta", - "PersonalQuestButNoPlayers": "Esta es una misión personal, pero ningún jugador puede verla.", - "PersonalQuestVisibleFor": "Esta es una misión personal para", - "RewardHidden": "La recompensa está oculta. Clic para mostrar.", - "RewardVisible": "La recompensa está visible. Clic para ocultar.", - "TaskHidden": ".La tarea está oculta. Clic para mostrar.", - "TaskVisible": ".La tarea está visible. Clic para ocultar.", - "ToggleImage": "Alternar entre imagen de token o actor" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "Open „{name}” Quest", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Edit": "Editar", + "HiddenQuestNoPlayers": "Esta misión está oculta a todos los jugadores.", + "PrimaryQuest": "Misión Principal" } } } diff --git a/lang/fi-FI.json b/lang/fi-FI.json new file mode 100644 index 00000000..96491bfe --- /dev/null +++ b/lang/fi-FI.json @@ -0,0 +1,256 @@ +{ + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Avaa \"{name}\"" + }, + "Notifications": { + "NoQuest": "API virhe: Makroa ei voi luoda kelvottomalla tehtävän tunnisteella." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Uusi tehtävä" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (palkkio): {userName} raahasi tavaran \"{itemName}\" hahmolle {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Entiteettiä ei voitu ladata UUID:llä \"{uuid}\".", + "NoPermission": "Sinulla ei ole tarvittavia oikeuksia nähdä tämän entiteetin lomaketta." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Tämä tavoite ja sen tiedot poistetaan lopullisesti.", + "BodyQuest": "Tämä tehtävä ja sen tiedot poistetaan lopullisesti.", + "BodyReward": "Tämä palkkio ja sen tiedot poistetaan lopullisesti.", + "Cancel": "Peruuta", + "Delete": "Poista", + "HeaderDel": "Haluatko varmasti poistaa:

\"{name}\"

", + "TitleDel": "Poista {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Näytä pelaajille" + }, + "Quest": "Tehtävä" + }, + "Migration": { + "ChatMessage": { + "Footer": "Sinun täytyy päivittää yllä olevat tehtävät käsin käyttämään kelpaavia dokumenttien tietoja hakemistoista tai maailmastasi.
", + "Header": "Forien's Quest Log (tietokannan siirto)
Alla olevien tehtävien linkittämättömät tehtävänantajat ja palkkiotavarat poistettiin:

", + "Notification": "Forien's Quest Log - Linkittämätön tehtävänantaja tai palkkiotavaroita poistettiin yhdestä tai useammasta tehtävästä. Katso lisätietoja keskusteluviestistä.", + "QuestGiver": "Tehtävänantaja", + "QuestRewards": "Tehtävän palkkiot" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Tietojen siirto valmis.", + "CouldNotMigrate": "Forien's Quest Log - Tehtävää \"{name}\" ei voitu siirtää.", + "Schema": "Forien's Quest Log - Tietoja siirretään skeeman versioon \"{version}\".", + "Start": "Forien's Quest Log - Tietoja siirretään, ethän lataa sivua uudelleen." + } + }, + "Notifications": { + "CannotOpen": "Tehtävän yksityiskohtia ei voi avata. Tämä tehtävä ei välttämättä ole havaittavissa tai sitä ei enää ole.", + "FinishQuestAdded": "Teethän muokkauksesi loppuun ja suljet nykyisen tehtävän, ennen kuin lisäät uuden.", + "LinkCopied": "Entiteetin linkki tähän tehtävään kopioitiin leikepöydälle.", + "QuestAdded": "Uusi tehtävä \"{name}\" lisättiin {status}-tilassa.", + "QuestIDCopied": "Tämän tehtävän tunniste kopioitiin leikepöydälle.", + "QuestMoved": "\"{name}\" siirrettiin ja uudeksi tilaksi asetettiin \"{target}\".", + "QuestPrimary": "\"{name}\" on uusi päätehtävä.", + "QuestTrackerNoActive": "Tehtäväseurain on päällä, mutta keskeneräisiä tehtäviä ei tällä hetkellä ole.", + "UserCantOpen": "Käyttäjällä \"{user}\" ei ole oikeuksia avata tätä tehtävää." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Lisää tehtävä" + }, + "ContextMenu": { + "CopyEntityLink": "Kopioi entiteetin sisältölinkki", + "CopyQuestID": "Kopioi tehtävän tunniste", + "PrimaryQuest": "Aseta/poista päätehtäväksi" + }, + "Labels": { + "TableHeader": "{0} tehtävää", + "SubTitle": "Alitehtävä ({0})" + }, + "Title": "Tehtäväloki", + "Tooltips": { + "Objectives": "Tavoitteet" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Käyttäjän Määrittelemä", + "RewardHide": "Piilota", + "RewardLock": "Lukita", + "RewardShow": "Näyttää", + "RewardUnlock": "Avata" + }, + "Labels": { + "CustomSource": "Mukautettu alkuperä", + "Description": "Kuvaus:", + "DragDropActor": "Raahaa & pudota hahmo, tavara tai muistiinpano tai napsauta vasemmalla asettaaksesi mukautetun alkuperän.", + "DragDropActorPlayer": "Raahaa & pudota hahmo, tavara tai muistiinpano.", + "DragDropRewards": "Raahaa & pudota tavaroita tähän asettaaksesi ne palkkioiksi.", + "GMNotes": "PJ:n muistiinpanot:", + "Objective": "Tavoite", + "Objectives": "Tavoitteet:", + "PlayerNotes": "Pelaajan muistiinpanot:", + "Reward": "Palkkio", + "Rewards": "Palkkiot:" + }, + "Management": { + "AddSubquest": "Lisää alitehtävä", + "ConfigurePermissions": "Määritä oikeudet", + "QuestBranching": "Alitehtävät:", + "QuestSettings": "Tehtävän asetukset:", + "SplashArt": "Kansikuva:", + "SplashInfo": "Napsauta asettaaksesi kuvan.", + "SplashQuestIcon": "Aseta tehtävän kuvakkeeksi" + }, + "Notifications": { + "BadUUID": "Dokumenttia ei löytynyt UUID:llä \"{uuid}\".", + "WrongDocType": "Forien's Quest Log hyväksyy vain maailman/hakemiston hahmoja, tavaroita ja muistiinpanoja tehtävänantajiksi.", + "WrongItemType": "Forien's Quest Log hyväksyy vain maailman ja hakemiston tavaroita palkkioiksi." + }, + "Tabs": { + "Details": "Yksityiskohdat", + "GMNotes": "PJ:n muistiinpanot", + "PlayerNotes": "Pelaajan muistiinpanot", + "QuestManagement": "Hallitse tehtävää" + }, + "Title": "Tehtävän yksityiskohdat - {name}", + "Tooltips": { + "AddCustom": "Lisää käyttäjän määrittämä", + "AddObjective": "Lisää tavoite", + "ChangeSplashPos": "Muuta kansikuvan tasausta.", + "DeleteQuestGiver": "Poista tehtävänantaja.", + "DeleteSplash": "Poista kansikuva.", + "HideAll": "Piilota kaikki", + "LockAll": "Lukitse kaikki", + "PrimaryQuestSet": "Napsauta asettaaksesi päätehtäväksi.", + "PrimaryQuestUnset": "Napsauta poistaaksesi päätehtävistä.", + "RewardHidden": "Palkkio on piilotettu. Napsauta näyttääksesi sen.", + "RewardLocked": "Palkkio on lukittu. Napsauta avataksesi sen.", + "RewardLockedPlayer": "Palkkio on lukittu.", + "RewardUnlocked": "Palkkio on lukitsematon. Napsauta lukitaksesi sen.", + "RewardUnlockedPlayer": "Palkkiota ei ole lukittu.", + "RewardVisible": "Palkkio on näkyvillä. Paina piilottaaksesi sen.", + "ShowAll": "Näytä kaikki", + "TaskHidden": "Tavoite on piilotettu. Napsauta näyttääksesi sen.", + "TaskVisible": "Tavoite on näkyvillä. Napsauta piilottaaksesi sen.", + "ToggleImage": "Näytä/piilota pelinappulan/hahmon kuva.", + "UnlockAll": "Avaa lukitus kaikista", + "ViewSplashArt": "Katso kansikuva." + } + }, + "QuestTracker": { + "NoPrimary": "Päätehtävää ei saatavilla.", + "Title": "Tehtäväseurain", + "Tooltips": { + "BackgroundShow": "Napsauta näyttääksesi taustan.", + "BackgroundUnshow": "Napsauta piilottaaksesi taustan.", + "PrimaryQuestShow": "Napsauta näyttääksesi päätehtävän.", + "PrimaryQuestUnshow": "Napsauta näyttääksesi kaikki tehtävät." + } + }, + "QuestTypes": { + "Labels": { + "Active": "Kesken", + "active": "kesken", + "Available": "Saatavilla", + "available": "saatavilla", + "Completed": "Suoritettu", + "completed": "suoritettu", + "Failed": "Epäonnistunut", + "failed": "epäonnistunut", + "InActive": "Ei-aktiivinen", + "inactive": "ei-aktiivinen", + "Status": "Tehtävä on {statusLabel}." + }, + "Tooltips": { + "SetActive": "Aseta kesken-tilaan", + "SetAvailable": "Aseta saatavilla-tilaan", + "SetCompleted": "Aseta suoritettu-tilaan", + "SetFailed": "Aseta epäonnistunut-tilaan", + "SetInactive": "Aseta ei-aktiivinen-tilaan", + "Status": "Tila: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Pelaajat voivat ottaa vastaan tehtäviä", + "EnableHint": "Valitse salliaksesi pelaajien ottaa vastaan tehtäviä Saatavilla-välilehdeltä." + }, + "allowPlayersCreate": { + "Enable": "Pelaajat voivat luoda tehtäviä", + "EnableHint": "Valitse salliaksesi pelaajien luoda tehtäviä. Pelaajien luomat tehtävät ilmestyvät Saatavilla-välilehdelle Pelaajan muokattava -oikeuksilla. VAATII Luoda muistiinpanoja -ydinoikeuden." + }, + "allowPlayersDrag": { + "Enable": "Salli pelaajien raahata palkkioita", + "EnableHint": "Valitse salliaksesi pelaajien raahata palkkioita tehtävän yksityiskohdista omistamilleen hahmoille." + }, + "countHidden": { + "Enable": "Laske piilotetut tavoitteet", + "EnableHint": "Jos valittu, niin myös piilotetut tavoitteet lasketaan mukaan suoritettuihin/kaikkiin tavoitteisiin." + }, + "defaultPermissionLevel": { + "Enable": "Tehtävän oletusoikeudet", + "EnableHint": "Asettaa oletuksena tämän oikeuden uusille tehtäville.", + "NONE": "Ei mitään", + "OBSERVER": "Tarkastelija", + "OWNER": "Omistaja" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynaaminen kirjanmerkin tausta", + "EnableHint": "Jos valittu, kirjanmerkkivälilehden tausta asetetaan dynaamisesti sen ikkunan sisällöstä." + }, + "hideFQLFromPlayers": { + "Enable": "Piilota tehtäväloki pelaajilta", + "EnableHint": "Jos valittu, tehtäväloki piilotetaan kaikilta pelaajilta. Vain pelinjohtajat voivat käyttää sitä." + }, + "navStyle": { + "bookmarks": "Kirjanmerkit", + "classic": "Klassiset välilehdet", + "Enable": "Navigaatiotyyli", + "EnableHint": "Valitse, miltä tehtävälokin navigaatio näyttää." + }, + "notifyRewardDrop": { + "Enable": "Näytä palkkioiden raahausilmoitukset", + "EnableHint": "Valitse nähdäksesi ilmoituksen, kun tehtävien palkkioita raahataan hahmolomakkeille." + }, + "questTrackerResizable": { + "Enable": "Muutettava tehtäväseurain", + "EnableHint": "Valitse salliaksesi tehtäväseuraimen koon muuttamisen käsin." + }, + "showFolder": { + "Enable": "Näytä tehtäväkansio", + "EnableHint": "Valitse näyttääksesi tehtävien tiedot kansiossa Muistiinpanot-välilehdellä. Vain TESTAUSKÄYTTÖÖN." + }, + "showTasks": { + "default": "Näytä tavoitteet: suoritettu/kaikki", + "Enable": "Näytä tavoitteet tehtävälokissa", + "EnableHint": "Valitse kuinka tavoitteiden määrä näytetään tehtävän otsikon vieressä tehtävälokissa. Tämä ei vaikuta tehtävän esikatseluun.", + "no": "Piilota Tavoitteet-sarake", + "onlyCurrent": "Näytä tavoitteet: suoritettu" + }, + "trustedPlayerEdit": { + "Enable": "Salli luotetun pelaajan muokata tehtäviä", + "EnableHint": "Valitse antaaksesi luotetuille pelaajille laajemmat työkalut omistamiensa tehtävien sisällön ja tilan hallintaan." + } + }, + "Tooltips": { + "Delete": "Poista", + "Edit": "Muokkaa", + "HiddenQuestNoPlayers": "Tämä tehtävä on piilotettu kaikilta pelaajilta.", + "PrimaryQuest": "Päätehtävä" + } + } +} diff --git a/lang/fr.json b/lang/fr.json index 92ae697a..3ca80ded 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -1,202 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "Nouvelle quête", - "QuestLogButton": "Journal des quêtes", - "Quests": "Quêtes {0}", - "SampleReward": "ex : 300 Points d’Expérience", - "SampleTask": "ex : Tuer tous les rats dans l’Auberge de „La cheville tordue” ", - - "QuestTypes": { - "InProgress": "En cours", - "Completed": "Achevée", - "Failed": "Échouée", - "Hidden": "Cachée", - "Labels": { - "available": "disponibles", - "active": "en cours", - "completed": "complétées", - "failed": "échouées", - "hidden": "cachées" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Ouvrir la Quête ”{name}”" + }, + "Notifications": { + "NoQuest": "Erreur API : impossible de créer une macro avec un ID de quête non valide." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nouvelle quête" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (récompense de quête) : {userName} a donné '{itemName}' à {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Une entité ne pourra pas être chargée pour IDuu : '{uuid}'.", + "NoPermission": "Vous n'avez pas la permission de voir la feuille de cette entité." + } } }, - - "Buttons": { - "AddNewQuest": "Ajouter nouvelle Quête", - "AddNewTask": "Ajouter nouvelle Tâche" + "DeleteDialog": { + "BodyObjective": "Cet objectif et ses données seront effacées définitivement.", + "BodyQuest": "Cette quête et ses données seront effacées définitivement.", + "BodyReward": "Cette récompense et ses données seront effacées définitivement.", + "Cancel": "Annuler", + "Delete": "Effacer", + "HeaderDel": "Êtes vous certain de vouloir effacer :

'{name}'

", + "TitleDel": "Effacer {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Montrer aux joueurs" + }, + "Quest": "Quête" + }, + "Migration": { + "ChatMessage": { + "Footer": "Vous devez mettre à jour manuellement les quêtes ci-dessus avec des données de document valide des compendiums ou de votre monde.
", + "Header": "Forien's Quest Log (migration de DB)
A retiré le donneur de quête sans lien ou les objets récompense des quêtes ci-dessous :

", + "Notification": "Forien's Quest Log - A retiré le donneur de quête sans lien ou les objets récompenses de quête d'une ou plusieurs quêtes. Regardez la conversation pour plus d'information.", + "QuestGiver": "Donneur de quête", + "QuestRewards": "Récompenses de quête" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migration des données achevée.", + "CouldNotMigrate": "Forien's Quest Log - ne peut pas migrer la quête : '{name}'.", + "Schema": "Forien's Quest Log - Migration des données sous le schéma de version : '{version}'.", + "Start": "Forien's Quest Log - Migration en cours des données, ne pas recharger." + } }, - - "QuestForm": { - "Title": "Ajouter nouvelle Quête", - "QuestGiver": "Commanditaire", - "QuestGiverPlaceholder": "Nom ou ID du commanditaire(Acteur ou Journal)", - "QuestTitle": "Nom de la Quête", - "DragDropActor": "Glisser/Déposer un acteur ou un journal ici pour en faire un commanditaire", - "QuestDescription": "Description de la quête", - "QuestGMNotes": "Notes du MJ", - "Submit": "Valider", - "SubquestOf": "Quête secondaire de {name}" + "Notifications": { + "CannotOpen": "Ne peut ouvrir les détails de la Quête. Soit vous n’avez pas la permission, soit la quête n’existe plus ou l’ID fournie était invalide.", + "FinishQuestAdded": "Finissez d'éditer et fermez la nouvelle quête actuelle avant d'en ajouter une autre.", + "LinkCopied": "Le lien vers cette quête a été copié dans le presse-papier.", + "QuestAdded": "'{name}' est ajouté comme nouvelle quête avec le statut : '{status}'.", + "QuestIDCopied": "L'ID de cette quête a été copiée vers le presse-papier.", + "QuestMoved": "Quête déplacée avec un nouveau statut : '{target}'.", + "QuestPrimary": "'{name}' est la nouvelle quête primaire.", + "QuestTrackerNoActive": "Le traqueur de quête est activé, mais il n'y a pas pas de quête en cours actuellement.", + "UserCantOpen": "L'utilisateur '{user}' n’a pas la permission d'ouvrir cette quête." }, - "QuestLog": { - "Title": "Journal de Quête", - "SubTitle": "Quête faisant parti de: {0}", - "Table": { - "QuestGiver": "Commanditaire", - "QuestTitle": "Nom de la Quête", - "Tasks": "Tâche(s)", - "Actions": "Actions" + "Buttons": { + "AddQuest": "Ajouter Quête" }, - "Tabs": { - "Available": "Disponible", - "InProgress": "En cours", - "Completed": "Achevée", - "Failed": "Échouée", - "Hidden": "Cachée" + "ContextMenu": { + "CopyEntityLink": "Copier le contenu du lien de l'entité", + "CopyQuestID": "Copier l'ID de la quête", + "PrimaryQuest": "Activer / Désactiver comme quête primaire" + }, + "Labels": { + "TableHeader": "Quêtes {0}", + "SubTitle": "Quête faisant parti de : {0}" + }, + "Title": "Journal de Quête", + "Tooltips": { + "Objectives": "Objectifs" } }, - "QuestPreview": { - "Title": "Détails sur la quête", - "SubTitle": "Quête faisant parti de: {0}", - "Objectives": "Objectifs", - "Rewards": "Récompenses", - "DragDropRewards": "Glisser/Déposer les objets ici pour en faire des récompenses", - "InvalidQuestId": "Impossible d'ouvrir la prévisualisation de la quête en raison d’une ID de Quête invalide.", - "HeaderButtons": { - "Show": "Montrer aux joueurs" - }, - + "Buttons": { + "RewardCustom": "Défini par l'utilisateur", + "RewardHide": "Cacher", + "RewardLock": "Bloquer", + "RewardShow": "Montrer", + "RewardUnlock": "Ouvrir" + }, + "Labels": { + "CustomSource": "Source personnalisée", + "Description": "Description :", + "DragDropActor": "Glisser/déposer un acteur, un objet, une entrée de journal ou clic gauche pour une source personnalisée.", + "DragDropActorPlayer": "Glisser/déposer un acteur, un objet ou une entrée de journal.", + "DragDropRewards": "Glisser/Déposer les objets ici pour en faire des récompenses.", + "GMNotes": "Notes du MJ :", + "Objective": "Objectif", + "Objectives": "Objectifs :", + "PlayerNotes": "Notes des joueurs:", + "Reward": "Récompense", + "Rewards": "Récompenses :" + }, "Management": { - "IsPersonalQuest": "Est-ce une quête personnelle ?", - "IsPersonalQuestDescription": "Cocher pour donner à la quête un personnage. Cela sera invisible pour tous les joueurs sauf ceux spécifiquement marqués ci-dessous. Décocher cette option retirera toutes les permissions et déplacera la quête dans l’onglet Cachée.", - "SplashArt": "Illustration", - "QuestBranching": "Quêtes secondaires", "AddSubquest": "Créer une quête secondaire", - "CanPlayerEdit": "Autoriser les joueurs a modifier les détails de la quête" + "ConfigurePermissions": "Configurer les permissions", + "QuestBranching": "Quêtes secondaires :", + "QuestSettings": "Réglages des quêtes :", + "SplashArt": "Illustration :", + "SplashInfo": "Cliquer pour définir une image.", + "SplashQuestIcon": "Définissez comme icône de quête" + }, + "Notifications": { + "BadUUID": "Ne peut récupérr le document pour IDuu : '{uuid}'.", + "WrongDocType": "Forien's Quest Log accepte seulement les acteurs, les objets et les entrées de journaux du monde /compendium comme commanditaires de quêtes.", + "WrongItemType": "Forien's Quest Log n'accepte que les objets du monde et des compendium items comme récompenses." }, - "Tabs": { "Details": "Détails", - "QuestManagement": "Modifier la Quête", - "GMNotes": "Notes MJ" + "GMNotes": "Notes MJ", + "PlayerNotes": "Notes des joueurs", + "QuestManagement": "Modifier la Quête" + }, + "Title": "Détails sur la quête", + "Tooltips": { + "AddCustom": "Ajouter défini par l'utilisateur", + "AddObjective": "Ajouter Objectif", + "ChangeSplashPos": "Modifier l'alignement de l'image d'illustration.", + "DeleteQuestGiver": "Supprimer le commanditaire.", + "DeleteSplash": "Supprimer l'illustration.", + "HideAll": "Tout cacher", + "LockAll": "Tout verrouiller", + "PrimaryQuestSet": "Ciquer pour activer la quête primaire.", + "PrimaryQuestUnset": "Cliquer pour désactiver la quête primaire.", + "RewardHidden": "Récompense cachée. Cocher pour la montrer.", + "RewardLocked": "Récompense bloquée. Cliquer pour débloquer.", + "RewardLockedPlayer": "Récompense bloquée.", + "RewardUnlocked": "Récompense débloquée. Cliquer pour bloquer.", + "RewardUnlockedPlayer": "Récompense est débloquée.", + "RewardVisible": "Récompense visible. Cocher pour la cacher.", + "ShowAll": "Montrer à tous", + "TaskHidden": "Tache cachée. Cocher pour la montrer.", + "TaskVisible": "Tâche visible. Cocher pour la cacher.", + "ToggleImage": "Activer l’image du token/acteur.", + "UnlockAll": "Tout débloquer", + "ViewSplashArt": "Voir l'illustration." } - }, - - "DeleteDialog": { - "Title": "Effacer {name}", - "Header": "Êtes-vous sûr ?", - "Body": "Cette quête et ces données seront effacées définitivement.", - "Delete": "Effacer", - "Cancel": "Annuler" - }, - - "CloseDialog": { - "Title": "Quitter le Formulaire", - "Header": "Êtes vous sûrs ?", - "Body": "Êtes vous sûrs de vouloir fermer le formulaire ? Toutes données non sauvegardées serons perdus.", - "Discard": "Abandonner les modifications", - "Cancel": "Annuler" + "QuestTracker": { + "NoPrimary": "Pas de quête primaire disponible.", + "Title": "Traqueur de quêtes", + "Tooltips": { + "BackgroundShow": "Cliquez pour afficher l'arrière-plan.", + "BackgroundUnshow": "Cliquer pour un arrière plan transparent.", + "PrimaryQuestShow": "Cliquer pour montrer la quête primaire.", + "PrimaryQuestUnshow": "Cliquer pour cacher toutes les quêtes." + } }, - - "Notifications": { - "CannotOpen": "Ne peut ouvrir les détails de la Quête. Soit vous n’avez pas la permission, soit la quête n’existe plus ou l’ID fournie était invalide.", - "UserCantOpen": "L'utilisateur {user} n’a pas la permission d'ouvrir cette quête.", - "LinkCopied": "Le lien vers cette quête a était copier dans le presse-papier.", - "QuestMoved": "Quête déplacée dans un nouveau dossier avec un nouveau : {target}" + "QuestTypes": { + "Labels": { + "Active": "En cours", + "active": "en cours", + "Available": "Disponible", + "available": "disponibles", + "Completed": "Achevée", + "completed": "complétées", + "Failed": "Échouée", + "failed": "échouées", + "InActive": "Inactif", + "inactive": "inactif", + "Status": "La quête est {statusLabel}." + }, + "Tooltips": { + "SetActive": "Définir comme En cours", + "SetAvailable": "Définir comme Disponible", + "SetCompleted": "Définir comme Achevée", + "SetFailed": "Définir comme Échouée", + "SetInactive": "Définir comme Inactif", + "Status": "Statut : {statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "Joueurs peuvent accepter", + "EnableHint": "Cocher pour permettre aux joueurs d'accepter les quêtes dans l'onglet Disponible." + }, + "allowPlayersCreate": { + "Enable": "Joueurs peuvent créer", + "EnableHint": "Cocher pour permettre aux joueurs de créer des Quêtes. Les quêtes créées par des joueurs atterissent dans l'onglet 'Disponible', NECESSITE l'autorisation 'Créer Journal' dans le core." + }, "allowPlayersDrag": { "Enable": "Permettre aux joueurs de récupérer les récompenses", - "EnableHint": "Cocher pour autoriser les joueurs à récupérer les récompenses de la fenêtre Détails de la quête à leur propres Acteurs." - }, - "availableQuests": { - "Enable": "Montre l’onglet disponible", - "EnableHint": "Cocher pour montrer un nouvelle onglet \"Disponible\" dans le journal des quêtes, où les joueurs pourront voir toutes les quêtes non-cachées avant qu’elles soient acceptées." + "EnableHint": "Cocher pour autoriser les joueurs à récupérer les récompenses de la fenêtre Détails de la quête vers leur propres Acteurs." }, "countHidden": { "Enable": "Montrer le nombre de tâches cachées", "EnableHint": "Cocher pour que le nombre de tâche Completer/Total inclue aussi les tâches cachées." }, + "defaultPermissionLevel": { + "Enable": "Niveau de permission des quêtes nouvelles", + "EnableHint": "Fixer les permissions par défaut lorsque de nouvelles quêtes sont créées.", + "NONE": "Aucun", + "OBSERVER": "Observateur", + "OWNER": "Propriétaire" + }, + "dynamicBookmarkBackground": { + "Enable": "Marques pages dynamiques en arrière plan", + "EnableHint": "Cochez pour que l'onglet marque page soit affichée de manière dynamique sur la fenêtre de contenu de l'arrière plan." + }, + "hideFQLFromPlayers": { + "Enable": "Cacher le journal de quête aux joueurs", + "EnableHint": "Activer cette option cache le journal de quêts à tous les joueurs. Seul less joueurs ayant le nivveau MJ seront capables d'accéder au journal de quêtes." + }, "navStyle": { - "Enable": "Style de navigation ", - "EnableHint": "Décider comment la navigation dans le journal de quête sera affichée.", "bookmarks": "Marque-page", - "classic": "Onglets classiques" + "classic": "Onglets classiques", + "Enable": "Style de navigation", + "EnableHint": "Décider comment la navigation dans le journal de quête sera affichée." + }, + "notifyRewardDrop": { + "Enable": "Montrer les notification de récompenses accordées", + "EnableHint": "Cocher pour voir les notification de l'Interface utilisateur lorsque des récompenses de quêtes sont déposées sur les feuilles des joueurs." + }, + "questTrackerResizable": { + "Enable": "Traqueur de quête redimensionnable", + "EnableHint": "Cocher pour autoriser le contrôle manuel du redimensionnement du Traqueur de quête." }, "showFolder": { "Enable": "Montre le dossier des quêtes", "EnableHint": "Cocher pour montrer le dossier des données de quête dans l’onglet Journal. Seulement pour DÉBUGGER." }, "showTasks": { - "Enable": "Montre les tâches dans le Journal de quête", - "EnableHint": "Décide si ou comment montrer le montant des tâches (objectifs) à côté du titre de la Quête dans le journal de quête. Cela n’a pas d’effet sur la prévisualisation de la Quête individuelle.", "default": "montre les tâches : effectuées/total", - "onlyCurrent": "Montre les tâches : effectuées", - "no": "Cache la colonne \"tâches\"" - }, - "titleAlign": { - "Enable": "Alignement du titre de la quête", - "EnableHint": "Décide comment positionner les titres de quête dans la table de l’onglet du Journal de quête.", - "left": "Alignés à gauche", - "center": "Centrés" - }, - "playersWelcomeScreen": { - "Enable": "Affiche un message bienvenue aux Joueurs.", - "EnableHint": "Décocher pour empécher les joueurs de voir le message de bienvenue qui apparait aprés une mise a jour, ce dernier est toujours accessible en cliquant sur le bouton 'Aide' trouvable dans le journal quête" - }, - "allowPlayersAccept": { - "Enable": "Les joueurs peuvent Accepter", - "EnableHint": "Cocher pour permettre aux joueurs d'accepter les quêtes dans l'onglet Disponible" + "Enable": "Montre les tâches dans le Journal de quête", + "EnableHint": "Décide si ou comment montrer le nombre d'objectifs à côté du Titre de la quête dans le Journal de quête. Cela n’a pas d’effet sur la prévisualisation de la Quête.", + "no": "Cacher la colonne \"objectifs\"", + "onlyCurrent": "Montre les tâches : effectuées" }, - "allowPlayersCreate": { - "Enable": "Les joueurs peuvent créer", - "EnableHint": "Cocher pour permettre aux joueurs de créer des Quêtes. Les quêtes créer par des joueurs atterissent dans l'onglet 'Disponible', NECESSITE 'Créer Journal' Permission" + "trustedPlayerEdit": { + "Enable": "Permettre aux joueurs de confiance d'éditer", + "EnableHint": "Cochez pour permettre aux joueurs de disposer de capacités d'édition et de contrôle du statut des quêtes étendues pour les quêtes dont ils sont propriétaires." } }, - "Tooltips": { - "SetAvailable": "Définir comme Disponible", - "SetActive": "Définir comme En cours", - "SetCompleted": "Définir comme Achevée", - "SetFailed": "Définir comme Échouée", - "Hide": "Cacher", "Delete": "Effacer", - "AddAbstractReward": "Ajouter une récompense abstraite", - "PersonalQuestButNoPlayers": "C’est une quête personnelle, personne ne peut la voir", - "PersonalQuestVisibleFor": "C’est une quête personnelle pour ", - "RewardHidden": "Récompense cachée. Cocher pour la montrer.", - "RewardVisible": "Récompense visible. Cocher pour la cacher.", - "TaskHidden": "Tache cachée. Cocher pour la montrer.", - "TaskVisible": "Tâche visible. Cocher pour la cacher.", - "ToggleImage": "Activer l’image du token/acteur" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "Ouvrir la Quête „{name}”", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Edit": "Éditer", + "HiddenQuestNoPlayers": "Cette quête est cachée à tous les joueurs.", + "PrimaryQuest": "Quête primaire" } } -} +} \ No newline at end of file diff --git a/lang/it.json b/lang/it.json new file mode 100644 index 00000000..a28f6e7e --- /dev/null +++ b/lang/it.json @@ -0,0 +1,256 @@ +{ + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Apri ”{name}”" + }, + "Notifications": { + "NoQuest": "API Error: Impossibile creare macro con un ID missione non valido." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nuova Missione" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (Ricompensa missione): {userName} ha assegnato '{itemName}' a {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "L'entità non può essere caricata per UUID: '{uuid}'.", + "NoPermission": "Non hai sufficienti autorizzazioni per vedere questa scheda entità." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Questo obiettivo e i suoi dati saranno cancellati definitivamente.", + "BodyQuest": "Questa missione e i suoi dati saranno cancellati in modo permanente.", + "BodyReward": "Questa ricompensa e i suoi dati saranno cancellati definitivamente.", + "Cancel": "Annulla", + "Delete": "Rimuovi", + "HeaderDel": "Sei sicuro di voler eliminare:

'{name}'

", + "TitleDel": "Elimina {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Mostra ai Giocatori" + }, + "Quest": "Missione" + }, + "Migration": { + "ChatMessage": { + "Footer": "Devi aggiornare manualmente le missioni qui sopra con dati validi dai compendi o dal tuo mondo.
", + "Header": "Forien's Quest Log (migrazione DB)
Sono stati rimossi Assegnatari Missione o Ricompense non collegati dalla missione sottostante:

", + "Notification": "Forien's Quest Log - Rimosso il donatore di missioni o gli oggetti premio non collegati da una o più missioni. Si prega di rivedere il messaggio di chat per ulteriori informazioni.", + "QuestGiver": "Assegnatario Missione", + "QuestRewards": "Ricompense Missione" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migrazione dei dati completa.", + "CouldNotMigrate": "Forien's Quest Log - Impossibile migrare la missione: '{name}'.", + "Schema": "Forien's Quest Log - Migrazione dei dati alla versione dello schema '{version}'.", + "Start": "Forien's Quest Log - Migrazione dei dati, si prega di non ricaricare." + } + }, + "Notifications": { + "CannotOpen": "Impossibile aprire i dettagli della missione. Potrebbe non essere osservabile o potrebbe non esistere più.", + "FinishQuestAdded": "Si prega di terminare la modifica e chiudere la nuova missione corrente prima di aggiungerne un'altra.", + "LinkCopied": "Entity Link per questa missione è stato copiato negli appunti.", + "QuestAdded": "Aggiunto '{name}' come nuova missione con status: '{status}'.", + "QuestIDCopied": "L'ID missione è stato copiato negli appunti.", + "QuestMoved": "Spostato '{name}' e impostato nuovo stato su: '{target}'.", + "QuestPrimary": "'{name}' è la nuova missione principale.", + "QuestTrackerNoActive": "Quest Tracker è abilitato, ma al momento non ci sono missioni in corso.", + "UserCantOpen": "L'utente '{user}' non ha il permesso di aprire questa missione." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Agg. Missione" + }, + "ContextMenu": { + "CopyEntityLink": "Copia il link al contenuto dell'entità", + "CopyQuestID": "Copia ID Missione", + "PrimaryQuest": "Imposta / Rimuovi come missione primaria" + }, + "Labels": { + "TableHeader": "Missioni {0}", + "SubTitle": "Missione Annidata di {0}" + }, + "Title": "Log Missioni", + "Tooltips": { + "Objectives": "Obiettivi" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Definito dall'utente", + "RewardHide": "Nascondi", + "RewardLock": "Blocca", + "RewardShow": "Mostra", + "RewardUnlock": "Sblocca" + }, + "Labels": { + "CustomSource": "Fonte Personalizzata", + "Description": "Descrizione:", + "DragDropActor": "Trascina qui il personaggio, voce o nota o clic con il pulsante sinistro del mouse per impostare un'origine personalizzata.", + "DragDropActorPlayer": "Trascina qui il personaggio, l'oggetto o la voce del diario.", + "DragDropRewards": "Trascina e rilascia gli oggetti qui per aggiungerli come ricompense.", + "GMNotes": "Note GM:", + "Objective": "Obiettivo", + "Objectives": "Obiettivi:", + "PlayerNotes": "Note del giocatore:", + "Reward": "Ricompensa", + "Rewards": "Ricompense:" + }, + "Management": { + "AddSubquest": "Agg. Missione", + "ConfigurePermissions": "Configura Autorizzazioni", + "QuestBranching": "Missioni Annidate:", + "QuestSettings": "Impostazioni Missione:", + "SplashArt": "Foto Copertina:", + "SplashInfo": "Click per impostare l'immagine.", + "SplashQuestIcon": "Imposta come icona missione" + }, + "Notifications": { + "BadUUID": "Impossibile recuperare il documento per l'UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log Accetta solo attori, oggetti e voci di diario dal mondo / compendi, come assegnatari di missioni.", + "WrongItemType": "Forien's Quest Log Accetta solo oggetti del mondo e dal compendio come ricompensa." + }, + "Tabs": { + "Details": "Dettaglio", + "GMNotes": "Note GM", + "PlayerNotes": "Note del giocatore", + "QuestManagement": "Gestisci Missione" + }, + "Title": "Dettaglio Missione - {name}", + "Tooltips": { + "AddCustom": "Aggiungi definito dall'utente", + "AddObjective": "Aggiungi Obiettivo", + "ChangeSplashPos": "Modifica Allineamento Copertina.", + "DeleteQuestGiver": "Elimina Assegnatario Missione.", + "DeleteSplash": "Elimina copertina.", + "HideAll": "Nascondi Tutto", + "LockAll": "Blocca Tutto", + "PrimaryQuestSet": "Clic per rendere la missione principale.", + "PrimaryQuestUnset": "Fai clic per rimuovere da missione principale.", + "RewardHidden": "La ricompensa è nascosta. Clicca per mostrare.", + "RewardLocked": "La ricompensa è bloccata. Cliacca per sbloccare.", + "RewardLockedPlayer": "La ricompensa è bloccata.", + "RewardUnlocked": "La ricompensa è sbloccata. Cliacca per bloccare.", + "RewardUnlockedPlayer": "La ricompensa è sbloccata.", + "RewardVisible": "La ricompensa è visibile. Clicca per nascondere.", + "ShowAll": "Mostra Tutte", + "TaskHidden": "L'obiettivo è nascosto. Clicca per mostrare.", + "TaskVisible": "L'obiettivo è visibile. Cliicca per nascondere.", + "ToggleImage": "Attiva/Disattiva immagine Token/Personaggio.", + "UnlockAll": "Sblocca Tutte", + "ViewSplashArt": "Visualizza la foto di copertina." + } + }, + "QuestTracker": { + "NoPrimary": "Nessuna missione principale disponibile.", + "Title": "Tracker Missione", + "Tooltips": { + "BackgroundShow": "Clic per mostrare lo sfondo.", + "BackgroundUnshow": "Clic per rendere lo sfondo trasparente.", + "PrimaryQuestShow": "Clic per mostrare la missione principale.", + "PrimaryQuestUnshow": "Clic per mostrare tutte le missioni." + } + }, + "QuestTypes": { + "Labels": { + "Active": "In Corso", + "active": "in corso", + "Available": "Disponibile", + "available": "disponibile", + "Completed": "Completata", + "completed": "completata", + "Failed": "Fallita", + "failed": "fallita", + "InActive": "Non Attiva", + "inactive": "non attiva", + "Status": "La Missione è {statusLabel}." + }, + "Tooltips": { + "SetActive": "Imposta come In Corso", + "SetAvailable": "Imposta come Disponibile", + "SetCompleted": "Imposta come Completata", + "SetFailed": "Imposta come Fallita", + "SetInactive": "Imposta come Non Attiva", + "Status": "Stato: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "I giocatori possono accettare missioni", + "EnableHint": "Abilita per consentire ai giocatori di accettare le missioni tra quelle disponibili." + }, + "allowPlayersCreate": { + "Enable": "I giocatori possono creare missioni", + "EnableHint": "Abilita per consentire ai giocatori di creare missioni. Le missioni create dal giocatore appariranno nella scheda Disponibili con le autorizzazioni di modifica del giocatore. RICHIEDE l'autorizzazione di base di 'crea diario'." + }, + "allowPlayersDrag": { + "Enable": "Consenti il trascinamento della ricompensa del giocatore", + "EnableHint": "Abilita per consentire ai giocatori di trascinare le ricompense dalla finestra Dettagli missione ai propri personaggi." + }, + "countHidden": { + "Enable": "Conta le attività nascoste", + "EnableHint": "Se selezionato, il numero di attività completate/totali includerà le attività nascoste." + }, + "defaultPermissionLevel": { + "Enable": "Livello autorizzazione missione predefinito", + "EnableHint": "Imposta il livello di autorizzazione predefinito per quando vengono create nuove missioni.", + "NONE": "Nessuno", + "OBSERVER": "Osservatore", + "OWNER": "Proprietario" + }, + "dynamicBookmarkBackground": { + "Enable": "Sfondo di segnalibro dinamico", + "EnableHint": "Se selezionato, lo sfondo della scheda segnalibro è impostato dinamicamente sullo sfondo del contenuto della finestra." + }, + "hideFQLFromPlayers": { + "Enable": "Nascondi il registro missioni ai giocatori", + "EnableHint": "Quando abilitato, questa opzione nasconde il registro delle missioni a tutti i giocatori. Solo gli utenti di livello GM saranno in grado di accedere al registro delle missioni." + }, + "navStyle": { + "bookmarks": "Segnalibri", + "classic": "Schede Classiche", + "Enable": "Stile Navigazione", + "EnableHint": "Decidi come visualizzare la navigazione di Log Missioni." + }, + "notifyRewardDrop": { + "Enable": "Mostra Notifiche Trascinamento Ricompensa", + "EnableHint": "Abilita se vedere le notifiche dell'interfaccia utente quando le ricompense delle missioni vengono trascinate nelle schede dei giocatori." + }, + "questTrackerResizable": { + "Enable": "Tracker Missione Ridimensionabile", + "EnableHint": "Abilita per consentire il ridimensionamento manuale del Tracker Missione." + }, + "showFolder": { + "Enable": "Mostra Cartella Missione", + "EnableHint": "Abilita per mostrare la cartella dei dati delle missioni nella scheda Journal. Solo per scopi di DEBUG." + }, + "showTasks": { + "default": "Mostra Obiettivi: fatto/totale", + "Enable": "Mostra le attività nel registro delle missioni", + "EnableHint": "Decidi se o come mostrare la quantità di obiettivi accanto al titolo della missione nel registro delle missioni. Questo non ha alcun effetto sull'anteprima della missione.", + "no": "Nascondi colonna \"Obiettivi\"", + "onlyCurrent": "Mostra obiettivi: fatti" + }, + "trustedPlayerEdit": { + "Enable": "Consenti l'editing delle missioni dei giocatori fidati", + "EnableHint": "Abilita per consentire ai giocatori fidati di aver funzionalità di modifica delle missioni e controllo dello stato oltre alle missioni di loro proprietà." + } + }, + "Tooltips": { + "Delete": "Rimuovi", + "Edit": "Modifica", + "HiddenQuestNoPlayers": "Questa Missione è nascosta a tutti i giocatori.", + "PrimaryQuest": "Missione Primaria" + } + } +} \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 9e721ced..cd68160a 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1,202 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "クエストを追加", - "QuestLogButton": "クエストログ", - "Quests": "{0}個のクエスト", - "SampleReward": "例:300の経験点", - "SampleTask": "例:魔王を討伐する", - - "QuestTypes": { - "InProgress": "進行中", - "Completed": "完了", - "Failed": "失敗", - "Hidden": "秘匿", - "Labels": { - "available": "受注可能", - "active": "進行中", - "completed": "完了", - "failed": "失敗", - "hidden": "秘匿" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "{name} - 開く" + }, + "Notifications": { + "NoQuest": "API エラー: 無効なクエスト ID でマクロを作成できません。" + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "クエストを追加" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (quest reward):{userName}が'{itemName}'を{actorName}に移動しました。" + } + }, + "Utils": { + "Notifications": { + "NoDocument": "UUID:{uuid}のデータを読み込めませんでした。", + "NoPermission": "このデータを閲覧する権限を持っていません。" + } } }, - - "Buttons": { - "AddNewQuest": "新しいクエストを追加", - "AddNewTask": "追加" + "DeleteDialog": { + "BodyObjective": "この目標とその内容はすべて削除されます。", + "BodyQuest": "このクエストとその内容はすべて削除されます。", + "BodyReward": "この報酬とその内容はすべて削除されます。", + "Cancel": "取り消す", + "Delete": "削除", + "HeaderDel": "消去してもよろしいですか:

'{name}'

", + "TitleDel": "{title} デリート" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "プレイヤーに表示" + }, + "Quest": "クエスト" }, - - "QuestForm": { - "Title": "新しいクエストを追加", - "QuestGiver": "クエスト依頼主", - "QuestGiverPlaceholder": "Actor's name or entity's UUID", - "QuestTitle": "クエスト名", - "DragDropActor": "キャラクター、アイテムまたは資料をここにドラッグ&ドロップするとクエストの依頼主として設定できます。", - "QuestDescription": "クエスト内容", - "QuestGMNotes": "GMノート", - "Submit": "保存", - "SubquestOf": "{name}のサブクエストです" + "Migration": { + "ChatMessage": { + "Footer": "大要またはあなたの世界からの有効な文書データを使用して、上記のクエストを手動で更新する必要があります。
", + "Header": "Forien's Quest Log (DB 移動)
以下のクエストから、リンクされていないクエスト提供者または報酬アイテムを削除しました:

", + "Notification": "Forien's Quest Log - 1 つまたは複数のクエストから、リンクされていないクエスト提供者または報酬アイテムを削除しました。詳細については、チャット メッセージを確認してください。", + "QuestGiver": "クエストギバー", + "QuestRewards": "クエスト報酬" + }, + "Notifications": { + "Complete": "Forien's Quest Log - マイグレーション完了。", + "CouldNotMigrate": "Forien's Quest Log - クエスト'{name}'をマイグレーションできませんでした。", + "Schema": "Forien's Quest Log - データを最新スキーマバージョン'{version}'にマイグレーションしてます。", + "Start": "Forien's Quest Log - データマイグレーション中です、リロードしないでください。" + } + }, + "Notifications": { + "CannotOpen": "クエストの内容を開けませんでした。権限が無いかクエストが存在しないかもしれません。", + "FinishQuestAdded": "他のクエストを追加する前に現在のクエスト記入を完了してください。", + "LinkCopied": "このクエストのエンティティリンクをコピーしました。", + "QuestAdded": "「{name}」を新規追加し、状態を「{status}」に設定しました。", + "QuestIDCopied": "クエストIDがコピーされました。", + "QuestMoved": "「{name}」を移動し、状態を「{target}」に変更しました。", + "QuestPrimary": "「{name}」がメインクエストになりました。", + "QuestTrackerNoActive": "クエスト一覧が有効になっていますが、進行中のクエストがありません。", + "UserCantOpen": "ユーザー「{user}」はこのクエストを開く権限を持っていません。" }, - "QuestLog": { - "Title": "クエストログ", - "SubTitle": "{0}に含まれています", - "Table": { - "QuestGiver": "クエスト依頼主", - "QuestTitle": "クエスト名", - "Tasks": "内容", - "Actions": "行動" + "Buttons": { + "AddQuest": "クエスト追加" }, - "Tabs": { - "Available": "受注可能", - "InProgress": "進行中", - "Completed": "完了", - "Failed": "失敗", - "Hidden": "秘匿" + "ContextMenu": { + "CopyEntityLink": "データへのリンクをコピー", + "CopyQuestID": "クエストIDをコピー", + "PrimaryQuest": "メインクエスト指定切り替え" + }, + "Labels": { + "TableHeader": "{0}クエスト", + "SubTitle": "{0}に含まれています" + }, + "Title": "クエストログ", + "Tooltips": { + "Objectives": "目標" } }, - "QuestPreview": { - "Title": "クエスト詳細", - "SubTitle": "{0}に含まれています", - "Objectives": "目標", - "Rewards": "報酬", - "DragDropRewards": "アイテムをここにドラッグ&ドロップすると報酬として設定できます。", - "InvalidQuestId": "クエストIDが存在しないため開けません。", - "HeaderButtons": { - "Show": "プレイヤーに表示" - }, - + "Buttons": { + "RewardCustom": "ユーザー定義の", + "RewardHide": "隠す", + "RewardLock": "閉ざす", + "RewardShow": "示す", + "RewardUnlock": "ロック解除" + }, + "Labels": { + "CustomSource": "カスタムソース", + "Description": "詳細:", + "DragDropActor": "キャラ、アイテム、資料をドラッグ&ドロップすることでソースを手動で変更できます。", + "DragDropActorPlayer": "キャラ、アイテム、資料をドラッグ&ドロップしてください。", + "DragDropRewards": "アイテムをここにドラッグ&ドロップすると報酬として設定できます。", + "GMNotes": "GMノート:", + "Objective": "目標", + "Objectives": "目標:", + "PlayerNotes": "プレイヤーのメモ:", + "Reward": "報酬", + "Rewards": "報酬:" + }, "Management": { - "IsPersonalQuest": "個人クエスト?", - "IsPersonalQuestDescription": "個人クエストとして指定すると、そのプレイヤーにしか視認できなくなります。チェックをすべて外すとクエストが秘匿タブに表示されるようになります。", - "SplashArt": "画像", - "QuestBranching": "サブクエスト", - "AddSubquest": "サブクエストを作成する", - "CanPlayerEdit": "プレイヤーに編集権限を与える" - }, - + "AddSubquest": "サブクエスト追加", + "ConfigurePermissions": "権限設定", + "QuestBranching": "サブクエスト:", + "QuestSettings": "クエスト設定:", + "SplashArt": "表紙画像:", + "SplashInfo": "クリックしてカバー画像を設定します。", + "SplashQuestIcon": "クエストアイコンとして設定" + }, + "Notifications": { + "BadUUID": "{uuid}のオブジェクトを取得できませんでした。", + "WrongDocType": "Forien's Quest Logはワールドや辞典に指定されたキャラ、アイテム、資料のみをクエスト提供者として受け付けます。", + "WrongItemType": "Forien's Quest Logはワールドや辞典に指定された報酬のみを受け付けます。" + }, "Tabs": { "Details": "内容", - "QuestManagement": "クエスト管理", - "GMNotes": "GMノート" + "GMNotes": "GMノート", + "PlayerNotes": "プレイヤーのメモ", + "QuestManagement": "クエスト管理" + }, + "Title": "クエスト詳細", + "Tooltips": { + "AddCustom": "ユーザー定義を追加", + "AddObjective": "目標を追加", + "ChangeSplashPos": "画像の位置を調整。", + "DeleteQuestGiver": "クエスト依頼主削除。", + "DeleteSplash": "カバー画像を削除します。", + "HideAll": "全非表示", + "LockAll": "全ロック", + "PrimaryQuestSet": "メインクエストに設定。", + "PrimaryQuestUnset": "メインクエストから削除。", + "RewardHidden": "報酬は非表示になっています。クリックで表示できます。", + "RewardLocked": "報酬はロックされています。クリックでアンロックできます。", + "RewardLockedPlayer": "報酬はロックされています。", + "RewardUnlocked": "報酬はアンロックされています。", + "RewardUnlockedPlayer": "報酬はアンロックされています。", + "RewardVisible": "報酬は表示されています。クリックで非表示できます。", + "ShowAll": "全表示", + "TaskHidden": "目標は非表示になっています。クリックで表示できます。", + "TaskVisible": "目標表示されています。クリックで非表示できます。", + "ToggleImage": "キャラ・コマ画像切替。", + "UnlockAll": "全アンロック", + "ViewSplashArt": "表紙画像を見る。" } - }, - - "DeleteDialog": { - "Title": "{name}を削除します", - "Header": "本当によろしいですか?", - "Body": "このクエストとその内容はすべて削除されます。", - "Delete": "削除", - "Cancel": "取り消す" - }, - - "CloseDialog": { - "Title": "記入をやめようとしています", - "Header": "本当によろしいですか?", - "Body": "保存されていない内容は失われます。", - "Discard": "保存を破棄する", - "Cancel": "戻る" + "QuestTracker": { + "NoPrimary": "メインクエスト無し。", + "Title": "クエスト一覧", + "Tooltips": { + "BackgroundShow": "クリックすると背景が表示されます。", + "BackgroundUnshow": "クリックすると背景が透明になります。", + "PrimaryQuestShow": "クリックでメインクエストを表示。", + "PrimaryQuestUnshow": "クリックですべてのクエスト表示。" + } }, - - "Notifications": { - "CannotOpen": "クエストの内容を開けませんでした。権限が無いかクエストが存在しないかもしれません。", - "UserCantOpen": "{user}はこのクエストを開く権限を持っていません。", - "LinkCopied": "このクエストのエンティティリンクをコピーしました。", - "QuestMoved": "クエストを新たなフォルダに移動し状態を{target}に変更しました。" + "QuestTypes": { + "Labels": { + "Active": "進行中", + "active": "進行中", + "Available": "受注可能", + "available": "受注可能", + "Completed": "完了", + "completed": "完了", + "Failed": "失敗", + "failed": "失敗", + "InActive": "無効", + "inactive": "無効", + "Status": "クエスト状態{statusLabel}。" + }, + "Tooltips": { + "SetActive": "進行中として設定", + "SetAvailable": "利用可能に設定", + "SetCompleted": "完了として設定", + "SetFailed": "失敗として設定", + "SetInactive": "非アクティブに設定", + "Status": "状態:{statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "プレイヤー受注可", + "EnableHint": "プレイヤーたちは受注可能タブからクエストを受注することが可能になります。" + }, + "allowPlayersCreate": { + "Enable": "プレイヤー作成可", + "EnableHint": "チェックをいれることでプレイヤーが独自にクエストを作成できるようになります。プレイヤーが作成したクエストは受注可能タブに表示されます。この機能を使うにはプレイヤーの権限に「資料の作成」を付与している必要があります。" + }, "allowPlayersDrag": { "Enable": "報酬のドラッグを許可", "EnableHint": "プレイヤーはクエスト報酬にあるアイテム等を自分のキャラシにドラッグできるようになります。" }, - "availableQuests": { - "Enable": "受注可能タブを表示する", - "EnableHint": "許可するとプレイヤーは新たな「受注可能」タブを閲覧できるようになり、受注・秘匿されていないクエストの閲覧が可能となります。" - }, "countHidden": { "Enable": "隠しタスクをカウントする", "EnableHint": "オンにすることでクエストの合計目標数に隠し目標も含まれるようになります。" }, + "defaultPermissionLevel": { + "Enable": "デフォルト", + "EnableHint": "新規クエスト作成時の権限レベルを設定します。", + "NONE": "なし", + "OBSERVER": "監視者(Observer)", + "OWNER": "所有者(Owner)" + }, + "dynamicBookmarkBackground": { + "Enable": "動的しおり背景", + "EnableHint": "有効化時、しおりタブは自動的に現在のウィンドウの背景に置かれます。" + }, + "hideFQLFromPlayers": { + "Enable": "PLからクエスト一覧を隠す", + "EnableHint": "有効化のとき、PLはクエスト一覧を閲覧できなくなります。GM権限を持っているユーザは依然として閲覧できます。" + }, "navStyle": { - "Enable": "タブのスタイル", - "EnableHint": "クエストログのタブをどのように表示するのかを設定します", "bookmarks": "しおり風", - "classic": "クラシック風" + "classic": "クラシック風", + "Enable": "タブのスタイル", + "EnableHint": "クエストログのタブをどのように表示するのかを設定します。" + }, + "notifyRewardDrop": { + "Enable": "報酬ドロップ通知", + "EnableHint": "プレイヤーのキャラクターに報酬が移動されたことを通知します。" + }, + "questTrackerResizable": { + "Enable": "クエスト一覧リサイズ可能", + "EnableHint": "クエスト一覧の大きさを手動で変更できるようにします。" }, "showFolder": { "Enable": "クエストフォルダーを表示する", "EnableHint": "クエストデータが含まれているフォルダを表示する。デバッグ用。" }, "showTasks": { + "default": "完了/合計", "Enable": "クエストログにクエスト内容を表示する", "EnableHint": "クエストの名前の隣にそのクエストに付随する内容の表示の仕方を設定します。クエストの詳細画面には効果ありません。", - "default": "完了/合計", - "onlyCurrent": "完了のみ", - "no": "内容の列を隠す" - }, - "titleAlign": { - "Enable": "クエスト名位置合わせ", - "EnableHint": "クエストログのクエスト名の位置を調整するための設定です", - "left": "左寄せ", - "center": "中央" - }, - "playersWelcomeScreen": { - "Enable": "プレイヤーにウェルカム画面を表示する", - "EnableHint": "チェックを外すとプレイヤーたちがアップデート後にログインしたときに表示されるウェルカム画面を見えなくなります。クエストログのヘルプボタンを押すことで再び表示できます。" + "no": "内容の列を隠す", + "onlyCurrent": "完了のみ" }, - "allowPlayersAccept": { - "Enable": "プレイヤー受注可", - "EnableHint": "プレイヤーたちは受注可能タブからクエストを受注することが可能になります。" - }, - "allowPlayersCreate": { - "Enable": "プレイヤー作成可", - "EnableHint": "チェックをいれることでプレイヤーが独自にクエストを作成できるようになります。プレイヤーが作成したクエストは受注可能タブに表示されます。この機能を使うにはプレイヤーの権限に「資料の作成」を付与している必要があります。" + "trustedPlayerEdit": { + "Enable": "優良プレイヤークエスト編集権限", + "EnableHint": "優良プレイヤーとして指定されているPLに所持しているクエストを編集するための上位の権限を付与します。" } }, - "Tooltips": { - "SetAvailable": "受注可能にする", - "SetActive": "進行中にする", - "SetCompleted": "完了にする", - "SetFailed": "失敗にする", - "Hide": "秘匿", "Delete": "削除", - "AddAbstractReward": "抽象報酬を設定する", - "PersonalQuestButNoPlayers": "これは個人クエストですが誰も見ることができません。", - "PersonalQuestVisibleFor": "これは個人クエストです", - "RewardHidden": "報酬は秘匿されています。クリックして表示します。", - "RewardVisible": "報酬は表示されています。クリックして秘匿します。", - "TaskHidden": "内容が秘匿されています。クリックして表示します。", - "TaskVisible": "内容が表示されています。クリックして秘匿します。", - "ToggleImage": "コマ/キャラの画像切替" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "「{name}」クエストを開くt", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Edit": "編集", + "HiddenQuestNoPlayers": "このクエストはPLに表示されていません。", + "PrimaryQuest": "メインクエスト" } } -} +} \ No newline at end of file diff --git a/lang/ko.json b/lang/ko.json index 6a23e54e..8139b55c 100644 --- a/lang/ko.json +++ b/lang/ko.json @@ -1,202 +1,256 @@ { "ForienQuestLog": { - "NewQuest": "새 퀘스트", - "QuestLogButton": "퀘스트 로그", - "Quests": "{0} 퀘스트", - "SampleReward": "예시. 300 경험치", - "SampleTask": "예시. 꼬인 발목 여관에 있는 모든 쥐를 처리해주세요", - - "QuestTypes": { - "InProgress": "진행 중", - "Completed": "성공", - "Failed": "실패", - "Hidden": "비공개", - "Labels": { - "available": "활성화", - "active": "진행 중", - "completed": "성공", - "failed": "실패", - "hidden": "비공개" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "열려 있는 ”{name}”" + }, + "Notifications": { + "NoQuest": "API 오류: 잘못된 퀘스트 ID로 매크로를 생성할 수 없습니다." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "새 퀘스트" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (quest reward): {userName} 유저가 '{itemName}' 아이템을 {actorName} 액터에 가져갔습니다." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "UUID: {uuid}에 대해 엔티티를 불러올 수 없습니다.", + "NoPermission": "사용 권한이 부족하여 이 엔티티 시트를 볼 수 없습니다." + } } }, - - "Buttons": { - "AddNewQuest": "새 퀘스트 추가", - "AddNewTask": "새 목표 추가" + "DeleteDialog": { + "BodyObjective": "이 목표와 데이터가 영구적으로 삭제됩니다.", + "BodyQuest": "이 퀘스트와 데이터가 영구적으로 삭제됩니다.", + "BodyReward": "이 보상과 데이터가 영구적으로 삭제됩니다.", + "Cancel": "취소", + "Delete": "삭제", + "HeaderDel": "정말로 삭제하시겠습니까? :

'{name}'

", + "TitleDel": "{name} 삭제" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "플레이어에게 표시" + }, + "Quest": "퀘스트" + }, + "Migration": { + "ChatMessage": { + "Footer": "위 퀘스트를 컴펜디움 또는 월드의 유효 문서 데이터로 수동으로 업데이트 해야 합니다.
", + "Header": "Forien's Quest Log (DB 마이그레이션)
아래 퀘스트에서 연결되지 않은 퀘스트 제공자 또는 보상 아이템을 제거했습니다.

", + "Notification": "Forien's Quest Log - 하나 이상의 연결되지 않은 퀘스트 제공자 또는 보상 아이템이 제거되었습니다. 자세한 내용은 채팅 메시지를 확인하십시오.", + "QuestGiver": "퀘스트 제공자", + "QuestRewards": "퀘스트 보상" + }, + "Notifications": { + "Complete": "Forien's Quest Log - 데이터 마이그레이션 완료.", + "CouldNotMigrate": "Forien's Quest Log - 마이그레이션 할 수 없었음. 퀘스트: '{name}'.", + "Schema": "Forien's Quest Log - 스키마 버전 '{version}' 데이터 마이그레이션 중.", + "Start": "Forien's Quest Log - 데이터 마이그레이션 중, 리로드하지 마십시오." + } }, - - "QuestForm": { - "Title": "새 퀘스트 추가", - "QuestGiver": "퀘스트 의뢰인", - "QuestGiverPlaceholder": "액터의 이름 혹은 엔티티의 UUID", - "QuestTitle": "퀘스트 제목", - "DragDropActor": "액터, 아이템, 저널을 여기로 끌어다 놓아 퀘스트 의뢰인을 추가합니다", - "QuestDescription": "퀘스트 설명", - "QuestGMNotes": "GM 노트", - "Submit": "완료", - "SubquestOf": "{name}의 서브 퀘스트" + "Notifications": { + "CannotOpen": "퀘스트 세부 정보를 열 수 없습니다. 권한이 없거나 퀘스트가 더 이상 존재하지 않거나 제공된 ID가 유효하지 않을 수 있습니다.", + "FinishQuestAdded": "다른 퀘스트를 추가하기 전 현재 새 퀘스트를 편집 완료하여 닫으십시오.", + "LinkCopied": "이 퀘스트에 대한 엔티티 링크가 클립보드에 복사 되었습니다.", + "QuestAdded": "'{status}' 상태로 '{name}' 새 퀘스트가 추가되었습니다.", + "QuestIDCopied": "이 퀘스트의 퀘스트 ID가 클립보드에 복사되었습니다.", + "QuestMoved": "퀘스트를 새 폴더로 이동하고 새 상태를 부여했습니다: '{target}'.", + "QuestPrimary": "'{name}' 퀘스트는 새로운 중요 퀘스트입니다..", + "QuestTrackerNoActive": "퀘스트 트래커가 활성화 되었지만 진행 중인 퀘스트가 없습니다.", + "UserCantOpen": "'{user}' 플레이어는 이 퀘스트를 열람할 권한을 가지고 있지 않습니다." }, - "QuestLog": { - "Title": "퀘스트 로그", - "SubTitle": "연관 : {0}", - "Table": { - "QuestGiver": "퀘스트 의뢰인", - "QuestTitle": "제목", - "Tasks": "목표", - "Actions": "액션" + "Buttons": { + "AddQuest": "퀘스트 추가" }, - "Tabs": { - "Available": "활성화", - "InProgress": "진행 중", - "Completed": "완료", - "Failed": "실패", - "Hidden": "비공개" + "ContextMenu": { + "CopyEntityLink": "엔티티 내용 링크 복사", + "CopyQuestID": "퀘스트 ID 복사", + "PrimaryQuest": "중요 퀘스트로 설정/해제" + }, + "Labels": { + "TableHeader": "{0} 퀘스트", + "SubTitle": "서브 퀘스트 : {0}" + }, + "Title": "퀘스트 로그", + "Tooltips": { + "Objectives": "목표" } }, - "QuestPreview": { - "Title": "퀘스트 세부 정보", - "SubTitle": "연관 : {0}", - "Objectives": "목표", - "Rewards": "보상", - "DragDropRewards": "보상으로 추가할 아이템을 여기로 끌어다 놓으십시오", - "InvalidQuestId": "퀘스트 ID가 잘못되어 미리보기를 열 수 없음.", - "HeaderButtons": { - "Show": "플레이어들에게 보이기" - }, - + "Buttons": { + "RewardCustom": "사용자 정의", + "RewardHide": "모두 숨기기", + "RewardLock": "모두 잠그다", + "RewardShow": "모두 보기", + "RewardUnlock": "모두 잠금 해제" + }, + "Labels": { + "CustomSource": "사용자 지정 공급자", + "Description": "설명:", + "DragDropActor": "액터, 아이템, 저널 엔트리를 끌어다 놓아 퀘스트 제공자로 지정합니다.", + "DragDropActorPlayer": "액터, 아이템, 저널 엔트리를 드래그&드롭 하십시오.", + "DragDropRewards": "보상으로 추가할 아이템을 여기로 끌어다 놓으십시오.", + "GMNotes": "GM 메모:", + "Objective": "목표", + "Objectives": "목표:", + "PlayerNotes": "플레이어 노트:", + "Reward": "보상", + "Rewards": "보상:" + }, "Management": { - "IsPersonalQuest": "개인적 퀘스트입니까?", - "IsPersonalQuestDescription": "퀘스트를 개인적으로 표시하려면 체크한다. 체크된 플레이어를 제외한 모든 플레이어들이 볼 수 없으며 이 옵션을 선택 해제하면 모든 권한이 제거되고 퀘스트가 비공개 탭으로 이동한다.", - "SplashArt": "스플래시 아트", - "QuestBranching": "서브 퀘스트", - "AddSubquest": "서브 퀘스트 생성", - "CanPlayerEdit": "플레이어의 세부 정보 수정 허용" - }, - + "AddSubquest": "새 서브 퀘스트 생성", + "ConfigurePermissions": "권한 설정", + "QuestBranching": "서브 퀘스트:", + "QuestSettings": "퀘스트 설정:", + "SplashArt": "스플래시 아트:", + "SplashInfo": "클릭하여 이미지 설정.", + "SplashQuestIcon": "퀘스트 아이콘으로 설정" + }, + "Notifications": { + "BadUUID": "UUID: {uuid}에 대한 문서를 검색할 수 없습니다.", + "WrongDocType": "Forien's Quest Log는 월드/컴펜디움 액터, 아이템, 저널 항목만을 퀘스트 제공자로 받을 수 있습니다.", + "WrongItemType": "Forien's Quest Log는 월드 아이템과 컴펜디움 아이템만을 보상으로 할 수 있습니다." + }, "Tabs": { "Details": "세부 정보", - "QuestManagement": "퀘스트 관리", - "GMNotes": "GM 노트" + "GMNotes": "GM 노트", + "PlayerNotes": "플레이어 노트", + "QuestManagement": "퀘스트 관리" + }, + "Title": "퀘스트 세부 정보 - {name}", + "Tooltips": { + "AddCustom": "사용자 정의 추가", + "AddObjective": "목표 추가", + "ChangeSplashPos": "스플래시 아트 정렬 변경.", + "DeleteQuestGiver": "퀘스트 제공자 제거.", + "DeleteSplash": "스플래시 아트 제거.", + "HideAll": "모두 숨기기", + "LockAll": "모두 잠그다", + "PrimaryQuestSet": "중요 퀘스트를 만들려면 클릭한다.", + "PrimaryQuestUnset": "중요 퀘스트를 해제하려면 클릭한다.", + "RewardHidden": "보상이 비공개 상태입니다. 공개하려면 클릭하십시오.", + "RewardLocked": "보상이 잠금 상태입니다. 해제하려면 클릭하십시오.", + "RewardLockedPlayer": "보상이 잠겼습니다.", + "RewardUnlocked": "보상이 잠금 해제 상태입니다. 잠그려면 클릭하십시오.", + "RewardUnlockedPlayer": "보상이 잠금 해제 되었습니다.", + "RewardVisible": "보상이 공개 상태입니다. 비공개하려면 클릭하십시오.", + "ShowAll": "모두 보기", + "TaskHidden": "목표가 비공개 상태입니다. 공개하려면 클릭하십시오.", + "TaskVisible": "목표가 공개 상태입니다. 비공개하려면 클릭하십시오.", + "ToggleImage": "토큰/액터 이미지 전환.", + "UnlockAll": "모두 잠금 해제", + "ViewSplashArt": "스플래시 아트를 봅니다." } - - }, - - "DeleteDialog": { - "Title": "{name} 삭제", - "Header": "확실합니까?", - "Body": "이 퀘스트와 데이터가 영구적으로 삭제됩니다.", - "Delete": "삭제", - "Cancel": "취소" }, - - "CloseDialog": { - "Title": "삭제", - "Header": "확실합니까??", - "Body": "작성 화면을 닫으시겠습니까? 저장되지 않은 데이터가 손실됩니다.", - "Discard": "폐기", - "Cancel": "취소" + "QuestTracker": { + "NoPrimary": "가능한 중요 퀘스트가 없습니다.", + "Title": "퀘스트 트래커", + "Tooltips": { + "BackgroundShow": "클릭하여 배경을 표시.", + "BackgroundUnshow": "클릭하여 배경을 투명화.", + "PrimaryQuestShow": "중요 퀘스트를 표시하려면 클릭한다.", + "PrimaryQuestUnshow": "모든 퀘스트를 표시하려면 클릭한다." + } }, - - "Notifications": { - "CannotOpen": "퀘스트 세부 정보를 열 수 없습니다. 권한이 없거나 퀘스트가 더 이상 존재하지 않거나 제공된 ID가 유효하지 않을 수 있습니다.", - "UserCantOpen": "{user} 플레이어는 이 퀘스트를 열람할 권한을 가지고 있지 않습니다.", - "LinkCopied": "이 퀘스트에 대한 엔티티 링크가 클립보드에 복사 되었습니다.", - "QuestMoved": "퀘스트를 새 폴더로 이동하고 새 상태를 부여했습니다 : {target}" + "QuestTypes": { + "Labels": { + "Active": "진행 중", + "active": "진행 중", + "Available": "활성화", + "available": "활성화", + "Completed": "성공", + "completed": "완료된", + "Failed": "실패", + "failed": "실패한", + "InActive": "비활성화", + "inactive": "비공개", + "Status": "{statusLabel} 상태인 퀘스트입니다." + }, + "Tooltips": { + "SetActive": "진행 중으로 설정", + "SetAvailable": "활성화로 설정", + "SetCompleted": "완료로 설정", + "SetFailed": "실패로 설정", + "SetInactive": "비활성으로 설정", + "Status": "상태: {statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "플레이어가 수락 가능", + "EnableHint": "플레이어가 활성화 탭에서 퀘스트를 수락할 수 있도록 하려면 선택한다." + }, + "allowPlayersCreate": { + "Enable": "플레이어가 생성 가능", + "EnableHint": "플레이어가 퀘스트를 만들 수 있게 하려면 선택한다. 플레이어가 만든 퀘스트는 플레이어 편집 권한으로 활성화 탭으로 이동된다. 코어 욥션의 '저널 생성' 권한 필요." + }, "allowPlayersDrag": { - "Enable": "플레이어에게 보상 드래그 허용", + "Enable": "플레이어에게 보상을 인벤토리로 드래그 허용", "EnableHint": "플레이어들이 퀘스트 세부 정보 창에서 자신의 소유 액터로 보상을 드래그 할 수 있도록 하려면 선택한다." }, - "availableQuests": { - "Enable": "활성화 탭 표시", - "EnableHint": "퀘스트 로그에 \"활성화\" 탭을 표시하려면 선택한다. 비공개 상태가 아닌 모든 퀘스트들을 수락하기 전에 볼 수 있다." - }, "countHidden": { "Enable": "비공개 목표 세기", "EnableHint": "선택될 경우 완료/전체 목표의 수에 비공개된 목표가 포함된다." }, + "defaultPermissionLevel": { + "Enable": "퀘스트 권한 레벨 기본값", + "EnableHint": "새 퀘스트를 생성할 때 기본 권한 레벨을 설정한다.", + "NONE": "없음", + "OBSERVER": "관찰자", + "OWNER": "소유자" + }, + "dynamicBookmarkBackground": { + "Enable": "동적 북마크 배경", + "EnableHint": "이 옵션을 활성화하면 북마크 탭 배경이 퀘스트 창 컨텐츠 배경으로 설정된다." + }, + "hideFQLFromPlayers": { + "Enable": "퀘스트 로그를 플레이어에게 숨기기", + "EnableHint": "활성화할 경우 퀘스트 로그가 모든 플레이어에게 숨겨진다. GM 레벨 유저만 퀘스트 로그에 액세스 할 수 있다." + }, "navStyle": { - "Enable": "내비게이션 스타일", - "EnableHint": "퀘스트 로그의 내비게이션 표시 방법을 정한다.", "bookmarks": "북마크", - "classic": "고전적 탭" + "classic": "고전적 탭", + "Enable": "내비게이션 스타일", + "EnableHint": "퀘스트 로그의 내비게이션 표시 방법을 정한다." + }, + "notifyRewardDrop": { + "Enable": "보상 드롭 알림 표시", + "EnableHint": "퀘스트 보상을 플레이어 시트에 놓을 때 UI 알림을 확인한다." + }, + "questTrackerResizable": { + "Enable": "퀘스트 트래커 크기 조절 기능", + "EnableHint": "활성화시 퀘스트 트래커의 크기를 수동으로 조절할 수 있다." }, "showFolder": { "Enable": "퀘스트 폴더 보이기", "EnableHint": "저널 탭에 퀘스트 데이터 폴더를 표시하려면 선택한다. 디버그만을 위한 것이다." }, "showTasks": { + "default": "목표 보기: 완료/전체", "Enable": "퀘스트 로그에 목표 표시", "EnableHint": "퀘스트 로그에서 퀘스트 제목 옆에 목표를 표시할 것인지, 또는 어떻게 표시할 지를 결정한다. 이는 퀘스트 개별 미리보기에는 영향을 미치지 않는다.", - "default": "목표 보기: 완료/전체", - "onlyCurrent": "목표 보기: 완료", - "no": "\"목표\" 열 숨김" - }, - "titleAlign": { - "Enable": "퀘스트 제목 정렬", - "EnableHint": "퀘스트 기록에 퀘스트 제목을 배치하는 방법을 정한다.", - "left": "좌측 정렬", - "center": "중앙 정렬" - }, - "playersWelcomeScreen": { - "Enable": "플레이어에게 환영 화면 표시", - "EnableHint": "업데이트 후 로그인 시 플레이어가 시작 화면을 볼 수 없도록 하려면 선택을 취소할 것. 시작 화면은 Quest Log의 Help 아이콘을 클릭하여 볼 수 있다." - }, - "allowPlayersAccept": { - "Enable": "플레이어가 수락 가능", - "EnableHint": "플레이어가 활성화 탭에서 퀘스트를 수락할 수 있도록 하려면 선택한다." + "no": "\"목표\" 열 숨김", + "onlyCurrent": "목표 보기: 완료" }, - "allowPlayersCreate": { - "Enable": "플레이어가 생성 가능", - "EnableHint": "플레이어가 퀘스트를 만들 수 있게 하려면 선택한다. 플레이어가 만든 퀘스트는 플레이어 편집 권한으로 활성화 탭으로 이동된다. 코어 욥션의 '저널 생성' 권한 필요." + "trustedPlayerEdit": { + "Enable": "신뢰할 수 있는 플레이어의 퀘스트 편집 허용", + "EnableHint": "신뢰할 수 있는 플레이어가 퀘스트 소유를 넘어 퀘스트 편집 권한 확장과 상태 제어 권한을 허용하도록 하려면 선택하십시오." } }, - "Tooltips": { - "SetAvailable": "활성화로 설정", - "SetActive": "진행 중으로 설정", - "SetCompleted": "완료로 설정", - "SetFailed": "실패로 설정", - "Hide": "비공개", "Delete": "삭제", - "AddAbstractReward": "추상적 보상 추가", - "PersonalQuestButNoPlayers": "이 퀘스트는 개인적인 것이며 누구도 볼 수 없습니다.", - "PersonalQuestVisibleFor": "다음 플레이어를 위한 개인적인 퀘스트입니다 ", - "RewardHidden": "보상이 비공개 상태입니다. 공개하려면 클릭하십시오.", - "RewardVisible": "보상이 공개 상태입니다. 비공개하려면 클릭하십시오.", - "TaskHidden": "목표가 비공개 상태입니다. 공개하려면 클릭하십시오.", - "TaskVisible": "목표가 공개 상태입니다. 비공개하려면 클릭하십시오.", - "ToggleImage": "토큰/액터 이미지 토글" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "„{name}” 퀘스트 열기", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Edit": "편집", + "HiddenQuestNoPlayers": "이 퀘스트는 모든 플레이어에게 숨겨져 있습니다.", + "PrimaryQuest": "중요 퀘스트" } } -} +} \ No newline at end of file diff --git a/lang/missing/cn.json b/lang/missing/cn.json deleted file mode 100644 index 55b389fe..00000000 --- a/lang/missing/cn.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ForienQuestLog.QuestPreview.SubTitle": "", - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/de.json b/lang/missing/de.json deleted file mode 100644 index 9143a11a..00000000 --- a/lang/missing/de.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "ForienQuestLog.Api.hooks.createOpenQuestMacro.error.noQuest": "", - "ForienQuestLog.Api.hooks.createOpenQuestMacro.name": "", - "ForienQuestLog.Notifications.CannotOpen": "", - "ForienQuestLog.Api.create.title": "", - "ForienQuestLog.QuestForm.SubquestOf": "", - "ForienQuestLog.CloseDialog.Title": "", - "ForienQuestLog.CloseDialog.Header": "", - "ForienQuestLog.CloseDialog.Body": "", - "ForienQuestLog.CloseDialog.Cancel": "", - "ForienQuestLog.CloseDialog.Discard": "", - "ForienQuestLog.QuestPreview.InvalidQuestId": "", - "ForienQuestLog.QuestPreview.HeaderButtons.Show": "", - "ForienQuestLog.Notifications.LinkCopied": "", - "ForienQuestLog.SampleReward": "", - "ForienQuestLog.Tooltips.PersonalQuestVisibleFor": "", - "ForienQuestLog.Tooltips.PersonalQuestButNoPlayers": "", - "ForienQuestLog.Api.reward.create.type": "", - "ForienQuestLog.Api.reward.create.data": "", - "ForienQuestLog.Api.task.create.name": "", - "ForienQuestLog.Settings.availableQuests.Enable": "", - "ForienQuestLog.Settings.availableQuests.EnableHint": "", - "ForienQuestLog.Settings.allowPlayersDrag.Enable": "", - "ForienQuestLog.Settings.allowPlayersDrag.EnableHint": "", - "ForienQuestLog.Settings.allowPlayersCreate.Enable": "", - "ForienQuestLog.Settings.allowPlayersCreate.EnableHint": "", - "ForienQuestLog.Settings.allowPlayersAccept.Enable": "", - "ForienQuestLog.Settings.allowPlayersAccept.EnableHint": "", - "ForienQuestLog.Settings.countHidden.Enable": "", - "ForienQuestLog.Settings.countHidden.EnableHint": "", - "ForienQuestLog.Settings.playersWelcomeScreen.Enable": "", - "ForienQuestLog.Settings.playersWelcomeScreen.EnableHint": "", - "ForienQuestLog.Settings.showFolder.Enable": "", - "ForienQuestLog.Settings.showFolder.EnableHint": "", - "ForienQuestLog.Notifications.UserCantOpen": "", - "ForienQuestLog.QuestPreview.SubTitle": "", - "ForienQuestLog.Tooltips.SetAvailable": "", - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.Tooltips.TaskHidden": "", - "ForienQuestLog.Tooltips.TaskVisible": "", - "ForienQuestLog.Tooltips.AddAbstractReward": "", - "ForienQuestLog.Tooltips.RewardHidden": "", - "ForienQuestLog.Tooltips.RewardVisible": "", - "ForienQuestLog.QuestPreview.Management.CanPlayerEdit": "", - "ForienQuestLog.QuestPreview.Management.IsPersonalQuest": "", - "ForienQuestLog.QuestPreview.Management.IsPersonalQuestDescription": "", - "ForienQuestLog.QuestPreview.Management.SplashArt": "", - "ForienQuestLog.QuestPreview.Management.QuestBranching": "", - "ForienQuestLog.QuestPreview.Management.AddSubquest": "", - "ForienQuestLog.QuestForm.DragDropActor": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.QuestLog.Tabs.Available": "", - "ForienQuestLog.Buttons.AddNewFolder": "", - "ForienQuestLog.QuestPreview.Tabs.QuestManagement": "" -} \ No newline at end of file diff --git a/lang/missing/en.json b/lang/missing/en.json deleted file mode 100644 index 9e26dfee..00000000 --- a/lang/missing/en.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/lang/missing/es.json b/lang/missing/es.json deleted file mode 100644 index 7eaddf9c..00000000 --- a/lang/missing/es.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/fr.json b/lang/missing/fr.json deleted file mode 100644 index 7eaddf9c..00000000 --- a/lang/missing/fr.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/ja.json b/lang/missing/ja.json deleted file mode 100644 index 7eaddf9c..00000000 --- a/lang/missing/ja.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/ko.json b/lang/missing/ko.json deleted file mode 100644 index 7eaddf9c..00000000 --- a/lang/missing/ko.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/pl.json b/lang/missing/pl.json deleted file mode 100644 index db914e22..00000000 --- a/lang/missing/pl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "ForienQuestLog.Api.hooks.createOpenQuestMacro.error.noQuest": "", - "ForienQuestLog.Api.create.title": "", - "ForienQuestLog.QuestForm.SubquestOf": "", - "ForienQuestLog.QuestPreview.InvalidQuestId": "", - "ForienQuestLog.Notifications.LinkCopied": "", - "ForienQuestLog.Api.reward.create.type": "", - "ForienQuestLog.Api.reward.create.data": "", - "ForienQuestLog.Api.task.create.name": "", - "ForienQuestLog.Settings.allowPlayersCreate.Enable": "", - "ForienQuestLog.Settings.allowPlayersCreate.EnableHint": "", - "ForienQuestLog.Settings.allowPlayersAccept.Enable": "", - "ForienQuestLog.Settings.allowPlayersAccept.EnableHint": "", - "ForienQuestLog.Settings.countHidden.Enable": "", - "ForienQuestLog.Settings.countHidden.EnableHint": "", - "ForienQuestLog.QuestPreview.SubTitle": "", - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestPreview.Management.CanPlayerEdit": "", - "ForienQuestLog.QuestPreview.Management.SplashArt": "", - "ForienQuestLog.QuestPreview.Management.QuestBranching": "", - "ForienQuestLog.QuestPreview.Management.AddSubquest": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/missing/pt-BR.json b/lang/missing/pt-BR.json deleted file mode 100644 index 55b389fe..00000000 --- a/lang/missing/pt-BR.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ForienQuestLog.QuestPreview.SubTitle": "", - "ForienQuestLog.Tooltips.Edit": "", - "ForienQuestLog.QuestForm.QuestGiverNamePlaceholder": "", - "ForienQuestLog.Buttons.AddNewFolder": "" -} \ No newline at end of file diff --git a/lang/nl.json b/lang/nl.json new file mode 100644 index 00000000..9fff87ec --- /dev/null +++ b/lang/nl.json @@ -0,0 +1,256 @@ +{ + "ForienQuestLog": { + "API": { + "QuestDB": { + "Labels": { + "NewQuest": "Nieuwe Missie" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (missiebeloning): {userName} heeft '{itemName}' op {actorName} neergezet." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Entiteit met UUID '{uuid}' kan niet geladen worden.", + "NoPermission": "Je hebt niet de juiste machtigingen om het blad van deze entiteit te bekijken." + } + }, + "Hooks": { + "Labels": { + "OpenMacro": "Open ”{name}”" + }, + "Notifications": { + "NoQuest": "API-fout: Macro met invalide Quest-ID kan niet aangemaakt worden." + } + } + }, + "DeleteDialog": { + "Delete": "Verwijderen", + "Cancel": "Annuleren", + "TitleDel": "Verwijder {title}", + "BodyObjective": "Dit doel en de bijbehorende data zullen permanent verwijderd worden.", + "BodyQuest": "Deze missie en de bijbehorende data zullen permanent verwijderd worden.", + "BodyReward": "Deze beloning en de bijbehorende data zullen permanent verwijderd worden.", + "HeaderDel": "Weet je zeker dat je

'{name}'

wil verwijderen" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Aan Spelers tonen" + }, + "Quest": "Missie" + }, + "Migration": { + "ChatMessage": { + "Notification": "Forien's Quest Log - Onverbonden missiegever of beloningen verwijderd uit een of verscheidene missies verwijderd. Lees het chatbericht voor meer informatie.", + "QuestGiver": "Missiegever", + "QuestRewards": "Missiebeloningen", + "Footer": "Je moet handmatig de bovenstaande missies updaten met valide documentdata uit compendiums of je wereld.
", + "Header": "Forien's Quest Log (DB-migratie)
Onverbonden missiegever of beloningen verwijderd uit onderstaande missies:

" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Datamigratie afgerond.", + "CouldNotMigrate": "Forien's Quest Log - Missie kan niet gemigreerd worden: '{name}'.", + "Schema": "Forien's Quest Log - Data wordt naar schemaversie '{version}' gemigreerd.", + "Start": "Forien's Quest Log - Data wordt gemigreerd, herlaad de pagina niet." + } + }, + "Notifications": { + "CannotOpen": "Kan missiedetails niet openen. Mogelijk is de missie niet meer te bekijken of bestaat de missie niet meer.", + "FinishQuestAdded": "Rond het wijzigen af en sluit de missie voordat je een nieuwe missie toevoegt.", + "QuestMoved": "'{name}' verplaatst en nieuwe status op '{target}' gezet.", + "UserCantOpen": "Gebruiker '{user}' heeft niet de juiste machtigingen om deze missie te openen.", + "LinkCopied": "Entiteitslink voor deze missie is naar het klembord gekopieerd.", + "QuestAdded": "'{name}' als nieuwe missie met status '{status}' toegevoegd.", + "QuestIDCopied": "Missie-ID voor deze missie is naar het klembord gekopieerd.", + "QuestPrimary": "'{name}' is de nieuwe primaire missie.", + "QuestTrackerNoActive": "Missietracker is geactiveerd maar er zijn momenteel geen actieve missies." + }, + "QuestLog": { + "ContextMenu": { + "CopyEntityLink": "Kopieer entiteitslink", + "CopyQuestID": "Kopieer missie-ID", + "PrimaryQuest": "Instellen/uitschakelen als primaire missie" + }, + "Labels": { + "TableHeader": "{0} Missies", + "SubTitle": "Sub-missie van {0}" + }, + "Title": "Missielogboek", + "Tooltips": { + "Objectives": "Doelen" + }, + "Buttons": { + "AddQuest": "Voeg missie toe" + } + }, + "QuestPreview": { + "Buttons": { + "RewardHide": "Verbergen", + "RewardLock": "Op slot doen", + "RewardCustom": "Gebruikergedefinieerd", + "RewardShow": "Weergeven", + "RewardUnlock": "Ontgrendelen" + }, + "Labels": { + "CustomSource": "Gebruikergedefinieerde Bron", + "Description": "Beschrijving:", + "DragDropActorPlayer": "Acteur, Voorwerp of Journaalboeking hiernaartoe slepen.", + "DragDropRewards": "Sleep voorwerpen hiernaartoe om als beloning toe te voegen.", + "GMNotes": "GM-Notities:", + "Objective": "Doel", + "Objectives": "Doelen:", + "PlayerNotes": "Speler-Notities:", + "Reward": "Beloning", + "Rewards": "Beloningen:", + "DragDropActor": "Acteur, object of journaalboeking hiernaartoe slepen of (links) klikken om als gebruikergedefinieerde bron vast te leggen." + }, + "Management": { + "AddSubquest": "Sub-missie toevoegen", + "QuestBranching": "Sub-missies:", + "ConfigurePermissions": "Machtigingen Configureren", + "SplashArt": "Omslagfoto:", + "SplashInfo": "Klik om afbeelding in te stellen.", + "SplashQuestIcon": "Als missieicoon instellen", + "QuestSettings": "Missieinstellingen:" + }, + "Tabs": { + "Details": "Details", + "GMNotes": "GM-Notities", + "PlayerNotes": "Speler-Notities", + "QuestManagement": "Missie beheren" + }, + "Title": "Missiedetails - {name}", + "Tooltips": { + "AddCustom": "Gebruikergedefinieerd Toevoegen", + "AddObjective": "Doel toevoegen", + "ChangeSplashPos": "Omslagfoto-uitlijning wijzigen.", + "DeleteQuestGiver": "Missiegever verwijderen.", + "DeleteSplash": "Omslagfoto verwijderen.", + "HideAll": "Alles verbergen", + "LockAll": "Alles vergrendelen", + "PrimaryQuestSet": "Klik om in te stellen als primaire missie.", + "RewardLocked": "Belong is vergrendeld. Klik om te ontgrendelen.", + "RewardLockedPlayer": "Beloning is vergrendeld.", + "RewardUnlocked": "Beloning is ontgrendeld. Klik om te vergrendelen.", + "RewardUnlockedPlayer": "Beloning is ontgrendeld.", + "RewardVisible": "Beloning is zichtbaar. Klik om te verbergen.", + "ShowAll": "Toon aan allen", + "TaskVisible": "Doel is zichtbaar. Klik om te verbergen.", + "ToggleImage": "Token/Acteurafbeelding omschakelen.", + "UnlockAll": "Alles ontgrendelen", + "ViewSplashArt": "Omslagfoto bekijken.", + "PrimaryQuestUnset": "Klik om uit te schakelen als primaire missie.", + "RewardHidden": "Beloning is verborgen. Klik om weer te geven.", + "TaskHidden": "Doel is verborgen. Klik om te tonen." + }, + "Notifications": { + "BadUUID": "Kon document met UUID {uuid} niet vinden.", + "WrongItemType": "Foriens Questlog accepteert allen wereld- en compendiumvoorwerpen als beloningen.", + "WrongDocType": "Foriens Questlog accepteert alleen wereld-/compendiumacteurs, -voorwerpen en -journaalboekingen als missiegever." + } + }, + "QuestTracker": { + "NoPrimary": "Geen primaire missie beschikbaar.", + "Title": "Missietracker", + "Tooltips": { + "BackgroundUnshow": "Klik om achtergrond transparent te maken.", + "PrimaryQuestShow": "Klik om primaire missie te tonen.", + "PrimaryQuestUnshow": "Klik om alle missies te tonen.", + "BackgroundShow": "Klik om achtergrond te tonen." + } + }, + "QuestTypes": { + "Labels": { + "Active": "Actief", + "active": "actief", + "Available": "Beschikbaar", + "available": "beschikbaar", + "Completed": "Voltooid", + "completed": "voltooid", + "InActive": "Inactief", + "inactive": "inactief", + "Status": "Missie is {statusLabel}.", + "Failed": "Gefaald", + "failed": "gefaald" + }, + "Tooltips": { + "SetActive": "Instellen als Actief", + "SetCompleted": "Instellen als Voltooid", + "SetFailed": "Instellen als Gefaald", + "SetInactive": "Instellen als Inactief", + "Status": "Status: {statusI18n}", + "SetAvailable": "Instellen als Beschikbaar" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Spelers kunnen missies accepteren", + "EnableHint": "Schakel deze optie in om spelers toe te staan missies uit het Beschikbaar-tabblad te accepteren." + }, + "allowPlayersCreate": { + "Enable": "Spelers kunnen missies aanmaken", + "EnableHint": "Schakel deze optie in om spelers toe te staan missies aan te maken. Door spelers gemaakte missies zullen in het Beschikbaar-tabblad verschijnen met Wijzigingsmachtigingen voor Spelers. VEREIST kernrecht 'maak journaal aan'." + }, + "allowPlayersDrag": { + "Enable": "Sta spelers toe beloningen te verslepen", + "EnableHint": "Schakel deze optie in om spelers toe te staan beloningen uit het missiedetailvenster naar hun eigen Acteurs te verslepen." + }, + "countHidden": { + "Enable": "Tel verborgen doelen", + "EnableHint": "Indien aangevinkt bevat het aantal voltooide/totale doelen ook verborgen doelen." + }, + "defaultPermissionLevel": { + "EnableHint": "Stelt het standaard machtigingsniveau in voor nieuw aangemaakte missies.", + "NONE": "Geen", + "OBSERVER": "Waarnemer", + "OWNER": "Eigenaar", + "Enable": "Standaard machtigingsniveau voor missies" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynamische bladwijzerachtergrond", + "EnableHint": "Indien aangevinkt wordt de vensterinhoud dynamisch ingesteld als achtergrond voor het bladwijzertabblad." + }, + "hideFQLFromPlayers": { + "Enable": "Verberg Missielogboek voor spelers", + "EnableHint": "Indien aangevinkt verbergt deze optie het missielogboek voor alle spelers. Alleen GM-gebruikers zullen toegang hebben tot het missielogboek." + }, + "navStyle": { + "bookmarks": "Bladwijzers", + "classic": "Klassieke tabbladen", + "Enable": "Navigatiestijl", + "EnableHint": "Stel in hoe de navigatie van het missielogboek wordt weergegeven." + }, + "notifyRewardDrop": { + "EnableHint": "Indien aangevinkt worden in de UI meldingen getoond als missiebeloningen op spelerbladen worden neergezet.", + "Enable": "Beloningsmeldingen tonen" + }, + "questTrackerResizable": { + "Enable": "Missietracker schaalbaar", + "EnableHint": "Indien aangevinkt kan de missietracker handmatig herschaald worden." + }, + "showFolder": { + "Enable": "Missiemap tonen", + "EnableHint": "Indien aangevinkt wordt de missiedatamap in het Journaal-tabblad getoond. Alleen voor DEBUG-doeleinden." + }, + "showTasks": { + "default": "Toon doelen: voltooid/totaal", + "Enable": "Doelen in missielogboek tonen", + "EnableHint": "Stel in of en hoe het aantal doelen naast de missietitel in het missielogboek wordt getoond. Dit heeft geen effect op het missievoorbeeld.", + "no": "Verberg \"doel\"-kolom", + "onlyCurrent": "Toon doelen: voltooid" + }, + "trustedPlayerEdit": { + "Enable": "Sta Vertrouwde Spelers toe om missies te wijzigen", + "EnableHint": "Indien aangevinkt hebben vertrouwde spelers uitgebreide wijzigings- en statusbeheermogelijkheden voor missies die ze in eigendom hebben." + } + }, + "Tooltips": { + "Delete": "Verwijderen", + "Edit": "Wijzigen", + "HiddenQuestNoPlayers": "Deze missie is verborgen voor alle spelers.", + "PrimaryQuest": "Primaire Missie" + } + } +} diff --git a/lang/pl.json b/lang/pl.json index 423df43d..95758501 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -1,109 +1,256 @@ { - "ForienQuestLog.QuestLog.Title": "Dziennik Misji", - "ForienQuestLog.QuestLog.Tabs.Available": "Dostępne", - "ForienQuestLog.QuestLog.Tabs.InProgress": "W trakcie", - "ForienQuestLog.QuestLog.Tabs.Completed": "Zakończone", - "ForienQuestLog.QuestLog.Tabs.Failed": "Nieudane", - "ForienQuestLog.QuestLog.Tabs.Hidden": "Ukryte", - - "ForienQuestLog.NewQuest": "Nowa misja", - "ForienQuestLog.QuestLogButton": "Dziennik", - "ForienQuestLog.Quests": "Misje", - - "ForienQuestLog.Notifications.QuestMoved": "Przeniesiono misję do nowego folderu, a jej nowy status to: {target}", - "ForienQuestLog.Notifications.CannotOpen": "Nie można otworzyć okna Misji, gdyż ona nie istnieje, podane ID jest niepoprawne, lub najzwyczajniej nie masz uprawnień do jej wyświetlenia", - "ForienQuestLog.Notifications.UserCantOpen": "Użytkownik {user} nie posiada uprawnień do otwarcia tej Misji.", - - "ForienQuestLog.Buttons.AddNewQuest": "Dodaj nową misję", - "ForienQuestLog.Buttons.AddNewTask": "dodaj", - - "ForienQuestLog.QuestTypes.InProgress": "Aktywne", - "ForienQuestLog.QuestTypes.Completed": "Zakończone", - "ForienQuestLog.QuestTypes.Failed": "Nieudane", - "ForienQuestLog.QuestTypes.Hidden": "Ukryte", - - "ForienQuestLog.QuestLog.Table.QuestGiver": "Zleceniodawca", - "ForienQuestLog.QuestLog.Table.QuestTitle": "Tytuł", - "ForienQuestLog.QuestLog.Table.Tasks": "Zadania", - "ForienQuestLog.QuestLog.Table.Actions": "Akcje", - - "ForienQuestLog.Settings.showFolder.Enable": "Pokaż Folder", - "ForienQuestLog.Settings.showFolder.EnableHint": "Kliknij, aby pokazać Folder z danymi Misji. Tylko w celach developerskich.", - - "ForienQuestLog.Settings.availableQuests.Enable": "Dostępne Misje", - "ForienQuestLog.Settings.availableQuests.EnableHint": "Kliknij, aby włączyć widoczność Dostępnych Misji. Doda to nową zakładkę w Dzienniku, która wyświetli wszystkie dostępne (nie ukryte) misje, które nie są Aktywne, Zakońćzone lub Nieudane.", - - "ForienQuestLog.Settings.allowPlayersDrag.Enable": "Pozwól graczom przeciągać nagrody", - "ForienQuestLog.Settings.allowPlayersDrag.EnableHint": "Domyślnie gracze nie mogą przeciągać nagród z Misji na swoich Aktorów. Aby na to pozwolić, zaznacż tę opcję.", - - "ForienQuestLog.Settings.showTasks.Enable": "Pokaż zadania w Dzienniku", - "ForienQuestLog.Settings.showTasks.EnableHint": "Zdecyduj czy, i w jaki sposób wyświetlać ilość Zadań obok tytułu Misji w Dzienniku Misji. Nie wpływa na widoczność Zadań na ekranie podglądu Misji.", - "ForienQuestLog.Settings.showTasks.default": "Pokaż zadania: wykonane/wszystkie", - "ForienQuestLog.Settings.showTasks.onlyCurrent": "Pokaż zadania: wykonane", - "ForienQuestLog.Settings.showTasks.no": "Ukryj kolumnę zadań", - - "ForienQuestLog.Settings.navStyle.Enable": "Styl nawigacji", - "ForienQuestLog.Settings.navStyle.EnableHint": "Zdecyduj jak chcesz przełączać się między rodzajami Misji.", - "ForienQuestLog.Settings.navStyle.bookmarks": "Zakładki", - "ForienQuestLog.Settings.navStyle.classic": "Klasyczne karty", - - "ForienQuestLog.Settings.titleAlign.Enable": "Wyrównanie tytułów Misji", - "ForienQuestLog.Settings.titleAlign.EnableHint": "Zdecyduj jak wyrównać tytuły Misji w Dzienniku Misji.", - "ForienQuestLog.Settings.titleAlign.left": "Wyrównaj do lewej", - "ForienQuestLog.Settings.titleAlign.center": "Wyśrodkuj", - - "ForienQuestLog.Settings.playersWelcomeScreen.Enable": "Pokazuj graczom Ekran Powitalny", - "ForienQuestLog.Settings.playersWelcomeScreen.EnableHint": "Odznacz, aby graczom nie wyświetlać po zalogowaniu Ekranu Powitalnego z każdą nową wersją. Gracze wciąż mogą go wyświetlić klikając na ikonę \"pomocy\" w Dzienniku Misji.", - - "ForienQuestLog.QuestForm.Title": "Stwórz nową Misję", - "ForienQuestLog.QuestForm.QuestGiver": "Zleceniodawca", - "ForienQuestLog.QuestForm.QuestGiverPlaceholder": "Imię Aktora lub UUID obiektu", - "ForienQuestLog.QuestForm.QuestTitle": "Tytuł misji", - "ForienQuestLog.QuestForm.QuestDescription": "Opis misji", - "ForienQuestLog.QuestForm.QuestGMNotes": "Notatki MG", - "ForienQuestLog.QuestForm.Submit": "Zapisz", - "ForienQuestLog.QuestForm.DragDropActor": "Przeciągnij Aktora, Przedmiot lub Notatkę w to miejsce, aby ustawić jako zleceniodawcę", - - "ForienQuestLog.SampleTask": "np. Zabij wszystkie szczury w karczmie „Pod Skręconą Kostką”", - "ForienQuestLog.SampleReward": "np. „100 Punktów Doświadczenia”", - - "ForienQuestLog.QuestPreview.Title": "Podgląd Misji", - "ForienQuestLog.QuestPreview.Objectives": "Zadania", - "ForienQuestLog.QuestPreview.Rewards": "Nagrody", - "ForienQuestLog.QuestPreview.Tabs.Details": "Szczegóły", - "ForienQuestLog.QuestPreview.Tabs.QuestManagement": "Zarządzaj Misją", - "ForienQuestLog.QuestPreview.Tabs.GMNotes": "Notatki MG", - "ForienQuestLog.QuestPreview.DragDropRewards": "Kliknij i Przeciągnij przedmioty w to miejsce, aby dodać je jako nagrody", - "ForienQuestLog.QuestPreview.Management.IsPersonalQuest": "Misja Prywatna", - "ForienQuestLog.QuestPreview.Management.IsPersonalQuestDescription": "Zaznacz, aby utworzyć misję prywatną. Będzie ona niewidoczna dla wszystkich graczy z wyjątkiem tych oznaczonych poniżej. Wyłączenie tej opcji usunie wszystkie uprawnienia i przeniesie tę misję do Ukrytych.", - "ForienQuestLog.QuestPreview.HeaderButtons.Show": "Pokaż Graczom", - - "ForienQuestLog.DeleteDialog.Title": "Usuń {name}", - "ForienQuestLog.DeleteDialog.Header": "Na pewno?", - "ForienQuestLog.DeleteDialog.Body": "Ta misja i związane z nią dane zostaną utracone na stałe.", - "ForienQuestLog.DeleteDialog.Delete": "Usuń", - "ForienQuestLog.DeleteDialog.Cancel": "Anuluj", - - "ForienQuestLog.CloseDialog.Title": "Opuszczasz formularz", - "ForienQuestLog.CloseDialog.Header": "Czy na pewno?", - "ForienQuestLog.CloseDialog.Body": "Opuszczasz formularz bez zapisania go. Jeśli to zrobisz, utracisz niezapisane zmiany.", - "ForienQuestLog.CloseDialog.Cancel": "Anuluj", - "ForienQuestLog.CloseDialog.Discard": "Odrzuć zmiany", - - "ForienQuestLog.Tooltips.ToggleImage": "Przełącz obraz Aktora/Tokenu", - "ForienQuestLog.Tooltips.SetActive": "Ustaw na Aktywną", - "ForienQuestLog.Tooltips.SetCompleted": "Ustaw jako Zakończoną", - "ForienQuestLog.Tooltips.SetFailed": "Ustaw jako Nieudaną", - "ForienQuestLog.Tooltips.Hide": "Ukryj", - "ForienQuestLog.Tooltips.Delete": "Usuń", - "ForienQuestLog.Tooltips.SetAvailable": "Ustaw jako Dostępną", - "ForienQuestLog.Tooltips.TaskHidden": "Zadanie ukryte. Kliknij aby pokazać.", - "ForienQuestLog.Tooltips.TaskVisible": "Zadanie widoczne. Kliknij aby ukryć.", - "ForienQuestLog.Tooltips.AddAbstractReward": "Dodaj sztuczną nagrodę", - "ForienQuestLog.Tooltips.RewardHidden": "Nagroda ukryta. Kliknij aby pokazać.", - "ForienQuestLog.Tooltips.RewardVisible": "Nagroda widoczna. Kliknij aby ukryć.", - "ForienQuestLog.Tooltips.PersonalQuestVisibleFor": "Misja prywatna dla:", - "ForienQuestLog.Tooltips.PersonalQuestButNoPlayers": "Misja prywatna, ale nikt jej nie widzi.", - - "ForienQuestLog.Api.hooks.createOpenQuestMacro.name": "Otwórz „{name}”" -} + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Otwórz ”{name}”" + }, + "Notifications": { + "NoQuest": "Błąd interfejsu API: nie można utworzyć makra z nieprawidłowym identyfikatorem Misja." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nowa Misja" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (nagroda za misję): {userName} umieszczony '{itemName}' na {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Nie można załadować dokumentu dla UUID: '{uuid}'.", + "NoPermission": "Nie masz wystarczających uprawnień, aby wyświetlić ten arkusz encji." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Cel ten i jego dane zostaną trwale usunięte.", + "BodyQuest": "Ta misja i jej dane zostaną trwale usunięte.", + "BodyReward": "Ta nagroda i jej dane zostaną trwale usunięte.", + "Cancel": "Anuluj", + "Delete": "Usuń", + "HeaderDel": "Czy na pewno chcesz usunąć:

'{name}'

", + "TitleDel": "Usuń {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Pokaż do Graczom" + }, + "Quest": "Misja" + }, + "Migration": { + "ChatMessage": { + "Footer": "Musisz ręcznie zaktualizować powyższe misje prawidłowymi danymi dokumentów z kompendiów lub twojego świata.
", + "Header": "Forien's Quest Log (DB migracja)
Usunięto nie połączone zleceniodawcy lub nagrody z poniższych zadań:

", + "Notification": "Forien's Quest Log - Usunięto nie połączone zleceniodawcy lub nagrody z jednego lub więcej zadań. Przejrzyj wiadomość na czacie, aby uzyskać więcej informacji.", + "QuestGiver": "Zleceniodawca Zadań", + "QuestRewards": "Nagrody za Zadania" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migracja danych zakończona.", + "CouldNotMigrate": "Forien's Quest Log - Nie udało się przenieść misji: '{name}'.", + "Schema": "Forien's Quest Log - Migracja danych do wersji schematu '{version}'.", + "Start": "Forien's Quest Log - Migracja danych, nie ładuj przeładować." + } + }, + "Notifications": { + "CannotOpen": "Nie można otworzyć szczegółów misji. Ta misja może nie być obserwowalna lub może już nie istnieć.", + "FinishQuestAdded": "Zakończ edycję i zamknij bieżącą misję przed dodaniem kolejnej.", + "LinkCopied": "Link do dokumentu dla tej misji został skopiowany do schowka.", + "QuestAdded": "Dodany '{name}' jako nowa misja ze statusem: '{status}'.", + "QuestIDCopied": "Identyfikator dokumentu tej misji został skopiowany do schowka.", + "QuestMoved": "Wzruszony '{name}' i ustaw nowy status na: '{target}'.", + "QuestPrimary": "'{name}' to nowa główna misja.", + "QuestTrackerNoActive": "Śledzenie misji jest włączone, ale obecnie nie ma żadnych misji w toku.", + "UserCantOpen": "Użytkownik '{user}' nie posiada uprawnień do otwarcia tej Misji." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Dodaj misję" + }, + "ContextMenu": { + "CopyEntityLink": "Skopiuj link treści encji", + "CopyQuestID": "Skopiuj identyfikator misji", + "PrimaryQuest": "Ustaw / wyłącz jako misję główną" + }, + "Labels": { + "TableHeader": "{0} Misje", + "SubTitle": "Zadanie podrzędne z {0}" + }, + "Title": "Dziennik Misji", + "Tooltips": { + "Objectives": "Cele" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Określony przez użytkownika", + "RewardHide": "Schowaj", + "RewardLock": "Zablokuj", + "RewardShow": "Pokazać", + "RewardUnlock": "Odemknąć" + }, + "Labels": { + "CustomSource": "Niestandardowe źródło", + "Description": "Opis:", + "DragDropActor": "Przeciągnij i upuść aktora, przedmiot lub wpis do dziennika lub kliknij lewym przyciskiem myszy, aby ustawić niestandardowe źródło.", + "DragDropActorPlayer": "Przeciągnij i upuść aktora, przedmiot lub wpis do dziennika.", + "DragDropRewards": "Przeciągnij i upuść elementy tutaj, aby dodać je jako nagrody.", + "GMNotes": "Notatki GM:", + "Objective": "Cel", + "Objectives": "Cele:", + "PlayerNotes": "Notatki Gracza:", + "Reward": "Nagroda", + "Rewards": "Nagrody:" + }, + "Management": { + "AddSubquest": "Dodaj misję podrzędną", + "ConfigurePermissions": "Konfiguruj Uprawnienia", + "QuestBranching": "Zadania podrzędne:", + "QuestSettings": "Ustawienia Misji:", + "SplashArt": "Sztuka Rozchlapania:", + "SplashInfo": "Kliknij, aby ustawić obraz.", + "SplashQuestIcon": "Ustaw jako ikonę misji" + }, + "Notifications": { + "BadUUID": "Nie można pobrać dokumentu dla UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log akceptuje tylko aktorów świata / kompendium, przedmioty i wpisy do dziennika jako zleceniodawców zadań.", + "WrongItemType": "Forien's Quest Log akceptuje tylko przedmioty ze świata i kompendium jako nagrody." + }, + "Tabs": { + "Details": "Szczegóły", + "GMNotes": "Notatki GM", + "PlayerNotes": "Notatki Gracza", + "QuestManagement": "Zarządzaj Misją" + }, + "Title": "Szczegóły Misji - {name}", + "Tooltips": { + "AddCustom": "Dodaj zdefiniowane przez użytkownika", + "AddObjective": "Dodaj Cel", + "ChangeSplashPos": "Zmień pozycję obrazu powitalnego.", + "DeleteQuestGiver": "Usuń zleceniodawcę misji.", + "DeleteSplash": "Usuń grafikę pluśnięcie.", + "HideAll": "Schowaj wszystko", + "LockAll": "Zablokuj wszystko", + "PrimaryQuestSet": "Kliknij, aby ustawić misję główną.", + "PrimaryQuestUnset": "Kliknij, aby rozbroić misję główną.", + "RewardHidden": "Nagroda jest ukryta. Kliknij, aby pokazać.", + "RewardLocked": "Nagroda jest zablokowana. Kliknij, aby odblokować.", + "RewardLockedPlayer": "Nagroda jest zablokowana.", + "RewardUnlocked": "Nagroda jest odblokowana. Kliknij, aby zablokować.", + "RewardUnlockedPlayer": "Nagroda jest odblokowana.", + "RewardVisible": "Nagroda jest widoczna. Kliknij, aby ukryć.", + "ShowAll": "Pokazać wszystko", + "TaskHidden": "Zadanie jest ukryte. Kliknij, aby pokazać.", + "TaskVisible": "Zadanie jest widoczne. Kliknij, aby ukryć.", + "ToggleImage": "Przełącz obraz Aktora/Tokenu.", + "UnlockAll": "Odblokuj wszystkie", + "ViewSplashArt": "Obejrzeć sztuka rozchlapania." + } + }, + "QuestTracker": { + "NoPrimary": "Brak dostępnych głównych zadań.", + "Title": "Śledzenie Misji", + "Tooltips": { + "BackgroundShow": "Kliknij, aby pokazać tło.", + "BackgroundUnshow": "Kliknij, aby tło było przezroczyste.", + "PrimaryQuestShow": "Kliknij, aby wyświetlić główne zadanie.", + "PrimaryQuestUnshow": "Kliknij, aby wyświetlić wszystkie misje." + } + }, + "QuestTypes": { + "Labels": { + "Active": "W trakcie", + "active": "w trakcie", + "Available": "Dostępne", + "available": "dostępne", + "Completed": "Zakończone", + "completed": "zakończone", + "Failed": "Nieudane", + "failed": "nieudane", + "InActive": "Nieaktywny", + "inactive": "nieaktywny", + "Status": "Misja to {statusLabel}." + }, + "Tooltips": { + "SetActive": "Ustaw na Aktywną", + "SetAvailable": "Ustaw jako Dostępną", + "SetCompleted": "Ustaw jako Zakończoną", + "SetFailed": "Ustaw jako Nieudaną", + "SetInactive": "Ustaw jako Nieaktywne", + "Status": "Status: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Gracze mogą przyjmować Zadania", + "EnableHint": "Zaznacz, aby umożliwić graczom przyjmowanie Zadań z zakładki Dostępne." + }, + "allowPlayersCreate": { + "Enable": "Gracze mogą tworzyć Questy", + "EnableHint": "Zaznacz, aby umożliwić graczom tworzenie zadań. Zadania stworzone przez graczy pojawią się w zakładce Dostępne z uprawnieniami do edycji gracza. WYMAGA uprawnienia rdzenia do tworzenia dziennika." + }, + "allowPlayersDrag": { + "Enable": "Zezwalaj na przeciąganie nagród gracza", + "EnableHint": "Zaznacz, aby umożliwić graczom przeciąganie nagród z okna szczegółów misji do posiadanych aktorów." + }, + "countHidden": { + "Enable": "Policz ukryte zadania", + "EnableHint": "Jeśli zaznaczone, liczba ukończonych/ogółem zadań będzie obejmować zadania ukryte." + }, + "defaultPermissionLevel": { + "Enable": "Domyślny poziom uprawnień do misji", + "EnableHint": "Ustawia domyślny poziom uprawnień podczas tworzenia nowych misji.", + "NONE": "Żaden", + "OBSERVER": "Obserwator", + "OWNER": "Właściciel" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynamiczne Tło Zakładki", + "EnableHint": "Jeśli zaznaczone, tło zakładki jest dynamicznie ustawiane na tło zawartości okna." + }, + "hideFQLFromPlayers": { + "Enable": "Ukryj dziennik zadań przed graczami", + "EnableHint": "Po włączeniu ta opcja ukrywa dziennik zadań przed wszystkimi graczami. Tylko użytkownicy na poziomie GM będą mieli dostęp do dziennika zadań." + }, + "navStyle": { + "bookmarks": "Zakładki", + "classic": "Klasyczne zakładki", + "Enable": "Styl Nawigacji", + "EnableHint": "Zdecyduj, jak powinna być wyświetlana nawigacja w dzienniku misji." + }, + "notifyRewardDrop": { + "Enable": "Pokaż powiadomienia o zrzuceniu nagrody", + "EnableHint": "Zaznacz, aby zobaczyć powiadomienia interfejsu użytkownika, gdy nagrody za misje zostaną upuszczone na arkusze graczy." + }, + "questTrackerResizable": { + "Enable": "Zmiana Rozmiaru śledzenia misji", + "EnableHint": "Zaznacz, aby umożliwić ręczną zmianę rozmiaru z Śledzenie misji." + }, + "showFolder": { + "Enable": "Pokaż Folder Misji", + "EnableHint": "Zaznacz, aby wyświetlić folder danych misji w zakładce Dziennik. Wyłącznie do celów DEBUGOWANIA." + }, + "showTasks": { + "default": "Pokaż Zadania: gotowe/ogółem", + "Enable": "Pokaż zadania w Dzienniku Misji", + "EnableHint": "Zdecyduj, czy lub jak wyświetlać liczbę celów obok tytułu zadania w dzienniku zadań. Nie ma to wpływu na podgląd zadania.", + "no": "Ukryj kolumnę „Cele”", + "onlyCurrent": "Pokaż cele: gotowe" + }, + "trustedPlayerEdit": { + "Enable": "Zezwalaj na edycję misji zaufanych graczy", + "EnableHint": "Zaznacz, aby zaufani gracze mogli mieć rozszerzone możliwości edycji misji i kontroli statusu nad misjami, których są właścicielami." + } + }, + "Tooltips": { + "Delete": "Usuń", + "Edit": "Edytować", + "HiddenQuestNoPlayers": "To zadanie jest ukryte przed wszystkimi graczami.", + "PrimaryQuest": "Główny Misja" + } + } +} \ No newline at end of file diff --git a/lang/pt-BR.json b/lang/pt-BR.json index e6b8e570..f7576b5e 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -1,130 +1,235 @@ { "ForienQuestLog": { - "NewQuest": "Nova Missão", - "QuestLogButton": "Registro De Missões", - "Quests": "Missões", - "SampleReward": "ex. 300 pontos de Experiências", - "SampleTask": "ex. Matar todos os ratos da pousada „Trornozelo Torcido”", - - "QuestTypes": { - "InProgress": "Ativas", - "Completed": "Completas", - "Failed": "Perdidas", - "Hidden": "Ocultas", - "Labels": { - "available": "disponivel", - "active": "em progresso", - "completed": "completa", - "failed": "perdida", - "hidden": "oculta" + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Abrir a Missão ”{name}”" + }, + "Notifications": { + "NoQuest": "Erro de API: não é possível criar macro com ID de Quest inválido." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nova Missão" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (recompensa de missão) : {userName} deu '{itemName}' para {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Uma entidade não pôde ser carregada para IDuu: '{uuid}'.", + "NoPermission": "Você não tem permissão para visualizar o documento desta entidade." + } } }, - - "Buttons": { - "AddNewQuest": "Adicionar Nova Missão", - "AddNewTask": "adicionar nova" + "DeleteDialog": { + "BodyObjective": "Este objetivo e seus dados serão apagados permanentemente.", + "BodyQuest": "Esta missão e seus dados serão apagados permanentemente.", + "BodyReward": "Esta recompensa e seus dados serão apagados permanentemente.", + "Cancel": "Cancelar", + "Delete": "Remover", + "HeaderDel": "Tem certeza de que deseja excluir:

'{name}'

", + "TitleDel": "Excluir {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Exibir aos Jogadores" + }, + "Quest": "Missão" + }, + "Migration": { + "ChatMessage": { + "Footer": "Você deve manualmente atualizar as missões acima com o dado valido de documentação de um dos compendiums de seu mundo.
", + "Header": "Forien's Quest Log (DB migração)
Removido entregador de missão ou items de recompensa das quests abaixo:

", + "Notification": "Forien's Quest Log - Removido entregador de missão ou items de recompensa de uma ou mais missões. Por favor revise a mensagem no chat para mais informação.", + "QuestGiver": "Entregador de Missão", + "QuestRewards": "Recompensa de Missão" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migração de dados completa.", + "CouldNotMigrate": "Forien's Quest Log - Não é possível migrar: '{name}'.", + "Schema": "Forien's Quest Log - Migrando dados sob a versão: '{version}'.", + "Start": "Forien's Quest Log - Migrando dados, não recarregue." + } }, - - "QuestForm": { - "Title": "Adicionar nova Missão", - "QuestGiver": "Missão Entregue por", - "QuestGiverPlaceholder": "nome do Ator ou ID", - "QuestTitle": "Titulo da Missão", - "DragDropActor": "Arraste e solte ator que entregará a missão aqui", - "QuestDescription": "Descritivo da Missão", - "QuestGMNotes": "Notas do Mestre de Jogo", - "Submit": "Enviar", - "SubquestOf": "Desdobramento de {name}" + "Notifications": { + "CannotOpen": "Não é possível abrir os detalhes da missão. Você não tem permissão, a missão não existe mais ou o ID fornecido é inválido.", + "FinishQuestAdded": "Termine a edição e feche a nova missão atual antes de adicionar outra.", + "LinkCopied": "O link para esta missão foi copiado para a área de transferência.", + "QuestAdded": "'{name}' foi adicionado como uma nova missão com a condição: '{status}'.", + "QuestIDCopied": "Este ID de missão foi copiado para a área de transferência.", + "QuestMoved": "Missão movida com nova condição: '{target}'.", + "QuestPrimary": "'{name}' é a nova missão principal.", + "QuestTrackerNoActive": "O rastreador de missões está ativado, mas não há missões ativas no momento.", + "UserCantOpen": "O usuário '{user}' não tem permissão para abrir esta missão." }, - "QuestLog": { - "Title": "Registro de Missão", - "SubTitle": "parte de: {0}", - "Table": { - "QuestGiver": "Missão Entregue por", - "QuestTitle": "Titulo", - "Tasks": "Tarefas", - "Actions": "Ações" + "Buttons": { + "AddQuest": "Adicionar Missão" }, - "Tabs": { - "Available": "Disponivel", - "InProgress": "em Progresso", - "Completed": "Completas", - "Failed": "Perdidas", - "Hidden": "Ocultas" + "ContextMenu": { + "CopyEntityLink": "Copiar conteúdo do link da entidade", + "CopyQuestID": "Copiar ID da missão", + "PrimaryQuest": "Ativar / Desativar como missão principal" + }, + "Labels": { + "TableHeader": "Missões {0}", + "SubTitle": "Parte da missão de: {0}" + }, + "Title": "Registro de Missões", + "Tooltips": { + "Objectives": "Objetivos" } }, - "QuestPreview": { - "Title": "Detalhes das Missões", - "SubTitle": "part of: {0}", - "Objectives": "Objetivos", - "Rewards": "Recompensas", - "DragDropRewards": "Arraste e Solte um item aqui apra recompensas", - "InvalidQuestId": "Não pode abrir a Visualização da missão, ID da Missão inválido.", - "HeaderButtons": { - "Show": "Exibir aos Jogadores" - }, - + "Buttons": { + "RewardCustom": "Usuário Definido", + "RewardHide": "Esconder", + "RewardLock": "Trancar", + "RewardShow": "Mostrar", + "RewardUnlock": "Desbloquear" + }, + "Labels": { + "CustomSource": "Fonte personalizada", + "Description": "Descrição:", + "DragDropActor": "Arraste e solte o ator, item, journal ou clique com o botão esquerdo para definir uma fonte customizada.", + "DragDropActorPlayer": "Arraste e solte o ator, um objeto ou uma entrada do journal.", + "DragDropRewards": "Arraste e solte itens aqui para torná-los recompensas.", + "GMNotes": "Notas do Mestre :", + "Objective": "Objetivos", + "Objectives": "Objetivos:", + "PlayerNotes": "Notas do Jogador:", + "Reward": "Recompensa", + "Rewards": "Recompensas:" + }, "Management": { - "IsPersonalQuest": "É uma Missão Pessoal?", - "IsPersonalQuestDescription": "Marque a missão como pessoalCheck to mark quest as Personal. Será invisível para todos os jogadores, exceto aqueles especificamente marcados abaixo. Desmarcar esta opção removerá todas as permissões e moverá a missão para a guia Ocultas.", - "SplashArt": "Arte de Exibição", - "QuestBranching": "Desdobramento da Missão", - "AddSubquest": "Criar um desdobramento", - "CanPlayerEdit": "Permitir aos jogadores editar os Detalhes." - }, - + "AddSubquest": "Criar uma Missão Secundária", + "ConfigurePermissions": "Configurar as permissões", + "QuestBranching": "Missões Secundárias:", + "QuestSettings": "Configurações da missão:", + "SplashArt": "Ilustração:", + "SplashInfo": "Clique para definir uma imagem.", + "SplashQuestIcon": "Definir como ícone de Missão" + }, + "Notifications": { + "BadUUID": "Não é possível recuperar documento para IDuu: '{uuid}'.", + "WrongDocType": "Forien's Quest Log só aceita atores, itens e entradas de log de mundo/compêndio como entregadores de missões.", + "WrongItemType": "O Registro de Missões de Forien só aceita itens de mundo e itens de compêndio como recompensas." + }, "Tabs": { "Details": "Detalhes", - "QuestManagement": "Controle de Missões", - "GMNotes": "Notas do Mestre" + "GMNotes": "Notas do Mestre", + "PlayerNotes": "Notas do Jogador", + "QuestManagement": "Controle de Missões" + }, + "Title": "Detalhes da Missão", + "Tooltips": { + "AddCustom": "Adicionar definido pelo usuário", + "AddObjective": "Adicionar Objetivo", + "ChangeSplashPos": "Modificar o alinhamento da imagem de ilustração.", + "DeleteQuestGiver": "Excluir entragor da missão.", + "DeleteSplash": "Excluir a ilustração.", + "HideAll": "Esconder Tudo", + "LockAll": "Bloquear Tudo", + "PrimaryQuestSet": "Clique para ativar a missão principal.", + "PrimaryQuestUnset": "Clique para desativar a missão principal.", + "RewardHidden": "Recompensa escondida. Clique para mostrar.", + "RewardLocked": "Recompensa bloqueada. Clique para desbloquear.", + "RewardLockedPlayer": "Recompensa bloqueada.", + "RewardUnlocked": "Recompensa desbloqueada. Clique para bloquear.", + "RewardUnlockedPlayer": "A recompensa é desbloqueada.", + "RewardVisible": "Recompensa visível. Marque para ocultá-la.", + "ShowAll": "Mostrar Todo", + "TaskHidden": "Tarefa oculta. Marque para mostrar.", + "TaskVisible": "Tarefa visível. Marque para ocultá-la.", + "ToggleImage": "Ativar a imagem do token/ator.", + "UnlockAll": "Destrancar Tudo", + "ViewSplashArt": "Ver ilustração." } - }, - - "DeleteDialog": { - "Title": "Removida {name}", - "Header": "Você tem certeza?", - "Body": "Esta missão e todos os seus arquivos serão removidos permanentemente.", - "Delete": "Remover", - "Cancel": "Cancelar" - }, - - "CloseDialog": { - "Title": "Sair Do Formulário", - "Header": "Você tem certeza?", - "Body": "Você tem certeza que quer fechar o formulário? Informações não salvas serão perdidas.", - "Discard": "Descartar Alterações", - "Cancel": "Cancelar" + "QuestTracker": { + "Title": "Registro de Missões", + "NoPrimary": "Nenhuma missão principal disponível.", + "Tooltips": { + "BackgroundShow": "Clique para mostrar o plano de fundo.", + "BackgroundUnshow": "Clique para tornar o fundo transparente.", + "PrimaryQuestShow": "Clique para mostrar a missão principal.", + "PrimaryQuestUnshow": "Clique para ocultar todas as missões." + } }, - - "Notifications": { - "CannotOpen": "Não é possível abrir os detalhes da missão. Você pode não ter permissões, a Missão pode não existir mais ou o ID é inválido.", - "UserCantOpen": "o usuário {user} Não tem permissão apra abrir esta missão.", - "LinkCopied": "Um link da Entidade para esta missão foi copiada na área de transferência", - "QuestMoved": "A missão foi movida para a nova pasta e deu um novo status: {target}" + "QuestTypes": { + "Labels": { + "Active": "Em Progresso", + "active": "em progresso", + "Available": "Disponíveis", + "available": "disponível", + "Completed": "Completas", + "completed": "completa", + "Failed": "Perdidas", + "failed": "perdida", + "InActive": "Inativas", + "inactive": "inativa", + "Status": "A missão está {statusLabel}." + }, + "Tooltips": { + "SetActive": "Definir como Em Progresso", + "SetAvailable": "Definir como Disponível", + "SetCompleted": "Definir como Completa", + "SetFailed": "Definir como Perdida", + "SetInactive": "Definir como Inativo", + "Status": "Condição : {statusI18n}" + } }, - "Settings": { + "allowPlayersAccept": { + "Enable": "Os jogadores podem aceitar", + "EnableHint": "Marque para permitir que os jogadores aceitem missões na guia Disponível." + }, + "allowPlayersCreate": { + "Enable": "Jogadores podem Criar", + "EnableHint": "Ative para permitir que os jogadores possam criar Missões. Jogadores criam Missões diretamente na aba Disponivel com permissão de edição. REQUER a Permissão de núcleo 'criar diário'." + }, "allowPlayersDrag": { "Enable": "Permitir que os jogadores arrastem recompensas", "EnableHint": "Marque para permitir que os jogadores arrastem recompensas da janela Detalhes da missão para seus proprios atores." }, - "availableQuests": { - "Enable": "Exibir Guia de Disponiveis", - "EnableHint": "Marque para mostrar a nova guia \"Disponível\" no Registro de Missões, onde os jogadores podem ver todas as Missões não ocultas antes de serem aceitas" - }, "countHidden": { "Enable": "Contar as tarefas ocultas", "EnableHint": "Se ativo, o numero total de tarefas completas/total incluirá as tarefas ocultas." }, + "defaultPermissionLevel": { + "Enable": "Novo nível de permissão de missões", + "EnableHint": "Definir permissões como padrão quando novas missões forem criadas.", + "NONE": "Nenhum", + "OBSERVER": "Observador", + "OWNER": "Proprietário" + }, + "dynamicBookmarkBackground": { + "Enable": "Marcadores dinâmicos em segundo plano", + "EnableHint": "Marque para que a guia de favoritos seja exibida dinamicamente na janela de conteúdo em segundo plano." + }, + "hideFQLFromPlayers": { + "Enable": "Ocultar registro de missões dos jogadores", + "EnableHint": "Ativar esta opção oculta o registro de missões de todos os jogadores. Somente jogadores com nível GM poderão acessar o registro de missões." + }, "navStyle": { "Enable": "Estilo de Navegação", - "EnableHint": "Decide como a navegação do Registro de Missões deve ser exibida.", - "bookmarks": "marcador de livros", + "EnableHint": "Decida como a navegação do registro de missões será exibida.", + "bookmarks": "Marcador de página", "classic": "Guias Clássicas" }, + "notifyRewardDrop": { + "Enable": "Mostrar notificações de recompensas concedidas", + "EnableHint": "Verifique para ver as notificações quando as recompensas das missões forem para a fichas dos jogadores." + }, + "questTrackerResizable": { + "Enable": "Registro de Missões redimensionável", + "EnableHint": "Marque para permitir o controle manual do redimensionamento do Registro de Missões." + }, "showFolder": { "Enable": "Exibir a Pasta de Missões", "EnableHint": "Marque para mostrar a pasta de dados da missão na guia Diário. Apenas para fins de DEBUG." @@ -133,70 +238,19 @@ "Enable": "Exibir tarefas no Registro de Missões", "EnableHint": "Decida se ou como mostrar a quantidade de tarefas (objetivos) ao lado do título da Missão no Registro de Missões. Isso não afeta a visualização individual da missão.", "default": "Exibir tarefas: feitas/total", - "onlyCurrent": "Exibir tarefas: feitas", - "no": "Esconder a coluna \"Tarefas\"." - }, - "titleAlign": { - "Enable": "Alinhamento do Titulo da Missão", - "EnableHint": "Decide como a posição do titulo é exibida no Registro de Missões.", - "left": "Alinhada a Esquerda", - "center": "Centralizada" + "no": "Esconder a coluna \"Tarefas\"", + "onlyCurrent": "Exibir tarefas: feitas" }, - "playersWelcomeScreen": { - "Enable": "Exibir a Tela de Boas vindas aos Jogadores", - "EnableHint": "Desmarque para impedir que os jogadores vejam a tela de boas-vindas no login após uma atualização. Eles ainda podem vê-lo clicando no ícone 'ajuda' no Registro de missões." - }, - "allowPlayersAccept": { - "Enable": "Jogadores podem Aceitar", - "EnableHint": "Ative para permitir que os jogadores possam aceitar Missões na ana Disponiveis." - }, - "allowPlayersCreate": { - "Enable": "Jogadores podem Criar", - "EnableHint": "Ative para permitir que os jogadores possam criar Missões. Jogadores criam Missões diretamente na aba Disponivel com permissão de edição. REQUER a Permissão de núcleo 'criar diário'." + "trustedPlayerEdit": { + "Enable": "Permitir que jogadores confiáveis editem", + "EnableHint": "Marque para permitir que os jogadores tenham recursos expandidos de edição e verificação de status de missões para as missões que possuem." } }, - "Tooltips": { - "SetAvailable": "Defina como Disponivel", - "SetActive": "Defina como Em Progresso", - "SetCompleted": "Defina como Completa", - "SetFailed": "Defina como Perdida", - "Hide": "Esconda", - "Delete": "Remova", - "AddAbstractReward": "Adicionar Recompensa abstrata", - "PersonalQuestButNoPlayers": "Esta é uma Missão Pessoal, mas ningu[em a vê", - "PersonalQuestVisibleFor": "Esta é uma missão pessoal para", - "RewardHidden": "Recompensas estão Ocultas. Clique para exibi-las.", - "RewardVisible": "Recompensas estão Visíveis. Clique para Oculta-las.", - "TaskHidden": "Tarefas estão Ocultas. Clique para exibir.", - "TaskVisible": "Tarefas estão visíveis. Clique para Ocultar.", - "ToggleImage": "Alternar entre imagem de token/actor" - }, - - "Api": { - "__COMMENT__": "No need for translating lines starting with 'API ERROR', they show in console for developers only.", - "create": { - "title": "API Error: Title property is required to create new Quest" - }, - "hooks": { - "createOpenQuestMacro": { - "name": "Open „{name}” Quest", - "error": { - "noQuest": "API Error: Can't create macro with invalid Quest ID" - } - } - }, - "reward": { - "create": { - "data": "API Error: Data property with at least {name, img} is required to create new Reward", - "type": "API Error: Type property is required to create new Reward" - } - }, - "task": { - "create": { - "name": "API Error: Name property is required to create new Task" - } - } + "Delete": "Excluir", + "Edit": "Editar", + "HiddenQuestNoPlayers": "Esta missão está escondida de todos os jogadores.", + "PrimaryQuest": "Missão Principal" } } } diff --git a/lang/ru.json b/lang/ru.json new file mode 100644 index 00000000..ed1e7d09 --- /dev/null +++ b/lang/ru.json @@ -0,0 +1,256 @@ +{ + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Открыть ”{name}”" + }, + "Notifications": { + "NoQuest": "Ошибка API: невозможно создать макрос с недопустимым идентификатором квеста." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Новое задание" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (награда): {userName} передал '{itemName}' к {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "Не удаётся загрузить элемент для UUID: '{uuid}'.", + "NoPermission": "У вас нет прав доступа к листу этого документа." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Задача и все её данные будут навсегда удалены.", + "BodyQuest": "Задание и все его данные будут навсегда удалены.", + "BodyReward": "Награда и все её данные будут навсегда удалены.", + "Cancel": "Отмена", + "Delete": "Удаление", + "HeaderDel": "Вы уверены, что хотите удалить:

'{name}'

", + "TitleDel": "Удаление {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Показать игрокам" + }, + "Quest": "Задание" + }, + "Migration": { + "ChatMessage": { + "Footer": "Вы должны вручную обновить эти задания с корректным типом документов из вашей библиотеки или мира.
", + "Header": "Forien's Quest Log (миграция базы)
Удалены несвязанные выдающие задания или награды из заданий:

", + "Notification": "Forien's Quest Log - Удалён несвязанный выдающий задание или награда из одного или нескольких заданий. Подробности в чате.", + "QuestGiver": "Выдающий задание", + "QuestRewards": "Награды задания" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Перенос данных завершён.", + "CouldNotMigrate": "Forien's Quest Log - Не удалось перенести задание: '{name}'.", + "Schema": "Forien's Quest Log - Обновление данных до версии '{version}'.", + "Start": "Forien's Quest Log - Перенос данных, подождите." + } + }, + "Notifications": { + "CannotOpen": "Не удаётся открыть подробности задания. Это задания может быть не доступно вам или больше не существует.", + "FinishQuestAdded": "Завершите редактирование и закройте текущее задание перед добавлением нового.", + "LinkCopied": "Ссылка на документ этого задания была скопирована в буфер.", + "QuestAdded": "Добавлено новое задание '{name}' со статусом '{status}'.", + "QuestIDCopied": "ID этого задания был скопирована в буфер.", + "QuestMoved": "'{name}' перемещено в новую папку со статусом: '{target}'.", + "QuestPrimary": "'{name}' теперь основное задание.", + "QuestTrackerNoActive": "Трекер включён, но нет активных заданий.", + "UserCantOpen": "Участник '{user}' не имеет разрешения для просмотра этого задания." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Добавить квест" + }, + "ContextMenu": { + "CopyEntityLink": "Копирование ссылки на документ", + "CopyQuestID": "Копирование ID задания", + "PrimaryQuest": "Переключение главного задания" + }, + "Labels": { + "TableHeader": "{0} задания", + "SubTitle": "Подзадание у {0}" + }, + "Title": "Журнал заданий", + "Tooltips": { + "Objectives": "Задачи" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Уникальный", + "RewardHide": "Скрывать", + "RewardLock": "Запирать", + "RewardShow": "Показывать", + "RewardUnlock": "Разблокировать" + }, + "Labels": { + "CustomSource": "Свой источник", + "Description": "Описание:", + "DragDropActor": "Перенесите актёра, предмет или запись журнала или ЛКМ для выбора своего источника.", + "DragDropActorPlayer": "Перенесите актёра, предмет или запись журнала.", + "DragDropRewards": "Перенесите предмет, чтобы сделать его наградой.", + "GMNotes": "Заметки GM:", + "Objective": "Задача", + "Objectives": "Задачи:", + "PlayerNotes": "Примечания игрока:", + "Reward": "Награда", + "Rewards": "Награда:" + }, + "Management": { + "AddSubquest": "Добавить подзадание", + "ConfigurePermissions": "Настройка прав", + "QuestBranching": "Подзадания:", + "QuestSettings": "Настройки задания:", + "SplashArt": "Обложка:", + "SplashInfo": "Выбор изображения.", + "SplashQuestIcon": "Выбор иконки задания" + }, + "Notifications": { + "BadUUID": "Не удаётся получить документ для UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log принимает только актёров, предметы и записи журнала в качестве выдающего задание.", + "WrongItemType": "Forien's Quest Log принимает только предметы из мира и библиотеки в качестве наград." + }, + "Tabs": { + "Details": "Подробности", + "GMNotes": "Примечания GM", + "PlayerNotes": "Примечания игрока", + "QuestManagement": "Управление заданием" + }, + "Title": "Подробности задания - {name}", + "Tooltips": { + "AddCustom": "Добавить уникальный", + "AddObjective": "Добавить цель", + "ChangeSplashPos": "Изменить расположение обложки.", + "DeleteQuestGiver": "Удалить выдающего задание.", + "DeleteSplash": "Удалить обложку.", + "HideAll": "Скрыть все", + "LockAll": "Заблокировать все", + "PrimaryQuestSet": "Сделать основным заданием.", + "PrimaryQuestUnset": "Сделать неосновным заданием.", + "RewardHidden": "Награда скрыта. Нажмите, чтобы показать.", + "RewardLocked": "Награда заблокирована. Нажмите для разблокирования.", + "RewardLockedPlayer": "Награда заблокирована.", + "RewardUnlocked": "Награда разблокирована. Нажмите для блокировки.", + "RewardUnlockedPlayer": "Награда разблокирована.", + "RewardVisible": "Награда видима. Нажмите, чтобы скрыть.", + "ShowAll": "Показывать все", + "TaskHidden": "Задача скрыта. Нажмите, чтобы показать.", + "TaskVisible": "Задача видима. Нажмите, чтобы скрыть.", + "ToggleImage": "Переключение портрета/токена.", + "UnlockAll": "Разблокировать все", + "ViewSplashArt": "Посмотреть обложку." + } + }, + "QuestTracker": { + "NoPrimary": "Нет основного задания.", + "Title": "Трекер заданий", + "Tooltips": { + "BackgroundShow": "Сделать фон видимым.", + "BackgroundUnshow": "Сделать фон прозрачным.", + "PrimaryQuestShow": "Показать основное задание.", + "PrimaryQuestUnshow": "Показать все задания." + } + }, + "QuestTypes": { + "Labels": { + "Active": "В процессе", + "active": "в процессе", + "Available": "Доступные", + "available": "доступные", + "Completed": "Завершённые", + "completed": "завершённые", + "Failed": "Проваленные", + "failed": "проваленные", + "InActive": "Неактивные", + "inactive": "неактивные", + "Status": "Задание {statusLabel}." + }, + "Tooltips": { + "SetActive": "Сделать текущим", + "SetAvailable": "Сделать доступным", + "SetCompleted": "Сделать завершённым", + "SetFailed": "Сделать проваленным", + "SetInactive": "Сделать неактивным", + "Status": "Состояние: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Игроки могут принимают задания", + "EnableHint": "Если включено, игроки смогут сами принимать задания из вкладки Доступных." + }, + "allowPlayersCreate": { + "Enable": "Игроки могут создавать задания", + "EnableHint": "Если включено, игроки смогут сами создавать задания. Созданные игроками задания отображаются во вкладке Доступные. ТРЕБУЮТСЯ права на 'Создание журналов' в Foundry." + }, + "allowPlayersDrag": { + "Enable": "Разрешить игрокам перемещать награды", + "EnableHint": "Если включено, игроки смогут перемещать награды из окна задания в свой инвентарь." + }, + "countHidden": { + "Enable": "Считать скрытые задачи", + "EnableHint": "Если включено, число завершено/всего задач будет включать скрытые." + }, + "defaultPermissionLevel": { + "Enable": "Доступ к заданиям по умолчанию", + "EnableHint": "Настройте права доступа по умолчанию к новым заданиям.", + "NONE": "Нет", + "OBSERVER": "Наблюдатель", + "OWNER": "Владелец" + }, + "dynamicBookmarkBackground": { + "Enable": "Динамический фон закладки", + "EnableHint": "Если включено, цвет закладки будет соответствовать фону содержимого." + }, + "hideFQLFromPlayers": { + "Enable": "Скрывать список заданий от игроков", + "EnableHint": "Если включено, список заданий будет скрыт для всех игроков. Только участники с правами GM могут просматривать список заданий." + }, + "navStyle": { + "bookmarks": "Закладки", + "classic": "Вкладки", + "Enable": "Способ навигации", + "EnableHint": "Вид отображение заданий в журнале." + }, + "notifyRewardDrop": { + "Enable": "Отображение уведомления передачи награды", + "EnableHint": "Включение уведомлений при перемещении награды на листы актёров." + }, + "questTrackerResizable": { + "Enable": "Изменяемый размер трекера", + "EnableHint": "Включение ручного изменения размера Трекера заданий." + }, + "showFolder": { + "Enable": "Отображение папки заданий", + "EnableHint": "Если включено, папка заданий будет отображаться в журналах. ТОЛЬКО ДЛЯ РАЗРАБОТЧИКОВ." + }, + "showTasks": { + "default": "Показывать задачи: завершено/всего", + "Enable": "Отображение задач в журнале заданий", + "EnableHint": "Если включено, число задач будет отображаться около названия задания.", + "no": "Скрыть колонку \"задачи\"", + "onlyCurrent": "Показывать задачи: завершено" + }, + "trustedPlayerEdit": { + "Enable": "Разрешить доверенным игрокам изменять задания", + "EnableHint": "Если включено, доверенные игроки будут видеть расширенное управление заданиями, включая управление статусом тех заданий, к которым у них есть доступ." + } + }, + "Tooltips": { + "Delete": "Удаление", + "Edit": "Изменение", + "HiddenQuestNoPlayers": "Это задание скрыто от всех игроков.", + "PrimaryQuest": "Основное задание" + } + } +} \ No newline at end of file diff --git a/lang/sv.json b/lang/sv.json new file mode 100644 index 00000000..09f9061b --- /dev/null +++ b/lang/sv.json @@ -0,0 +1,256 @@ +{ + "ForienQuestLog": { + "API": { + "Hooks": { + "Labels": { + "OpenMacro": "Öppna ”{name}”" + }, + "Notifications": { + "NoQuest": "API-fel: Kan inte skapa makro med ogiltigt uppdrags-ID." + } + }, + "QuestDB": { + "Labels": { + "NewQuest": "Nytt uppdrag" + } + }, + "Socket": { + "Notifications": { + "RewardDrop": "FQL (uppdragsbelöning): {userName} släppte '{itemName}' på {actorName}." + } + }, + "Utils": { + "Notifications": { + "NoDocument": "En entitet kunde inte laddas med UUID: '{uuid}'.", + "NoPermission": "Du har inte tillräckliga behörigheter för att visa detta entitetsblad." + } + } + }, + "DeleteDialog": { + "BodyObjective": "Detta mål och dess data kommer att raderas permanent.", + "BodyQuest": "Detta uppdrag och dess data kommer att raderas permanent.", + "BodyReward": "Denna belöning och dess data kommer att raderas permanent.", + "Cancel": "Avbryt", + "Delete": "Radera", + "HeaderDel": "Är du säker att du vill radera:

'{name}'

", + "TitleDel": "Radera {title}" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "Visa för spelare" + }, + "Quest": "Uppdrag" + }, + "Migration": { + "ChatMessage": { + "Footer": "Du måste manuellt uppdatera ovanstående uppdrag med giltiga dokumentdata från kompendier eller din värld.
", + "Header": "Forien's Quest Log (DB flyttning)
Tog bort olänkade uppdragsgivare eller belöningsobjekt från uppdragen nedan:

", + "Notification": "Forien's Quest Log - Tog bort olänkade uppdragsgivare eller belöningsobjekt från ett eller flera uppdrag. Läs chattmeddelandet för mer information.", + "QuestGiver": "Uppdrag Givare", + "QuestRewards": "Uppdrag Belöningar" + }, + "Notifications": { + "Complete": "Forien's Quest Log - Migrering av data klar.", + "CouldNotMigrate": "Forien's Quest Log - Det gick inte att migrera uppdraget: '{name}'.", + "Schema": "Forien's Quest Log - Migrerar data till schemaversion '{version}'.", + "Start": "Forien's Quest Log - Migrerar data, vänligen ladda inte om." + } + }, + "Notifications": { + "CannotOpen": "Kan inte öppna Uppdragsdetaljer. Detta uppdrag kanske inte går att observera eller kanske inte längre existerar.", + "FinishQuestAdded": "Slutför redigeringen och stäng aktuellt nytt uppdrag innan du lägger till ett nytt.", + "LinkCopied": "Entitetslänk för detta uppdrag har kopierats till urklipp.", + "QuestAdded": "Lagt till '{name}' som ett nytt uppdrag med status: '{status}'.", + "QuestIDCopied": "Uppdrags-ID för detta uppdrag har kopierats till urklipp.", + "QuestMoved": "Flyttat '{name}' och satt dess status till: '{target}'.", + "QuestPrimary": "'{name}' är det nya primära uppdraget.", + "QuestTrackerNoActive": "Uppdragsspårare är aktiverad, men det finns inga pågående uppdrag för närvarande.", + "UserCantOpen": "Användare '{user}' har inte behörighet att öppna det här uppdraget." + }, + "QuestLog": { + "Buttons": { + "AddQuest": "Lägg till uppdrag" + }, + "ContextMenu": { + "CopyEntityLink": "Kopiera entitetsinnehållslänk", + "CopyQuestID": "Kopiera uppdrags-ID", + "PrimaryQuest": "Välj / välj bort som primärt uppdrag" + }, + "Labels": { + "TableHeader": "{0} Uppdrag", + "SubTitle": "Deluppdrag till {0}" + }, + "Title": "Uppdragslogg", + "Tooltips": { + "Objectives": "Mål" + } + }, + "QuestPreview": { + "Buttons": { + "RewardCustom": "Användardefinierad", + "RewardHide": "Dölja", + "RewardLock": "Låsa", + "RewardShow": "Visa", + "RewardUnlock": "Låsa Upp" + }, + "Labels": { + "CustomSource": "Anpassad källa", + "Description": "Beskrivning:", + "DragDropActor": "Dra och släpp aktörer, saker eller journalanteckningar eller vänsterklicka för att ställa in en anpassad källa.", + "DragDropActorPlayer": "Dra och släpp aktörer, saker eller journalanteckningar.", + "DragDropRewards": "Dra och släpp objekt här för att lägga till dem som belöningar.", + "GMNotes": "SL-anteckningar:", + "Objective": "Mål", + "Objectives": "Mål:", + "PlayerNotes": "Spelare-anteckningar:", + "Reward": "Belöning", + "Rewards": "Belöningar:" + }, + "Management": { + "AddSubquest": "Skapa underuppdrag", + "ConfigurePermissions": "Konfigurera Behörigheter", + "QuestBranching": "Underuppdrag:", + "QuestSettings": "Uppdragsinställningar:", + "SplashArt": "Skvätt Konst:", + "SplashInfo": "Klicka för att ställa in bild.", + "SplashQuestIcon": "Ställ in som uppdragsikon" + }, + "Notifications": { + "BadUUID": "Det gick inte att hämta dokumentet för UUID: '{uuid}'.", + "WrongDocType": "Forien's Quest Log accepterar endast världs-/kompendieaktörer, saker och journalanteckningar som uppdragsgivare.", + "WrongItemType": "Forien's Quest Log accepterar endast världs- och kompendiumobjekt som belöningar." + }, + "Tabs": { + "Details": "Detaljer", + "GMNotes": "SL-anteckningar", + "PlayerNotes": "Spelare-anteckningar", + "QuestManagement": "Hantera uppdrag" + }, + "Title": "Uppdragsdetaljer", + "Tooltips": { + "AddCustom": "Lägg till användardefinierad", + "AddObjective": "Lägg till Mål", + "ChangeSplashPos": "Ändra blänkarbild-justering.", + "DeleteQuestGiver": "Ta bort uppdragsgivare.", + "DeleteSplash": "Ta bort blänkar-bild.", + "HideAll": "Dölj Alla", + "LockAll": "Lås allt", + "PrimaryQuestSet": "Klicka för att göra primärt uppdrag.", + "PrimaryQuestUnset": "Klicka för att avaktivera primärt uppdrag.", + "RewardHidden": "Belöningen är dold. Klicka för att visa.", + "RewardLocked": "Belöningen är låst. Klicka för att låsa upp.", + "RewardLockedPlayer": "Belöningen är låst.", + "RewardUnlocked": "Belöningen är upplåst. Klicka för att låsa.", + "RewardUnlockedPlayer": "Belöningen är upplåst.", + "RewardVisible": "Belöningen är synlig. Klicka för att dölja.", + "ShowAll": "Visa Allt", + "TaskHidden": "Mål är dolt. Klicka för att visa.", + "TaskVisible": "Mål är synligt. Klicka för att dölja.", + "ToggleImage": "Toggla Spelfigursbild / Aktörsbild.", + "UnlockAll": "Lås upp Alla", + "ViewSplashArt": "Se blänkarbild." + } + }, + "QuestTracker": { + "NoPrimary": "Inget primärt uppdrag tillgängligt.", + "Title": "Uppdragsspårare", + "Tooltips": { + "BackgroundShow": "Klicka för att visa bakgrund.", + "BackgroundUnshow": "Klicka för att göra bakgrunden genomskinlig.", + "PrimaryQuestShow": "Klicka för att visa primärt uppdrag.", + "PrimaryQuestUnshow": "Klicka för att visa alla uppdrag." + } + }, + "QuestTypes": { + "Labels": { + "Active": "Aktiv", + "active": "aktiv", + "Available": "Tillgängliga", + "available": "tillgängliga", + "Completed": "Avklarade", + "completed": "avklarade", + "Failed": "Misslyckade", + "failed": "misslyckade", + "InActive": "Inaktiva", + "inactive": "inaktiva", + "Status": "Uppdrag är {statusLabel}." + }, + "Tooltips": { + "SetActive": "Ställ in som Pågående", + "SetAvailable": "Ställ in som tillgänglig", + "SetCompleted": "Ställ in som slutfört", + "SetFailed": "Ställ in som misslyckad", + "SetInactive": "Ställ in som inaktiv", + "Status": "Status: {statusI18n}" + } + }, + "Settings": { + "allowPlayersAccept": { + "Enable": "Spelare kan acceptera uppdrag", + "EnableHint": "Markera för att tillåta spelare att acceptera uppdrag från fliken Tillgänglig." + }, + "allowPlayersCreate": { + "Enable": "Spelare kan skapa", + "EnableHint": "Markera för att tillåta spelare att skapa uppdrag. Spelare skapade uppdrag kommer att visas på fliken Tillgänglig med spelarredigeringsbehörigheter. KRÄVER skapa journal tillstånd i själva Foundry." + }, + "allowPlayersDrag": { + "Enable": "Tillåt spelare att dra belöningar till deras utrustningslista", + "EnableHint": "Markera för att tillåta spelare att dra belöningar från ett Uppdragsdetaljsfönster till sina ägda aktör." + }, + "countHidden": { + "Enable": "Räkna dolda uppgifter", + "EnableHint": "Om detta väljs kommer antalet slutförda / totala uppgifter att inkludera dolda uppgifter." + }, + "defaultPermissionLevel": { + "Enable": "Standardbehörighetsnivå för uppdrag", + "EnableHint": "Ställer in standardbehörighetsnivån när nya uppdrag skapas.", + "NONE": "Ingen", + "OBSERVER": "Observatör", + "OWNER": "Ägare" + }, + "dynamicBookmarkBackground": { + "Enable": "Dynamisk bokmärkebakgrund", + "EnableHint": "Om markerad, ställs bokmärkesflikens bakgrund dynamiskt in på fönstrets innehållsbakgrund." + }, + "hideFQLFromPlayers": { + "Enable": "Dölj Uppdragslogg från spelare", + "EnableHint": "När det här alternativet är aktiverat döljer Uppdragsloggen för alla spelare. Endast användare på SL-nivå kommer att kunna komma åt Uppdragsloggen." + }, + "navStyle": { + "bookmarks": "Bokmärken", + "classic": "Klassiska flikar", + "Enable": "Navigationsstil", + "EnableHint": "Bestäm hur Uppdragsloggens navigering ska visas." + }, + "notifyRewardDrop": { + "Enable": "Visa belöningsaviseringar", + "EnableHint": "Markera för att se UI-aviseringar när uppdragsbelöningar släpps på spelarark." + }, + "questTrackerResizable": { + "Enable": "Uppdragsspåraren kan ändra storlek", + "EnableHint": "Markera för att tillåta manuell storleksändringskontroll av Quest Tracker." + }, + "showFolder": { + "Enable": "Visa Uppdragsmapp", + "EnableHint": "Markera för att visa sökdatamappen på fliken Journal. Endast för DEBUG-syften." + }, + "showTasks": { + "default": "Visa mål: gjort / totalt", + "Enable": "Visa uppgifter i Uppdragslogg", + "EnableHint": "Bestäm om eller hur man ska visa mängden uppgifter bredvid Uppdragstiteln i Uppdragsloggen. Detta har ingen effekt på Quest Preview.", + "no": "Dölj Målkolumn", + "onlyCurrent": "Visa mål: klar" + }, + "trustedPlayerEdit": { + "Enable": "Tillåt pålitlig spelare uppdrag-redigering", + "EnableHint": "Markera kryssrutan för att tillåta betrodda spelare att ha utökade uppdragsredigerings- och statuskontrollmöjligheter över uppdrag som de äger." + } + }, + "Tooltips": { + "Delete": "Radera", + "Edit": "Redigera", + "HiddenQuestNoPlayers": "Detta uppdrag är dolt för alla spelare.", + "PrimaryQuest": "Primärt Uppdrag" + } + } +} diff --git a/lang/zh-tw.json b/lang/zh-tw.json new file mode 100644 index 00000000..4236320a --- /dev/null +++ b/lang/zh-tw.json @@ -0,0 +1,259 @@ +{ + "ForienQuestLog" : { + "API" : { + "Hooks": { + "Labels": { + "OpenMacro": "打開任務 „{name}”" + }, + "Notifications": { + "NoQuest": "API Error: Can't create macro with invalid Quest ID。" + } + }, + "QuestDB": { + "Labels": { + "NewQuest" : "新任務" + } + }, + "Socket": { + "Notifications": { + "RewardDrop" : "FQL(任務獎勵):{userName} 將 '{itemName}' 放到 {actorName} 上。" + } + }, + "Utils": { + "Notifications": { + "NoDocument" : "無法加載 UUID:'{uuid}'。", + "NoPermission" : "您沒有足夠的權限查看此entity表格。" + } + } + }, + "DeleteDialog" : { + "BodyObjective" : "該目標及其資料將被永久刪除。", + "BodyQuest" : "此任務及其資料將被永久刪除。", + "BodyReward" : "此獎勵及其資料將被永久刪除。", + "Cancel" : "取消", + "Delete" : "刪除", + "HeaderDel" : "你確定要刪除:

'{name}'

", + "TitleDel" : "刪除 {title}" + }, + "HeaderLabels": { + "Show": "展示給玩家" + }, + "Labels": { + "AppHeader": { + "ShowPlayers": "展示給玩家" + }, + "Quest" : "任務" + }, + "Migration" : { + "ChatMessage" : { + "Footer" : "您必須使用來自Compendiums或你的World中的有效文檔資料,來手動更新上述任務。
", + "Header" : "Forien's Quest Log (DB migration)
從以下任務中移除了未連結的任務給予者或獎勵物品:

", + "Notification" : "Forien's Quest Log - 從一個或多個任務中刪除未連結的任務給予者或獎勵物品。請查看聊天消息以獲取更多信息。", + "QuestGiver" : "任務給予者", + "QuestRewards" : "任務獎勵" + }, + "Notifications": { + "Complete" : "Forien's Quest Log - 遷移資料完成。", + "CouldNotMigrate" : "Forien's Quest Log - 無法遷移任務:'{name}'。", + "Schema" : "Forien's Quest Log - 將資料遷移到版本 '{version}'。", + "Start" : "Forien's Quest Log - 遷移資料中,請不要重新加載。" + } + }, + "Notifications" : { + "CannotOpen" : "無法查看任務詳情。你可能缺少訪問權限,或者該任務ID錯誤,或者該任務已被刪除。", + "FinishQuestAdded" : "請先完成編輯並關閉當前的新任務,然後再添加另一個任務。", + "LinkCopied" : "該任務的連結已複製到剪貼板中。", + "QuestAdded" : "添加了 '{name}' 作為新任務,狀態為:'{status}'。", + "QuestIDCopied" : "此任務的任務 ID 已複製到剪貼板。", + "QuestMoved" : "移動該任務至新文件夾並更改其狀態為:'{target}'。", + "QuestPrimary" : "'{name}' 是新的主要任務。", + "QuestTrackerNoActive" : "任務追踪器已啟用,但目前沒有正在進行的任務。", + "UserCantOpen" : "玩家 '{user}' 沒有查看該任務的權限。" + }, + "QuestLog" : { + "Buttons": { + "AddQuest": "添加任務" + }, + "ContextMenu" : { + "CopyEntityLink" : "複製內容連結", + "CopyQuestID" : "複製任務 ID", + "PrimaryQuest" : "設定/取消設定為主要任務" + }, + "Labels": { + "TableHeader": "{0}任務", + "SubTitle": "的子任務 {0}" + }, + "Title" : "任務欄", + "Tooltips" : { + "Objectives" : "目標" + } + }, + "QuestPreview" : { + "Buttons": { + "RewardCustom": "添加用戶定義", + "RewardHide": "隱藏所有", + "RewardLock": "鎖定所有", + "RewardShow": "顯示全部", + "RewardUnlock": "全部解鎖" + }, + "Labels": { + "CustomSource" : "自定義來源", + "Description" : "描述:", + "DragDropActor" : "拖放Actor、物品、日誌條目或左鍵單擊以設定自定義來源。", + "DragDropActorPlayer" : "拖放Actor、物品或日誌條目。", + "DragDropRewards" : "將物品拖拽到此處以將其設為獎勵。", + "GMNotes" : "GM 筆記:", + "Objective" : "目標", + "Objectives" : "目標:", + "PlayerNotes": "玩家 筆記:", + "Reward" : "獎勵", + "Rewards" : "獎勵:" + }, + "Management" : { + "AddSubquest" : "新建支線任務", + "ConfigurePermissions" : "配置權限", + "QuestBranching" : "支線任務:", + "QuestSettings" : "任務設定:", + "SplashArt" : "插畫:", + "SplashInfo" : "點擊設定圖片.", + "SplashQuestIcon" : "設定為任務圖示" + }, + "Notifications" : { + "BadUUID" : "無法搜索到 UUID 文檔:'{uuid}'。", + "WrongDocType" : "Forien 的任務日誌只接受World/Compendium Actor、物品和日誌條目作為任務提供者。", + "WrongItemType" : "Forien 的任務日誌只接受World和Compendium items作為獎勵。" + }, + "Tabs" : { + "Details" : "詳情", + "GMNotes" : "GM筆記", + "PlayerNotes": "玩家 筆記", + "QuestManagement" : "管理任務" + }, + "Title" : "任務詳情", + "Tooltips" : { + "AddCustom" : "添加用戶定義", + "AddObjective": "添加目標", + "ChangeSplashPos" : "改變插畫對齊方式。", + "DeleteQuestGiver" : "刪除任務給予者。", + "DeleteSplash" : "刪除插畫。", + "HideAll" : "隱藏所有", + "LockAll" : "鎖定所有", + "PrimaryQuestSet" : "點擊進行主要任務。", + "PrimaryQuestUnset" : "點擊取消設定主要任務。", + "RewardHidden" : "獎勵已隱藏,點擊查看。", + "RewardLocked" : "獎勵已鎖定,點擊解鎖。", + "RewardLockedPlayer" : "獎勵已鎖定。", + "RewardUnlocked" : "獎勵已解鎖,點擊鎖定。", + "RewardUnlockedPlayer" : "獎勵已解鎖。", + "RewardVisible" : "獎勵已可見,點擊隱藏。", + "ShowAll" : "顯示全部", + "TaskHidden" : "目標已隱藏,點擊查看。", + "TaskVisible" : "目標已可見,點擊隱藏。", + "ToggleImage" : "打開或關閉token/角色圖片。", + "UnlockAll" : "全部解鎖", + "ViewSplashArt": "查看插圖。" + } + }, + "QuestTracker" : { + "NoPrimary" : "沒有可用的主要任務。", + "Title" : "任務追踪器", + "Tooltips" : { + "BackgroundShow" : "點擊顯示背景。", + "BackgroundUnshow" : "點擊使背景透明。", + "PrimaryQuestShow" : "點擊顯示主要任務。", + "PrimaryQuestUnshow" : "點擊顯示所有任務。" + } + }, + "QuestTypes" : { + "Labels" : { + "Active" : "進行中", + "active" : "進行中", + "Available" : "可選", + "available" : "可選", + "Completed" : "已完成", + "completed" : "已完成", + "Failed" : "已失敗", + "failed" : "已失敗", + "InActive" : "未啓動", + "inactive" : "未啓動", + "Status" : "任務是 {statusLabel}。" + }, + "Tooltips": { + "SetActive" : "設定為進行中", + "SetAvailable" : "設定為可選", + "SetCompleted" : "設定為已完成", + "SetFailed" : "設定為已失敗", + "SetInactive" : "設置為非活動", + "Status" : "狀態:{statusI18n}" + } + }, + "Settings" : { + "allowPlayersAccept" : { + "Enable" : "玩家可以接受任務", + "EnableHint" : "勾選以允許玩家從可選任務中接受任務。" + }, + "allowPlayersCreate" : { + "Enable" : "玩家可以新建任務", + "EnableHint" : "勾選以允許玩家新建任務。玩家新建的任務將出現在可選任務欄中並且可被玩家編輯。需要給予玩家“新建日誌”權限。" + }, + "allowPlayersDrag" : { + "Enable" : "允許玩家拖拽任務獎勵", + "EnableHint" : "勾選以允許玩家將獎勵物品從任務詳情中拖拽到自己的角色卡上。" + }, + "countHidden" : { + "Enable" : "包括隱藏任務數量", + "EnableHint" : "勾選以將隱藏任務的數量算入已完成任務/所有任務數量中。" + }, + "defaultPermissionLevel" : { + "Enable" : "預設任務權限等級", + "EnableHint" : "新增任務時,任務所默認的權限級別.", + "NONE" : "無", + "OBSERVER" : "查看者", + "OWNER" : "所有者" + }, + "dynamicBookmarkBackground" : { + "Enable" : "動態書籤背景", + "EnableHint" : "啟用此選項後,書籤標籤背景將動態設定為視窗內容背景。" + }, + "hideFQLFromPlayers" : { + "Enable" : "對玩家隱藏任務日誌", + "EnableHint" : "啟用此選項後,所有玩家都無法看到任務日誌。只有 GM 級別的用戶才能訪問任務日誌。" + }, + "navStyle" : { + "bookmarks" : "書籤", + "classic" : "經典標籤頁", + "Enable" : "導航方式", + "EnableHint" : "選擇如何展示任務日誌的導航。" + }, + "notifyRewardDrop" : { + "Enable" : "顯示獎勵掉落通知", + "EnableHint" : "當任務獎勵被放到玩家表上時,將彈出 UI 通知。" + }, + "questTrackerResizable" : { + "Enable" : "任務跟踪器可調整大小", + "EnableHint" : "啟用此選項後,將允許對 任務跟踪器 進行手動調整大小控制。" + }, + "showFolder" : { + "Enable" : "顯示任務文件夾", + "EnableHint" : "勾選以在日誌欄中顯示目標文件夾。僅用於DEBUG。" + }, + "showTasks" : { + "default" : "展示目標:已完成/全部", + "Enable" : "顯示任務欄中的目標", + "EnableHint" : "選擇是否或如何在任務日誌欄中展示任務的目標數量。", + "no" : "隱藏 \"目標\" 列表", + "onlyCurrent" : "展示目標:已完成" + }, + "trustedPlayerEdit" : { + "Enable" : "允許受信任的玩家編輯任務", + "EnableHint" : "啟用此選項後,將允許受信任的玩家對他們擁有的擴展任務,進行編輯和狀態控制。" + } + }, + "Tooltips" : { + "Delete" : "刪除", + "Edit" : "編輯", + "HiddenQuestNoPlayers" : "這個任務對所有玩家都是隱藏的。", + "PrimaryQuest" : "主要任務" + } + } +} \ No newline at end of file diff --git a/module.json b/module.json index af25ff4c..06504d8e 100644 --- a/module.json +++ b/module.json @@ -1,35 +1,44 @@ { - "name": "forien-quest-log", + "id": "forien-quest-log", "title": "Forien's Quest Log", "description": "Provides comprehensive Quest Log system for players and Game Masters", - "author": "Forien — Forien#2130", "authors": [ { "name": "Forien", "url": "https://www.patreon.com/forien" + }, + { + "name": "Michael Leahy (TyphonJS)", + "url": "https://www.typhonjs.io" } ], - "version": "0.5.1", - "minimumCoreVersion": "0.6.0", - "compatibleCoreVersion": "0.7.1", - "url": "https://github.com/Forien/foundryvtt-forien-quest-log", - "manifest": "https://raw.githubusercontent.com/Forien/foundryvtt-forien-quest-log/master/module.json", - "download": "https://github.com/Forien/foundryvtt-forien-quest-log/releases/download/v0.5.1/v0.5.1.zip", - "readme": "https://github.com/Forien/foundryvtt-forien-quest-log/blob/v0.5.1/README.md", - "changelog": "https://github.com/Forien/foundryvtt-forien-quest-log/blob/v0.5.1/changelog.md", - "bugs": "https://github.com/Forien/foundryvtt-forien-quest-log/issues", - "wiki": "https://github.com/Forien/foundryvtt-forien-quest-log/wiki", + "url": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log", + "readme": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/blob/master/README.md", + "bugs": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/issues", + "changelog": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/releases/latest/", + "version": "0.8.0", + "compatibility": { + "minimum": "11", + "verified": "12", + "maximum": "12" + }, + "esmodules": [ + "./src/init.js" + ], + "styles": [ + "./css/init.css" + ], "languages": [ - { - "lang": "de", - "name": "Deutsch", - "path": "lang/de.json" - }, { "lang": "cn", "name": "中文(简体", "path": "lang/cn.json" }, + { + "lang": "de", + "name": "Deutsch", + "path": "lang/de.json" + }, { "lang": "en", "name": "English", @@ -40,11 +49,21 @@ "name": "Español", "path": "lang/es.json" }, + { + "lang": "fi-Fl", + "name": "Finnish", + "path": "lang/fi-FI.json" + }, { "lang": "fr", "name": "Français", "path": "lang/fr.json" }, + { + "lang": "it", + "name": "Italiano", + "path": "lang/it.json" + }, { "lang": "ja", "name": "日本語", @@ -55,6 +74,11 @@ "name": "한국어", "path": "lang/ko.json" }, + { + "lang": "nl", + "name": "Nederlands", + "path": "lang/nl.json" + }, { "lang": "pl", "name": "Polski", @@ -64,16 +88,47 @@ "lang": "pt-BR", "name": "Português (Brasil)", "path": "lang/pt-BR.json" + }, + { + "lang": "ru", + "name": "Russian", + "path": "lang/ru.json" + }, + { + "lang": "sv", + "name": "Svenska", + "path": "lang/sv.json" + }, + { + "lang": "zh-tw", + "name": "正體中文", + "path": "lang/zh-tw.json" } ], - "scripts": [ - "./scripts/prompt.mjs" - ], - "esmodules": [ - "./modules/init.mjs" - ], - "styles": [ - "./styles/init.css" + "packs": [ + { + "name": "macros-gm", + "label": "FQL Macros (GM)", + "module": "forien-quest-log", + "path": "./packs/macro-gm.db", + "type": "Macro", + "ownership": { + "PLAYER": "NONE", + "ASSISTANT": "OWNER" + } + }, + { + "name": "macros-player", + "label": "FQL Macros (Player)", + "module": "forien-quest-log", + "path": "./packs/macro-player.db", + "type": "Macro" + } ], - "socket": true -} + "socket": true, + "manifest": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/releases/latest/download/module.json", + "download": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-quest-log/releases/download/0.8.0/module.zip", + "protected": false, + "coreTranslation": false, + "library": false +} \ No newline at end of file diff --git a/modules/api/hooks.js b/modules/api/hooks.js deleted file mode 100644 index 478fefcf..00000000 --- a/modules/api/hooks.js +++ /dev/null @@ -1,57 +0,0 @@ -import Quest from "../entities/quest.mjs"; -import Utils from "../utility/utils.mjs"; - -/** - * Function for registering API-related Hooks. - */ -export default function registerApiHooks() { - // Create "open quest" Macro when Quest is dropped onto Hotbar. - Hooks.on("hotbarDrop", async (bar, data, slot) => { - if (data.type === "Quest") { - let questId = data.id; - - let quest = Quest.get(questId); - if (!quest) - throw new Error(game.i18n.localize("ForienQuestLog.Api.hooks.createOpenQuestMacro.error.noQuest")); - - let command = `Quests.open("${questId}");`; - let macroData = { - name: game.i18n.format("ForienQuestLog.Api.hooks.createOpenQuestMacro.name", {name: quest.title}), - type: "script", - command: command - }; - - let actor = Utils.findActor(quest.giver); - if (actor) { - if (quest.image === 'actor') - macroData.img = actor.img; - else - macroData.img = actor.data.token.img; - } else { - if (quest.giver) { - let entity = await fromUuid(quest.giver); - macroData.img = entity.img; - } - } - - let macro = game.macros.entities.find(m => (m.data.command === command)); - if (!macro) { - macro = await Macro.create(macroData, {displaySheet: false}) - } - - game.user.assignHotbarMacro(macro, slot); - } - return false; - }); - - // open quest details on link click - $('body').on("click", "a.entity-link[data-entity='Quest']", function (event) { - event.preventDefault(); - event.stopImmediatePropagation(); - const a = event.currentTarget; - - Quests.open(a.dataset.id); - - return false; - }); -} diff --git a/modules/api/quest-api.mjs b/modules/api/quest-api.mjs deleted file mode 100644 index 13eb9978..00000000 --- a/modules/api/quest-api.mjs +++ /dev/null @@ -1,70 +0,0 @@ -import QuestPreview from "../apps/quest-preview.mjs"; -import Quest from "../entities/quest.mjs"; -import Reward from "../entities/reward.mjs"; -import Task from "../entities/task.mjs"; -import Socket from "../utility/socket.mjs"; - -/** - * Quest public Api available under `Quests.` - */ -export default class QuestApi { - /** - * Retrieves Quest instance for given quest ID - * - * @param questId - * @returns {Quest} - */ - static get(questId) { - return Quest.get(questId); - } - - /** - * Opens Quest Details for given quest ID - * - * @param questId - * @param notif - */ - static open(questId, notif = true) { - try { - let questPreview = new QuestPreview(questId); - questPreview.render(true); - } catch (error) { - if (notif) - ui.notifications.error(game.i18n.localize("ForienQuestLog.Notifications.CannotOpen"), {}); - else - Socket.userCantOpenQuest(); - } - } - - /** - * Creates new Quest programmatically through API - * - * @param data - * @returns {Quest} - */ - static create(data = {}) { - if (data.title === undefined) { - throw new Error(game.i18n.localize("ForienQuestLog.Api.create.title")); - } - - return new Quest({}); - } - - /** - * Tunnel to `game.quests.reward.create()`. Creates new Reward programmatically through API. - * - * @returns {Reward} - */ - static get reward() { - return Reward; - } - - /** - * Tunnel to `game.quests.task.create()`. Creates new Task programmatically through API. - * - * @returns {Task} - */ - static get task() { - return Task; - } -} diff --git a/modules/apps/quest-form.mjs b/modules/apps/quest-form.mjs deleted file mode 100644 index ffb56451..00000000 --- a/modules/apps/quest-form.mjs +++ /dev/null @@ -1,280 +0,0 @@ -import QuestFolder from "../entities/quest-folder.mjs"; -import Utils from "../utility/utils.mjs"; -import Task from "../entities/task.mjs"; -import Quest from "../entities/quest.mjs"; -import Socket from "../utility/socket.mjs"; - -export default class QuestForm extends FormApplication { - submitted = false; - - /** - * Default Application options - * - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - id: "forien-quest-log-form", - template: "modules/forien-quest-log/templates/quest-form.html", - title: game.i18n.localize("ForienQuestLog.QuestForm.Title"), - width: 940, - height: 640, - closeOnSubmit: true - }); - } - - /** - * Retrieves Data to be used in rendering template. - * - * @param options - * @returns {Promise} - */ - async getData(options = {}) { - this.subquest = (this.object._id !== undefined); - const parent = this.subquest ? this.object : null; - - if (this.subquest) - this.options.title += ` – ${game.i18n.format('ForienQuestLog.QuestForm.SubquestOf', {name: parent.name})}`; - - return { - isGM: game.user.isGM, - subquest: this.subquest, - parent: parent, - options: mergeObject(this.options, options) - }; - } - - /** - * Proxy for QuestFolder.get('hidden'). - * Needed? probably not... - * - * @returns {*} - */ - getHiddenFolder() { - return QuestFolder.get('hidden'); - } - - /** - * Called "on submit". Handles saving Form's data - * - * @param event - * @param formData - * @private - */ - async _updateObject(event, formData) { - const actor = Utils.findActor(formData.giver); - let giver = null; - let permission = 0; - - if (actor !== false) { - giver = actor.uuid; - } else { - try { - let entity = await fromUuid(formData.giver); - giver = entity.uuid; - } catch (e) { - giver = null; - } - } - - let title = formData.title; - if (title.length === 0) - title = game.i18n.localize("ForienQuestLog.NewQuest"); - - let tasks = []; - if (formData.tasks !== undefined) { - if (!Array.isArray(formData.tasks)) { - formData.tasks = [formData.tasks]; - } - tasks = formData.tasks.filter(t => t.length > 0).map(t => { - return new Task({name: t}); - }); - } - - let description = (formData.description !== undefined && formData.description.length) ? formData.description : this.description; - let gmnotes = (formData.gmnotes !== undefined && formData.gmnotes.length) ? formData.gmnotes : this.gmnotes; - - let data = { - giver: giver, - title: title, - description: description, - gmnotes: gmnotes, - tasks: tasks - }; - - if (!game.user.isGM) { - data.status = 'available'; - permission = 3; - } - - if (formData.giver === 'abstract') { - data.giver = formData.giver; - data.image = formData.sourceImage; - data.giverName = formData.giverName; - } - - if (this.subquest) { - data.parent = this.object._id; - } - - data = new Quest(data); - - let folder = this.getHiddenFolder(); - - return JournalEntry.create({ - name: title, - content: JSON.stringify(data), - folder: folder._id, - permission: {default: permission} - }).then((entry) => { - if (this.subquest) { - this.object.addSubquest(entry._id); - this.object.save().then(() => { - Socket.refreshQuestPreview(this.object._id); - }); - } - // players don't see Hidden tab, but assistant GM can, so emit anyway - Socket.refreshQuestLog(); - this.submitted = true; - - return entry; - }); - } - - - async close() { - if (this.submitted) { - return super.close(); - } - - new Dialog({ - title: game.i18n.localize("ForienQuestLog.CloseDialog.Title"), - content: `

${game.i18n.localize("ForienQuestLog.CloseDialog.Header")}

-

${game.i18n.localize("ForienQuestLog.CloseDialog.Body")}

`, - buttons: { - no: { - icon: ``, - label: game.i18n.localize("ForienQuestLog.CloseDialog.Cancel") - }, - yes: { - icon: ``, - label: game.i18n.localize("ForienQuestLog.CloseDialog.Discard"), - callback: () => { - this.submitted = true; - this.close(); - } - } - }, - default: "no" - }).render(true); - - - } - - /** - * Need to override because of earlier bad design caused bug with text editors inheriting parent's data - * - * @param div - * @private - */ - _activateEditor(div) { - const temp = this.object; - this.object = undefined; - super._activateEditor(div); - this.object = temp; - } - - /** - * Fired whenever any of TinyMCE editors is saved. - * Just pass data to object's property, we handle save in one go after submit - * - * @see _updateObject() - * - * @param target - * @param element - * @param content - * @returns {Promise} - * @private - */ - async _onEditorSave(target, element, content) { - this[target] = content; - - // keep function to override parent function - // we don't need to submit form on editor save - } - - /** - * Defines all event listeners like click, drag, drop etc. - * - * @param html - */ - activateListeners(html) { - super.activateListeners(html); - - html.on("change", "#giver", async (event) => { - const giverId = $(event.currentTarget).val(); - let giver; - - try { - giver = Utils.findActor(giverId); - - if (giver === false) { - giver = await fromUuid(giverId); - } - } catch (e) { - giver = false; - } - - if (giver) { - if (giver.data.img.length) { - html.find('.giver-portrait').attr({ - 'style': 'background-image:url(' + giver.data.img + ')', - 'title': giver.name - }).removeClass('hidden'); - } else { - html.find('.giver-portrait').attr('style', '').addClass('hidden'); - } - html.find('.drop-info').addClass('hidden'); - } else { - html.find('.giver-portrait').addClass('hidden'); - html.find('.drop-info').removeClass('hidden'); - } - }); - - html.on("drop", ".giver-data-fieldset", async (event) => { - event.preventDefault(); - let data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); - if (['Actor', 'Item', 'JournalEntry'].includes(data.type)) { - let uuid = `${data.type}.${data.id}`; - html.find('#giver').val(uuid).prop('readonly', false).change(); - html.find('.quest-giver-name').slideUp(); - } - }); - - html.on("click", ".add-new-task", () => { - renderTemplate('modules/forien-quest-log/templates/partials/quest-form/task.html', {}).then(el => { - - html.find('.list').append(el); - html.find('.del-btn').unbind(); - html.on("click", ".del-btn", (event) => { - $(event.target).parent().remove(); - }); - }); - }); - - html.on("click", ".source-image", () => { - let currentPath = html.find('.quest-giver-name').val(); - new FilePicker({ - type: "image", - current: currentPath, - callback: path => { - html.find('#giver').val('abstract').prop('readonly', true); - html.find('#sourceImage').val(path); - html.find('.quest-giver-name').slideDown(); - html.find('.giver-portrait').css('background-image', `url(${path})`).removeClass('hidden'); - html.find('.drop-info').addClass('hidden'); - }, - }).browse(currentPath); - }); - } -}; diff --git a/modules/apps/quest-log.mjs b/modules/apps/quest-log.mjs deleted file mode 100644 index 742712a5..00000000 --- a/modules/apps/quest-log.mjs +++ /dev/null @@ -1,138 +0,0 @@ -import Quest from "../entities/quest.mjs"; -import QuestPreview from "./quest-preview.mjs"; -import QuestForm from "./quest-form.mjs"; -import Socket from "../utility/socket.mjs"; - -export default class QuestLog extends Application { - sortBy = null; - sortDirection = 'asc'; - - /** - * Default Application options - * - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - id: "forien-quest-log", - classes: ["forien-quest-log"], - template: "modules/forien-quest-log/templates/quest-log.html", - width: 700, - height: 480, - minimizable: true, - resizable: true, - title: game.i18n.localize("ForienQuestLog.QuestLog.Title"), - tabs: [{navSelector: ".log-tabs", contentSelector: ".log-body", initial: "progress"}] - }); - } - - /** - * Retrieves Data to be used in rendering template. - * - * @param options - * @returns {Promise} - */ - getData(options = {}) { - let available = game.settings.get("forien-quest-log", "availableQuests"); - return mergeObject(super.getData(), { - options: options, - isGM: game.user.isGM, - availableTab: available, - canAccept: game.settings.get("forien-quest-log", "allowPlayersAccept"), - canCreate: game.settings.get("forien-quest-log", "allowPlayersCreate"), - showTasks: game.settings.get("forien-quest-log", "showTasks"), - style: game.settings.get("forien-quest-log", "navStyle"), - // titleAlign: game.settings.get("forien-quest-log", "titleAlign"), - questTypes: Quest.getQuestTypes(), - quests: Quest.getQuests(this.sortBy, this.sortDirection, available, true) - }); - } - - /** - * Set sort target and toggle direction. Refresh window - * - * @param target - */ - toggleSort(target, direction = undefined) { - if (this.sortBy === target) { - this.sortDirection = (this.sortDirection === 'desc') ? 'asc' : 'desc'; - } else { - this.sortBy = target; - this.sortDirection = 'asc'; - } - if (direction !== undefined && (direction === 'asc' || direction === 'desc')) - this.sortDirection = direction; - - this.render(true); - } - - /** - * Defines all event listeners like click, drag, drop etc. - * - * @param html - */ - activateListeners(html) { - super.activateListeners(html); - - html.on("click", ".new-quest-btn", () => { - new QuestForm({}).render(true); - }); - - html.on("click", ".actions i", event => { - const canPlayerAccept = game.settings.get("forien-quest-log", "allowPlayersAccept"); - const target = $(event.target).data('target'); - const questId = $(event.target).data('quest-id'); - - if (target === 'active' && canPlayerAccept) Socket.acceptQuest(questId); - if (!game.user.isGM) return; - - const classList = $(event.target).attr('class'); - if (classList.includes('move')) { - Quest.move(questId, target); - } else if (classList.includes('delete')) { - Quest.delete(questId); - } - }); - - html.on("click", ".title", event => { - let questId = $(event.target).closest('.title').data('quest-id'); - let questPreview = new QuestPreview(questId); - questPreview.render(true); - }); - - html.on("click", ".sortable", event => { - let el = $(event.target); - this.toggleSort(el.data('sort')); - }); - - html.on("dragstart", ".drag-quest", event => { - let dataTransfer = { - type: "Quest", - id: $(event.target).data('quest-id') - }; - event.originalEvent.dataTransfer.setData("text/plain", JSON.stringify(dataTransfer)); - - }); - - html.on("drop", ".tab", event => { - const dt = event.target.closest('.drag-quest') || null; - if (!dt) return; - - const data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); - const id = data.id; - const journal = game.journal.get(id); - if (!journal) return; - - const quest = Quest.get(id); - if (!quest) return; - - const sortData = {sortKey: "sort", sortBefore: true}; - const targetId = dt.dataset.questId; - sortData.target = game.journal.get(targetId); - const ids = Quest.getQuests()[quest.status].map(q => q.id); - sortData.siblings = game.journal.filter(e => (e._id !== data.id && ids.includes(e._id))); - - journal.sortRelative(sortData).then(() => this.render()); - }); - } -}; diff --git a/modules/apps/quest-preview.mjs b/modules/apps/quest-preview.mjs deleted file mode 100644 index 2e3b0d0e..00000000 --- a/modules/apps/quest-preview.mjs +++ /dev/null @@ -1,525 +0,0 @@ -import Quest from "../entities/quest.mjs"; -import Socket from "../utility/socket.mjs"; -import QuestForm from "./quest-form.mjs"; - -export default class QuestPreview extends FormApplication { - /** - * Since Quest Preview shows data for single Quest, it needs a Quest instance or - * there is no point in rendering it. - * - * @param questId - * @param options - */ - constructor(questId, options = {}) { - super(options); - this.quest = Quest.get(questId); - if (!this.quest) throw new Error(game.i18n.localize("ForienQuestLog.QuestPreview.InvalidQuestId")); - } - - set object(value) {} - - get object() { - return this.quest; - } - - /** - * Default Application options - * - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["forien-quest-preview"], - template: "modules/forien-quest-log/templates/quest-preview.html", - width: 700, - height: 540, - minimizable: true, - resizable: true, - title: game.i18n.localize("ForienQuestLog.QuestPreview.Title"), - tabs: [{navSelector: ".quest-tabs", contentSelector: ".quest-body", initial: "details"}] - }); - } - - /** @override */ - get id() { - return `quest-${this.quest.id}`; - } - - /** - * Retrieves Data to be used in rendering template. - * - * @param options - * @returns {Promise} - */ - async getData(options = {}) { - let quest = duplicate(this.quest); - let content = Quest.populate(quest, this.quest.entry); - this.canEdit = (content.playerEdit || game.user.isGM); - this.playerEdit = content.playerEdit; - - let data = { - id: this.quest.id, - isGM: game.user.isGM, - canEdit: this.canEdit - }; - - if (game.user.isGM) { - let entry = game.journal.get(this.quest.id); - data.users = game.users; - data.observerLevel = CONST.ENTITY_PERMISSIONS.OBSERVER; - - data.users = game.users.map(u => { - if (u.isGM) return; - return { - user: u, - level: entry.data.permission[u._id], - hidden: u.isGM - }; - }).filter(u => u !== undefined); - } - - return mergeObject(content, data); - } - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - - // Share Entry - if (game.user.isGM) { - buttons.unshift({ - label: game.i18n.localize("ForienQuestLog.QuestPreview.HeaderButtons.Show"), - class: "share-quest", - icon: "fas fa-eye", - onclick: () => Socket.showQuestPreview(this.quest.id) - }); - } - - if (this.quest.splash.length) { - buttons.unshift({ - label: '', - class: "splash-image", - icon: "far fa-image", - onclick: () => { - (new ImagePopout(this.quest.splash, {shareable: true})).render(true) - } - }); - } - - buttons.unshift({ - label: '', - class: "copy-link", - icon: "fas fa-link", - onclick: (event) => { - const el = document.createElement('textarea'); - el.value = `@Quest[${this.quest.id}]{${this.quest.title}}`; - document.body.appendChild(el); - el.select(); - document.execCommand('copy'); - document.body.removeChild(el); - ui.notifications.info(game.i18n.localize("ForienQuestLog.Notifications.LinkCopied"), {}); - } - }); - - return buttons; - } - - getQuestName () { - return this.quest.name; - } - - /** - * This might be a FormApplication, but we don't want Submit event to fire. - * @param event - * @param formData - * @returns {Promise} - * @private - */ - async _onSubmit(event, {updateData = null, preventClose = false, preventRender = false} = {}) { - event.preventDefault(); - - return false; - } - - /** - * This might be a FormApplication, but we don't want to update anything on submit. - * @param event - * @param formData - * @returns {Promise} - * @private - */ - async _updateObject(event, formData) { - event.preventDefault(); - - return false; - } - - /** - * When editor is saved, we want to update and save quest. - * - * @param target - * @param element - * @param content - * @returns {Promise} - * @private - */ - async _onEditorSave(target, element, content) { - this.quest[target] = content; - this.saveQuest(); - } - - /** - * Save associated quest and refresh window - * - * @returns {Promise} - */ - async saveQuest() { - this.quest.save().then(() => { - this.refresh(); - }); - } - - /** - * Refreshes the Quest Details window and emits Socket so other players get updated view as well - * - * @returns {Promise} - */ - async refresh() { - this.render(true); - Socket.refreshQuestLog(); - Socket.refreshQuestPreview(this.quest.id); - if (this.quest.parent) { - Socket.refreshQuestPreview(this.quest.parent); - } - } - - /** - * When rendering window, add reference to global variable. - * - * @see close() - * @returns {Promise} - */ - render(force = false, options = {}) { - game.questPreview[this.quest.id] = this; - if (force) this.quest.refresh(); - return super.render(force, options); - } - - /** - * When closing window, remove reference from global variable. - * - * @see render() - * @returns {Promise} - */ - async close() { - delete game.questPreview[this.quest.id]; - await super.close(); - } - - /** - * Retrieves Item from Compendium - * - * @param packId - * @param itemId - */ - async getItemFromPack(packId, itemId) { - const pack = game.packs.get(packId); - if (pack.metadata.entity !== "Item") - return; - return await pack.getEntity(itemId).then(ent => { - delete ent._id; - return ent; - }); - } - - /** - * Defines all event listeners like click, drag, drop etc. - * - * @param html - */ - activateListeners(html) { - super.activateListeners(html); - - html.on('click', '.splash-image-link', (event) => { - (new ImagePopout(this.quest.splash, {shareable: true})).render(true) - }); - - html.on('dragstart', '.fa-sort', (event) => { - event.stopPropagation(); - const li = event.target.closest('li') || null; - if (!li) return; - let dataTransfer = { - mode: "Sort", - index: $(li).data('index') - }; - event.originalEvent.dataTransfer.setData("text/plain", JSON.stringify(dataTransfer)); - }); - - html.on('dragstart', '.item-reward', (event) => { - let dataTransfer = { - type: "Item", - data: $(event.target).data('transfer') - }; - event.originalEvent.dataTransfer.setData("text/plain", JSON.stringify(dataTransfer)); - }); - - html.on("click", ".item-reward", (event) => { - let data = $(event.currentTarget).data('transfer'); - delete data._id; - delete data.permission; - let item = new CONFIG.Item.entityClass(data); - item.sheet.render(true); - }); - - html.on("click", ".quest-name", (event) => { - let id = $(event.currentTarget).data('id'); - Quests.open(id); - }); - - html.on("click", ".open-actor-sheet", (event) => { - let actorId = $(event.target).data('actor-id'); - let actor = game.actors.get(actorId); - if (actor?.permission > 0) - actor.sheet.render(true); - }); - - if (this.canEdit) { - html.on("click", ".actions i", async (event) => { - const target = $(event.target).data('target'); - const questId = $(event.target).data('id'); - const classList = $(event.target).attr('class'); - - if (classList.includes('move')) { - Quest.move(questId, target).then(() => this.refresh()); - } else if (classList.includes('delete')) { - Quest.delete(questId, this.quest.id).then(() => this.refresh()); - } - }); - - html.on("drop", ".rewards-box", async (event) => { - event.preventDefault(); - let item; - let data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); - if (data.mode === 'Sort') { - this.quest.sortRewards(event, data); - } else if (data.type === 'Item') { - if (data.pack) { - item = await this.getItemFromPack(data.pack, data.id); - } else if (data.data) { - item = data.data; - } else { - let witem = game.items.get(data.id); - if (!witem) - return; - item = duplicate(witem); - } - if (item) { - this.quest.addReward({type: "Item", data: item}); - this.saveQuest(); - } - } - }); - - html.on("drop", ".tasks-box", async (event) => { - event.preventDefault(); - let data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); - if (data.mode === 'Sort') { - this.quest.sortTasks(event, data); - } - }); - - html.on("drop", ".quest-giver-gc", (event) => { - event.preventDefault(); - let data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); - if (['Actor', 'Item', 'JournalEntry'].includes(data.type)) { - this.quest.giver = `${data.type}.${data.id}`; - this.saveQuest(); - } - }); - - html.on("click", '.editable', (event) => { - let target = $(event.target).data('target'); - if (target === undefined) return; - - let value = this.quest[target]; - let index = undefined; - if (target === 'task.name') { - index = $(event.target).data('index'); - value = this.quest.tasks[index].name; - } - if (target === 'reward.name') { - index = $(event.target).data('index'); - value = this.quest.rewards[index].data.name; - } - - value = value.replace(/"/g, '"'); - let input = $(``); - let parent = $(event.target).closest('.actions').prev('.editable-container'); - - parent.html(''); - parent.append(input); - input.focus(); - - input.focusout((event) => { - let target = $(event.target).data('target'); - let value = $(event.target).val(); - let index; - - switch (target) { - case 'task.name': - index = $(event.target).data('index'); - this.quest.tasks[index].name = value; - break; - case 'reward.name': - index = $(event.target).data('index'); - this.quest.rewards[index].data.name = value; - break; - default: - if (this.quest[target] !== undefined) - this.quest[target] = value; - } - this.saveQuest(); - }); - }); - - html.on("click", '.del-btn', (event) => { - let index = $(event.target).data('index'); - let target = $(event.target).data('target'); - - if (target === 'tasks') { - this.quest.removeTask(index); - } else if (target === 'rewards') { - this.quest.removeReward(index); - } - this.saveQuest(); - }); - - html.on("click", '.task .toggleState', (event) => { - let index = $(event.target).data('task-index'); - this.quest.tasks[index].toggle(); - this.saveQuest(); - }); - - html.on("click", ".toggleImage", (event) => { - this.quest.toggleImage().then(() => { - this.saveQuest(); - }); - }); - - html.on("click", ".add-new-task", (event) => { - event.preventDefault(); - let li = $('
  • '); - let placeholder = $(''); - let input = $(``); - let box = $(event.target).closest('.quest-tasks').find('.tasks-box ul'); - - li.append(placeholder); - li.append(input); - box.append(li); - - input.focus(); - - input.focusout((event) => { - let value = $(event.target).val(); - if (value !== undefined && value.length) { - this.quest.addTask({ - name: value - }) - } - this.saveQuest(); - }); - }); - - html.on("click", ".toggleHidden", (event) => { - let target = $(event.target).data('target'); - let index = $(event.target).data('index'); - - if (target === 'task') { - this.quest.toggleTask(index).then(() => this.saveQuest()); - } else if (target === 'reward') { - this.quest.toggleReward(index).then(() => this.saveQuest()); - } - }); - - html.on("click", ".add-abstract", (event) => { - let li = $('
  • '); - let input = $(``); - let box = $(event.target).closest('.quest-rewards').find('.rewards-box ul'); - - // $(box).children('.drop-info').each(function () { - // $(this).remove(); - // }); - - li.append(input); - box.append(li); - - input.focus(); - - input.focusout((event) => { - let value = $(event.target).val(); - if (value !== undefined && value.length) { - this.quest.addReward({ - data: { - name: value, - img: 'icons/svg/mystery-man.svg' - }, - type: 'Abstract' - }) - } - this.saveQuest(); - }); - }); - - html.on("click", ".abstract-reward .reward-image", (event) => { - let index = $(event.target).data('index'); - let currentPath = this.quest.rewards[index].data.img; - new FilePicker({ - type: "image", - current: currentPath, - callback: path => { - this.quest.rewards[index].data.img = path; - this.saveQuest(); - }, - }).browse(currentPath); - }); - } - - if (game.user.isGM) { - html.on("click", "#personal-quest", (event) => { - this.quest.togglePersonal().then(() => { - this.saveQuest(); - }); - }); - - html.on("click", ".personal-user", (event) => { - let userId = $(event.target).data('user-id'); - let value = $(event.target).data('value'); - let permission = value ? 0 : (this.playerEdit ? 3 : 2); - - this.quest.savePermission(userId, permission).then(() => this.saveQuest()); - }); - - html.on("click", ".quest-splash", (event) => { - let currentPath = this.quest.splash; - new FilePicker({ - type: "image", - current: currentPath, - callback: path => { - this.quest.splash = path; - this.saveQuest(); - }, - }).browse(currentPath); - }); - - html.on("click", ".add-subquest-btn", (event) => { - new QuestForm(this.quest, {subquest:true}).render(true); - }); - - html.on("click", "#player-edit", event => { - let checked = $(event.target).prop('checked'); - let permission = checked ? 3 : 2; - this.quest.savePermission('*', permission).then(() => this.saveQuest()); - }); - } - } -}; diff --git a/modules/constants.mjs b/modules/constants.mjs deleted file mode 100644 index bd9a9b63..00000000 --- a/modules/constants.mjs +++ /dev/null @@ -1,5 +0,0 @@ -let constants = { - moduleName: "forien-quest-log", - moduleLabel: "Forien's Quest Log" -}; -export default constants; diff --git a/modules/entities/collection/quests-collection.mjs b/modules/entities/collection/quests-collection.mjs deleted file mode 100644 index 77e486b0..00000000 --- a/modules/entities/collection/quests-collection.mjs +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Class that acts "kind of" like Entity, to help Manage everything Quest Related - * in a more structured way, than to call JournalEntry every time. - */ -import Quest from "../quest.mjs"; - -export default class QuestsCollection { - static _entities = undefined; - static get entities() { - if (this._entities === undefined) { - let quests = Quest.getQuests(); - let entities = [...quests.active, ...quests.completed, ...quests.failed, ...quests.hidden]; - - this._entities = entities.map(e => { - let data = e; - data.name = data.title; - - return { - _id: e.id, - id: e.id, - name: e.title, - data: data - } - }) - } - - return this._entities; - } - - static get(questId) { - return Quest.get(questId); - } - - static getName(name) { - return this.entities.find(e => e.name === name); - } - - static get instance() { - return this; - } -} diff --git a/modules/entities/quest-folder.mjs b/modules/entities/quest-folder.mjs deleted file mode 100644 index 9dd81ab5..00000000 --- a/modules/entities/quest-folder.mjs +++ /dev/null @@ -1,41 +0,0 @@ -export default class QuestFolder { - static questDirName = '_fql_quests'; - static _questDirId = null; - - /** - * Returns true if quest directory has been created - * - * @param folder - * @returns {boolean} - */ - static folderExists(folder = 'root') { - let result = game.journal.directory.folders.find(f => f.name === this.questDirName); - - return result !== undefined; - } - - /** - * Initializes the creation of quest folders - * - * @returns {Promise} - */ - static async initializeJournals() { - let dirExists = this.folderExists(); - - if (!dirExists) { - await Folder.create({name: this.questDirName, type: "JournalEntry", parent: null}); - } - - let folder = await game.journal.directory.folders.find(f => f.name === this.questDirName); - this._questDirId = folder._id; - } - - /** - * Retrieves instance of Quest folder - * - * @returns {*} - */ - static get() { - return game.journal.directory.folders.find(f => f.name === this.questDirName); - } -} diff --git a/modules/entities/quest.mjs b/modules/entities/quest.mjs deleted file mode 100644 index aac1cda5..00000000 --- a/modules/entities/quest.mjs +++ /dev/null @@ -1,722 +0,0 @@ -import Socket from "../utility/socket.mjs"; -import QuestFolder from "./quest-folder.mjs"; -import Reward from "./reward.mjs"; -import Task from "./task.mjs"; -import QuestsCollection from "./collection/quests-collection.mjs"; -import constants from "../constants.mjs"; - -/** - * Class that acts "kind of" like Entity, to help Manage everything Quest Related - * in a more structured way, than to call JournalEntry every time. - */ -export default class Quest { - constructor(data = {}, entry = null) { - this._id = data.id || null; - this.initData(data); - this.entry = entry; - this._data = data; - } - - /** - * Normally would be in constructor(), but is extracted for usage in different methods as well - * - * @see refresh() - * @param data - */ - initData(data) { - this._giver = data.giver || null; - this._title = data.title || game.i18n.localize("ForienQuestLog.NewQuest"); - this._status = data.status || 'hidden'; - this._description = data.description || ''; - this._gmnotes = data.gmnotes || ''; - this._image = data.image || 'actor'; - this._giverName = data.giverName || 'actor'; - this._splash = data.splash || ''; - this._personal = data.personal || false; - this._parent = data.parent || null; - this._permission = data.permission || 0; - this._subquests = data.subquests || []; - this._tasks = []; - this._rewards = []; - this._populated = false; - - if (data.tasks !== undefined && Array.isArray(data.tasks)) - this._tasks = data.tasks.map((task) => { - return new Task(task); - }); - if (data.rewards !== undefined && Array.isArray(data.rewards)) - this._rewards = data.rewards.map((reward) => { - return new Reward(reward); - }); - } - - /** - * Creates new and adds Quest to task array of quest. - * - * @param questId - */ - addSubquest(questId) { - this._subquests.push(questId); - } - - /** - * Creates new and adds Task to task array of quest. - * - * @param data - */ - addTask(data = {}) { - let task = new Task(data); - if (task.isValid) - this._tasks.push(task); - } - - /** - * Creates new and adds Reward to reward array of quest. - * - * @param data - */ - addReward(data = {}) { - let reward = new Reward(data); - if (reward.isValid) - this._rewards.push(reward); - } - - /** - * Deletes Task from Quest - * - * @param questId - */ - removeSubquest(questId) { - this._subquests = this._subquests.filter(id => id !== questId); - } - - /** - * Deletes Task from Quest - * - * @param index - */ - removeTask(index) { - if (this._tasks[index] !== undefined) - this._tasks.splice(index, 1); - } - - /** - * Deletes Reward from Quest - * - * @param index - */ - removeReward(index) { - if (this._rewards[index] !== undefined) - this._rewards.splice(index, 1); - } - - /** - * Toggles visibility of Reward - * - * @param index - * @returns {Promise} - */ - async toggleReward(index) { - if (this._rewards[index] !== undefined) - return await this._rewards[index].toggleVisible(); - } - - /** - * Toggles visibility of Task - * - * @param index - * @returns {Promise} - */ - async toggleTask(index) { - if (this._tasks[index] !== undefined) - return await this._tasks[index].toggleVisible(); - } - - /** - * Toggles Actor image between sheet's and token's images - */ - async toggleImage() { - if (this._image === 'actor') { - this._image = 'token'; - } else { - this._image = 'actor'; - } - } - - /** - * Refreshes data without need of destroying and reinstantiating Quest object - */ - refresh() { - let entry = game.journal.get(this._id); - let content = Quest.getContent(entry); - - this.initData(content); - } - - /** - * Toggles quest between Public and Personal. In both cases, it hides the quest from everyone. - * If new status is public, then hide it. - * - * @returns {Promise} - */ - async togglePersonal() { - this._personal = !this._personal; - this.entryPermission = {default: 0}; - if (this._personal === false) { - this.status = 'hidden'; - } - } - - /** - * Saves new permissions for users. Used by Personal Quests feature. - * - * @param userId - * @param permission - * @returns {Promise} - */ - async savePermission(userId, permission) { - if ([ - CONST.ENTITY_PERMISSIONS.OWNER, - CONST.ENTITY_PERMISSIONS.OBSERVER, - CONST.ENTITY_PERMISSIONS.NONE - ].includes(permission) === false) return; - - let entryData = duplicate(game.journal.get(this._id)); - let permissionData; - - if (userId === '*') { - permissionData = entryData.permission - } else { - permissionData = {[userId]: userId}; - } - - for (let p in permissionData) { - if (this.personal && p === 'default') continue; - if (p !== 'default') { - let user = game.users.get(p); - if (user === null) { - delete entryData.permission[p]; - continue; - } - } - - if (permission === CONST.ENTITY_PERMISSIONS.NONE && p !== 'default') { - delete entryData.permission[p]; - } else { - entryData.permission[p] = permission; - } - } - - this.entryPermission = entryData.permission; - } - - sortRewards(event, data) { - const dt = event.target.closest('li.reward') || null; - const index = data.index; - let targetIdx = dt?.dataset.index; - - this.sortParts(index, targetIdx, this.rewards) - } - - sortTasks(event, data) { - const dt = event.target.closest('li.task') || null; - const index = data.index; - let targetIdx = dt?.dataset.index; - this.sortParts(index, targetIdx, this.tasks) - } - - sortParts(index, targetIdx, array) { - const entry = array.splice(index, 1)[0]; - if (targetIdx) { - if (index < targetIdx) targetIdx--; - array.splice(targetIdx, 0, entry); - } else { - array.push(entry); - } - this.save().then(() => Socket.refreshQuestPreview(this.id)); - } - - /** - * Saves Quest to JournalEntry's content, and if needed, moves JournalEntry to different folder. - * Can also update JournalEntry's permissions. - * - * @returns {Promise} - */ - async save() { - if (this._populated) { - throw new Error(`Can't save populated Quest (${this._id})`); - } - - let update = { - content: JSON.stringify(this) - }; - if (this.entryPermission !== undefined) { - update.permission = this.entryPermission; - } - - let entry = game.journal.get(this._id); - await entry.update(update, {diff: false}); - } - - static get(questId) { - let entry = game.journal.get(questId); - if (!entry) return undefined; - let content = this.getContent(entry); - content.permission = entry.permission; - - if (entry.permission < 2) return undefined; - - return new Quest(content, entry); - } - - /** - * Populates content with a lot of additional data, that doesn't necessarily have to be saved - * with Quest itself, such as Actor's data. - * - * This method also performs content manipulation, for example enriching HTML or calculating amount - * of done/total tasks etc. - * - * Be advised, that even if `content` parameter is Quest object, after populating it cannot be saved. - * If you need to keep Quest instance to be edited and saved, duplicate() it and populate copy. - * - * @param content - * @param entry - * @returns {*} - */ - static populate(content, entry = undefined) { - // let actor = Utils.findActor(content.actor); - let isGM = game.user.isGM; - let canPlayerDrag = game.settings.get("forien-quest-log", "allowPlayersDrag"); - let countHidden = game.settings.get("forien-quest-log", "countHidden"); - - if (content.giver) { - if (content.giver === 'abstract') { - content.giver = { - name: content.giverName, - img: content.image - }; - content.image = undefined; - } else { - fromUuid(content.giver).then((entity) => { - if (entity === null) { - content.giver = false; - return; - } - content.giver = duplicate(entity); - - switch (entity.entity) { - case Actor.entity: - if (content.image === 'token') - content.giver.img = entity.data.token.img; - break; - case Item.entity: - case JournalEntry.entity: - break; - default: - content.giver = false; - } - }); - } - } - - content.isSubquest = false; - if (content.parent !== null) { - content.isSubquest = true; - content.parent = Quest.get(content.parent); - } - content.statusLabel = game.i18n.localize(`ForienQuestLog.QuestTypes.Labels.${content.status}`); - - if (countHidden) { - content.checkedTasks = content.tasks.filter(t => t.completed).length; - content.totalTasks = content.tasks.length; - } else { - content.checkedTasks = content.tasks.filter(t => t.hidden === false && t.completed).length; - content.totalTasks = content.tasks.filter(t => t.hidden === false).length; - } - - if (content.rewards === undefined) { - content.rewards = []; - } - - content.tasks = content.tasks.map((t) => { - let task = new Task(t); - task.name = TextEditor.enrichHTML(task.name); - return task; - }); - - content.noRewards = (content.rewards.length === 0); - content.rewards.forEach((item) => { - item.transfer = JSON.stringify(item.data); - item.type = item.type.toLowerCase(); - item.draggable = ((isGM || canPlayerDrag) && item.type !== 'abstract'); - }); - content.subquests = (content.subquests !== undefined) - ? content.subquests.map(questId => Quest.get(questId)) - : []; - - if (entry) - content.playerEdit = Object.values(entry.data.permission).some(p => p === 3); - - - if (!(isGM || content.playerEdit)) { - content.description = TextEditor.enrichHTML(content.description); - content.tasks = content.tasks.filter(t => t.hidden === false); - content.rewards = content.rewards.filter(r => r.hidden === false); - } - - if (entry) { - if (isGM && content.personal) { - let users = [`${game.i18n.localize('ForienQuestLog.Tooltips.PersonalQuestVisibleFor')}:`]; - - for (let perm in entry.data.permission) { - if (perm === 'default') continue; - if (entry.data.permission[perm] >= 2) { - let user = game.users.get(perm); - users.push(user.name); - } - } - - if (users.length > 1) { - content.users = users.join('\r'); - } else { - content.users = game.i18n.localize('ForienQuestLog.Tooltips.PersonalQuestButNoPlayers'); - } - } - } - - return content; - } - - - /** - * Retrieves JournalEntry's content (which is Quest's data) and optionally populates it. - * - * @see populate() - * - * @param entry - * @param populate - * @returns {*} - */ - static getContent(entry, populate = false) { - let content = entry.data.content; - - try { - content = JSON.parse(content); - content.id = entry._id; - } catch (e) { - console.log(`${constants.moduleLabel} | Quest Folder contains invalid entry. The "${entry.data.name}" is either corrupted Quest Entry, or non-Quest Journal Entry.`); - console.error(e); - return null; - } - - if (populate) - content = this.populate(content, entry); - - return content; - } - - /** - * Retrieves all Quests, grouped by folders. - * - * @param sortTarget sort by - * @param sortDirection sort direction - * @param availableTab true if Available tab is visible - * @param populate - * @returns {{}} - */ - static getQuests(sortTarget = undefined, sortDirection = 'asc', availableTab = false, populate = false) { - let folder = QuestFolder.get(); - let entries = []; - - folder.content.forEach(entry => { - let content = this.getContent(entry, populate); - if (content) entries.push(content); - }); - - if (sortTarget !== undefined) { - entries = this.sort(entries, sortTarget, sortDirection) - } - - const quests = { - available: entries.filter(e => e.status === 'available' && e.parent == null), - active: entries.filter(e => e.status === 'active'), - completed: entries.filter(e => e.status === 'completed' && e.parent == null), - failed: entries.filter(e => e.status === 'failed' && e.parent == null), - hidden: entries.filter(e => e.status === 'hidden' && e.parent == null) - }; - - if (!availableTab) { - quests.hidden = [...quests.available, ...quests.hidden]; - quests.hidden = this.sort(quests.hidden, sortTarget, sortDirection) - } - - return quests; - } - - /** - * Returns localization strings for quest types (statuses) - * - * @returns {{hidden: string, available: string, active: string, completed: string, failed: string}} - */ - static getQuestTypes() { - return { - active: "ForienQuestLog.QuestTypes.InProgress", - completed: "ForienQuestLog.QuestTypes.Completed", - failed: "ForienQuestLog.QuestTypes.Failed", - hidden: "ForienQuestLog.QuestTypes.Hidden", - available: "ForienQuestLog.QuestLog.Tabs.Available" - } - } - - /** - * Sort function to sort quests. - * - * @see getQuests() - * - * @param entries - * @param sortTarget - * @param sortDirection - * @returns -1 | 0 | 1 - */ - static sort(entries, sortTarget, sortDirection) { - return entries.sort((a, b) => { - let targetA; - let targetB; - - if (sortTarget === 'actor') { - targetA = (a.actor) ? (a.actor.name || 'ZZZZZ') : 'ZZZZZ'; - targetB = (b.actor) ? (b.actor.name || 'ZZZZZ') : 'ZZZZZ'; - } else { - targetA = a[sortTarget]; - targetB = b[sortTarget]; - } - - if (sortDirection === 'asc') - return (targetA < targetB) ? -1 : (targetA > targetB) ? 1 : 0; - - return (targetA > targetB) ? -1 : (targetA < targetB) ? 1 : 0; - }); - } - - /** - * Moves Quest (and Journal Entry) to different Folder and updates permissions if needed. - * - * @param questId - * @param target - * @param permission - * @returns {Promise} - */ - static async move(questId, target, permission = undefined) { - let journal = game.journal.get(questId); - let quest = this.getContent(journal); - if (permission === undefined) { - permission = journal.data.permission; - } - - if (!quest.personal) { - if (permission.default < CONST.ENTITY_PERMISSIONS.OWNER) { - if (target === 'hidden') - permission = {default: CONST.ENTITY_PERMISSIONS.NONE}; - else - permission = {default: CONST.ENTITY_PERMISSIONS.OBSERVER}; - } - } - - let content = Quest.getContent(journal); - content.status = target; - content = JSON.stringify(content); - - return journal.update({content: content, "permission": permission}).then(() => { - Socket.refreshQuestLog(); - Socket.refreshQuestPreview(questId); - let dirname = game.i18n.localize(this.getQuestTypes()[target]); - ui.notifications.info(game.i18n.format("ForienQuestLog.Notifications.QuestMoved", {target: dirname}), {}); - }); - } - - /** - * Calls a delete quest dialog. - * - * @param questId - * @param parentId - * @returns {Promise} - */ - static async delete(questId, parentId = null) { - let entry = this.get(questId); - - new Dialog({ - title: game.i18n.format("ForienQuestLog.DeleteDialog.Title", entry.name), - content: `

    ${game.i18n.localize("ForienQuestLog.DeleteDialog.Header")}

    ` + - `

    ${game.i18n.localize("ForienQuestLog.DeleteDialog.Body")}

    `, - buttons: { - yes: { - icon: '', - label: game.i18n.localize("ForienQuestLog.DeleteDialog.Delete"), - callback: () => this.deleteConfirm(questId, parentId) - }, - no: { - icon: '', - label: game.i18n.localize("ForienQuestLog.DeleteDialog.Cancel") - } - }, - default: 'yes' - }).render(true); - } - - /** - * Called when user confirms the delete. - * Deletes the Quest by deleting related JournalEntry. - * - * @param questId - * @param parentId - * @returns {Promise} - */ - static async deleteConfirm(questId, parentId = null) { - let entry = game.journal.get(questId); - - if (parentId !== null) { - let quest = Quests.get(parentId); - quest.removeSubquest(questId); - await quest.save(); - Socket.refreshQuestPreview(parentId); - } - - entry.delete().then(() => { - Socket.refreshQuestLog(); - Socket.closeQuest(questId); - }); - } - - get id() { - return this._id; - } - - set id(value) { - this._id = value; - } - - get giver() { - return this._giver; - } - - set giver(value) { - this._giver = value; - } - - get title() { - return this._title; - } - - set title(value) { - this._title = value; - } - - get description() { - return this._description; - } - - set description(value) { - this._description = value; - } - - get gmnotes() { - return this._gmnotes; - } - - set gmnotes(value) { - this._gmnotes = value; - } - - get subquests() { - return this._subquests; - } - - get tasks() { - return this._tasks; - } - - get rewards() { - return this._rewards; - } - - set image(image) { - if (image === 'actor' || image === 'token') - this._image = image; - } - - get image() { - return this._image; - } - - set splash(splash) { - this._splash = splash; - } - - get splash() { - return this._splash; - } - - get personal() { - return this._personal; - } - - set personal(value) { - this._personal = (value === true); - } - - get status() { - return this._status; - } - - set status(value) { - this._status = value; - } - - get parent() { - return this._parent; - } - - set parent(value) { - this._parent = value; - } - - static get collection() { - return QuestsCollection; - } - - get giverName() { - return this._giverName; - } - - set giverName(value) { - this._giverName = value; - } - - get name() { - return this._title; - } - - get permission() { - return this._permission; - } - - toJSON() { - return { - giver: this._giver, - title: this._title, - status: this._status, - description: this._description, - gmnotes: this._gmnotes, - personal: this._personal, - image: this._image, - giverName: this._giverName, - splash: this._splash, - parent: this._parent, - subquests: this._subquests, - tasks: this._tasks, - rewards: this._rewards - } - } -} - -window.Quest = Quest; \ No newline at end of file diff --git a/modules/entities/reward.mjs b/modules/entities/reward.mjs deleted file mode 100644 index 15014024..00000000 --- a/modules/entities/reward.mjs +++ /dev/null @@ -1,60 +0,0 @@ -export default class Reward { - constructor(data = {}) { - this._type = data.type || null; - this._data = data.data || {}; - this._hidden = data.hidden || false; - } - - get type() { - return this._type; - } - - set type(type) { - this._type = type; - } - - get data() { - return this._data; - } - - set data(data) { - this._data = data; - } - - get isValid() { - return (this._type !== null) - } - - get hidden() { - return this._hidden; - } - - set hidden(value) { - this._hidden = value; - } - - async toggleVisible() { - this._hidden = !this._hidden; - - return this._hidden; - } - - static create(data = {}) { - if (data.type === undefined) { - throw new Error(game.i18n.localize("ForienQuestLog.Api.reward.create.type")); - } - if (data.data === undefined || data.data.name === undefined || data.data.img === undefined) { - throw new Error(game.i18n.localize("ForienQuestLog.Api.reward.create.data")); - } - - return new Reward(data); - } - - toJSON() { - return { - type: this._type, - data: this._data, - hidden: this._hidden - } - } -} diff --git a/modules/entities/task.mjs b/modules/entities/task.mjs deleted file mode 100644 index c77a6d67..00000000 --- a/modules/entities/task.mjs +++ /dev/null @@ -1,87 +0,0 @@ -export default class Task { - constructor(data = {}) { - this._name = data.name || null; - this._completed = data.completed || false; - this._failed = data.failed || false; - this._hidden = data.hidden || false; - } - - toggle() { - if (this._completed === false && this._failed === false) { - this._completed = true; - } else if (this._completed === true) { - this._failed = true; - this._completed = false; - } else { - this._failed = false; - } - } - - get state() { - if (this._completed) { - return 'check-square'; - } else if (this._failed) { - return 'minus-square'; - } - return 'square'; - } - - get name() { - return this._name; - } - - set name(name) { - this._name = name; - } - - get completed() { - return this._completed; - } - - set completed(completed) { - this._completed = (completed === true); - } - - get failed() { - return this._failed; - } - - set failed(failed) { - this._failed = (failed === true); - } - - get hidden() { - return this._hidden; - } - - set hidden(hidden) { - this._hidden = (hidden === true); - } - - async toggleVisible() { - this._hidden = !this._hidden; - - return this._hidden; - } - - get isValid() { - return (this._name.length) - } - - static create(data = {}) { - if (data.name === undefined) { - throw new Error(game.i18n.localize("ForienQuestLog.Api.task.create.name")); - } - - return new Task(data); - } - - toJSON() { - return { - name: this._name, - completed: this._completed, - failed: this._failed, - hidden: this._hidden - } - } -} diff --git a/modules/init.mjs b/modules/init.mjs deleted file mode 100644 index 1d5a9ba9..00000000 --- a/modules/init.mjs +++ /dev/null @@ -1,66 +0,0 @@ -import registerApiHooks from "./api/hooks.js"; -import QuestApi from "./api/quest-api.mjs"; -import QuestLogClass from "./apps/quest-log.mjs"; -import QuestFolder from "./entities/quest-folder.mjs"; -import ModuleSettings from "./utility/config.mjs"; -import Socket from "./utility/socket.mjs"; -import Utils from "./utility/utils.mjs"; -import Quest from "./entities/quest.mjs"; -import QuestsCollection from "./entities/collection/quests-collection.mjs"; - - -Hooks.once('init', () => { - ModuleSettings.register(); - - CONST.ENTITY_TYPES?.push("Quest"); - CONST.ENTITY_LINK_TYPES?.push("Quest"); - CONFIG["Quest"] = { - entityClass: Quest, - collection: QuestsCollection, - sidebarIcon: 'far fa-question-circle', - }; - - Utils.preloadTemplates(); - Utils.registerHandlebarsHelpers(); - - Hooks.callAll("ForienQuestLog.afterInit"); -}); - -Hooks.once('setup', () => { - window.Quests = QuestApi; - window.QuestLog = new QuestLogClass(); - game.questPreview = {}; - - Hooks.callAll("ForienQuestLog.afterSetup"); -}); - -Hooks.once("ready", () => { - QuestFolder.initializeJournals(); - registerApiHooks(); - - // Allow and process incoming socket data - Socket.listen(); - - Hooks.callAll("ForienQuestLog.afterReady"); -}); - -Hooks.on("renderJournalDirectory", (app, html, data) => { - const button = $(``); - let footer = html.find(".directory-footer"); - if (footer.length === 0) { - footer = $(`
    `); - html.append(footer); - } - footer.append(button); - - button.click(ev => { - QuestLog.render(true) - }); - - if (!(game.user.isGM && game.settings.get('forien-quest-log', 'showFolder'))) { - let folderId = QuestFolder.get('root')._id; - let folder = html.find(`.folder[data-folder-id="${folderId}"]`); - - folder.remove(); - } -}); diff --git a/modules/utility/config.mjs b/modules/utility/config.mjs deleted file mode 100644 index 9c91d2ad..00000000 --- a/modules/utility/config.mjs +++ /dev/null @@ -1,134 +0,0 @@ -export default class ModuleSettings { - /** - * Registers various configuration settings for Module - */ - static register() { - game.settings.register("forien-quest-log", "availableQuests", { - name: "ForienQuestLog.Settings.availableQuests.Enable", - hint: "ForienQuestLog.Settings.availableQuests.EnableHint", - scope: "world", - config: true, - default: false, - type: Boolean, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "allowPlayersDrag", { - name: "ForienQuestLog.Settings.allowPlayersDrag.Enable", - hint: "ForienQuestLog.Settings.allowPlayersDrag.EnableHint", - scope: "world", - config: true, - default: false, - type: Boolean, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "allowPlayersCreate", { - name: "ForienQuestLog.Settings.allowPlayersCreate.Enable", - hint: "ForienQuestLog.Settings.allowPlayersCreate.EnableHint", - scope: "world", - config: true, - default: false, - type: Boolean, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "allowPlayersAccept", { - name: "ForienQuestLog.Settings.allowPlayersAccept.Enable", - hint: "ForienQuestLog.Settings.allowPlayersAccept.EnableHint", - scope: "world", - config: true, - default: false, - type: Boolean, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "countHidden", { - name: "ForienQuestLog.Settings.countHidden.Enable", - hint: "ForienQuestLog.Settings.countHidden.EnableHint", - scope: "world", - config: true, - default: true, - type: Boolean, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "showTasks", { - name: "ForienQuestLog.Settings.showTasks.Enable", - hint: "ForienQuestLog.Settings.showTasks.EnableHint", - scope: "world", - config: true, - default: "default", - type: String, - choices: { - "default": "ForienQuestLog.Settings.showTasks.default", - "onlyCurrent": "ForienQuestLog.Settings.showTasks.onlyCurrent", - "no": "ForienQuestLog.Settings.showTasks.no" - }, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - - game.settings.register("forien-quest-log", "navStyle", { - name: "ForienQuestLog.Settings.navStyle.Enable", - hint: "ForienQuestLog.Settings.navStyle.EnableHint", - scope: "client", - config: true, - default: "bookmarks", - type: String, - choices: { - "bookmarks": "ForienQuestLog.Settings.navStyle.bookmarks", - "classic": "ForienQuestLog.Settings.navStyle.classic" - }, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); - -/* - game.settings.register("forien-quest-log", "titleAlign", { - name: "ForienQuestLog.Settings.titleAlign.Enable", - hint: "ForienQuestLog.Settings.titleAlign.EnableHint", - scope: "client", - config: true, - default: "left", - type: String, - choices: { - "left": "ForienQuestLog.Settings.titleAlign.left", - "center": "ForienQuestLog.Settings.titleAlign.center" - }, - onChange: value => { - if (QuestLog && QuestLog.rendered) - QuestLog.render(); - } - }); -*/ - game.settings.register("forien-quest-log", "showFolder", { - name: "ForienQuestLog.Settings.showFolder.Enable", - hint: "ForienQuestLog.Settings.showFolder.EnableHint", - scope: "world", - config: true, - default: false, - type: Boolean, - onChange: value => game.journal.render() - }); - } -} diff --git a/modules/utility/socket.mjs b/modules/utility/socket.mjs deleted file mode 100644 index 8032fd4c..00000000 --- a/modules/utility/socket.mjs +++ /dev/null @@ -1,106 +0,0 @@ -import QuestApi from "../api/quest-api.mjs"; -import Quest from "../entities/quest.mjs"; - -export default class Socket { - static refreshQuestLog() { - if (QuestLog.rendered) - QuestLog.render(true); - game.socket.emit("module.forien-quest-log", { - type: "questLogRefresh" - }) - } - - static refreshQuestPreview(questId) { - if (game.questPreview[questId] !== undefined) - game.questPreview[questId].render(true); - game.socket.emit("module.forien-quest-log", { - type: "questPreviewRefresh", - payload: { - questId: questId - } - }) - } - - static showQuestPreview(questId) { - game.socket.emit("module.forien-quest-log", { - type: "showQuestPreview", - payload: { - questId: questId - } - }) - } - - static userCantOpenQuest() { - game.socket.emit("module.forien-quest-log", { - type: "userCantOpenQuest", - payload: { - user: game.user.name - } - }) - } - - static acceptQuest(questId) { - game.socket.emit("module.forien-quest-log", { - type: "acceptQuest", - payload: { - questId: questId - } - }) - } - - static closeQuest(questId) { - if (game.questPreview[questId] !== undefined) - game.questPreview[questId].close(); - game.socket.emit("module.forien-quest-log", { - type: "closeQuest", - payload: { - questId: questId - } - }) - } - - static listen() { - game.socket.on("module.forien-quest-log", data => { - if (data.type === "questLogRefresh") { - if (QuestLog.rendered) - QuestLog.render(true); - return; - } - - if (data.type === "questPreviewRefresh") { - if (game.questPreview[data.payload.questId] !== undefined) - game.questPreview[data.payload.questId].render(true); - - if (QuestLog.rendered) - QuestLog.render(true); - - return; - } - - if (data.type === "showQuestPreview") { - QuestApi.open(data.payload.questId, false); - - return; - } - - if (data.type === "userCantOpenQuest") { - if (game.user.isGM) { - ui.notifications.warn(game.i18n.format("ForienQuestLog.Notifications.UserCantOpen", {user: data.payload.user}), {}); - } - - return; - } - - if (data.type === "acceptQuest") { - if (game.user.isGM) { - Quest.move(data.payload.questId, 'active'); - } - } - - if (data.type === "closeQuest") { - if (game.questPreview[data.payload.questId] !== undefined) - game.questPreview[data.payload.questId].close(); - } - }); - } -}; diff --git a/modules/utility/utils.mjs b/modules/utility/utils.mjs deleted file mode 100644 index 7cfc0671..00000000 --- a/modules/utility/utils.mjs +++ /dev/null @@ -1,41 +0,0 @@ -export default class Utils { - static findActor(actorId) { - let actor = game.actors.get(actorId); - if (actor === undefined || actor === null) { - actor = game.actors.find(a => a.name === actorId); - } - - if (actor === undefined || actor === null) { - return false; - } - - return actor; - } - - /** - * Preloads templates for partials - */ - static preloadTemplates() { - let templates = [ - "templates/partials/quest-log/tab.html", - "templates/partials/quest-preview/gmnotes.html", - "templates/partials/quest-preview/details.html", - "templates/partials/quest-preview/management.html" - ]; - - templates = templates.map(t => `modules/forien-quest-log/${t}`); - loadTemplates(templates); - } - - static registerHandlebarsHelpers() { - Handlebars.registerHelper('format', function(stringId, ...arrData) { - let objData; - if (typeof arrData[0] === 'object') - objData = arrData[0]; - else - objData = {...arrData}; - - return game.i18n.format(stringId, objData); - }); - } -}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..6ebff92f --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "forien-quest-log", + "description": "This package.json just provides a PostCSS build script for Sass to CSS", + "license": "MIT", + "private": true, + "type": "module", + "author": "Michael Leahy (https://github.com/typhonrt)", + "contributors": [ + "Michael Leahy (https://github.com/typhonrt)" + ], + "dependencies": { + "collect.js": "^4.36.0", + "dompurify": "^3.1.5" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.0", + "@rollup/plugin-node-resolve": "^15.2.0", + "@rollup/plugin-virtual": "^3.0.0", + "@rollup/plugin-terser": "^0.4.0", + "@types/jquery": "^3.5.29", + "@typhonjs-config/eslint-config": "^0.6.0", + "@typhonjs-fvtt/eslint-config-foundry.js": "^0.8.0", + "autoprefixer": "^10.4.19", + "cssnano": "^7.0.4", + "eslint": "^8.42.0", + "postcss": "^8.4.39", + "postcss-cli": "^11.0.0", + "postcss-preset-env": "^9.6.0", + "rollup": "^4.18.0", + "sass": "^1.77.0" + }, + "browserslist": [">5%", "not IE 11"], + "scripts": { + "build-css": "sass ./styles/init.scss ./css/init.css && postcss ./css/init.css -p sass -u autoprefixer postcss-preset-env cssnano -m -o ./css/init.css", + "eslint": "eslint .", + "rollup-external": "rollup --config" + } +} diff --git a/packs/macro-gm/000046.ldb b/packs/macro-gm/000046.ldb new file mode 100644 index 00000000..4dd04cb7 Binary files /dev/null and b/packs/macro-gm/000046.ldb differ diff --git a/packs/macro-gm/000050.log b/packs/macro-gm/000050.log new file mode 100644 index 00000000..e69de29b diff --git a/packs/macro-gm/CURRENT b/packs/macro-gm/CURRENT new file mode 100644 index 00000000..a980eefb --- /dev/null +++ b/packs/macro-gm/CURRENT @@ -0,0 +1 @@ +MANIFEST-000049 diff --git a/packs/macro-gm/LOCK b/packs/macro-gm/LOCK new file mode 100644 index 00000000..e69de29b diff --git a/packs/macro-gm/LOG b/packs/macro-gm/LOG new file mode 100644 index 00000000..077484b5 --- /dev/null +++ b/packs/macro-gm/LOG @@ -0,0 +1,3 @@ +2024/07/04-12:43:50.350 597c Recovering log #48 +2024/07/04-12:43:50.355 597c Delete type=0 #48 +2024/07/04-12:43:50.355 597c Delete type=3 #47 diff --git a/packs/macro-gm/LOG.old b/packs/macro-gm/LOG.old new file mode 100644 index 00000000..9468d2b7 --- /dev/null +++ b/packs/macro-gm/LOG.old @@ -0,0 +1,3 @@ +2024/07/02-19:31:19.293 10f8 Recovering log #44 +2024/07/02-19:31:19.306 10f8 Delete type=0 #44 +2024/07/02-19:31:19.306 10f8 Delete type=3 #42 diff --git a/packs/macro-gm/MANIFEST-000049 b/packs/macro-gm/MANIFEST-000049 new file mode 100644 index 00000000..5ad5a312 Binary files /dev/null and b/packs/macro-gm/MANIFEST-000049 differ diff --git a/packs/macro-player/000046.ldb b/packs/macro-player/000046.ldb new file mode 100644 index 00000000..a222a2f0 Binary files /dev/null and b/packs/macro-player/000046.ldb differ diff --git a/packs/macro-player/000050.log b/packs/macro-player/000050.log new file mode 100644 index 00000000..e69de29b diff --git a/packs/macro-player/CURRENT b/packs/macro-player/CURRENT new file mode 100644 index 00000000..a980eefb --- /dev/null +++ b/packs/macro-player/CURRENT @@ -0,0 +1 @@ +MANIFEST-000049 diff --git a/packs/macro-player/LOCK b/packs/macro-player/LOCK new file mode 100644 index 00000000..e69de29b diff --git a/packs/macro-player/LOG b/packs/macro-player/LOG new file mode 100644 index 00000000..ecf9b019 --- /dev/null +++ b/packs/macro-player/LOG @@ -0,0 +1,3 @@ +2024/07/04-12:43:50.357 1eac Recovering log #48 +2024/07/04-12:43:50.364 1eac Delete type=0 #48 +2024/07/04-12:43:50.364 1eac Delete type=3 #47 diff --git a/packs/macro-player/LOG.old b/packs/macro-player/LOG.old new file mode 100644 index 00000000..214c2bc1 --- /dev/null +++ b/packs/macro-player/LOG.old @@ -0,0 +1,3 @@ +2024/07/02-19:31:19.309 4958 Recovering log #44 +2024/07/02-19:31:19.321 4958 Delete type=0 #44 +2024/07/02-19:31:19.321 4958 Delete type=3 #42 diff --git a/packs/macro-player/MANIFEST-000049 b/packs/macro-player/MANIFEST-000049 new file mode 100644 index 00000000..25cd56bb Binary files /dev/null and b/packs/macro-player/MANIFEST-000049 differ diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..6c491688 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,68 @@ +import path from 'node:path'; + +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; // Terser is used for minification / mangling +import virtual from '@rollup/plugin-virtual'; + +// Terser config; refer to respective documentation for more information. +const terserConfig = { + compress: { passes: 3 }, + mangle: { toplevel: true, keep_classnames: true, keep_fnames: true }, + ecma: 2021, + module: true +}; + +// The deploy path for the server bundle which includes the common code. +const s_DEPLOY_PATH = './external'; + +const s_DEPLOY_MINIFY = true; + +// Produce sourcemaps or not +const s_SOURCEMAP = true; + +// Defines potential output plugins to use conditionally if the .env file indicates the bundles should be +// minified / mangled. +const outputPlugins = []; +if (s_DEPLOY_MINIFY) +{ + outputPlugins.push(terser(terserConfig)); +} + +export default () => +{ + return [ + { + input: 'pack', + output: [{ + file: `${s_DEPLOY_PATH}${path.sep}collect.js`, + format: 'es', + plugins: outputPlugins, + generatedCode: { constBindings: true }, + sourcemap: s_SOURCEMAP, + }], + plugins: [ + virtual({ + pack: `export { collect as default } from './node_modules/collect.js/src/index.js';` + }), + resolve({ browser: true }), + commonjs() + ] + }, + { + input: 'pack', + output: [{ + file: `${s_DEPLOY_PATH}${path.sep}DOMPurify.js`, + format: 'es', + plugins: outputPlugins, + generatedCode: { constBindings: true }, + sourcemap: s_SOURCEMAP, + }], + plugins: [ + virtual({ + pack: `export { default } from './node_modules/dompurify/dist/purify.es.mjs';` + }) + ] + } + ]; +}; diff --git a/scripts/prompt.js b/scripts/prompt.js deleted file mode 100644 index ec6b8152..00000000 --- a/scripts/prompt.js +++ /dev/null @@ -1,154 +0,0 @@ -(() => { - const module = "Forien's Quest Log"; - const author = "Forien"; - const message = "

    Thank you for downloading our modules! We are implementing a new, unified Welcome Screen to contain information for any/all of our Foundry Workshop modules.

    We strongly recommend you install it so that you are updated and notified about new versions. The new Welcome Screen is highly customizable and offers several different display options.

    "; - const messageEnable = "You have installed Foundry Workshop Welcome Screen. Do you want to enable it now?"; - const disclaimer = "Clicking 'Install' will download the 'Foundry Workshop Welcome Screen' module and install it into your Foundry instance. It will also send you back to the setup screen where you will need to re-launch your world."; - const ending = "Sincerely,"; - const manifest = 'https://raw.githubusercontent.com/Foundry-Workshop/welcome-screen/master/module.json'; - const wsID = 'workshop-welcome-screen'; - - let testSetup = async () => { - let response = {}; - try { - response = await fetch(SetupConfiguration.setupURL, { - method: "POST", - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({}) - }); - } catch (e) { - return false; - } - - return response.status !== 403; - }; - - let tryInstall = async () => { - let test = await testSetup(); - - if (test === true) { - ui.notifications.active = []; - ui.notifications.info("Preparing to download module…", {permanent: true}); - const notif = ui.notifications.active[0]; - game.socket.on("progress", data => { - notif.html(data.msg); - }); - await SetupConfiguration.installPackage({type: "module", manifest: manifest}); - await game.shutDown(); - } else { - new Dialog({ - title: `Foundry is protected`, - content: `

    Your Foundry VTT instance's setup is password protected (which is good!). Because of it, you need to 'back to setup' and install the module manually, or come back after you login as an administrator.

    Click on the link below to copy it:

    `, - buttons: { - shutdown: { - label: "Back to Setup", - callback: () => { - game.shutDown(); - } - }, - close: { - label: "Close", - } - } - })._render(true).then(() => { - document.getElementById("workshop-welcome-screen-manifest").onclick = function () { - this.select(); - document.execCommand('copy'); - ui.notifications.info("Manifest URL copied!", {}); - } - }); - } - }; - - let installPrompt = () => { - if (window.workshopWS.app) return; - game.settings.register(wsID, 'showPrompt', {scope: "client", config: false, default: true}); - if (!game.settings.get(wsID, 'showPrompt')) return; - - let authors = Object.keys(window.workshopWS.authors).join(' and '); - let modules = window.workshopWS.modules.map(m => `
  • ${m}
  • `).join(''); - - window.workshopWS.app = new Dialog({ - title: `Install Welcome Screen for Foundry Workshop's modules?`, - content: `${message}
    Installed modules:
      ${modules}

    ${ending}
    ${authors}

    ${disclaimer}

    `, - buttons: { - cancel: { - label: "No" - }, - never: { - label: "Never show again", - callback: () => { - game.settings.set(wsID, 'showPrompt', false) - } - }, - install: { - label: "Install", - callback: () => { - tryInstall() - } - } - }, - default: 'install' - }, - {id: `${wsID}-install-prompt`, width: 420, height: 540}); - window.workshopWS.app.render(true); - }; - - let enablePrompt = () => { - if (window.workshopWS.app) return; - game.settings.register(wsID, 'showPrompt', {scope: "client", config: false, default: true}); - if (!game.settings.get(wsID, 'showPrompt')) return; - - window.workshopWS.app = new Dialog({ - title: `Enable Welcome Screen?`, - content: `

    ${messageEnable}

    `, - buttons: { - cancel: { - label: "No" - }, - never: { - label: "Never show again", - callback: () => { - game.settings.set(wsID, 'showPrompt', false) - } - }, - install: { - label: "Enable", - callback: () => { - const settings = game.settings.get("core", ModuleManagement.CONFIG_SETTING); - const setting = mergeObject(settings, {[wsID]: true}); - game.settings.set("core", ModuleManagement.CONFIG_SETTING, setting); - } - } - }, - default: 'install' - }); - window.workshopWS.app.render(true); - }; - - Hooks.on("init", () => { - if (window.workshopWS === undefined) { - window.workshopWS = { - modules: [module], - authors: { - [author]: true - }, - app: undefined - }; - } else { - window.workshopWS.modules.push(module); - window.workshopWS.authors[author] = true; - } - }); - - Hooks.on("ready", () => { - if (!game.user.isGM) return; - - const ws = game.modules.get(wsID); - if (ws === undefined) { - installPrompt(); - } else if (!ws.active) { - enablePrompt(); - } - }); -})(); \ No newline at end of file diff --git a/src/control/FQLHooks.js b/src/control/FQLHooks.js new file mode 100644 index 00000000..e1ba64f7 --- /dev/null +++ b/src/control/FQLHooks.js @@ -0,0 +1,555 @@ +import { + FoundryUIManager, + FVTTCompat, + ModuleSettings, + QuestDB, + Socket, + ViewManager, + Utils } from './index.js'; + +import { QuestAPI } from './public/index.js'; + +import { Quest } from '../model/index.js'; + +import { QuestPreview } from '../view/index.js'; + +import { DBMigration } from '../../database/DBMigration.js'; + +import { + constants, + sessionConstants, + settings } from '../model/constants.js'; + +/** + * Provides implementations for all Foundry hooks that FQL responds to and registers under. Please view the + * {@link QuestDB} documentation for hooks that it fires in the QuestDB lifecycle. + * + * Foundry lifecycle: + * - `init` - {@link FQLHooks.foundryInit} + * - `ready` - {@link FQLHooks.foundryReady} - A hook `ForienQuestLog.Lifecycle.ready` is fired after FQL is ready. + * - `setup` - {@link FQLHooks.foundrySetup} + * + * Foundry game hooks: + * - `collapseSidebar` - {@link FoundryUIManager.collapseSidebar} - Handle tracking state of the sidebar. + * - `dropActorSheetData` - {@link FQLHooks.dropActorSheetData} - Handle drop data for reward items in actor sheet. + * - `dropCanvasData` - {@link FQLHooks.dropCanvasData} - Handle drop data for {@link Quest} on Foundry canvas. + * - `getSceneControlButtons` - {@link FQLHooks.getSceneControlButtons} - Add FQL scene controls to 'note'. + * - `hotbarDrop` - {@link FQLHooks.hotbarDrop} - Handle {@link Quest} drops to the macro hotbar. + * - `renderJournalDirectory` - {@link FQLHooks.renderJournalDirectory} - Add 'open quest log' / show FQL folder. + * - `renderJournalSheet` - {@link FQLHooks.renderJournalSheet} - Hide FQL directory from journal sheet option items. + * + * FQL hooks (response): + * - `ForienQuestLog.Open.QuestLog` - {@link FQLHooks.openQuestLog} - Open the quest log. + * - `ForienQuestLog.Open.QuestTracker` - {@link FQLHooks.openQuestTracker} - Open the quest tracker. + * - `ForienQuestLog.Run.DBMigration` - {@link FQLHooks.runDBMigration} - Allow GMs to run the DBMigration manually. + * + * FQL hooks (called): + * - `ForienQuestLog.Lifecycle.ready` - {@link FQLHooks.foundryReady} - Called at the end of the `ready` hook when FQL + * is fully setup. + */ +export class FQLHooks +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated'); + } + + /** + * Initializes all hooks that FQL responds to in the Foundry lifecycle and in game hooks. + */ + static init() + { + // Foundry startup hooks. + Hooks.once('init', FQLHooks.foundryInit); + Hooks.once('ready', FQLHooks.foundryReady); + Hooks.once('setup', FQLHooks.foundrySetup); + + // Respond to Foundry in game hooks. + Hooks.on('dropActorSheetData', FQLHooks.dropActorSheetData); + Hooks.on('dropCanvasData', FQLHooks.dropCanvasData); + Hooks.on('getSceneControlButtons', FQLHooks.getSceneControlButtons); + Hooks.on('hotbarDrop', FQLHooks.hotbarDrop); + Hooks.on('renderJournalDirectory', FQLHooks.renderJournalDirectory); + Hooks.on('renderJournalSheet', FQLHooks.renderJournalSheet); + + // FQL specific hooks. + Hooks.on('ForienQuestLog.Open.QuestLog', FQLHooks.openQuestLog); + Hooks.on('ForienQuestLog.Open.QuestTracker', FQLHooks.openQuestTracker); + Hooks.on('ForienQuestLog.Run.DBMigration', FQLHooks.runDBMigration); + } + + /** + * Responds to when a data drop occurs on an ActorSheet. If there is an {@link FQLDropData} instance attached by + * checking the `_fqlData.type` set to `reward` then process the reward item drop via {@link Socket.questRewardDrop} + * to remove the item from the associated quest. + * + * @param {Actor} actor - The Actor which received the data drop. + * + * @param {ActorSheet} sheet - The ActorSheet which received the data drop. + * + * @param {RewardDropData} data - Any data drop, but only handle RewardDropData. + * + * @returns {Promise} + * @see https://foundryvtt.com/api/classes/client.Actor.html + * @see https://foundryvtt.com/api/classes/client.ActorSheet.html + */ + static async dropActorSheetData(actor, sheet, data) + { + if (typeof data !== 'object' || data?._fqlData?.type !== 'reward') { return; } + + await Socket.questRewardDrop({ + actor: { id: actor.id, name: FVTTCompat.get(actor, 'name') }, + sheet: { id: sheet.id }, + data + }); + } + + /** + * Converts a Quest drop on the canvas type to `JournalEntry` if the quest exists in the QuestDB. + * + * @param {Canvas} foundryCanvas - The Foundry canvas. + * + * @param {object} data - Drop data for canvas. + */ + static dropCanvasData(foundryCanvas, data) + { + if (data.type === Quest.documentName && QuestDB.getQuest(data.id) !== void 0) + { + data.type = 'JournalEntry'; + data.uuid = `JournalEntry.${data.id}`; + } + } + + /** + * Provides FQL initialization during the `init` Foundry lifecycle hook. + */ + static foundryInit() + { + // Set the sheet to render quests. + Quest.setSheet(QuestPreview); + + // Register FQL module settings. + ModuleSettings.register(); + + // Preload Handlebars templates and register helpers. + Utils.preloadTemplates(); + Utils.registerHandlebarsHelpers(); + } + + /** + * Provides the remainder of FQL initialization during the `ready` Foundry lifecycle hook. + * + * @returns {Promise} + */ + static async foundryReady() + { + // Initialize all main GUI views. + ViewManager.init(); + + // Only attempt to run DB migration for GM. + if (game.user.isGM) { await DBMigration.migrate(); } + + // Initialize the in-memory QuestDB. Loads all quests that the user can see at this point. + await QuestDB.init(); + + // Allow and process incoming socket data. + Socket.listen(); + + // Start watching sidebar updates. + FoundryUIManager.init(); + + // Need to track any current primary quest as Foundry settings don't provide a old / new state on setting + // change. The current primary quest state is saved in session storage. + sessionStorage.setItem(sessionConstants.currentPrimaryQuest, + game.settings.get(constants.moduleName, settings.primaryQuest)); + + // Must set initial session storage state for quest tracker background if it doesn't exist. + const showBackgroundState = sessionStorage.getItem(sessionConstants.trackerShowBackground); + if (showBackgroundState !== 'true' && showBackgroundState !== 'false') + { + sessionStorage.setItem(sessionConstants.trackerShowBackground, 'true'); + } + + // Initialize current client based macro images based on current state. + await Utils.setMacroImage([settings.questTrackerEnable, settings.questTrackerResizable]); + + // Show quest tracker if applicable. + ViewManager.renderOrCloseQuestTracker(); + + // Fire our own lifecycle event to inform any other modules that depend on FQL QuestDB. + Hooks.callAll('ForienQuestLog.Lifecycle.ready'); + } + + /** + * Provides the setup FQL initialization during the `setup` Foundry lifecycle hook. Make the public QuestAPI + * accessible from `game.modules('forien-quest-log').public.QuestAPI`. + */ + static foundrySetup() + { + const moduleData = Utils.getModuleData(); + + /** + * @type {FQLPublicAPI} + */ + moduleData.public = { + QuestAPI + }; + + // Freeze the public API so it can't be modified. + Object.freeze(moduleData.public); + } + + /** + * Responds to the in game hook `getSceneControlButtons` to add the FQL quest log and floating quest log to the + * journal / 'notes' tool as sub categories. These controls are always added for the GM, but if FQL is hidden from + * players based on module setting {@link FQLSettings.hideFQLFromPlayers} the note controls do not display. + * + * @param {SceneControl[]} controls - The scene controls to add FQL controls. + * + * @see noteControls + * @see https://foundryvtt.com/api/classes/client.SceneControls.html + */ + static getSceneControlButtons(controls) + { + if (game.user.isGM || !game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) + { + const notes = controls.find((c) => c.name === 'notes'); + if (notes) { notes.tools.push(...FoundryUIManager.noteControls); } + } + } + + /** + * Handles Quest data drops. Also handles setting image state of any macro dropped from the FQL macro compendiums. + * + * @param {object} data - The dropped data object. + * + * @param {number} slot - The target hotbar slot + * + * @returns {Promise} + */ + static async handleMacroHotbarDrop(data, slot) + { + const uuid = Utils.getUUID(data); + const document = await fromUuid(uuid); + + if (!document) { return; } + + const macroCommand = FVTTCompat.get(document, 'command'); + + const existingMacro = game.macros.contents.find((m) => + { + return (FVTTCompat.authorID(m) === game.user.id && FVTTCompat.get(m, 'command') === macroCommand); + }); + + let macro = existingMacro; + + // If there is no existing macro then create one. + if (!existingMacro) + { + const macroData = { + name: FVTTCompat.get(document, 'name'), + type: FVTTCompat.get(document, 'type'), + command: FVTTCompat.get(document, 'command'), + img: FVTTCompat.get(document, 'img'), + flags: FVTTCompat.get(document, 'flags') + }; + + macro = await Macro.create(macroData, { displaySheet: false }); + } + + // If the macro is from the FQL macro compendiums then update the image state. + if (macro) + { + const macroSetting = macro.getFlag(constants.moduleName, 'macro-setting'); + + if (macroSetting) { await Utils.setMacroImage(macroSetting); } + + await game.user.assignHotbarMacro(macro, slot); + } + } + + /** + * Handles creating a macro for a Quest drop on the hotbar. Uses existing macro if possible before creating a macro. + * + * @param {object} data - The dropped data object. + * + * @param {number} slot - The target hotbar slot + * + * @returns {Promise} + */ + static async handleQuestHotbarDrop(data, slot) + { + const questId = data.id; + + const quest = QuestDB.getQuest(questId); + + // Early out if Quest isn't in the QuestDB. + if (!quest) + { + throw new Error(game.i18n.localize('ForienQuestLog.API.Hooks.Notifications.NoQuest')); + } + + // The macro script data to open the quest via the public QuestAPI. + const command = `game.modules.get('${constants.moduleName}').public.QuestAPI.open({ questId: '${questId}' });`; + + const macroData = { + name: game.i18n.format('ForienQuestLog.API.Hooks.Labels.OpenMacro', { name: quest.name }), + type: 'script', + command + }; + + // Determine the image for the macro. Use the splash image if `splashAsIcon` is true otherwise the giver image. + macroData.img = quest.splashAsIcon && quest.splash.length ? quest.splash : quest?.giverData?.img; + + // Search for an already existing macro with the same command. + let macro = game.macros.contents.find((m) => (FVTTCompat.get(m, 'command') === command)); + + // If not found then create a new macro with the command. + if (!macro) + { + macro = await Macro.create(macroData, { displaySheet: false }); + } + + // Assign the macro to the hotbar. + await game.user.assignHotbarMacro(macro, slot); + } + + /** + * Two cases are handled. Because hooks can not be asynchronous an immediate value is returned that reflects whether + * the drop was handled or not. + * + * The first case is when an FQL macro is dropped in from a compendium. + * + * The second is when a quest is dropped into the macro hotbar. A new Quest open macro is created. The macro command + * invokes opening the {@link QuestPreview} via {@link QuestAPI.open} by quest ID. + * + * @param {Hotbar} hotbar - The Hotbar application instance. + * + * @param {object} data - The dropped data object. + * + * @param {number} slot - The target hotbar slot + * + * @returns {boolean} - Whether the callback was handled. + * @see https://foundryvtt.com/api/classes/client.Hotbar.html + */ + static hotbarDrop(hotbar, data, slot) + { + let handled = false; + + // Verify if the hotbar drop is data that is handled; either a quest or macro from FQL macro compendium. + if (data.type === Quest.documentName || FVTTCompat.isFQLMacroDataTransfer(data)) + { + handled = true; + } + + // Wrap the handling code in an async IIFE. If this is a Quest data drop or a macro from the FQL macro compendium + // pack then handle it. + (async () => + { + if (FVTTCompat.isFQLMacroDataTransfer(data)) + { + await FQLHooks.handleMacroHotbarDrop(data, slot); + } + + if (data.type === Quest.documentName) + { + await FQLHooks.handleQuestHotbarDrop(data, slot); + } + })(); + + // Immediately return the handled state in the hook callback. Foundry expects false to stop the callback change. + return !handled; + } + + /** + * Opens the QuestLog if the game user is a GM or if FQL isn't hidden to players by module setting + * {@link FQLSettings.hideFQLFromPlayers}. + * + * @param {object} [opts] - Optional parameters. + * + * @param {number|null} [opts.left] - The left offset position in pixels. + * + * @param {number|null} [opts.top] - The top offset position in pixels. + * + * @param {number|null} [opts.width] - The application width in pixels. + * + * @param {number|string|null} [opts.height] - The application height in pixels. + * + * @param {string} [opts.tabId] - The quest status tab to open. + */ + static openQuestLog(opts) + { + if (!game.user.isGM && game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) { return; } + + let constraints = {}; + + let tabId; + + if (typeof opts === 'object') + { + // Select only constraint related parameters. + constraints = (({ left, top, width, height }) => ({ left, top, width, height }))(opts); + + if (typeof opts.tabId === 'string') { tabId = opts.tabId; } + } + + ViewManager.questLog.render(true, { focus: true, ...constraints, tabId }); + } + + /** + * Opens the {@link QuestTracker} if the game user is a GM or if FQL isn't hidden to players by module setting + * {@link FQLSettings.hideFQLFromPlayers}. + * + * @param {object} [opts] - Optional parameters. + * + * @param {number|null} [opts.left] - The left offset position in pixels. + * + * @param {number|null} [opts.top] - The top offset position in pixels. + * + * @param {number|null} [opts.width] - The application width in pixels. + * + * @param {number|null} [opts.height] - The application height in pixels. + * + * @param {boolean} [opts.pinned] - Sets the pinned state. + * + * @param {boolean} [opts.primary] - Sets whether showing the primary quest is enabled. + * + * @param {boolean} [opts.resizable] - Sets the resizable state. + */ + static async openQuestTracker(opts) + { + if (!game.user.isGM && game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) { return; } + + await game.settings.set(constants.moduleName, settings.questTrackerEnable, true); + + if (typeof opts === 'object') + { + // Handle setting quest tracker primary change. + if (typeof opts.primary === 'boolean') + { + sessionStorage.setItem(sessionConstants.trackerShowPrimary, (opts.primary).toString()); + } + + // Select only constraint related parameters. + const constraints = (({ left, top, width, height, pinned }) => ({ left, top, width, height, pinned }))(opts); + + if (Object.keys(constraints).length > 0) + { + // Set to indicate an override of any pinned state. + constraints.override = true; + + const tracker = ViewManager.questTracker; + + // Defer to make sure quest tracker is rendered before setting position. + setTimeout(() => + { + if (tracker.rendered) { tracker.setPosition(constraints); } + }, 0); + } + + // Handle setting quest tracker resizable change. + if (typeof opts.resizable === 'boolean') + { + setTimeout(() => + { + game.settings.set(constants.moduleName, settings.questTrackerResizable, opts.resizable); + }, 0); + } + } + } + + /** + * Handles adding the 'open quest log' button at the bottom of the journal directory. Always displayed for the GM, + * but only displayed to players if FQL isn't hidden via module setting {@link FQLSettings.hideFQLFromPlayers}. + * + * For GMs if module setting {@link FQLSettings.showFolder} is true then the hidden `_fql_quests` folder is shown. + * + * @param {JournalDirectory} app - The JournalDirectory app. + * + * @param {JQuery} html - The jQuery element for the window content of the app. + * + * @see https://foundryvtt.com/api/classes/client.JournalDirectory.html + */ + static renderJournalDirectory(app, html) + { + if (game.user.isGM || !game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) + { + const button = $(``); + + let footer = html.find('.directory-footer'); + if (footer.length === 0) + { + footer = $(`
    `); + html.append(footer); + } + footer.append(button); + + button.click(() => + { + ViewManager.questLog.render(true); + }); + } + + if (!(game.user.isGM && game.settings.get(constants.moduleName, settings.showFolder))) + { + const folder = Utils.getQuestFolder(); + if (folder !== void 0) + { + const element = html.find(`.folder[data-folder-id="${folder.id}"]`); + if (element !== void 0) + { + element.remove(); + } + } + } + } + + /** + * Remove option item for quest journal folder when any journal entry is rendered. This prevents users from placing + * non-quest journals into the quest journal folder. + * + * @param {JournalSheet} app - The JournalSheet app. + * + * @param {JQuery} html - The jQuery element for the window content of the app. + * + * @see https://foundryvtt.com/api/classes/client.JournalSheet.html + */ + static renderJournalSheet(app, html) + { + const folder = Utils.getQuestFolder(); + if (folder) + { + const option = html.find(`option[value="${folder.id}"]`); + + if (option) { option.remove(); } + } + } + + /** + * Provides a GM only hook to manually run DB Migration. When schemaVersion is not provided + * {@link DBMigration.migrate} will perform migration loading this value from module settings. However, a specific + * migration schema version can be supplied to force DB migration. To run all migration provide `0` otherwise a + * specific schema version to start migration at which is below the max version. + * + * @param {number} [schemaVersion] - A valid schema version from 0 to {@link DBMigration.version} + * + * @returns {Promise} + */ + static async runDBMigration(schemaVersion = void 0) + { + // Only GMs can run the migration. + if (!game.user.isGM) { return; } + + await DBMigration.migrate(schemaVersion); + } +} + +/** + * @typedef {object} FQLPublicAPI - Exposes a few FQL classes and instances publicly. + * + * @property {QuestAPI} QuestAPI - QuestAPI class - Exposes static methods to interact with the quest system. + */ diff --git a/src/control/ModuleSettings.js b/src/control/ModuleSettings.js new file mode 100644 index 00000000..80f32d69 --- /dev/null +++ b/src/control/ModuleSettings.js @@ -0,0 +1,427 @@ +import { + FoundryUIManager, + QuestDB, + Utils, + ViewManager } from './index.js'; + +import { + constants, + questStatus, + sessionConstants, + settings } from '../model/constants.js'; + +/** + * Provides registration for all module settings. + */ +export class ModuleSettings +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * The default location for the QuestTracker + * + * @type {{top: number, width: number}} + */ + static #defaultQuestTrackerPosition = { top: 80, width: 296 }; + + /** + * Constants for setting scope type. + * + * @type {{world: string, client: string}} + */ + static #scope = { + client: 'client', + world: 'world' + }; + + /** + * Registers all module settings. + * + * @see FQLSettings + */ + static register() + { + game.settings.register(constants.moduleName, settings.allowPlayersDrag, { + name: 'ForienQuestLog.Settings.allowPlayersDrag.Enable', + hint: 'ForienQuestLog.Settings.allowPlayersDrag.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.allowPlayersDrag, value); } + + // Must enrich all quests again in QuestDB. + await QuestDB.enrichAll(); + + // Render all views; immediately stops / enables player drag if Quest view is open. + ViewManager.renderAll({ force: true, questPreview: true }); + } + }); + + game.settings.register(constants.moduleName, settings.allowPlayersCreate, { + name: 'ForienQuestLog.Settings.allowPlayersCreate.Enable', + hint: 'ForienQuestLog.Settings.allowPlayersCreate.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.allowPlayersCreate, value); } + + // Render quest log to show / hide add quest button. + if (ViewManager.questLog.rendered) { ViewManager.questLog.render(); } + } + }); + + game.settings.register(constants.moduleName, settings.allowPlayersAccept, { + name: 'ForienQuestLog.Settings.allowPlayersAccept.Enable', + hint: 'ForienQuestLog.Settings.allowPlayersAccept.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.allowPlayersAccept, value); } + + // Must enrich all quests again in QuestDB. + await QuestDB.enrichAll(); + + // Render all views as quest status actions need to be shown or hidden for some players. + ViewManager.renderAll({ questPreview: true }); + } + }); + + game.settings.register(constants.moduleName, settings.trustedPlayerEdit, { + name: 'ForienQuestLog.Settings.trustedPlayerEdit.Enable', + hint: 'ForienQuestLog.Settings.trustedPlayerEdit.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.trustedPlayerEdit, value); } + + // Must perform a consistency check as there are possible quests that need to be added / removed + // from the in-memory DB based on trusted player edit status. + await QuestDB.consistencyCheck(); + + // Render all views as trusted player edit adds / removes capabilities. + ViewManager.renderAll({ questPreview: true }); + } + }); + + game.settings.register(constants.moduleName, settings.countHidden, { + name: 'ForienQuestLog.Settings.countHidden.Enable', + hint: 'ForienQuestLog.Settings.countHidden.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.countHidden, value); } + + // Must enrich all quests again in QuestDB. + await QuestDB.enrichAll(); + + // Must render the quest log / floating quest log / quest tracker. + ViewManager.renderAll(); + } + }); + + game.settings.register(constants.moduleName, settings.dynamicBookmarkBackground, { + name: 'ForienQuestLog.Settings.dynamicBookmarkBackground.Enable', + hint: 'ForienQuestLog.Settings.dynamicBookmarkBackground.EnableHint', + scope: this.#scope.world, + config: true, + default: true, + type: Boolean, + onChange: () => + { + // Must render the quest log. + if (ViewManager.questLog.rendered) { ViewManager.questLog.render(); } + } + }); + + game.settings.register(constants.moduleName, settings.navStyle, { + name: 'ForienQuestLog.Settings.navStyle.Enable', + hint: 'ForienQuestLog.Settings.navStyle.EnableHint', + scope: this.#scope.client, + config: true, + default: 'bookmarks', + type: String, + choices: { + bookmarks: 'ForienQuestLog.Settings.navStyle.bookmarks', + classic: 'ForienQuestLog.Settings.navStyle.classic' + }, + onChange: async () => + { + // Must enrich all quests again in QuestDB. + await QuestDB.enrichAll(); + + // Must render the quest log. + if (ViewManager.questLog.rendered) { ViewManager.questLog.render(); } + } + }); + + game.settings.register(constants.moduleName, settings.showTasks, { + name: 'ForienQuestLog.Settings.showTasks.Enable', + hint: 'ForienQuestLog.Settings.showTasks.EnableHint', + scope: this.#scope.world, + config: true, + default: 'default', + type: String, + choices: { + default: 'ForienQuestLog.Settings.showTasks.default', + onlyCurrent: 'ForienQuestLog.Settings.showTasks.onlyCurrent', + no: 'ForienQuestLog.Settings.showTasks.no' + }, + onChange: async () => + { + // Must enrich all quests again in QuestDB. + await QuestDB.enrichAll(); + + // Must render the quest log. + ViewManager.renderAll(); + } + }); + + game.settings.register(constants.moduleName, settings.defaultPermission, { + name: 'ForienQuestLog.Settings.defaultPermissionLevel.Enable', + hint: 'ForienQuestLog.Settings.defaultPermissionLevel.EnableHint', + scope: this.#scope.world, + config: true, + default: 'Observer', + type: String, + choices: { + OBSERVER: 'ForienQuestLog.Settings.defaultPermissionLevel.OBSERVER', + NONE: 'ForienQuestLog.Settings.defaultPermissionLevel.NONE', + OWNER: 'ForienQuestLog.Settings.defaultPermissionLevel.OWNER' + } + }); + + game.settings.register(constants.moduleName, settings.hideFQLFromPlayers, { + name: 'ForienQuestLog.Settings.hideFQLFromPlayers.Enable', + hint: 'ForienQuestLog.Settings.hideFQLFromPlayers.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async(value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.hideFQLFromPlayers, value); } + + if (!game.user.isGM) + { + // Hide all FQL apps from non GM user and remove the ui.controls for FQL. + if (value) + { + ViewManager.closeAll({ questPreview: true, updateSetting: false }); + + const notes = ui?.controls?.controls.find((c) => c.name === 'notes'); + if (notes) { notes.tools = notes?.tools.filter((c) => !c.name.startsWith(constants.moduleName)); } + + // Remove all quests from in-memory DB. This is required so that users can not retrieve quests + // from the QuestAPI or content links in Foundry resolve when FQL is hidden. + QuestDB.removeAll(); + } + else + { + // Initialize QuestDB loading all quests that are currently observable for the user. + await QuestDB.init(); + + // Add back ui.controls + const notes = ui?.controls?.controls.find((c) => c.name === 'notes'); + if (notes) { notes.tools.push(...FoundryUIManager.noteControls); } + } + + ui?.controls?.render(true); + } + + // Render the journal to show / hide open quest log button & folder. + game?.journal?.render(); + + // Close or open the quest tracker based on active quests (users w/ FQL hidden will have no quests in + // QuestDB) + ViewManager.renderOrCloseQuestTracker({ updateSetting: false }); + } + }); + + game.settings.register(constants.moduleName, settings.notifyRewardDrop, { + name: 'ForienQuestLog.Settings.notifyRewardDrop.Enable', + hint: 'ForienQuestLog.Settings.notifyRewardDrop.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + // Swap macro image based on current state. No need to await. + if (game.user.isGM) { await Utils.setMacroImage(settings.notifyRewardDrop, value); } + } + }); + + game.settings.register(constants.moduleName, settings.showFolder, { + name: 'ForienQuestLog.Settings.showFolder.Enable', + hint: 'ForienQuestLog.Settings.showFolder.EnableHint', + scope: this.#scope.world, + config: true, + default: false, + type: Boolean, + onChange: () => game.journal.render() // Render the journal to show / hide the quest folder. + }); + +// Settings not displayed in the module settings --------------------------------------------------------------------- + + // Currently provides a hidden setting to set the default abstract reward image. + // It may never be displayed in the module settings menu, but if it is in the future this is where it would go. + game.settings.register(constants.moduleName, settings.defaultAbstractRewardImage, { + scope: this.#scope.world, + config: false, + default: 'icons/svg/item-bag.svg', + type: String + }); + + game.settings.register(constants.moduleName, settings.questTrackerEnable, { + scope: this.#scope.client, + config: false, + default: false, + type: Boolean, + onChange: async (value) => + { + // Potentially Post notification that the quest tracker is enabled, but not visible as there are no active + // quests. + if (value && QuestDB.getCount({ status: questStatus.active }) === 0) + { + ViewManager.notifications.info(game.i18n.localize('ForienQuestLog.Notifications.QuestTrackerNoActive')); + } + + // Swap macro image based on current state. No need to await. + await Utils.setMacroImage(settings.questTrackerEnable, value); + + ViewManager.renderOrCloseQuestTracker(); + } + }); + + /** + * This is the most complex module setting handling as quite a bit of logic is contained below to handle + * setting the primary quest. Since the onChange event does not pass the old and new value the old value for + * {@link FQLSettings.primaryQuest} is stored in {@link FQLSessionConstants.currentPrimaryQuest} which is + * initially set in {@link FQLHooks.foundryReady}. + * + * This setting is set from {@link Socket.setQuestPrimary} or the handler in Socket. + * + * `value` may be a quest ID or an empty string. + */ + game.settings.register(constants.moduleName, settings.primaryQuest, { + scope: this.#scope.world, + config: false, + default: '', + type: String, + onChange: async (value) => + { + // Any current primary quest. + const currentQuestEntry = QuestDB.getQuestEntry( + sessionStorage.getItem(sessionConstants.currentPrimaryQuest)); + + // The new primary quest or none at all if value is an empty string. + const newQuestEntry = QuestDB.getQuestEntry(value); + + // Store all quest IDs that need to be updated which include parent / subquests. + const updateQuestIds = []; + + // Store the new primary quest name to post a notification. + let newPrimaryQuestName = void 0; + + // Store the old primary quest IDs that need UI updates. + if (currentQuestEntry && currentQuestEntry.id !== value) + { + updateQuestIds.push(...currentQuestEntry.questIds); + + // If there is a new quest then store the quest name and also all quests that need UI updates. + if (newQuestEntry) + { + newPrimaryQuestName = newQuestEntry.quest.name; + updateQuestIds.push(...newQuestEntry.questIds); + } + } + else if (newQuestEntry) + { + // There was not a presently set primary quest, so store only the new + updateQuestIds.push(...newQuestEntry.questIds); + + if (value.length) { newPrimaryQuestName = newQuestEntry.quest.name; } + } + + // Store the current primary quest. Either Quest ID or empty string. + sessionStorage.setItem(sessionConstants.currentPrimaryQuest, value); + + // If any quest IDs were stored then update all QuestPreviews after enriching the quest data. + if (updateQuestIds.length) + { + await QuestDB.enrichQuests(...updateQuestIds); + ViewManager.refreshQuestPreview(updateQuestIds); + ViewManager.renderAll({ force: true }); + } + + // Post a notification if a new primary quest was set. + if (newPrimaryQuestName) + { + ViewManager.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestPrimary', + { name: newPrimaryQuestName })); + } + } + }); + + game.settings.register(constants.moduleName, settings.questTrackerPinned, { + scope: this.#scope.client, + config: false, + type: Boolean, + default: false, + onChange: () => + { + // The quest tracker pinned state has changed so update any Foundry UI management. + FoundryUIManager.updateTrackerPinned(); + } + }); + + game.settings.register(constants.moduleName, settings.questTrackerPosition, { + scope: this.#scope.client, + config: false, + default: this.#defaultQuestTrackerPosition, + }); + + game.settings.register(constants.moduleName, settings.questTrackerResizable, { + name: 'ForienQuestLog.Settings.questTrackerResizable.Enable', + hint: 'ForienQuestLog.Settings.questTrackerResizable.EnableHint', + scope: this.#scope.client, + config: true, + default: false, + type: Boolean, + onChange: async (value) => + { + ViewManager.renderOrCloseQuestTracker(); + + // Swap macro image based on current state. No need to await. + await Utils.setMacroImage(settings.questTrackerResizable, value); + } + }); + } +} diff --git a/src/control/Socket.js b/src/control/Socket.js new file mode 100644 index 00000000..a8ddb97c --- /dev/null +++ b/src/control/Socket.js @@ -0,0 +1,727 @@ +import { QuestAPI } from './public/index.js'; + +import { + QuestDB, + Utils, + ViewManager } from './index.js'; + +import { + constants, + questStatus, + questStatusI18n, + settings } from '../model/constants.js'; + +/** + * Provides a basic Socket.io implementation to send events between all connected clients. The various methods have + * at as local and remote control mostly of GUI related actions. FQL appears to be a reactive application, but it really + * is Socket doing a lot of the heavy lifting to notify clients that particular GUI apps need to be refreshed when the + * underlying Quest data changes. + * + * There are also various actions that require a GM or trusted played with edit capability to act upon mostly moving + * quests from one status to another. Reward item drops into actor sheets invokes {@link Socket.questRewardDrop} from + * the {@link FQLHooks.dropActorSheetData} hook, but at least one GM level user must be logged in to receive this + * message to perform the drop / removal of the reward from a Quest. + * + * Please see the following view control classes and the QuestDB for socket related usage: + * + * @see HandlerAny + * @see HandlerDetails + * @see HandlerLog + * @see HandlerManage + * @see QuestDB.deleteQuest + */ +export class Socket +{ + /** + * Defines the event name to send all messages to over `game.socket`. + * + * @type {string} + */ + static #eventName = 'module.forien-quest-log'; + + /** + * Defines the different message types that FQL sends over `game.socket`. + */ + static #messageTypes = { + deletedQuest: 'deletedQuest', + questSetPrimary: 'questSetPrimary', + questSetStatus: 'questSetStatus', + questRewardDrop: 'questRewardDrop', + refreshAll: 'refreshAll', + refreshQuestPreview: 'refreshQuestPreview', + savePlayerNotes: 'savePlayerNotes', + showQuestLog: 'showQuestLog', + showQuestPreview: 'showQuestPreview', + showQuestTracker: 'showQuestTracker', + userCantOpenQuest: 'userCantOpenQuest' + }; + + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Refreshes the parent & subquest GUI apps as applicable and closes the associated QuestPreview for the quest that + * was deleted. This method is invoked from the private module method `QuestDB.#handleJournalEntryDelete`. + * + * Handled on the receiving side by `#handleDeletedQuest`. + * + * @param {DeleteData} deleteData - A data object containing the views that need to be updated and which quest was + * deleted by quest ID. + * + * @returns {Promise} + * @see QuestDB / QuestDB.#handleJournalEntryDelete + */ + static async deletedQuest(deleteData) + { + if (typeof deleteData === 'object') + { + const questId = deleteData.deleteId; + const questPreview = ViewManager.questPreview.get(questId); + + // Close the associated QuestPreview for the deleted Quest. + if (questPreview !== void 0) + { + // Must always use `noSave` as the quest has already been deleted; no auto-save of QuestPreview is allowed. + await questPreview.close({ noSave: true }); + } + + game.socket.emit(this.#eventName, { + type: this.#messageTypes.deletedQuest, + payload: { + questId, + } + }); + + // Send a refresh quest preview message for the views that need to be updated. + Socket.refreshQuestPreview({ questId: deleteData.savedIds }); + } + } + + /** + * Provides the main incoming message registration and distribution of socket messages on the receiving side. + */ + static listen() + { + game.socket.on(this.#eventName, this.#handleEvent.bind(this)); + } + + /** + * Handles the reward drop in actor sheet action from the {@link FQLHooks.dropActorSheetData} hook. If the local user + * is a GM handle this action right away otherwise send a message across the wire for the first GM user reached to + * handle the action remotely. The reward is removed from the associated quest. + * + * Handled on the receiving side by `#handleQuestRewardDrop`. + * + * @param {RewardDropData} data - The reward drop data generated from the hook. + * + * @returns {Promise} + */ + static async questRewardDrop(data = {}) + { + let handled = false; + + // Perform the immediate reward removal action if the current user is the GM and set `handled` to true. + if (game.user.isGM) + { + /** + * @type {FQLDropData} + */ + const fqlData = data.data._fqlData; + + const quest = QuestDB.getQuest(fqlData.questId); + if (quest) + { + quest.removeReward(fqlData.uuidv4); + await quest.save(); + Socket.refreshQuestPreview({ questId: fqlData.questId }); + } + handled = true; + } + + // Emit the reward drop event. + game.socket.emit(this.#eventName, { + type: this.#messageTypes.questRewardDrop, + payload: { + ...data, + handled + } + }); + } + + /** + * Renders all GUI apps via {@link ViewManager.renderAll}. With the option `questPreview` set to true all + * QuestPreviews are also rendered. Remaining options are forwarded onto the Foundry Application render method. + * Sends a socket message over the wire for all remote clients to do the same. + * + * Handled on the receiving side by `#handleRefreshAll`. + * + * @param {object} options - Optional parameters + * + * @param {boolean} [options.force] - Forces a data refresh. + * + * @param {boolean} [options.questPreview] - Render all open QuestPreview apps. + */ + static refreshAll(options = {}) + { + // QuestDB Journal update hook is now async, so schedule on next microtask so local display is correct. + setTimeout(() => ViewManager.renderAll({ force: true, ...options }), 10); + + game.socket.emit(this.#eventName, { + type: this.#messageTypes.refreshAll, + payload: { + options + } + }); + } + + /** + * Refreshes local {@link QuestPreview} apps and sends a message indicating which QuestPreview apps need to be + * rendered. + * + * Handled on the receiving side by `#handleRefreshQuestPreview`. + * + * @param {object} opts - Optional parameters. + * + * @param {string|string[]} opts.questId - A single quest ID or an array of IDs to update. + * + * @param {boolean} [opts.updateLog=true] - Updates the quest log and all other GUI apps if true. + * + * @param {...RenderOptions} [opts.options] - Any options to pass onto QuestPreview render method invocation. + */ + static refreshQuestPreview({ questId, updateLog = true, ...options }) + { + // QuestDB Journal update hook is now async, so schedule on next microtask so local display is correct. + setTimeout(() => ViewManager.refreshQuestPreview(questId, options), 10); + + // Send a socket message for remote clients to render. + game.socket.emit(this.#eventName, { + type: this.#messageTypes.refreshQuestPreview, + payload: { + questId, + options + } + }); + + // Also update the quest log and other GUIs + if (updateLog) { Socket.refreshAll(); } + } + + /** + * Saves player notes by delegating to an active GM. + * + * @param {object} opts - Optional parameters. + * + * @param {Quest} opts.quest - The current quest being manipulated. It + * + * @param {string} opts.playernotes - The player notes to save. + */ + static savePlayerNotes({ quest, playernotes }) + { + // Send a socket message for any remote GMs logged in to handle request. + game.socket.emit(this.#eventName, { + type: this.#messageTypes.savePlayerNotes, + payload: { + questId: quest.id, + playernotes, + handled: false + } + }); + } + + /** + * Sets a new primary quest if a GM user or sends a socket message if set by a trusted player w/ edit. If the + * current user is not a GM a GM level user must be logged in for a successful completion of the set status + * operation. + * + * @param {object} opts - Optional parameters. + * + * @param {Quest} opts.quest - The current quest being manipulated. It + * + * @returns {Promise} + */ + static async setQuestPrimary({ quest }) + { + // If the current user is a GM immediately set the primary quest. + if (game.user.isGM) + { + // Get any currently set primary quest. + const currentQuestEntry = QuestDB.getQuestEntry(game.settings.get( + constants.moduleName, settings.primaryQuest)); + + // If the current set primary quest is different from provided quest then set new primary quest. + if (currentQuestEntry !== void 0 && currentQuestEntry.id !== quest.id) + { + await game.settings.set(constants.moduleName, settings.primaryQuest, quest.id); + } + else + { + // There isn't a primary quest set or the same quest is potentially being unset. + await game.settings.set(constants.moduleName, settings.primaryQuest, quest.isPrimary ? '' : quest.id); + } + } + else + { + // Otherwise send a socket message for any remote GMs logged in to handle request. + game.socket.emit(this.#eventName, { + type: this.#messageTypes.questSetPrimary, + payload: { + questId: quest.id, + handled: false + } + }); + } + } + + /** + * Handles setting a new quest status then refreshes the appropriate views including parent and + * subquests as applicable. On the invocation side if the user is a GM or trusted player with edit and ownership of + * the quest being updated then the action is immediately taken and `handled` set to true which is part of the + * message sent across the wire. If this is a player who can accept quests the local action is skipped and a socket + * message is sent out and the first GM level user to receive it will perform the status update for the associated + * quest. If no GM level users are logged in this action is never handled and the user can not change the status of + * a quest. + * + * Handled on the receiving side by `#handleQuestSetStatus`. + * + * @param {object} options - Options. + * + * @param {Quest} options.quest - The quest to move. + * + * @param {string} options.target - The target status. One of five {@link questStatus}. + * + * @returns {Promise} + * @see HandlerAny.questStatusSet + */ + static async setQuestStatus({ quest, target }) + { + let handled = false; + + // If the current user is a GM or trusted player with edit capability and owner of the quest immediately perform + // the status move. + if (game.user.isGM || (Utils.isTrustedPlayerEdit() && quest.isOwner)) + { + await quest.setStatus(target); + handled = true; + + Socket.refreshQuestPreview({ questId: quest.getQuestIds() }); + Socket.refreshAll(); + + const dirname = game.i18n.localize(questStatusI18n[target]); + ViewManager.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestMoved', + { name: quest.name, target: dirname })); + } + else + { + // Provide a sanity check and early out if the player can't accept quests. + const canPlayerAccept = game.settings.get(constants.moduleName, settings.allowPlayersAccept); + if (questStatus.active !== target && !canPlayerAccept) { return; } + } + + game.socket.emit(this.#eventName, { + type: this.#messageTypes.questSetStatus, + payload: { + questId: quest.id, + handled, + target + } + }); + } + + /** + * This handles the `show to players` title bar button found in {@link QuestLog._getHeaderButtons} to open the + * QuestLog for all remote clients. + * + * Handled on the receiving side by `#handleShowQuestLog`. + * + * @param {string} tabId - A specific tab ID for the quest status to open. + */ + static showQuestLog(tabId) + { + game.socket.emit(this.#eventName, { + type: this.#messageTypes.showQuestLog, + payload: { + tabId + } + }); + } + + /** + * This handles the `show to players` title bar button found in {@link QuestPreview._getHeaderButtons} to open the + * associated QuestPreview for all remote clients. + * + * Handled on the receiving side by `#handleShowQuestPreview`. + * + * @param {string} questId - The quest ID to a QuestPreview. + */ + static showQuestPreview(questId) + { + game.socket.emit(this.#eventName, { + type: this.#messageTypes.showQuestPreview, + payload: { + questId + } + }); + } + + /** + * This handles the `show to players` title bar button found in {@link QuestTracker._getHeaderButtons} to open the + * QuestTracker for all remote clients. + * + * Handled on the receiving side by `#handleShowQuestTracker`. + */ + static showQuestTracker() + { + game.socket.emit(this.#eventName, { + type: this.#messageTypes.showQuestTracker + }); + } + + /** + * A message emitted for GM users when a player can't open a particular quest in {@link QuestAPI.open}. This is + * particularly useful if a GM tries to show a quest that the user doesn't have access to via the `show to players` + * header button in {@link QuestPreview._getHeaderButtons}. + * + * Handled on the receiving side by `#handleUserCantOpenQuest`. + */ + static userCantOpenQuest() + { + game.socket.emit(this.#eventName, { + type: this.#messageTypes.userCantOpenQuest, + payload: { + user: game.user.name + } + }); + } + + // Internal implementation (receiving message handling) ----------------------------------------------------------- + + /** + * Provides the main incoming message registration and distribution of socket messages on the receiving side. + * + * @param {object} data - Incoming data object from `game.socket`. + */ + static async #handleEvent(data) + { + if (typeof data !== 'object') { return; } + + try + { + // Dispatch the incoming message data by the message type. + switch (data.type) + { + case this.#messageTypes.deletedQuest: await this.#handleDeletedQuest(data); break; + case this.#messageTypes.questRewardDrop: await this.#handleQuestRewardDrop(data); break; + case this.#messageTypes.questSetPrimary: await this.#handleQuestSetPrimary(data); break; + case this.#messageTypes.questSetStatus: await this.#handleQuestSetStatus(data); break; + case this.#messageTypes.refreshAll: this.#handleRefreshAll(data); break; + case this.#messageTypes.refreshQuestPreview: this.#handleRefreshQuestPreview(data); break; + case this.#messageTypes.savePlayerNotes: await this.#handleSavePlayerNotes(data); break; + case this.#messageTypes.showQuestLog: this.#handleShowQuestLog(data); break; + case this.#messageTypes.showQuestPreview: this.#handleShowQuestPreview(data); break; + case this.#messageTypes.showQuestTracker: this.#handleShowQuestTracker(); break; + case this.#messageTypes.userCantOpenQuest: this.#handleUserCantOpenQuest(data); break; + } + } + catch (err) + { + console.error(err); + } + } + + /** + * Closes the associated QuestPreview for the quest that was deleted on the remote client. The payload is a the + * `questId` to close. QuestPreview by default saves the quest when a QuestPreview is closed. This quest has already + * been deleted, so it is important to pass `noSave: true` to {@link QuestPreview.close}. + * + * This message is sent from {@link Socket.deletedQuest}. + * + * @param {object} data - The data payload. + * + * @returns {Promise} + */ + static async #handleDeletedQuest(data) + { + const questPreview = ViewManager.questPreview.get(data.payload.questId); + if (questPreview !== void 0) + { + // Must always use `noSave` as the quest has already been deleted; no auto-save of QuestPreview is allowed. + await questPreview.close({ noSave: true }); + } + } + + /** + * Handles the reward item drop into actor sheet by the first GM level user receiving this message setting the + * handled state to `true`, so no further GM level users attempt to remove the item from the associated quest. + * + * This message is sent from {@link Socket.questRewardDrop}. + * + * @param {RewardDropData} data - The data payload is the reward drop data. + * + * @returns {Promise} + */ + static async #handleQuestRewardDrop(data) + { + if (game.user.isGM) + { + /** + * @type {FQLDropData} + */ + const fqlData = data.payload.data._fqlData; + + // Notify the GM that a user has dropped a reward item into an actor sheet. + const notify = game.settings.get(constants.moduleName, settings.notifyRewardDrop); + + if (notify) + { + ViewManager.notifications.info(game.i18n.format('ForienQuestLog.API.Socket.Notifications.RewardDrop', { + userName: fqlData.userName, + itemName: fqlData.itemName, + actorName: data.payload.actor.name + })); + } + + // The quest reward has already been removed by a GM user. + if (data.payload.handled) { return; } + + // Set handled to true so no more GM level users act upon this event. + data.payload.handled = true; + + const quest = QuestDB.getQuest(fqlData.questId); + if (quest) + { + quest.removeReward(fqlData.uuidv4); + await quest.save(); + Socket.refreshQuestPreview({ questId: fqlData.questId }); + } + } + } + + /** + * Handles setting a primary quest by a remote GM user. + * + * This message is sent from {@link Socket.setQuestPrimary}. + * + * @param {object} data - The data payload contains `questId` along with `handled`. + * + * @returns {Promise} + */ + static async #handleQuestSetPrimary(data) + { + // If this message has not already been handled and this user is a GM then handle it now then set `handled` to true. + if (game.user.isGM && !data.payload.handled) + { + const quest = QuestDB.getQuest(data.payload.questId); + if (quest === void 0) { return; } + + // Get any currently set primary quest. + const currentQuestEntry = QuestDB.getQuestEntry(game.settings.get(constants.moduleName, settings.primaryQuest)); + + // If the current set primary quest is different from provided quest then set new primary quest. + if (currentQuestEntry !== void 0 && currentQuestEntry.id !== quest.id) + { + await game.settings.set(constants.moduleName, settings.primaryQuest, quest.id); + } + else + { + // There isn't a primary quest set or the same quest is potentially being unset. + await game.settings.set(constants.moduleName, settings.primaryQuest, quest.isPrimary ? '' : quest.id); + } + + // Set handled to true so no other GM level users act upon the action. + data.payload.handled = true; + } + } + + /** + * Sets the associated quest status to the `target` by the first GM level user receiving this message setting the + * handled state to `true`, so no further GM level users attempt to update the quest. + * + * This message is sent from {@link Socket.questSetStatus}. + * + * @param {object} data - The data payload contains `questId` and `target` along with `handled`. + * + * @returns {Promise} + */ + static async #handleQuestSetStatus(data) + { + const target = data.payload.target; + + // If this message has not already been handled and this user is a GM then handle it now then set `handled` to true. + if (game.user.isGM && !data.payload.handled) + { + const quest = QuestDB.getQuest(data.payload.questId); + if (quest) + { + await quest.setStatus(target); + } + + // Set handled to true so no other GM level users act upon the move. + data.payload.handled = true; + + Socket.refreshQuestPreview({ + questId: quest.parent ? [quest.parent, quest.id, ...quest.subquests] : [quest.id, ...quest.subquests] + }); + + Socket.refreshAll(); + + const dirname = game.i18n.localize(questStatusI18n[target]); + ViewManager.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestMoved', + { name: quest.name, target: dirname })); + } + + // For non-GM users close QuestPreview when made hidden / inactive. + if (!game.user.isGM && target === questStatus.inactive) + { + const questPreview = ViewManager.questPreview.get(data.payload.questId); + if (questPreview !== void 0) + { + // Use `noSave` just for sanity in this case as this is a remote close. + await questPreview.close({ noSave: true }); + } + } + } + + /** + * Handles refreshing all GUI apps via {@link ViewManager.renderAll} passing the `options` data payload onward. + * + * This message is sent from {@link Socket.refreshAll}. + * + * @param {object} data - Please see {@link ViewManager.renderAll} for options. + */ + static #handleRefreshAll(data) + { + const options = typeof data.payload.options === 'object' ? data.payload.options : {}; + ViewManager.renderAll({ force: true, ...options }); + } + + /** + * Handles refreshing / rendering all QuestPreview apps specified or closes them if the quests specified in the payload + * are no longer available or observable to the current user. + * + * This message is sent from {@link Socket.refreshQuestPreview}. + * + * @param {object} data - Data payload contains `questId` which can be a string or array of strings. + */ + static #handleRefreshQuestPreview(data) + { + const questId = data.payload.questId; + const options = typeof data.payload.options === 'object' ? data.payload.options : {}; + + if (Array.isArray(questId)) + { + for (const id of questId) + { + const questPreview = ViewManager.questPreview.get(id); + if (questPreview !== void 0) + { + const quest = QuestDB.getQuest(id); + if (!quest) + { + questPreview.close(); + continue; + } + + if (quest.isObservable) { questPreview.render(true, options); } + else { questPreview.close(); } + } + } + } + else + { + const questPreview = ViewManager.questPreview.get(questId); + if (questPreview !== void 0) + { + const quest = QuestDB.getQuest(questId); + if (!quest) + { + questPreview.close(); + return; + } + + if (quest.isObservable) { questPreview.render(true, options); } + else { questPreview.close(); } + } + } + } + + /** + * Handles saving player notes via GM user. + * + * @param {object} data - Data payload contains a single `tabId` as a string. + */ + static async #handleSavePlayerNotes(data) + { + // If this message has not already been handled and this user is a GM then handle it now then set `handled` to true. + if (game.user.isGM && !data.payload.handled) + { + const quest = QuestDB.getQuest(data.payload.questId); + if (quest && typeof data.payload.playernotes === 'string') + { + quest.playernotes = data.payload.playernotes; + + await quest.save(); + + // Set handled to true so no other GM level users act upon the move. + data.payload.handled = true; + + Socket.refreshQuestPreview({ questId: quest.id }); + } + } + } + + /** + * Handles opening the QuestLog app. + * + * This message is sent from {@link Socket.showQuestLog}. + * + * @param {object} data - Data payload contains a single `tabId` as a string. + */ + static #handleShowQuestLog(data) + { + ViewManager.questLog.render(true, { focus: true, tabId: data.payload.tabId }); + } + + /** + * Handles opening a QuestPreview app specified by `questId` via {@link QuestAPI.open}. + * + * This message is sent from {@link Socket.showQuestPreview}. + * + * @param {object} data - Data payload contains a single `questId` as a string. + */ + static #handleShowQuestPreview(data) + { + QuestAPI.open({ questId: data.payload.questId, notify: false }); + } + + /** + * Handles opening the QuestTracker app. + * + * This message is sent from {@link Socket.showQuestTracker}. + */ + static #handleShowQuestTracker() + { + game.settings.set(constants.moduleName, settings.questTrackerEnable, true); + } + + /** + * Handles displaying a UI notification for GM level users regarding an attempt to show a quest that the user doesn't + * have the access to view. Uses {@link ViewManager.notification} to rate limit UI notification display. + * + * This message is sent from {@link Socket.userCantOpenQuest}. + * + * @param {object} data - Data payload contains a `user` as a string for the user name. + */ + static #handleUserCantOpenQuest(data) + { + if (game.user.isGM) + { + ViewManager.notifications.warn(game.i18n.format('ForienQuestLog.Notifications.UserCantOpen', + { user: data.payload.user })); + } + } +} \ No newline at end of file diff --git a/src/control/db/Enrich.js b/src/control/db/Enrich.js new file mode 100644 index 00000000..4f690936 --- /dev/null +++ b/src/control/db/Enrich.js @@ -0,0 +1,497 @@ +import { + QuestDB, + Utils } from '../index.js'; + +import { DOMPurify } from '../../../external/index.js'; + +import { + constants, + questStatus, + questStatusI18n, + settings } from '../../model/constants.js'; + +/** + * Enrich populates content with a lot of additional data that doesn't necessarily have to be saved + * with the Quest itself such as Actor data and provides embellishment for the Handlebars templates for tasks, rewards, + * subquests, status actions, and provides a UUID lookup for the quest giver image. + * + * All enrich methods should only be used in the {@link QuestDB} during the caching phase of the update / create + * lifecycle. + */ +export class Enrich +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Builds the quest status / icons div to control quest status. There are many possible states to construct across + * three different user states from GM, trusted player edit, to player accept, so it is easier to build and cache + * this data as performing this setup in the Handlebars template itself is cumbersome and error-prone. + * + * @param {Quest} quest - The quest to build status action div / icons for based on current user state. + * + * @returns {string} The HTML to insert into a Handlebars template for quest status div / icons. + */ + static statusActions(quest) + { + let result = ''; + + const isTrustedPlayerEdit = Utils.isTrustedPlayerEdit(); + const canAccept = game.settings.get(constants.moduleName, settings.allowPlayersAccept); + const canEdit = game.user.isGM || (isTrustedPlayerEdit && quest.isOwner); + + let addedAction = false; + + result += `
    `; + + if (canEdit || canAccept) + { + if (canEdit && questStatus.active === quest.status) + { + result += `\n`; + + result += `\n`; + + addedAction = true; + } + + // If the quest status is completed add a failed button to be able to move it directly to failed. + if (canEdit && questStatus.completed === quest.status) + { + result += `\n`; + + addedAction = true; + } + + // If the quest status is failed add a completed button to be able to move it directly to completed. + if (canEdit && questStatus.failed === quest.status) + { + result += `\n`; + + addedAction = true; + } + + if ((canEdit && questStatus.inactive === quest.status) || questStatus.available === quest.status) + { + result += `\n`; + + addedAction = true; + } + + if (canEdit && questStatus.inactive !== quest.status) + { + result += `\n`; + + addedAction = true; + } + + if ((canEdit && questStatus.inactive === quest.status) || questStatus.active === quest.status) + { + result += `\n`; + + addedAction = true; + } + + if (canEdit) + { + result += `\n`; + + addedAction = true; + } + + result += `
    \n`; + } + + return isTrustedPlayerEdit || addedAction ? result : ''; + } + + /** + * This method performs content manipulation based on the current state of {@link Quest} preparing data to be + * displayed in a {@link Handlebars} template. This data is cached in a {@link QuestEntry} in the {@link QuestDB} + * and only updated when the underlying {@link Quest} changes. + * + * @param {Quest} quest - Quest data to construct view data. + * + * @returns {Promise} A single quest view or SortedQuests upgraded + */ + static async quest(quest) + { + const data = JSON.parse(JSON.stringify(quest.toJSON())); + data.id = quest.id; + data.isActive = quest.isActive; + data.isHidden = quest.isHidden; + data.isInactive = quest.isInactive; + + const isOwner = quest.isOwner; + const isPrimary = quest.isPrimary; + const personalActors = quest.getPersonalActors(); + + const isTrustedPlayerEdit = Utils.isTrustedPlayerEdit(); + const canEdit = game.user.isGM || (isOwner && isTrustedPlayerEdit); + const playerEdit = isOwner; + + const canPlayerAccept = game.settings.get(constants.moduleName, settings.allowPlayersAccept); + const canPlayerDrag = game.settings.get(constants.moduleName, settings.allowPlayersDrag); + const countHidden = game.settings.get(constants.moduleName, settings.countHidden); + + data.canEdit = canEdit; + + data.wrapNameLengthCSS = 'player'; + if (canPlayerAccept || playerEdit) { data.wrapNameLengthCSS = 'player-edit'; } + if (canEdit) { data.wrapNameLengthCSS = 'can-edit'; } + + data.isPersonal = personalActors.length > 0; + data.personalActors = personalActors.map((a) => a.name).sort((a, b) => a.localeCompare(b)).join(' '); + + data.isPrimary = isPrimary; + + // Enrich w/ TextEditor, but first sanitize w/ DOMPurify, allowing only iframes with YouTube embed. + data.description = await TextEditor.enrichHTML(DOMPurify.sanitize(data.description), { + secrets: canEdit || playerEdit, + async: true + }); + + data.gmnotes = await TextEditor.enrichHTML(DOMPurify.sanitize(data.gmnotes), { async: true }); + + data.playernotes = await TextEditor.enrichHTML(DOMPurify.sanitize(data.playernotes), { async: true }); + + data.questIconType = void 0; + + if (data.splashAsIcon && data.splash.length) + { + data.questIconType = 'splash-image'; + } + else if (data.giverData && data.giverData.img) + { + data.questIconType = 'quest-giver'; + } + + const statusLabel = game.i18n.localize(`ForienQuestLog.QuestTypes.Labels.${data.status}`); + + // The quest status in the details section. + data.statusLabel = game.i18n.format(`ForienQuestLog.QuestTypes.Labels.Status`, { statusLabel }); + + data.statusActions = Enrich.statusActions(quest); + + data.isSubquest = false; + + data.data_parent = {}; + + if (data.parent !== null) + { + const parentQuest = QuestDB.getQuest(data.parent); + if (parentQuest) + { + data.isSubquest = parentQuest.isObservable; + + data.data_parent = { + id: data.parent, + giver: parentQuest.giver, + name: parentQuest.name, + status: parentQuest.status, + isPrimary: parentQuest.isPrimary + }; + } + } + + data.data_subquest = []; + + if (data.subquests !== void 0) + { + for (const questId of data.subquests) + { + const subquest = QuestDB.getQuest(questId); + + // isObservable filters out non-owned hidden quests for trustedPlayerEdit. + if (subquest && subquest.isObservable) + { + // Mirror Task data for state / button state + let state = 'square'; + switch (subquest.status) + { + case questStatus.completed: + state = 'check-square'; + break; + case questStatus.failed: + state = 'minus-square'; + break; + } + + const subPersonalActors = subquest.getPersonalActors(); + + const isInactive = subquest.isInactive; + const subIsPrimary = subquest.isPrimary; + + const statusTooltipData = isInactive ? + { statusI18n: game.i18n.localize(questStatusI18n[questStatus.inactive]) } : + { statusI18n: game.i18n.localize(questStatusI18n[subquest.status]) }; + + const statusTooltip = game.i18n.format('ForienQuestLog.QuestTypes.Tooltips.Status', statusTooltipData); + + const canEditSubquest = game.user.isGM || (subquest.isOwner && isTrustedPlayerEdit); + + data.data_subquest.push({ + id: questId, + giver: subquest.giver, + name: subquest.name, + status: subquest.status, + statusTooltip, + state, + statusActions: Enrich.statusActions(subquest), + canEdit: canEditSubquest, + isActive: subquest.isActive, + isHidden: subquest.isHidden, + isInactive, + isPersonal: subPersonalActors.length > 0, + personalActors: subPersonalActors.map((a) => a.name).sort((a, b) => a.localeCompare(b)).join(' '), + isPrimary: subIsPrimary + }); + } + } + } + + if (countHidden) + { + data.checkedTasks = data.tasks.filter((t) => t.completed).length; + + const finishedSubquests = data.data_subquest.filter((s) => questStatus.completed === s.status).length; + + data.checkedTasks += finishedSubquests; + + data.totalTasks = data.tasks.length + data.subquests.length; + } + else + { + data.checkedTasks = data.tasks.filter((t) => !t.hidden && t.completed).length; + + const finishedSubquests = data.data_subquest.filter( + (s) => !s.isObservable && !s.isInactive && questStatus.completed === s.status).length; + + data.checkedTasks += finishedSubquests; + + data.totalTasks = data.tasks.filter((t) => !t.hidden).length + + data.data_subquest.filter((s) => !s.isObservable && !s.isInactive).length; + } + + switch (game.settings.get(constants.moduleName, settings.showTasks)) + { + case 'default': + data.taskCountLabel = `(${data.checkedTasks}/${data.totalTasks})`; + break; + + case 'onlyCurrent': + data.taskCountLabel = `(${data.checkedTasks})`; + break; + + default: + data.taskCountLabel = ''; + break; + } + + data.data_tasks = await Promise.all(data.tasks.map(async (task) => + { + return { + ...task, + name: await TextEditor.enrichHTML(DOMPurify.sanitize(task.name), { async: true }) + }; + })); + + data.data_rewards = await Promise.all(data.rewards.map(async (item) => + { + const type = item.type.toLowerCase(); + + // Only items are potentially draggable when `can player drag` is enabled or `can edit`. + const draggable = type === 'item' && (canEdit || canPlayerDrag) && (canEdit || !item.locked); + + const lockedTooltip = canEdit ? game.i18n.localize('ForienQuestLog.QuestPreview.Tooltips.RewardLocked') : + game.i18n.localize('ForienQuestLog.QuestPreview.Tooltips.RewardLockedPlayer'); + + const unlockedTooltip = canEdit ? game.i18n.localize('ForienQuestLog.QuestPreview.Tooltips.RewardUnlocked') : + game.i18n.localize('ForienQuestLog.QuestPreview.Tooltips.RewardUnlockedPlayer'); + + // Defines if the pointer cursor is displayed. For abstract or actor reward it is always displayed for GM or + // when unlocked for players. + const isLink = (type === 'abstract' || type === 'actor') && (canEdit || !item.locked); + + // For item rewards make them links when `can player drag` is not enabled. + const itemLink = type === 'item' && !canEdit && !canPlayerDrag && !item.locked; + + return { + name: await TextEditor.enrichHTML(DOMPurify.sanitize(item.data.name), { async: true }), + img: item.data.img, + type, + hidden: item.hidden, + locked: item.locked, + lockedTooltip, + unlockedTooltip, + isLink: isLink || itemLink, + draggable, + transfer: type !== 'abstract' ? JSON.stringify( + { uuid: item.data.uuid, uuidv4: item.uuidv4, name: item.data.name }) : void 0, + uuidv4: item.uuidv4 + }; + })); + + if (!canEdit) + { + data.data_tasks = data.data_tasks.filter((t) => t.hidden === false); + data.data_rewards = data.data_rewards.filter((r) => r.hidden === false); + } + + data.hasObjectives = data.data_tasks.length + data.data_subquest.length > 0; + + // Determine if all rewards are visible / unlocked + data.allRewardsVisible = true; + data.allRewardsUnlocked = true; + for (const reward of data.data_rewards) + { + if (reward.hidden) { data.allRewardsVisible = false; } + if (reward.locked) { data.allRewardsUnlocked = false; } + } + + return data; + } +} + +/** + * @typedef {QuestData} EnrichData + * + * @property {boolean} allRewardsVisible - Are all rewards visible. Controls show all / hide all button. + * + * @property {boolean} allRewardsUnlocked - Are all rewards unlocked. Controls unlock all / lock all button. + * + * @property {boolean} canEdit - Is full editing allowed. Either GM or trusted player w/ edit capability. + * + * @property {number} checkedTasks - Number of completed tasks. + * + * @property {object} data_parent - A data object with parent quest details. + * + * @property {string|null} data_parent.id - The parent quest ID / {@link Quest.id} + * + * @property {string|null} data_parent.giver - The parent quest giver / {@link Quest.giver} + * + * @property {string} data_parent.name - The parent quest name / {@link Quest.name} + * + * @property {boolean} data_parent.isPrimary - The parent quest is the primary quest / {@link Quest.isPrimary} + * + * @property {string} data_parent.status - The parent quest status / {@link Quest.status} + * + * @property {object[]} data_rewards - A list of reward item details. + * + * @property {boolean} data_rewards.draggable - Can the player drag the reward to actor sheet. + * + * @property {boolean} data_rewards.hidden - Is the reward hidden / only 'canEdit' users can see it. + * + * @property {string} data_rewards.img - The image for the reward. + * + * @property {boolean} data_rewards.isLink - Is the reward a link / pointer cursor. + * + * @property {boolean} data_rewards.locked - Is the reward locked / only 'canEdit' manipulate it. + * + * @property {string} data_rewards.lockedTooltip - The tooltip to display for the locked icon. + * + * @property {string} data_rewards.name - The name of the reward. + * + * @property {string} data_rewards.type - The type of reward / 'abstract' for abstract rewards. + * + * @property {object} data_rewards.transfer - The data tranfer object. + * + * @property {string} data_rewards.transfer.name - The reward name. + * + * @property {string} data_rewards.transfer.uuid - The reward Foundry UUID. + * + * @property {string} data_rewards.transfer.uuidv4 - The reward FQL UUIDv4. + * + * @property {string} data_rewards.unlockedTooltip - The tooltip to display for the unlocked icon. + * + * @property {string} data_rewards.uuidv4 - The reward FQL UUIDv4. + * + * @property {object[]} data_subquest - A list of data objects with subquest details. + * + * @property {boolean} data_subquest.canEdit - Is full editing allowed. Either GM or trusted player w/ edit. + * + * @property {string|null} data_subquest.giver - The parent quest giver / {@link Quest.giver} + * + * @property {string|null} data_subquest.id - The parent quest ID / {@link Quest.id} + * + * @property {boolean} data_subquest.isActive - Is quest status 'active' + * + * @property {boolean} data_subquest.isHidden - Is quest hidden by permissions / {@link Quest.isHidden} + * + * @property {boolean} data_subquest.isInactive - Is quest status 'inactive' + * + * @property {boolean} data_subquest.isPersonal - Is quest personal / {@link Quest.isPersonal} + * + * @property {string} data_subquest.name - The parent quest name / {@link Quest.name} + * + * @property {string[]} data_subquest.personalActors - A sorted list of names / {@link Quest.personalActors} + * + * @property {string} data_subquest.state - The CSS class for quest toggle / task state + * + * @property {string} data_subquest.status - The parent quest status / {@link Quest.status} + * + * @property {string} data_subquest.statusActions - HTML for quest status actions / {@link Enrich.statusActions} + * + * @property {string} data_subquest.statusTooltip - The localized quest status tooltip / {@link Quest.status} + * + * @property {QuestTaskData[]} data_tasks - The task data. + * + * @property {string} description - The enriched quest description via {@link TextEditor.enrichHTML}. + * + * @property {string} gmnotes - The GM Notes. + * + * @property {boolean} hasObjectives - Is there visible tasks & subjects. + * + * @property {string} id - Quest ID / {@link Quest.id} + * + * @property {boolean} isActive - Is quest status 'active' + * + * @property {boolean} isHidden - Is quest hidden by permissions / {@link Quest.isHidden} + * + * @property {boolean} isInactive - Is quest status 'inactive' + * + * @property {boolean} isPersonal - Is quest personal / not all players can access it / {@link Quest.isPersonal} + * + * @property {boolean} isSubquest - Is quest a subquest. + * + * @property {string} playerNotes - The player notes. + * + * @property {string[]} personalActors - A sorted list of names for HTML tooltip / {@link Quest.personalActors} + * + * @property {string} questIconType - Indicates which icon to use 'splash-image' or 'quest-giver'. + * + * @property {string} statusActions - HTML for quest status icon actions / {@link Enrich.statusActions} + * + * @property {string} statusLabel - Localized label for {@link Quest.status} + * + * @property {string} taskCountLabel - A label of completed / total tasks depending on module settings. + * + * @property {number} totalTasks - Number of total tasks. + * + * @property {string} wrapNameLengthCSS - The CSS class to add for content length wrapping based on user type. + */ + +/** + * @typedef QuestImgNameData + * + * @property {string} name - Quest giver or item name + * + * @property {string} img - Quest giver or item image + * + * @property {boolean} hasTokenImg - boolean indicating the quest giver has a token prototype image. + * + * @property {string} [uuid] - Any associated Foundry UUID for the quest giver / item. + */ \ No newline at end of file diff --git a/src/control/db/QuestDB.js b/src/control/db/QuestDB.js new file mode 100644 index 00000000..32b8974c --- /dev/null +++ b/src/control/db/QuestDB.js @@ -0,0 +1,1405 @@ +import { Enrich } from './Enrich.js'; + +import { + FVTTCompat, + Socket, + Utils } from '../index.js'; + +import { Quest } from '../../model/index.js'; + +import { QuestPreviewShim } from '../../view/index.js'; + +import { collect } from '../../../external/index.js'; + +import { + constants, + questStatus, + settings } from '../../model/constants.js'; + +/** + * The QuestDB holds quests in-memory that are observable by the current user. By pre-sorting quests by status and + * observability this cuts down on sorting and filtering operations that need to be performed on quests in an ongoing + * basis. Based on the type of user quests may go in and out of observability for permission and status category + * changes. + * + * In time with future refactoring the reliance on {@link Socket} for notifications to connected clients will be + * reduced as the QuestDB lifecycle hooks can replace many of the notification concerns. + * + * All quests and stored in {@link QuestEntry} instances which hold a {@link Quest} and the {@link EnrichData} created + * by {@link Enrich.quest} which is used when rendering {@link Handlebars} templates. There are several data points + * cached in QuestEntry from the Quest itself on any update; mostly the getter functions of Quest are cached each update + * in `#questEntryHydrate`. When an update does occur in `#handleJournalEntryUpdate` a QuestEntry is either + * added, removed, or updated based on the observability test found in `#isObservable`. Another pre-processing + * step for performance is to store all QuestEntry instances by the status of the quest. There are also two different + * views for QuestEntry data. The first is a map of Maps, `#questsMap`, with main index keys being the quest + * status. This preprocessing step allows quick retrieval of all quests by status category and ID without the need to + * filter all quests by status. Additionally, when `#questsMap` is updated a second view of the data is + * constructed as well which is a map of {@link CollectJS} collections found in `#questsCollect`. This allows + * in depth manipulation of all QuestEntry instances as a single collection. CollectJS has many options available for + * chained processing. There are by default two methods which apply a sort ({@link QuestDB.sortCollect}) or filter + * ({@link QuestDB.filterCollect}) operation to retrieve the {@link QuestsCollect} bundle returning a single status + * category or all quests indexed by status category. Iterators for quests are available by + * {@link QuestDB.iteratorQuests} and QuestEntry instances from {@link QuestDB.iteratorEntries}. Additionally, there + * are several direct retrieval methods such as {@link QuestDB.getQuest} and {@link QuestDB.getQuestEntry}. + * + * QuestDB lifecycle hooks ({@link QuestDBHooks}): The QuestDB has familiar lifecycle hooks to Foundry itself such as + * `createQuestEntry`, `deleteQuestEntry` and `updateQuestEntry`, but provides more fine-grained visibility of quest + * data that is loaded into and out of the in-memory QuestDB. Additional lifecycle hooks are: `addedAllQuestEntries`, + * `addQuestEntry`, `removedAllQuestEntries`, and `removeQuestEntry`. These latter unique lifecycle events signify + * observability. A quest may exist in the Foundry document / journal entry system, but only is added to the QuestDB + * when it is observable and this corresponds to the `addedAllQuestEntries` and `addQuestEntry` hooks. Likewise, both + * remove quest hooks relate to when a quest is removed based on observability whether through permission or quest + * status category updates; IE the quest _is not_ deleted, but is no longer observable by the current user and is + * removed from the QuestDB. + * + * ``` + * - `addedAllQuestEntries` - All observable quests have been added in the {@link QuestDB.init} method. + * + * {@link QuestDB.init}: After all quests have been initialized this hook is called to inform any external modules that + * all observable quests have been loaded into the QuestDB in bulk. + * ``` + * + * ``` + * - `addQuestEntry` - A quest has become observable and a QuestEntry instance is added to the QuestDB. + * + * {@link QuestDB.consistencyCheck}: During a consistency check which is mainly used when module settings for + * trusted player edit is enabled / disabled quests can be added to QuestDB. + * + * `#handleJournalEntryUpdate`: During the journal entry update hook a quest may become observable for the current + * user and added to the QuestDB. + * ``` + * + * ``` + * - `createQuestEntry` + * + * `#handleJournalEntryCreate`: A new quest is added to the QuestDB through creation and is observable by the + * current user. + * ``` + * + * ``` + * - `deleteQuestEntry` + * + * `#handleJournalEntryDelete`: A Quest has been deleted and is removed from the QuestDB. + * ``` + * + * ``` + * - `removedAllQuestEntries` + * + * {@link QuestDB.removeAll}: All quests have been removed from QuestDB. + * ``` + * + * ``` + * - `removeQuestEntry` + * + * {@link QuestDB.consistencyCheck}: During a consistency check which is mainly used when module settings for + * trusted player edit is enabled / disabled quests can be removed from QuestDB. + * + * `#handleJournalEntryUpdate`: A quest is no longer observable for permission reasons or status category change. + * ``` + * + * ``` + * - `updateQuestEntry` + * + * `#handleJournalEntryUpdate`: A quest that is currently in QuestDB has been updated. + * ``` + */ +export class QuestDB +{ + /** + * Set to true after the first call to `QuestDB.init`. Protects against adding hooks multiple times. + * + * @type {boolean} + */ + static #initialized = false; + + /** + * Defines the DB Hook callbacks. Please see {@link QuestDB} for more documentation. + * + * @type {QuestDBHooks} + */ + static #dbHooks = Object.freeze({ + addedAllQuestEntries: 'addedAllQuestEntries', + addQuestEntry: 'addQuestEntry', + createQuestEntry: 'createQuestEntry', + deleteQuestEntry: 'deleteQuestEntry', + removedAllQuestEntries: 'removedAllQuestEntries', + removeQuestEntry: 'removeQuestEntry', + updateQuestEntry: 'updateQuestEntry', + }); + + /** + * @type {FilterFunctions} + */ + static #fnFilter = Object.freeze({ + IS_OBSERVABLE: (entry) => entry.isObservable + }); + + /** + * @type {SortFunctions} + */ + static #fnSort = Object.freeze({ + ALPHA: (a, b) => a.quest.name.localeCompare(b.quest.name), + DATE_CREATE: (a, b) => a.quest.date.create - b.quest.date.create, + DATE_START: (a, b) => a.quest.date.start - b.quest.date.start, + DATE_END: (a, b) => b.quest.date.end - a.quest.date.end + }); + + /** + * Stores all {@link QuestEntry} instances in a map of CollectJS collections. This provides rapid sorting, filtering, + * and many other potential operations that {@link collect} / CollectJS collections provide for working with arrays + * of object data. Each collection is built from the values of the `#questMap` per status category. + * + * @type {Record>} + * @see https://collect.js.org/api.html + */ + static #questsCollect = Object.seal({ + active: collect(), + available: collect(), + completed: collect(), + failed: collect(), + inactive: collect() + }); + + /** + * Provides an index into the `#questMap` for all QuestEntry instances by questId and the status category. + * This allows quick retrieval and removal of QuestEntry instances from `#questMap`. + * + * @type {Map} + */ + static #questIndex = new Map(); + + /** + * Stores all {@link QuestEntry} instances in a map of Maps. This provides fast retrieval and quick insert / removal + * with quests pre-sorted by status. + * + * @type {Record>} + */ + static #questsMap = Object.seal({ + active: new Map(), + available: new Map(), + completed: new Map(), + failed: new Map(), + inactive: new Map() + }); + + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Initializes the QuestDB. If FQL is hidden from the current user then no quests load. All quests are loaded based + * on observability by the current user. + * + * This method may be invoked multiple times, but it is generally important to only invoke `init` when the QuestDB + * is empty. + * + * @returns {Promise} + */ + static async init() + { + let folder = await Utils.initializeQuestFolder(); + + // If the folder doesn't exist then simulate the content parameter. This should only ever occur for a player + // logged in when a GM activates FQL for the first time or if the _fql_quests folder is deleted. + if (!folder) + { + if (game.user.isGM) + { + console.warn('ForienQuestLog - Failed to initialize QuestDB as the quest folder / _fql_quests is missing.'); + } + + folder = { content: [] }; + } + + // Skip initialization of data if FQL is hidden from the current player. FQL is never hidden from GM level users. + if (!Utils.isFQLHiddenFromPlayers()) + { + // Cache `isTrustedPlayerEdit`. + const isTrustedPlayerEdit = Utils.isTrustedPlayerEdit(); + + // Iterate over all journal entries in `_fql_quests` folder. + const folderContents = FVTTCompat.folderContents(folder); + + for (const entry of folderContents) + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + if (!content) { continue; } + + // Retrieve the flag content for the quest and if presently observable add a new QuestEntry to QuestDB. + if (this.#isObservable(content, entry, isTrustedPlayerEdit)) + { + const quest = new Quest(content, entry); + + // Must set a QuestEntry w/ an undefined enrich as all quest data must be loaded before enrichment. + // Also set `generate` to false as the CollectJS collections are rebuilt in below. + this.#setQuestEntry(new QuestEntry(quest, void 0), false); + } + else + { + // If JE / Quest is not observable then still set a QuestPreview shim. + entry._sheet = new QuestPreviewShim(entry.id); + } + } + + // Must hydrate all QuestEntry instances after all quests have been added to `#questsMap`. Hydration will build + // the cache of various getter functions and enriched data in QuestEntry. + for (const questEntry of QuestDB.iteratorEntries()) { await this.#questEntryHydrate(questEntry); } + + // Create the CollectJS collections in build after hydration. + for (const key of Object.keys(this.#questsMap)) + { + this.#questsCollect[key] = collect(Array.from(this.#questsMap[key].values())); + } + + Hooks.callAll(QuestDB.hooks.addedAllQuestEntries); + } + + // Only add the Foundry hooks once on first initialization. + if (!this.#initialized) + { + Hooks.on('createJournalEntry', this.#handleJournalEntryCreate.bind(this)); + Hooks.on('deleteJournalEntry', this.#handleJournalEntryDelete.bind(this)); + Hooks.on('updateJournalEntry', this.#handleJournalEntryUpdate.bind(this)); + } + + this.#initialized = true; + } + + /** + * @returns {QuestDBHooks} The QuestDB hooks. + */ + static get hooks() { return this.#dbHooks; } + + /** + * @returns {FilterFunctions} Various useful filter functions. + */ + static get Filter() { return this.#fnFilter; } + + /** + * @returns {SortFunctions} Various useful sorting functions. + */ + static get Sort() { return this.#fnSort; } + + /** + * Verifies all quests by observability removing any quests from QuestDB that are no longer observable by the current + * user or adding quests that are now observable. This only really needs to occur after particular module setting + * changes which right now is when trusted player edit is enabled / disabled. + * + * @see FQLSettings.trustedPlayerEdit + */ + static async consistencyCheck() + { + const folder = Utils.getQuestFolder(); + + // Early out if the folder is not available or FQL is hidden from the current player. + if (!folder || Utils.isFQLHiddenFromPlayers()) { return; } + + // Create a single map of all QuestEntry instances. + const questEntryMap = new Map(QuestDB.getAllQuestEntries().map((e) => [e.id, e])); + + // Cache if the current player has trusted player edit capabilities. + const isTrustedPlayerEdit = Utils.isTrustedPlayerEdit(); + + // Iterate over all quests. + const folderContents = FVTTCompat.folderContents(folder); + + for (const entry of folderContents) + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + if (content) + { + // If the quest is observable attempt to retrieve it. + if (this.#isObservable(content, entry, isTrustedPlayerEdit)) + { + let questEntry = questEntryMap.get(entry.id); + + // If the quest is not retrieved, but is observable add it to the QuestDB. + if (!questEntry) + { + questEntry = new QuestEntry(new Quest(content, entry)); + this.#setQuestEntry(await this.#questEntryHydrate(questEntry)); + + Hooks.callAll(QuestDB.hooks.addQuestEntry, questEntry, entry.flags, { diff: false, render: true }, + entry.id); + } + else + { + // Otherwise update the quest with current data. + await this.#questEntryUpdate(questEntry, content, entry); + } + } + else + { + // The quest is not observable so if it is retrieved from the flat `questEntryMap` remove it from the + // QuestDB. + const questEntry = questEntryMap.get(entry.id); + if (questEntry) + { + questEntryMap.delete(entry.id); + this.#removeQuestEntry(entry.id); + + // This quest is not deleted; it has been removed from the in-memory DB. + Hooks.callAll(QuestDB.hooks.removeQuestEntry, questEntry, entry.flags, { diff: false, render: true }, + entry.id); + } + } + } + } + + // Enrich all after all updates are complete. + await this.enrichAll(); + } + + /** + * Creates a new quest and waits for the journal entry to update and QuestDB to pick up the new Quest which + * is then returned. + * + * @param {object} options - Optional parameters. + * + * @param {object} [options.data] - Quest data to assign to new quest. + * + * @param {string} [options.parentId] - Any associated parent ID; if set then this is a subquest. + * + * @returns {Promise} The newly created quest. + */ + static async createQuest({ data = {}, parentId = void 0 } = {}) + { + // Get the default ownership setting and attempt to set it if found in DOCUMENT_PERMISSION_LEVELS. + const defaultPerm = game.settings.get(constants.moduleName, settings.defaultPermission); + + const ownership = { + default: typeof CONST.DOCUMENT_OWNERSHIP_LEVELS[defaultPerm] === 'number' ? + CONST.DOCUMENT_OWNERSHIP_LEVELS[defaultPerm] : CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER + }; + + // Used for a player created quest setting and the quest as 'available' for normal players or 'hidden' for + // trusted players. + if (!game.user.isGM) + { + data.status = Utils.isTrustedPlayerEdit() ? questStatus.inactive : questStatus.available; + ownership[game.user.id] = CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + } + + const parentQuest = QuestDB.getQuest(parentId); + if (parentQuest) + { + data.parent = parentId; + } + + // Creating a new quest will add any missing data / schema. + const tempQuest = new Quest(data); + + const folder = Utils.getQuestFolder(); + if (!folder) + { + console.warn('ForienQuestLog - QuestDB.createQuest - quest folder not found.'); + return; + } + + const entry = await JournalEntry.create({ + name: tempQuest.name, + folder: folder.id, + content: '', + ownership, + flags: { + [constants.moduleName]: { + json: tempQuest.toJSON() + } + } + }); + + if (parentQuest) + { + parentQuest.addSubquest(entry.id); + await parentQuest.save(); + Socket.refreshQuestPreview({ questId: parentQuest.id }); + } + + // QuestDB Journal update hook is now async, so schedule on next microtask to be able to retrieve new quest. + return new Promise((resolve) => + { + setTimeout(() => + { + const quest = QuestDB.getQuest(entry.id); + + // Players don't see Hidden tab, but assistant GM can, so emit anyway + Socket.refreshAll(); + + resolve(quest); + }, 10); + }); + } + + /** + * Invoke with either a Quest instance or quest ID to delete the quest and update the QuestDB and parent / child + * relationships. This is an atomic sequence such that the quest is deleted via deleting the backing journal entry + * and before control resumes to the invoke point the in-memory DB also has the associated QuestEntry deleted. + * + * Please use await when deleting a quest! + * + * @param {object} options - Optional parameters. + * + * @param {Quest} [options.quest] - The Quest instance to delete. + * + * @param {string} [options.questId] - The ID of the quest instance to delete. + * + * @returns {Promise} The IDs for quests that were updated. + */ + static async deleteQuest({ quest, questId } = {}) + { + const deleteId = quest ? quest.id : questId; + + const deleteQuest = QuestDB.getQuest(deleteId); + + if (!deleteQuest) { return; } + + const parentQuest = QuestDB.getQuest(deleteQuest.parent); + let parentId = null; + + // Stores the quest IDs which have been saved and need GUI / display aspects updated. + const savedIds = []; + + // Remove this quest from any parent + if (parentQuest) + { + parentId = parentQuest.id; + parentQuest.removeSubquest(deleteId); + } + + // Update children to point to any new parent. + for (const childId of deleteQuest.subquests) + { + const childQuest = QuestDB.getQuest(childId); + if (childQuest) + { + childQuest.parent = parentId; + + await childQuest.save(); + savedIds.push(childId); + + // Update parent with new subquests. + if (parentQuest) + { + parentQuest.addSubquest(childId); + } + } + } + + // Save the parent. + if (parentQuest) + { + await parentQuest.save(); + savedIds.push(parentId); + } + + // Delete the backing quest journal entry. This will cause the `deleteJournalEntry` hook to fire and QuestDB to + // delete the QuestEntry from the QuestDB. + if (deleteQuest.entry) + { + await deleteQuest.entry.delete(); + } + + // Return the deleted and saved IDs. + return { + deleteId, + savedIds + }; + } + + /** + * Enriches all stored {@link QuestEntry} instances. This is particularly useful in various callbacks when settings + * change in {@link ModuleSettings}. + */ + static async enrichAll() + { + for (const questEntry of QuestDB.iteratorEntries()) + { + questEntry.enrich = await Enrich.quest(questEntry.quest); + } + } + + /** + * Enriches specific {@link QuestEntry} instances. This is useful in various callbacks when settings state changes + * that is not stored in the {@link Quest} itself. An example is storing the primary quest in world / + * {@link ModuleSettings}. + * + * @param {...string} questIds - The quest IDs to enrich. + */ + static async enrichQuests(...questIds) + { + for (const questId of questIds) + { + const questEntry = QuestDB.getQuestEntry(questId); + if (questEntry) + { + questEntry.enrich = await Enrich.quest(questEntry.quest); + } + } + } + + /** + * Filter the entire QuestDB, returning an Array of entries which match a functional predicate. + * + * @param {Function} predicate The functional predicate to test. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.type] - The quest type / status to iterate. + * + * @returns {QuestEntry[]} An Array of matched values. + * @see Array#filter + */ + static filter(predicate, options) + { + const entries = []; + for (const questEntry of QuestDB.iteratorEntries(options)) + { + if (predicate(questEntry)) { entries.push(questEntry); } + } + return entries; + } + + /** + * Filters the CollectJS collections and returns a single collection if status is specified otherwise filters all + * quest collections and returns a QuestCollect object with all status categories. At minimum, you must provide a + * filter function `options.filter` which will be applied across all collections otherwise you may also provide + * separate filters for each status category. + * + * @param {object} options - Optional parameters. + * + * @param {string} [options.status] - Specific quest status to return filtered. + * + * @param {Function} [options.filter] - The filter function for any quest status that doesn't have a filter + * defined. + * + * @param {Function} [options.filterActive] - The filter function for active quests. + * + * @param {Function} [options.filterAvailable] - The filter function for available quests. + * + * @param {Function} [options.filterCompleted] - The filter function for completed quests. + * + * @param {Function} [options.filterFailed] - The filter function for failed quests. + * + * @param {Function} [options.filterInactive] - The filter function for inactive quests. + * + * @returns {QuestsCollect|collect|void} An object of all QuestEntries filtered by status or individual + * status or undefined. + */ + static filterCollect({ status = void 0, filter = void 0, filterActive = void 0, filterAvailable = void 0, + filterCompleted = void 0, filterFailed = void 0, filterInactive = void 0 } = {}) + { + // A particular status is requested so only filter and return the specific collection. + if (typeof status === 'string') + { + switch (status) + { + case questStatus.active: + return this.#questsCollect[questStatus.active].filter(filterActive ?? filter); + case questStatus.available: + return this.#questsCollect[questStatus.available].filter(filterAvailable ?? filter); + case questStatus.completed: + return this.#questsCollect[questStatus.completed].filter(filterCompleted ?? filter); + case questStatus.failed: + return this.#questsCollect[questStatus.failed].filter(filterFailed ?? filter); + case questStatus.inactive: + return this.#questsCollect[questStatus.inactive].filter(filterInactive ?? filter); + default: + console.error(`Forien Quest Log - QuestDB - filterCollect - unknown status: ${status}`); + return void 0; + } + } + + // Otherwise filter all status categories and return a QuestsCollect object. + return { + active: this.#questsCollect[questStatus.active].filter(filterActive || filter), + available: this.#questsCollect[questStatus.available].filter(filterAvailable || filter), + completed: this.#questsCollect[questStatus.completed].filter(filterCompleted || filter), + failed: this.#questsCollect[questStatus.failed].filter(filterFailed || filter), + inactive: this.#questsCollect[questStatus.inactive].filter(filterInactive || filter) + }; + } + + /** + * Find an entry in the QuestDB using a functional predicate. + * + * @param {Function} predicate - The functional predicate to test. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.status] - The quest type / status to iterate. + * + * @returns {QuestEntry} The QuestEntry, if found, otherwise undefined. + * @see Array#find + */ + static find(predicate, options) + { + for (const questEntry of QuestDB.iteratorEntries(options)) + { + if (predicate(questEntry)) { return questEntry; } + } + + return void 0; + } + + /** + * Returns all QuestEntry instances. + * + * @returns {QuestEntry[]} All QuestEntry instances. + */ + static getAllQuestEntries() + { + return this.#flattenQuestsMap(); + } + + /** + * Returns all Quest instances. + * + * @returns {Quest[]} All quest instances. + */ + static getAllQuests() + { + return this.#flattenQuestsMap().map((entry) => entry.quest); + } + + /** + * Provides a quicker method to get the count of quests by quest status or all quests. + * + * @param {object} [options] - Optional parameters. If no options are provided the count of all quests is returned. + * + * @param {string} [options.status] - The quest status category to count. + * + * @returns {number} Quest count for the specified type or the count for all quests. + */ + static getCount({ status = void 0 } = {}) + { + if (status === void 0) + { + return this.#questsMap[questStatus.active].size + this.#questsMap[questStatus.available].size + + this.#questsMap[questStatus.completed].size + this.#questsMap[questStatus.failed].size + + this.#questsMap[questStatus.inactive].size; + } + + return this.#questsMap[status] ? this.#questsMap[status].size : 0; + } + + /** + * Gets the Quest by quest ID. + * + * @param {string} questId - A Foundry ID + * + * @returns {Quest|void} The Quest. + */ + static getQuest(questId) + { + const entry = this.#getQuestEntry(questId); + return entry ? entry.quest : void 0; + } + + /** + * Retrieves a QuestEntry by quest ID. + * + * @param {string} questId - A Foundry ID + * + * @returns {QuestEntry} The QuestEntry. + */ + static getQuestEntry(questId) + { + return this.#getQuestEntry(questId); + } + + /** + * Provides an iterator across the QuestEntry map of maps returning all {@link QuestEntry} instances or instances of + * a particular status category. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.status] - The quest status category to iterate. + * + * @yields {QuestEntry} The QuestEntry iterator. + */ + static *iteratorEntries({ status = void 0 } = {}) + { + if (status === void 0) + { + for (const value of this.#questsMap[questStatus.active].values()) { yield value; } + for (const value of this.#questsMap[questStatus.available].values()) { yield value; } + for (const value of this.#questsMap[questStatus.completed].values()) { yield value; } + for (const value of this.#questsMap[questStatus.failed].values()) { yield value; } + for (const value of this.#questsMap[questStatus.inactive].values()) { yield value; } + } + else if (this.#questsMap[status]) + { + for (const value of this.#questsMap[status].values()) { yield value; } + } + } + + /** + * Provides an iterator across the QuestEntry map of maps returning all {@link Quest} instances or instances of a + * particular status category. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.status] - The quest status category to iterate. + * + * @yields {Quest} The Quest iterator. + */ + static *iteratorQuests({ status = void 0 } = {}) + { + if (status === void 0) + { + for (const value of this.#questsMap[questStatus.active].values()) { yield value.quest; } + for (const value of this.#questsMap[questStatus.available].values()) { yield value.quest; } + for (const value of this.#questsMap[questStatus.completed].values()) { yield value.quest; } + for (const value of this.#questsMap[questStatus.failed].values()) { yield value.quest; } + for (const value of this.#questsMap[questStatus.inactive].values()) { yield value.quest; } + } + else if (this.#questsMap[status]) + { + for (const value of this.#questsMap[status].values()) { yield value.quest; } + } + } + + /** + * Removes all quests from the QuestDB. + */ + static removeAll() + { + this.#questsMap[questStatus.active].clear(); + this.#questsMap[questStatus.available].clear(); + this.#questsMap[questStatus.completed].clear(); + this.#questsMap[questStatus.failed].clear(); + this.#questsMap[questStatus.inactive].clear(); + + this.#questIndex.clear(); + + this.#questsCollect[questStatus.active] = collect(); + this.#questsCollect[questStatus.available] = collect(); + this.#questsCollect[questStatus.completed] = collect(); + this.#questsCollect[questStatus.failed] = collect(); + this.#questsCollect[questStatus.inactive] = collect(); + + Hooks.callAll(QuestDB.hooks.removedAllQuestEntries); + } + + /** + * Sorts the CollectJS collections and returns a single collection if status is specified otherwise sorts all + * quest collections and returns a QuestCollect object with all status categories. By default, the sort functions + * are `Sort.DATE_END` for status categories of 'completed' / 'failed' and `Sort.ALPHA` for all other + * categories. + * + * @param {object} options - Optional parameters. + * + * @param {string} [options.status] - Quest status to return sorted. + * + * @param {Function} [options.sortActive] - The sort function for active quests. + * + * @param {Function} [options.sortAvailable] - The sort function for available quests. + * + * @param {Function} [options.sortCompleted] - The sort function for completed quests. + * + * @param {Function} [options.sortFailed] - The sort function for failed quests. + * + * @param {Function} [options.sortInactive] - The sort function for inactive quests. + * + * @returns {QuestsCollect|Collection|void} An object of all QuestEntries sorted by status or individual + * status. + */ + static sortCollect({ status = void 0, sortActive = this.Sort.ALPHA, sortAvailable = this.Sort.ALPHA, + sortCompleted = this.Sort.DATE_END, sortFailed = this.Sort.DATE_END, sortInactive = this.Sort.ALPHA } = {}) + { + if (typeof status === 'string') + { + switch (status) + { + case questStatus.active: + return this.#questsCollect[questStatus.active].sort(sortActive); + case questStatus.available: + return this.#questsCollect[questStatus.available].sort(sortAvailable); + case questStatus.completed: + return this.#questsCollect[questStatus.completed].sort(sortCompleted); + case questStatus.failed: + return this.#questsCollect[questStatus.failed].sort(sortFailed); + case questStatus.inactive: + return this.#questsCollect[questStatus.inactive].sort(sortInactive); + default: + console.error(`Forien Quest Log - QuestDB - sortCollect - unknown status: ${status}`); + return void 0; + } + } + + return { + active: this.#questsCollect[questStatus.active].sort(sortActive), + available: this.#questsCollect[questStatus.available].sort(sortAvailable), + completed: this.#questsCollect[questStatus.completed].sort(sortCompleted), + failed: this.#questsCollect[questStatus.failed].sort(sortFailed), + inactive: this.#questsCollect[questStatus.inactive].sort(sortInactive) + }; + } + + // Foundry CRUD hook callbacks ------------------------------------------------------------------------------------ + + /** + * Foundry hook callback when a new JournalEntry is created. For quests there are two cases to consider. The first + * is straight forward when a new quest is created from FQL. The second case is a bit more challenging and that + * occurs when a journal entry / quest is imported from a compendium. In this case we need to scrub the subquests + * that may no longer resolve to valid journal entries in the system. + * + * @param {JournalEntry} entry - A journal entry. + * + * @param {object} options - The create document options. + * + * @param {string} id - journal entry ID. + */ + static async #handleJournalEntryCreate(entry, options, id) + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + // Exit early if no FQL quest data is available. + if (!content) { return; } + + // Process the quest content if it is currently observable and FQL is not hidden from the current user. + if (this.#isObservable(content, entry) && !Utils.isFQLHiddenFromPlayers()) + { + const quest = new Quest(content, entry); + + const questEntry = new QuestEntry(quest); + this.#setQuestEntry(await this.#questEntryHydrate(questEntry)); + + Hooks.callAll(QuestDB.hooks.createQuestEntry, questEntry, options, id); + + // At this point a new quest will not have subquests, but an imported journal entry / quest from a compendium + // may have subquests. These may not resolve to any existing journal entries, so we scrub any non-resolving + // subquests. + if (quest.subquests.length > 0) + { + const removeSubs = []; + + // First push any subquest IDs that don't resolve to journal entries in `removeSubs`. + for (const subquest of quest.subquests) + { + if (!game.journal.get(subquest)) { removeSubs.push(subquest); } + } + + // Remove the non-resolving subquests from the quest. + for (const removeSub of removeSubs) + { + const index = quest.subquests.indexOf(removeSub); + if (index > -1) { quest.subquests.splice(index, 1); } + } + + // And save the quest. This will cause an update to occur and `#handleJournalEntryUpdate` will hydrate the + // change. + if (removeSubs.length > 0) { await quest.save(); } + } + } + else + { + // If JE / Quest is not observable then still set a QuestPreview shim. + entry._sheet = new QuestPreviewShim(entry.id); + } + } + + /** + * Process the Foundry hook for journal entry deletion. + * + * @param {JournalEntry} entry - Deleted journal entry. + * + * @param {object} options - The delete document options. + * + * @param {string} id - Journal entry ID. + * + * @returns {Promise} + */ + static async #handleJournalEntryDelete(entry, options, id) + { + // If the QuestEntry can be retrieved by this journal entry ID then remove it from the QuestDB. + const questEntry = this.#getQuestEntry(entry.id); + if (questEntry && this.#removeQuestEntry(entry.id)) + { + Hooks.callAll(QuestDB.hooks.deleteQuestEntry, questEntry, options, id); + + const quest = questEntry.quest; + const savedIds = quest.parent ? [quest.parent, ...quest.subquests] : [...quest.subquests]; + + // Send the delete quest socket message to all clients. + await Socket.deletedQuest({ + deleteId: entry.id, + savedIds + }); + + Socket.refreshAll(); + } + } + + /** + * Handles the Foundry update JournalEntry hook. If Quest content is retrieved from the flags process it for + * observability changes or update the associated QuestEntry if already in the QuestDB. + * + * @param {JournalEntry} entry - A journal entry. + * + * @param {object} flags - Journal entry flags. + * + * @param {object} options - The update document options. + * + * @param {string} id - The journal entry ID. + */ + static async #handleJournalEntryUpdate(entry, flags, options, id) + { + const content = entry.getFlag(constants.moduleName, constants.flagDB); + + if (content) + { + let questEntry = this.#getQuestEntry(entry.id); + + // Is the quest currently observable and not hidden from the current user. + const isObservable = this.#isObservable(content, entry) && !Utils.isFQLHiddenFromPlayers(); + + if (questEntry) + { + // If the QuestEntry already exists in the QuestDB and is observable then update it. + if (isObservable) + { + await this.#questEntryUpdate(questEntry, content, entry); + Hooks.callAll(QuestDB.hooks.updateQuestEntry, questEntry, flags, options, id); + } + else // Else remove it from the QuestDB (this is not a deletion). + { + this.#removeQuestEntry(questEntry.id); + + // Must hydrate any parent on a change. + if (typeof questEntry.quest.parent === 'string') + { + const parentEntry = this.#getQuestEntry(questEntry.quest.parent); + if (parentEntry) { await this.#questEntryHydrate(parentEntry); } + } + + // Must hydrate any subquests on a change. + for (const subquest of questEntry.quest.subquests) + { + const subquestEntry = this.#getQuestEntry(subquest); + if (subquestEntry) { await this.#questEntryHydrate(subquestEntry); } + } + + // This quest is not deleted; it has been removed from the in-memory DB. + Hooks.callAll(QuestDB.hooks.removeQuestEntry, questEntry, flags, options, id); + } + } + else if (isObservable) // The Quest is not in the QuestDB and is observable so add it. + { + questEntry = new QuestEntry(new Quest(content, entry)); + this.#setQuestEntry(await this.#questEntryHydrate(questEntry)); + + // Must hydrate any parent on a change. + if (typeof questEntry.quest.parent === 'string') + { + const parentEntry = this.#getQuestEntry(questEntry.quest.parent); + if (parentEntry) { await this.#questEntryHydrate(parentEntry); } + } + + // Must hydrate any subquests on a change. + for (const subquest of questEntry.quest.subquests) + { + const subquestEntry = this.#getQuestEntry(subquest); + if (subquestEntry) { await this.#questEntryHydrate(subquestEntry); } + } + + Hooks.callAll(QuestDB.hooks.addQuestEntry, questEntry, flags, options, id); + } + else + { + // If JE / Quest is not observable then still set a QuestPreview shim. + entry._sheet = new QuestPreviewShim(entry.id); + } + } + } + + // Internal implementation ---------------------------------------------------------------------------------------- + + /** + * Flattens the QuestEntry map of maps into and array of all entries. + * + * Please see {@link QuestDB.iteratorEntries} for an iterator across all entries. + * + * @returns {QuestEntry[]} An array of all QuestEntry values stored. + */ + static #flattenQuestsMap() + { + return [ + ...this.#questsMap[questStatus.active].values(), + ...this.#questsMap[questStatus.available].values(), + ...this.#questsMap[questStatus.completed].values(), + ...this.#questsMap[questStatus.failed].values(), + ...this.#questsMap[questStatus.inactive].values() + ]; + } + + /** + * @param {string} questId - The Quest / JournalEntry ID. + * + * @returns {QuestEntry} The stored QuestEntry. + */ + static #getQuestEntry(questId) + { + const currentStatus = this.#questIndex.get(questId); + return currentStatus && this.#questsCollect[currentStatus] ? this.#questsMap[currentStatus].get(questId) : void 0; + } + + /** + * Provides the observability test for a quest based on the user level and permissions of the backing journal entry. + * GM level users always can observe any quests. Trusted players w/ the module setting + * {@link FQLSettings.trustedPlayerEdit} enabled and the owner of the quest can observe quests in the inactive status. + * Otherwise, quests are only observable by players when the default or personal permission is + * {@link CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER} or higher. + * + * @param {QuestData} content - The serialized Quest data stored in the journal entry. + * + * @param {JournalEntry} entry - The backing journal entry. + * + * @param {boolean} [isTrustedPlayerEdit] - Is the user trusted and is the module setting to edit granted. + * + * @returns {boolean} Is quest observable by the current user? + */ + static #isObservable(content, entry, isTrustedPlayerEdit = Utils.isTrustedPlayerEdit()) + { + let isObservable; + + if (game.user.isGM) + { + isObservable = true; + } + else + { + const isInactive = questStatus.inactive === content.status; + + // Special handling for trusted player edit who can only see owned quests in the hidden / inactive category. + if (isTrustedPlayerEdit && isInactive) + { + isObservable = entry.isOwner; + } + else + { + // Otherwise no one can see hidden / inactive quests; perform user permission check for observer. + isObservable = !isInactive && entry.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER); + } + } + + return isObservable; + } + + /** + * Removes a QuestEntry by quest ID. Removes the quest index and then removes the QuestEntry from the map of maps + * If this results in a deletion and generate is true then rebuild the QuestEntry Collection. + * + * @param {string} questId - The Quest ID to delete. + * + * @param {boolean} [generate=true] - Generate the associated QuestEntry Collection. + * + * @returns {boolean} Whether a QuestEntry was deleted. + */ + static #removeQuestEntry(questId, generate = true) + { + const currentStatus = this.#questIndex.get(questId); + this.#questIndex.delete(questId); + + let result = false; + + if (this.#questsMap[currentStatus]) + { + result = this.#questsMap[currentStatus].delete(questId); + } + + if (result && generate) + { + this.#questsCollect[currentStatus] = collect(Array.from(this.#questsMap[currentStatus].values())); + } + + return result; + } + + /** + * Sets the QuestEntry by current status and regenerates any CollectJS collection if the status changes. + * + * @param {QuestEntry} entry - QuestEntry to set. + * + * @param {boolean} [generate=true] - Regenerate `#questsCollect`. + */ + static #setQuestEntry(entry, generate = true) + { + // Retrieve the current status from the quest entry index map. + const currentStatus = this.#questIndex.get(entry.id); + + // If defined and current status is different from the incoming QuestEntry status then delete the QuestEntry from + // the old map of map bin. + if (this.#questsMap[currentStatus] && currentStatus !== entry.status) + { + // If the delete action is successful and generate is true then regenerate the CollectJS collection of the + // old status. + if (this.#questsMap[currentStatus].delete(entry.id) && generate) + { + this.#questsCollect[currentStatus] = collect(Array.from(this.#questsMap[currentStatus].values())); + } + } + + if (!this.#questsMap[entry.status]) + { + console.error(`ForienQuestLog - QuestDB - set quest error - unknown status: ${entry.status}`); + return; + } + + // Set the quest index by quest id and new status and set the map of maps entry. + this.#questIndex.set(entry.id, entry.status); + this.#questsMap[entry.status].set(entry.id, entry); + + // If generate is true regenerate the new entry status CollectJS collection. + if (generate) + { + this.#questsCollect[entry.status] = collect(Array.from(this.#questsMap[entry.status].values())); + } + } + + /** + * Hydrates this QuestEntry caching the enriched data and several getter values from Quest. + * + * @param {QuestEntry} questEntry - Target quest entry. + * + * @returns {QuestEntry} This QuestEntry. + */ + static async #questEntryHydrate(questEntry) + { + questEntry.id = questEntry.quest.id; + questEntry.status = questEntry.quest.status; + + /** + * @type {boolean} + */ + questEntry.isActive = questEntry.quest.isActive; + + /** + * @type {boolean} + */ + questEntry.isHidden = questEntry.quest.isHidden; + + /** + * @type {boolean} + */ + questEntry.isInactive = questEntry.quest.isInactive; + + /** + * @type {boolean} + */ + questEntry.isObservable = questEntry.quest.isObservable; + + /** + * @type {boolean} + */ + questEntry.isOwner = questEntry.quest.isOwner; + + /** + * @type {boolean} + */ + questEntry.isPersonal = questEntry.quest.isPersonal; + + /** + * Stores all adjacent quest IDs including any parent, subquests, and this quest. + * + * @type {string[]} + */ + questEntry.questIds = questEntry.quest.getQuestIds(); + + /** + * @type {EnrichData} + */ + questEntry.enrich = await Enrich.quest(questEntry.quest); + + return questEntry; + } + + /** + * Updates an existing {@link QuestEntry} when the backing quest data changes. + * + * @param {QuestEntry} questEntry - Target quest entry. + * + * @param {QuestData} content - The FQL quest data from journal entry. + * + * @param {JournalEntry} entry - The backing journal entry. + * + * @returns {Promise} Was `#setQuestEntry` invoked. + */ + static async #questEntryUpdate(questEntry, content, entry) + { + questEntry.quest.entry = entry; + questEntry.quest.initData(content); + const status = questEntry.status; + await this.#questEntryHydrate(questEntry); + + // Must hydrate any parent on a change. + if (typeof questEntry.quest.parent === 'string') + { + const parentEntry = QuestDB.getQuestEntry(questEntry.quest.parent); + if (parentEntry) { await this.#questEntryHydrate(parentEntry); } + } + + // Must hydrate any subquests on a change. + for (const subquest of questEntry.quest.subquests) + { + const subquestEntry = QuestDB.getQuestEntry(subquest); + if (subquestEntry) { await this.#questEntryHydrate(subquestEntry); } + } + + if (status !== questEntry.quest.status) + { + this.#setQuestEntry(questEntry); + return true; + } + + return false; + } +} + +/** + * Provides the internal object stored in the QuestDB that contains the Quest and enriched data along with + * several public member variables that are cached from the Quest on any update allowing quick sorting. + */ +class QuestEntry +{ + /** + * @param {Quest} quest - The Quest object + * + * @param {EnrichData} [enrich] - The enriched Quest data. If not set be sure to hydrate. + */ + constructor(quest, enrich = void 0) + { + /** + * @type {string} + */ + this.id = quest.id; + + /** + * @type {string} + */ + this.status = quest.status; + + /** + * @type {Quest} + */ + this.quest = quest; + + /** + * @type {EnrichData} + */ + this.enrich = enrich; + + // Set in `#questEntryHydrate`. + + /** + * @type {boolean} + */ + this.isActive = void 0; + + /** + * @type {boolean} + */ + this.isHidden = void 0; + + /** + * @type {boolean} + */ + this.isInactive = void 0; + + /** + * @type {boolean} + */ + this.isObservable = void 0; + + /** + * @type {boolean} + */ + this.isOwner = void 0; + + /** + * @type {boolean} + */ + this.isPersonal = void 0; + + /** + * Stores all adjacent quest IDs including any parent, subquests, and this quest. + * + * @type {string[]} + */ + this.questIds = void 0; + } +} + +/** + * @typedef {object} DeleteData The data object returned from `delete` indicating which quests were updated. + * + * @property {string} deleteId - This quest ID which was deleted. + * + * @property {string[]} savedIds - The quest IDs of any parent / subquests that were updated. + */ + +/** + * @typedef {object} FilterFunctions + * + * @property {Function} IS_OBSERVABLE - Filters by `isObservable` cached in QuestEntry. + */ + +/** + * @typedef {object} QuestDBHooks + * + * @property {string} addedAllQuestEntries Invoked in {@link QuestDB.init} when all quests have been loaded. + * + * @property {string} addQuestEntry Invoked in {@link QuestDB.consistencyCheck} and `#handleJournalEntryUpdate` when a + * quest is added to the {@link QuestDB}. + * + * @property {string} createQuestEntry Invoked in `#handleJournalEntryCreate` in {@link QuestDB} when a quest is + * created. + * + * @property {string} deleteQuestEntry Invoked in `#handleJournalEntryDelete` in {@link QuestDB} when a quest is + * deleted. + * + * @property {string} removedAllQuestEntries Invoked in {@link QuestDB.removeAll} when all quests are removed. + * + * @property {string} removeQuestEntry Invoked in {@link QuestDB.consistencyCheck} and `#handleJournalEntryUpdate` + * when a quest is removed from the {@link QuestDB}. + * + * @property {string} updateQuestEntry - Invoked in `#handleJournalEntryUpdate` when a quest is updated in + * {@link QuestDB}. + */ + +/** + * @typedef {object} SortFunctions + * + * @property {Function} ALPHA Sort by quest name. + * + * @property {Function} DATE_CREATE Sort by quest creation date. + * + * @property {Function} DATE_END Sort by quest end date. When status is 'completed' or 'failed'. + * + * @property {Function} DATE_START Sort by quest start date. When status is 'active'. + */ + +/** + * @typedef {Record>} QuestsCollect Returns an object with keys indexed by + * {@link questStatus} of CollectJS collections of QuestEntry instances. + * + * @property {Collection} active Active quest entries CollectJS collections. + * + * @property {Collection} available Available quests entries CollectJS collections. + * + * @property {Collection} completed Completed quests entries CollectJS collections. + * + * @property {Collection} failed Failed quests entries CollectJS collections. + * + * @property {Collection} hidden Hidden quests entries CollectJS collections. + */ \ No newline at end of file diff --git a/src/control/index.js b/src/control/index.js new file mode 100644 index 00000000..f924ff99 --- /dev/null +++ b/src/control/index.js @@ -0,0 +1,6 @@ +export * from './db/QuestDB.js'; +export * from './ui/index.js'; +export * from './util/index.js'; +export * from './FQLHooks.js'; +export * from './ModuleSettings.js'; +export * from './Socket.js'; \ No newline at end of file diff --git a/src/control/public/QuestAPI.js b/src/control/public/QuestAPI.js new file mode 100644 index 00000000..9dee7b01 --- /dev/null +++ b/src/control/public/QuestAPI.js @@ -0,0 +1,96 @@ +import { QuestDBShim } from './QuestDBShim.js'; + +import { + Socket, + ViewManager } from '../index.js'; + +import { + constants, + settings } from '../../model/constants.js'; + +/** + * Quest public API. QuestAPI exposes control capabilities publicly. This functionality is gated as necessary depending + * on user level, quest observability and module settings. + * + * A shim to the {@link QuestDB} is available via {@link QuestAPI.DB} which exposes certain QuestDB methods that are + * available for any player as only currently observable quests are loaded into QuestDB. + */ +class QuestAPI +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * @returns {QuestDBShim} Public QuestDB access. + */ + static get DB() { return QuestDBShim; } + + /** + * Opens the Quest sheet / QuestPreview for the given questID. A check for the module setting + * {@link FQLSettings.hideFQLFromPlayers} provides an early out if FQL is hidden from players causing the sheet to + * not render. {@link ViewManager.questPreview} provides an object. + * + * @param {object} options - Optional parameters. + * + * @param {string} options.questId - Quest ID string to open. + * + * @param {boolean} [options.notify=true] - Post UI notification on any error. + */ + static open({ questId, notify = true }) + { + if (!game.user.isGM && game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) { return; } + + try + { + const questPreview = ViewManager.questPreview.get(questId); + + // Optimization to render an existing open QuestPreview with the given quest ID instead of opening a new + // app / view. + if (questPreview !== void 0) + { + questPreview.render(true, { focus: true }); + return; + } + + const quest = QuestDBShim.getQuest(questId); + + if (quest === void 0) + { + if (notify) + { + ViewManager.notifications.warn(game.i18n.localize('ForienQuestLog.Notifications.CannotOpen')); + } + else + { + Socket.userCantOpenQuest(); + } + return; + } + + if (quest.isObservable) + { + quest.sheet.render(true, { focus: true }); + } + } + catch (error) + { + if (notify) + { + ViewManager.notifications.error(game.i18n.localize('ForienQuestLog.Notifications.CannotOpen')); + } + else + { + Socket.userCantOpenQuest(); + } + } + } +} + +Object.freeze(QuestAPI); + +export { QuestAPI }; \ No newline at end of file diff --git a/src/control/public/QuestDBShim.js b/src/control/public/QuestDBShim.js new file mode 100644 index 00000000..0c24972b --- /dev/null +++ b/src/control/public/QuestDBShim.js @@ -0,0 +1,227 @@ +import { QuestDB } from '../index.js'; + +import { + constants, + settings } from '../../model/constants.js'; + +/** + * Provides a shim to the publicly exposed methods of QuestDB. Except for {@link QuestDBShim.createQuest} all other + * methods can be exposed without gating as the QuestDB only loads in-memory quests that are observable to the current + * user. + */ +class QuestDBShim +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * @returns {QuestDBHooks} The QuestDB hooks. + */ + static get hooks() { return QuestDB.hooks; } + + /** + * Creates a new quest and waits for the journal entry to update and QuestDB to pick up the new Quest which + * is returned. + * + * @param {object} options - Optional parameters. + * + * @param {object} [options.data] - Quest data to assign to new quest. + * + * @param {string} [options.parentId] - Any associated parent ID; if set then this is a subquest. + * + * @returns {Promise} The newly created quest. + */ + static async createQuest(options) + { + if (game.user.isGM) { return QuestDB.createQuest(options); } + + return game.settings.get(constants.moduleName, settings.allowPlayersCreate) && + !game.settings.get(constants.moduleName, settings.hideFQLFromPlayers) ? QuestDB.createQuest(options) : null; + } + + /** + * Filter the entire QuestDB, returning an Array of entries which match a functional predicate. + * + * @param {Function} predicate The functional predicate to test. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.type] - The quest type / status to iterate. + * + * @returns {QuestEntry[]} An Array of matched values + * @see Array#filter + */ + static filter(predicate, options) + { + return QuestDB.filter(predicate, options); + } + + /** + * Filters the CollectJS collections and returns a single collection if status is specified otherwise filters all + * quest collections and returns a QuestCollect object with all status categories. At minimum you must provide a + * filter function `options.filter` which will be applied across all collections otherwise you may also provide + * separate filters for each status category. + * + * @param {object} options - Optional parameters. + * + * @param {string} [options.type] - Specific quest status to return filtered. + * + * @param {Function} [options.filter] - The filter function for any quest status that doesn't have a filter + * defined. + * + * @param {Function} [options.filterActive] - The filter function for active quests. + * + * @param {Function} [options.filterAvailable] - The filter function for available quests. + * + * @param {Function} [options.filterCompleted] - The filter function for completed quests. + * + * @param {Function} [options.filterFailed] - The filter function for failed quests. + * + * @param {Function} [options.filterInactive] - The filter function for inactive quests. + * + * @returns {QuestsCollect|collect|void} An object of all QuestEntries filtered by status or individual + * status or undefined. + */ + static filterCollect(options) + { + return QuestDB.filterCollect(options); + } + + /** + * Find an entry in the QuestDB using a functional predicate. + * + * @param {Function} predicate - The functional predicate to test. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.type] - The quest type / status to iterate. + * + * @returns {QuestEntry} The QuestEntry, if found, otherwise undefined. + * @see Array#find + */ + static find(predicate, options) + { + return QuestDB.find(predicate, options); + } + + /** + * Returns all QuestEntry instances. + * + * @returns {QuestEntry[]} All QuestEntry instances. + */ + static getAllQuestEntries() + { + return QuestDB.getAllQuestEntries(); + } + + /** + * Returns all Quest instances. + * + * @returns {Quest[]} All quest instances. + */ + static getAllQuests() + { + return QuestDB.getAllQuests(); + } + + /** + * Provides a quicker method to get the count of quests by quest type / status or all quests. + * + * @param {object} [options] - Optional parameters. If no options are provided the count of all quests is returned. + * + * @param {string} [options.status] - The quest status category to count. + * + * @returns {number} Quest count for the specified type or the count for all quests. + */ + static getCount(options) + { + return QuestDB.getCount(options); + } + + /** + * Gets the Quest by quest ID. + * + * @param {string} questId - A Foundry ID + * + * @returns {Quest|void} The Quest or null. + */ + static getQuest(questId) + { + return QuestDB.getQuest(questId); + } + + /** + * Retrieves a QuestEntry by quest ID. + * + * @param {string} questId - A Foundry ID + * + * @returns {QuestEntry|null} The QuestEntry or null. + */ + static getQuestEntry(questId) + { + return QuestDB.getQuestEntry(questId); + } + + /** + * Provides an iterator across the QuestEntry map of maps. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.status] - The quest status category to iterate. + * + * @returns {Generator} A QuestEntry iterator. + */ + static iteratorEntries(options) + { + return QuestDB.iteratorEntries(options); + } + + /** + * Provides an iterator across the QuestEntry map of maps. + * + * @param {object} [options] - Optional parameters. If no options are provided the iteration occurs across all + * quests. + * + * @param {string} [options.status] - The quest status category to iterate. + * + * @returns {Generator} A QuestEntry iterator. + */ + static iteratorQuests(options) + { + return QuestDB.iteratorQuests(options); + } + + /** + * @param {object} options - Optional parameters. + * + * @param {string} [options.status] - Quest status to return sorted. + * + * @param {Function} [options.sortActive] - The sort function for active quests. + * + * @param {Function} [options.sortAvailable] - The sort function for available quests. + * + * @param {Function} [options.sortCompleted] - The sort function for completed quests. + * + * @param {Function} [options.sortFailed] - The sort function for failed quests. + * + * @param {Function} [options.sortInactive] - The sort function for inactive quests. + * + * @returns {QuestsCollect|collect|void} The complete sorted quests or just a particular quest status. + */ + static sortCollect(options) + { + return QuestDB.sortCollect(options); + } +} + +Object.freeze(QuestDBShim); + +export { QuestDBShim }; \ No newline at end of file diff --git a/src/control/public/index.js b/src/control/public/index.js new file mode 100644 index 00000000..e43c22f3 --- /dev/null +++ b/src/control/public/index.js @@ -0,0 +1 @@ +export * from './QuestAPI.js'; \ No newline at end of file diff --git a/src/control/ui/FoundryUIManager.js b/src/control/ui/FoundryUIManager.js new file mode 100644 index 00000000..942731cb --- /dev/null +++ b/src/control/ui/FoundryUIManager.js @@ -0,0 +1,413 @@ +import { ViewManager } from './ViewManager.js'; + +import { QuestTracker } from '../../view/index.js'; + +import { + constants, + settings } from '../../model/constants.js'; + +/** + * Defines a rectangle with essential contains check. Used to define the pinning rectangle next to the + * upper left of the sidebar. + */ +class FQLRect extends DOMRect +{ + /** + * Tests if the point is contained by this FQLRect. + * + * @param {number} x - Point X + * + * @param {number} y - Point Y + * + * @returns {boolean} Is point contained in rectangle. + */ + contains(x, y) + { + return this.x <= x && x <= this.x + this.width && this.y <= y && y <= this.y + this.height; + } +} + +/** + * Manages the state of the Foundry UI elements including the {@link Hotbar}, {@link SceneNavigation} and + * {@link Sidebar} providing management of the {@link QuestTracker}. Controls pinning the QuestTracker to the sidebar + * and modifications to the SceneNavigation width when pinned. + */ +export class FoundryUIManager +{ + /** + * Buffer space between sidebar and right side of quest tracker. + * + * @type {number} + */ + static #bufferSpaceX = 8; + + /** + * Buffer space between hotbar and bottom side of quest tracker. + * + * @type {number} + */ + static #bufferSpaceY = 8; + + /** + * Buffer space for the navigation bar. + * + * @type {number} + */ + static #bufferSpaceNavX = 22; + + /** + * Defines the left-hand UI control note buttons. + * + * @type {object[]} + */ + static #noteControls = [ + { + name: constants.moduleName, + title: 'ForienQuestLog.QuestLog.Title', + icon: 'fas fa-scroll', + visible: true, + onClick: () => ViewManager.questLog.render(true, { focus: true }), + button: true + }, + { + name: 'forien-quest-log-floating-window', + title: 'ForienQuestLog.QuestTracker.Title', + icon: 'fas fa-tasks', + visible: true, + onClick: async () => { await game.settings.set(constants.moduleName, settings.questTrackerEnable, true); }, + button: true + } + ]; + + /** + * Stores the constraints and other state tracked from various Foundry UI elements. + * + * @type {object} + */ + static #uiState = { + /** + * Stores the bounds of the hotbar. + */ + hotbar: { + gapX: -1, + gapY: -1, + top: -1, + left: -1, + width: -1, + height: -1 + }, + + /** + * Stores the navigation element parameters. + */ + navigation: { + left: '' + }, + + /** + * Stores the state of the sidebar. + */ + sidebar: { + currentCollapsed: false, + + collapsed: { + gapX: -1, + gapY: -1, + top: -1, + left: -1, + width: -1, + height: -1, + rectDock: new FQLRect(0, 0, 15, 30) + }, + + open: { + gapX: -1, + gapY: -1, + top: -1, + left: -1, + width: -1, + height: -1, + rectDock: new FQLRect(0, 0, 15, 30) + } + } + }; + + /** + * @returns {object[]} The left-hand UI note control button data. + */ + static get noteControls() + { + return this.#noteControls; + } + + /** + * Registers browser window resize event callback and Foundry render Hook for {@link SceneNavigation} and + * {@link QuestTracker}. + */ + static init() + { + window.addEventListener('resize', this.#handleWindowResize); + Hooks.on('collapseSidebar', this.collapseSidebar); + Hooks.on('renderSceneNavigation', this.updateTrackerPinned); + Hooks.on('renderQuestTracker', this.#handleQuestTrackerRendered); + + FoundryUIManager.#uiState.sidebar.currentCollapsed = ui?.sidebar?._collapsed || false; + this.#storeState(); + + FoundryUIManager.updateTrackerPinned(); + } + + /** + * Check the position against the sidebar and hotbar. + * + * @param {object} position - The complete position with top, left, width, height keys. + * + * @returns {boolean} True if the new position is within the sidebar pinned rectangle. + */ + static checkPosition(position) + { + const sidebarData = FoundryUIManager.#uiState.sidebar.currentCollapsed ? + FoundryUIManager.#uiState.sidebar.collapsed : FoundryUIManager.#uiState.sidebar.open; + + const tracker = ViewManager.questTracker; + + // Detect if the new position overlaps with the sidebar. + if (sidebarData.gapX >= 0 && position.left + tracker.position.width > sidebarData.left - + FoundryUIManager.#bufferSpaceX) + { + // This is a resize width change, so limit the new position width to the sidebar left side. + if (position.resizeWidth) + { + position.width = sidebarData.left - FoundryUIManager.#bufferSpaceX - position.left; + } + else // Otherwise move the new position to the left pinning the position to the sidebar left. + { + position.left = sidebarData.left - FoundryUIManager.#bufferSpaceX - tracker.position.width; + if (position.left < 0) { position.left = 0; } + } + } + + // If not pinned adjust the position top based on the hotbar top. + if (!tracker.pinned && FoundryUIManager.#uiState.hotbar.gapY >= 0 && + position.top + position.height > FoundryUIManager.#uiState.hotbar.top) + { + if (position.resizeHeight) + { + position.height = FoundryUIManager.#uiState.hotbar.top - FoundryUIManager.#bufferSpaceY - position.top; + tracker.position.height = position.height; + } + else + { + position.top = FoundryUIManager.#uiState.hotbar.top - FoundryUIManager.#bufferSpaceY - position.height; + if (position.top < 0) { position.top = 0; } + } + } + + // If pinned always make sure the position top is the sidebar top. + if (tracker.pinned) { position.top = sidebarData.top; } + + return sidebarData.rectDock.contains(position.left + position.width, position.top); + } + + /** + * The `collapseSidebar` Hook callback. Store the new state and update the tracker. + * + * @param {Sidebar} sidebarUI - The Foundry Sidebar. + * + * @param {boolean} collapsed - The sidebar collapsed state. + */ + static collapseSidebar(sidebarUI, collapsed) + { + FoundryUIManager.#uiState.sidebar.currentCollapsed = collapsed; + FoundryUIManager.#storeState(); + FoundryUIManager.updateTracker(); + } + + /** + * Updates the tracker bounds based on pinned state and invokes {@link QuestTracker.setPosition} if changes occur. + */ + static updateTracker() + { + const tracker = ViewManager.questTracker; + + // Make sure the tracker is rendered or rendering. + if (!tracker.rendered && Application.RENDER_STATES.RENDERING !== tracker._state) { return; } + + const sidebarData = FoundryUIManager.#uiState.sidebar.currentCollapsed ? + FoundryUIManager.#uiState.sidebar.collapsed : FoundryUIManager.#uiState.sidebar.open; + + // Store the current position before any modification. + const position = { + pinned: false, + top: tracker.position.top, + left: tracker.position.left, + width: tracker.position.width, + height: tracker.position.height + }; + + // If the tracker is pinned set the top / left based on the sidebar. + if (tracker.pinned) + { + position.top = sidebarData.top; + position.left = sidebarData.left - tracker.position.width - FoundryUIManager.#bufferSpaceX; + } + else // Make sure the tracker isn't overlapping the sidebar or hotbar. + { + const trackerRight = tracker.position.left + tracker.position.width; + if (trackerRight > sidebarData.left - FoundryUIManager.#bufferSpaceX) + { + position.left = sidebarData.left - tracker.position.width - FoundryUIManager.#bufferSpaceX; + + if (position.left < 0) { position.left = 0; } + } + + const trackerBottom = tracker.position.top + tracker.position.height; + if (trackerBottom > FoundryUIManager.#uiState.hotbar.top - FoundryUIManager.#bufferSpaceY) + { + position.top = FoundryUIManager.#uiState.hotbar.top - tracker.position.height - + FoundryUIManager.#bufferSpaceY; + + if (position.top < 0) { position.top = 0; } + } + } + + // Only post a position change if there are modifications. + if (position.top !== tracker.position.top || position.left !== tracker.position.left || + position.width !== tracker.position.width || position.height !== tracker.position.height) + { + tracker.setPosition(position); + } + } + + /** + * Updates state when the quest tracker is pinned / unpinned. Currently manipulates the Foundry + * {@link SceneNavigation} component width so that it doesn't overlap the pinned quest tracker. + */ + static updateTrackerPinned() + { + const tracker = ViewManager.questTracker; + const pinned = tracker.pinned; + const sidebarData = FoundryUIManager.#uiState.sidebar.open; + + let width = FoundryUIManager.#uiState.navigation.left + sidebarData.width + FoundryUIManager.#bufferSpaceNavX; + width += pinned ? tracker.position.width : 0; + ui?.nav?.element?.css('width', `calc(100% - ${width}px`); + } + + /** + * Unregisters browser window event callback and Foundry render hook for {@link QuestTracker}. + */ + static unregister() + { + window.removeEventListener('resize', this.#handleWindowResize); + Hooks.off('collapseSidebar', FoundryUIManager.collapseSidebar); + Hooks.off('renderSceneNavigation', FoundryUIManager.updateTrackerPinned); + Hooks.off('renderQuestTracker', this.#handleQuestTrackerRendered); + } + + // Internal Implementation ---------------------------------------------------------------------------------------- + + /** + * Invokes `updateTracker` when the QuestTracker is rendered. + * + * @param {Application} app - The Application instance being rendered. + */ + static #handleQuestTrackerRendered(app) + { + if (app instanceof QuestTracker) { FoundryUIManager.updateTracker(); } + } + + /** + * Callback for window resize events. Update tracker position. + */ + static #handleWindowResize() + { + FoundryUIManager.#storeState(); + FoundryUIManager.updateTracker(); + } + + /** + * Stores the current Foundry UI calculated bounds state. + */ + static #storeState() + { + const sidebarElem = ui?.sidebar?.element[0]; + const sidebarRect = sidebarElem?.getBoundingClientRect(); + + const navLeft = ui?.nav?.element?.css('left'); + if (typeof navLeft === 'string') { FoundryUIManager.#uiState.navigation.left = parseInt(navLeft, 10); } + + if (sidebarRect) + { + const sidebarData = FoundryUIManager.#uiState.sidebar.currentCollapsed ? + FoundryUIManager.#uiState.sidebar.collapsed : FoundryUIManager.#uiState.sidebar.open; + + // Store gapX / gapY calculating including any ::before elements if it has not already been set. + // This is only calculated one time on startup. + if (sidebarData.gapX < 0) + { + let beforeWidth; + let beforeHeight; + try + { + const style = window.getComputedStyle(sidebarElem, 'before'); + + const width = parseInt(style.getPropertyValue('width'), 10); + if (!Number.isNaN(width)) { beforeWidth = width; } + + const height = parseInt(style.getPropertyValue('height'), 10); + if (!Number.isNaN(height)) { beforeHeight = height; } + } + catch (err) { /**/ } + + sidebarData.gapX = beforeWidth && beforeWidth > sidebarRect.width ? beforeWidth - sidebarRect.width : 0; + + sidebarData.gapY = beforeHeight && beforeHeight > sidebarRect.height ? + beforeHeight - sidebarRect.height : 0; + } + + sidebarData.left = sidebarRect.left - sidebarData.gapX; + sidebarData.top = sidebarRect.top - sidebarData.gapY; + sidebarData.width = sidebarRect.width + sidebarData.gapX; + sidebarData.height = sidebarRect.height + sidebarData.gapY; + + sidebarData.rectDock.x = sidebarData.left - sidebarData.rectDock.width; + } + + const hotbarElem = ui?.hotbar?.element[0]; + const hotbarRect = hotbarElem?.getBoundingClientRect(); + + if (hotbarRect) + { + // Store gapX / gapY calculating including any ::before elements if it has not already been set. + // This is only calculated one time on startup. + if (FoundryUIManager.#uiState.hotbar.gapX < 0) + { + let beforeWidth; + let beforeHeight; + try + { + const style = window.getComputedStyle(hotbarElem, 'before'); + + const width = parseInt(style.getPropertyValue('width'), 10); + if (!Number.isNaN(width)) { beforeWidth = width; } + + const height = parseInt(style.getPropertyValue('height'), 10); + if (!Number.isNaN(height)) { beforeHeight = height; } + } + catch (err) { /**/ } + + FoundryUIManager.#uiState.hotbar.gapX = beforeWidth && beforeWidth > hotbarRect.width ? + beforeWidth - hotbarRect.width : 0; + + FoundryUIManager.#uiState.hotbar.gapY = beforeHeight && beforeHeight > hotbarRect.height ? + beforeHeight - hotbarRect.height : 0; + } + + FoundryUIManager.#uiState.hotbar.left = hotbarRect.left - FoundryUIManager.#uiState.hotbar.gapX; + FoundryUIManager.#uiState.hotbar.top = hotbarRect.top - FoundryUIManager.#uiState.hotbar.gapY; + FoundryUIManager.#uiState.hotbar.width = hotbarRect.width + FoundryUIManager.#uiState.hotbar.gapX; + FoundryUIManager.#uiState.hotbar.height = hotbarRect.height + FoundryUIManager.#uiState.hotbar.gapY; + } + } +} \ No newline at end of file diff --git a/src/control/ui/UINotifications.js b/src/control/ui/UINotifications.js new file mode 100644 index 00000000..30f8c3a7 --- /dev/null +++ b/src/control/ui/UINotifications.js @@ -0,0 +1,106 @@ +/** + * Provides a helper class to gate UI notifications that may come in from various players in a rapid fashion + * through Socket. By default, a 4-second delay is applied between each notification, but the last notification + * received will always be displayed. + */ +export class UINotifications +{ + /** + * Stores the last notify warn time epoch in MS. + * + * @type {number} + */ + #lastNotifyWarn = Date.now(); + + /** + * Stores the last notify info time epoch in MS. + * + * @type {number} + */ + #lastNotifyInfo = Date.now(); + + + /** + * Stores the last call to setTimeout for info messages, so that they can be cancelled as new notifications + * arrive. + * + * @type {number} + */ + #timeoutInfo = void 0; + + /** + * Stores the last call to setTimeout for warn messages, so that they can be cancelled as new notifications + * arrive. + * + * @type {number} + */ + #timeoutWarn = void 0; + + /** + * Potentially gates `warn` UI notifications to prevent overloading the UI notification system. + * + * @param {string} message - Message to post. + * + * @param {number} delay - The delay in MS between UI notifications posted. + */ + warn(message, delay = 4000) + { + if (Date.now() - this.#lastNotifyWarn > delay) + { + ui.notifications.warn(message); + this.#lastNotifyWarn = Date.now(); + } + else + { + if (this.#timeoutWarn) + { + clearTimeout(this.#timeoutWarn); + this.#timeoutWarn = void 0; + } + + this.#timeoutWarn = setTimeout(() => + { + ui.notifications.warn(message); + }, delay); + } + } + + /** + * Potentially gates `info` UI notifications to prevent overloading the UI notification system. + * + * @param {string} message - Message to post. + * + * @param {number} delay - The delay in MS between UI notifications posted. + */ + info(message, delay = 4000) + { + if (Date.now() - this.#lastNotifyInfo > delay) + { + ui.notifications.info(message); + this.#lastNotifyInfo = Date.now(); + } + else + { + if (this.#timeoutInfo) + { + clearTimeout(this.#timeoutInfo); + this.#timeoutInfo = void 0; + } + + this.#timeoutInfo = setTimeout(() => + { + ui.notifications.info(message); + }, delay); + } + } + + /** + * Post all error messages with no gating. + * + * @param {string} message - Message to post. + */ + error(message) + { + ui.notifications.error(message); + } +} diff --git a/src/control/ui/ViewManager.js b/src/control/ui/ViewManager.js new file mode 100644 index 00000000..57f24ad5 --- /dev/null +++ b/src/control/ui/ViewManager.js @@ -0,0 +1,395 @@ +import { QuestDB } from '../index.js'; + +import { + QuestLog, + QuestPreview, + QuestTracker } from '../../view/index.js'; + +import { UINotifications } from './UINotifications.js'; + +import { + constants, + questStatus, + questStatusI18n, + settings } from '../../model/constants.js'; + +/** + * Stores and manages all the GUI apps / view for FQL. + */ +export class ViewManager +{ + /** + * Locally stores the app instances which are accessible by getter methods. + * + * @type {{questLog: QuestLog, questPreview: Map, questTracker: QuestTracker}} + * + * @see ViewManager.questLog + * @see ViewManager.questPreview + * @see ViewManager.questTracker + */ + static #Apps = { + questLog: void 0, + questTracker: void 0, + questPreview: new Map() + }; + + /** + * Stores the QuestPreview app that is the current newly added quest. It needs to be closed before more quests can be + * added as a gate to prevent many quests from being added rapidly. + * + * @type {QuestPreview} + */ + static #newQuestPreviewApp = void 0; + + /** + * Stores the UINotifications instance to return in {@link ViewManager.notifications}. + * + * @type {UINotifications} + */ + static #uiNotifications = new UINotifications(); + + /** + * Initializes all GUI apps. + */ + static init() + { + this.#Apps.questLog = new QuestLog(); + this.#Apps.questTracker = new QuestTracker(); + + // Load and set the quest tracker position from settings. + try + { + const position = JSON.parse(game.settings.get(constants.moduleName, settings.questTrackerPosition)); + if (position && position.width && position.height) + { + this.#Apps.questTracker.position = position; + } + } + catch (err) { /**/ } + + ViewManager.renderOrCloseQuestTracker(); + + // Whenever a QuestPreview closes and matches any tracked app that is adding a new quest set it to undefined. + Hooks.on('closeQuestPreview', this.#handleQuestPreviewClosed.bind(this)); + Hooks.on('renderQuestPreview', this.#handleQuestPreviewRender.bind(this)); + + // Right now ViewManager responds to permission changes across add, remove, update of quests. + Hooks.on(QuestDB.hooks.addQuestEntry, this.#handleQuestEntryAdd.bind(this)); + Hooks.on(QuestDB.hooks.removeQuestEntry, this.#handleQuestEntryRemove.bind(this)); + Hooks.on(QuestDB.hooks.updateQuestEntry, this.#handleQuestEntryUpdate.bind(this)); + } + + /** + * @returns {UINotifications} Returns the UINotifications helper. + */ + static get notifications() { return this.#uiNotifications; } + + /** + * @returns {QuestLog} The main quest log app accessible from the left hand menu bar or + * `Hook.call('ForienQuestLog.Open.QuestLog')`. + * + * @see FQLHooks.openQuestLog + */ + static get questLog() { return this.#Apps.questLog; } + + /** + * @returns {Map} A Map that contains all currently rendered / visible QuestPreview instances + * indexed by questId / string which is the Foundry 'id' of quests and the + * backing journal entries. + */ + static get questPreview() { return this.#Apps.questPreview; } + + /** + * @returns {QuestTracker} Returns the quest tracker overlap app. This app is accessible when module setting + * {@link FQLSettings.questTrackerEnable} is enabled. + */ + static get questTracker() { return this.#Apps.questTracker; } + + /** + * @param {object} opts - Optional parameters + * + * @param {boolean} [opts.questPreview=false] - If true closes all QuestPreview apps. + * + * @param {...*} [opts.options] - Optional parameters passed onto {@link Application.close} + * + * @see https://foundryvtt.com/api/classes/client.Application.html#close + */ + static closeAll({ questPreview = false, ...options } = {}) + { + if (ViewManager.questLog.rendered) { ViewManager.questLog.close(options); } + if (ViewManager.questTracker.rendered) { ViewManager.questTracker.close(options); } + + if (questPreview) + { + for (const qp of ViewManager.questPreview.values()) { qp.close(options); } + } + } + + /** + * Convenience method to determine if the QuestTracker is visible to the current user. Always for the GM when + * QuestTracker is enabled, but only for users if `hideFromPlayers` is false. There must also be active quests for + * the tracker to be visible. + * + * @returns {boolean} Whether the QuestTracker is visible. + */ + static isQuestTrackerVisible() + { + return game.settings.get(constants.moduleName, settings.questTrackerEnable) && + (game.user.isGM || !game.settings.get(constants.moduleName, settings.hideFQLFromPlayers)) && + QuestDB.getCount({ status: questStatus.active }) > 0; + } + + /** + * Refreshes local {@link QuestPreview} apps. + * + * @param {string|string[]} questId - A single quest ID or an array of IDs to update. + * + * @param {RenderOptions} [options] - Any options to pass onto QuestPreview render method invocation. + */ + static refreshQuestPreview(questId, options = {}) + { + // Handle local QuestPreview rendering. + if (Array.isArray(questId)) + { + for (const id of questId) + { + const questPreview = ViewManager.questPreview.get(id); + if (questPreview !== void 0) { questPreview.render(true, options); } + } + } + else + { + const questPreview = ViewManager.questPreview.get(questId); + if (questPreview !== void 0) { questPreview.render(true, options); } + } + } + + /** + * Renders all GUI apps including the quest tracker which may also be closed depending on + * {@link ViewManager.isQuestTrackerVisible}. With the option `questPreview` set to true all QuestPreviews are also + * rendered. Remaining options are forwarded onto the Foundry Application render method. + * + * @param {object} opts - Optional parameters + * + * @param {boolean} [opts.force] - Forces a data refresh. + * + * @param {boolean} [opts.questPreview] - Render all open QuestPreview apps. + * + * @param {...*} [opts.options] - Remaining options for the {@link Application.render} method. + * + * @see https://foundryvtt.com/api/classes/client.Application.html#render + */ + static renderAll({ force = false, questPreview = false, ...options } = {}) + { + // Never force render the quest log to maintain quest details pages above the log. + if (ViewManager.questLog.rendered) { ViewManager.questLog.render(false, options); } + + ViewManager.renderOrCloseQuestTracker({ updateSetting: false }); + + if (questPreview) + { + for (const qp of ViewManager.questPreview.values()) + { + if (qp.rendered) { qp.render(force, options); } + } + } + } + + /** + * If the QuestTracker is visible then render it otherwise close it. + * + * @param {object} [options] - Optional parameters. + * + * @param {boolean} [options.updateSetting=true] - If closed true then {@link settings.questTrackerEnable} is set + * to false. + */ + static renderOrCloseQuestTracker(options = {}) + { + if (ViewManager.isQuestTrackerVisible()) + { + ViewManager.questTracker.render(true, { focus: false }); + } + else + { + // Necessary to check rendered state as the setting is set to false in the close method. + if (ViewManager.questTracker.rendered) { ViewManager.questTracker.close(options); } + } + } + + /** + * Performs the second half of the quest addition view management. + * + * @param {object} options - Optional parameters. + * + * @param {Quest} options.quest - The new quest being added. + * + * @param {boolean} [options.notify=true] - Post a UI notification with the quest name and the status / category. + * + * @param {boolean} [options.swapTab=true] - If rendered switch to the QuestLog tab of the new quest status. + */ + static questAdded({ quest, notify = true, swapTab = true } = {}) + { + if (notify) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestAdded', { + name: quest.name, + status: game.i18n.localize(questStatusI18n[quest.status]) + })); + } + + if (swapTab) + { + const questLog = ViewManager.questLog; + if (questLog._tabs[0] && quest.status !== questLog?._tabs[0]?.active && null !== questLog?._tabs[0]?._nav) + { + questLog._tabs[0].activate(quest.status); + } + } + + if (quest.isObservable) + { + const questSheet = quest.sheet; + questSheet.render(true, { focus: true }); + + // Set current QuestPreview being tracked as the add app. + this.#newQuestPreviewApp = questSheet; + } + } + + /** + * The first half of the add quest action which verifies if there is a current "add" QuestPreview open. If so it + * will bring the current add QuestPreview app to front, post a UI notification and return false. Otherwise returns + * true indicating that a new quest can be added / created. + * + * @returns {boolean} Whether a new quest can be added. + */ + static verifyQuestCanAdd() + { + if (this.#newQuestPreviewApp !== void 0) + { + if (this.#newQuestPreviewApp.rendered) + { + this.#newQuestPreviewApp.bringToTop(); + ViewManager.notifications.warn(game.i18n.localize('ForienQuestLog.Notifications.FinishQuestAdded')); + return false; + } + else + { + this.#newQuestPreviewApp = void 0; + } + } + + return true; + } + + // Internal implementation ---------------------------------------------------------------------------------------- + + /** + * Handles the `addQuestEntry` hook. + * + * @param {QuestEntry} questEntry - The added QuestEntry. + * + * @param {object} flags - Quest flags. + * + * @returns {Promise} + */ + static async #handleQuestEntryAdd(questEntry, flags) + { + if ('ownership' in flags) + { + ViewManager.refreshQuestPreview(questEntry.questIds); + ViewManager.renderAll(); + } + } + + /** + * Handles the `removeQuestEntry` hook. + * + * @param {QuestEntry} questEntry - The added QuestEntry. + * + * @param {object} flags - Quest flags. + * + * @returns {Promise} + */ + static async #handleQuestEntryRemove(questEntry, flags) + { + const quest = questEntry.quest; + + const questPreview = ViewManager.questPreview.get(quest.id); + if (questPreview && questPreview.rendered) { await questPreview.close({ noSave: true }); } + + if ('ownership' in flags) + { + ViewManager.refreshQuestPreview(questEntry.questIds); + ViewManager.renderAll(); + } + } + + /** + * Handles the `updateQuestEntry` hook. + * + * @param {QuestEntry} questEntry - The added QuestEntry. + * + * @param {object} flags - Quest flags. + */ + static #handleQuestEntryUpdate(questEntry, flags) + { + if ('ownership' in flags) + { + ViewManager.refreshQuestPreview(questEntry.questIds); + ViewManager.renderAll(); + } + } + + /** + * Handles the `closeQuestPreview` hook. Removes the QuestPreview from tracking and removes any current set + * `#newQuestPreviewApp` state if QuestPreview matches. + * + * @param {QuestPreview} questPreview - The closed QuestPreview. + */ + static #handleQuestPreviewClosed(questPreview) + { + if (!(questPreview instanceof QuestPreview)) { return; } + + if (this.#newQuestPreviewApp === questPreview) { this.#newQuestPreviewApp = void 0; } + + const quest = questPreview.quest; + if (quest !== void 0) { this.questPreview.delete(quest.id); } + } + + /** + * Handles the `renderQuestPreview` hook; adding the quest preview to tracking. + * + * @param {QuestPreview} questPreview - The rendered QuestPreview. + */ + static #handleQuestPreviewRender(questPreview) + { + if (questPreview instanceof QuestPreview) + { + const quest = questPreview.quest; + if (quest !== void 0) { ViewManager.questPreview.set(quest.id, questPreview); } + } + } +} + +/** + * @typedef {object} RenderOptions Additional rendering options which are applied to customize the way that the + * Application is rendered in the DOM. + * + * @property {number} [left] - The left positioning attribute. + * + * @property {number} [top] - The top positioning attribute. + * + * @property {number} [width] - The rendered width. + * + * @property {number} [height] - The rendered height. + * + * @property {number} [scale] - The rendered transformation scale. + * + * @property {boolean} [focus=false] - Apply focus to the application, maximizing it and bringing it to the top + * of the vertical stack. + * + * @property {string} [renderContext] - A context-providing string which suggests what event triggered the render. + * + * @property {object} [renderData] - The data change which motivated the render request. + */ \ No newline at end of file diff --git a/src/control/ui/index.js b/src/control/ui/index.js new file mode 100644 index 00000000..7366002e --- /dev/null +++ b/src/control/ui/index.js @@ -0,0 +1,2 @@ +export * from './FoundryUIManager.js'; +export * from './ViewManager.js'; \ No newline at end of file diff --git a/src/control/util/FVTTCompat.js b/src/control/util/FVTTCompat.js new file mode 100644 index 00000000..d165cbde --- /dev/null +++ b/src/control/util/FVTTCompat.js @@ -0,0 +1,186 @@ +import { constants } from '../../model/constants.js'; + +/** + * Provides potential shimming for the Foundry core API and potential support for other 3rd party modules like Monk's + * Enhanced Journal (MEJ). In the case for MEJ this module stores the image for a journal documents it owns in custom + * flags. + * + * Previously `FVTTCompat` provided v9 / v10+ shims for accessing Foundry core API. This compatibility layer is + * maintained in the codebase, but for the time being the latest FQL is released for v11+ and the shimming + * below just returns the current core API call / data. See the below sample code for how the shim is supposed to work + * if necessary to re-implement shims in the future. + * + * Example of how the shimming works: + * ```js + * let isV10 = false; + * + * Hooks.once('init', () => + * { + * isV10 = !foundry.utils.isNewerVersion(10, game.version ?? game?.data?.version); + * }); + * + * export class FVTTCompat + * { + * static get isV10() { return isV10; } + * + * static authorID(doc) + * { + * if (!doc) { return void 0; } + * + * return isV10 ? doc?.author?.id : doc?.data?.author; + * } + * } + * ``` + */ +export class FVTTCompat +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Returns the author ID of a document depending on v10. + * + * @param {foundry.abstract.Document|Document} doc - + * + * @returns {string} Author ID + */ + static authorID(doc) + { + if (!doc) { return void 0; } + + return doc?.author?.id; + } + + /** + * Returns folder contents. + * + * @param {Folder} folder - + * + * @returns {*[]} Folder contents; + */ + static folderContents(folder) + { + if (!folder) { return void 0; } + return folder?.contents ?? []; + } + + /** + * @param {object} data - data transfer from macro hot bar drop. + * + * @returns {boolean} Data transfer object is an FQL macro. + */ + static isFQLMacroDataTransfer(data) + { + if (data?.type !== 'Macro') { return false; } + + return typeof data?.uuid === 'string' && data.uuid.startsWith(`Compendium.${constants.moduleName}`); + } + + /** + * Returns the data property depending on v10. + * + * @param {foundry.abstract.Document|Document} doc - + * + * @param {string} property - Property field. + * + * @returns {string} Data value. + */ + static get(doc, property) + { + if (!doc || typeof property !== 'string') { return void 0; } + return doc[property]; + } + + /** + * Retrieves the content from either TinyMCE or ProseMirror based editors. This is important because the content is + * being retrieved directly from the editor instance to store in flags. Additional 3rd party modules may override + * the default editor (ProseMirror) and use TinyMCE. Treating the editors neutrally allows support for any editor. + * + * @param {object} editor - Editor object from `FormApplication`. + * + * @returns {string | undefined} Editor HTML content. + */ + static getEditorContent(editor) + { + let content; + + try + { + // Attempt to retrieve content from TinyMCE editor instance. + content = editor?.mce?.getContent?.(); + + if (typeof content === 'string') { return content; } + + // Attempt to retrieve content from ProseMirror editor instance. + if (editor?.instance?.view) + { + content = globalThis.ProseMirror.dom.serializeString(editor.instance.view.state.doc.content); + } + } + catch (err) { /**/ } + + return content; + } + + /** + * Returns any associated journal image. For v10 journal docs this is the first page that is an image. + * + * @param {foundry.abstract.Document|Document} doc - + * + * @returns {string | undefined} Journal image. + */ + static journalImage(doc) + { + if (!doc) { return void 0; } + + // Support Monk's Enhanced Journal which stores images in flags. + if (typeof doc?.flags?.['monks-enhanced-journal']?.img === 'string') + { + return doc.flags['monks-enhanced-journal'].img; + } + else + { + // Treat as normal Foundry journal doc and search for the first JournalEntryPage embedded collection for an + // image. + try + { + const pages = doc.getEmbeddedCollection('JournalEntryPage'); + for (const page of pages) + { + if (page?.type === 'image') { return page?.src; } + } + } + catch (err) { /**/ } + } + + return void 0; + } + + /** + * @param {foundry.abstract.Document|Document} doc - + * + * @returns {string} Foundry ownership / permission object. + */ + static ownership(doc) + { + if (!doc) { return void 0; } + return doc.ownership; + } + + /** + * @param {foundry.abstract.Document|Document} doc - + * + * @returns {string} Token image path. + */ + static tokenImg(doc) + { + if (!doc) { return void 0; } + + return doc?.prototypeToken?.texture?.src; + } +} diff --git a/src/control/util/Utils.js b/src/control/util/Utils.js new file mode 100644 index 00000000..bf05a4f0 --- /dev/null +++ b/src/control/util/Utils.js @@ -0,0 +1,472 @@ +import { FVTTCompat } from './index.js'; + +import { + constants, + jquery, + settings } from '../../model/constants.js'; + +/** + * Provides several general utility methods interacting with Foundry via UUID lookups to generating UUIDv4 internal + * FQL IDs. There are also several general methods for Handlebars setup. + */ +export class Utils +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * The hidden FQL quests folder name. + * + * @type {string} + */ + static #questDirName = '_fql_quests'; + + /** + * Uses `navigator.clipboard` if available then falls back to `document.execCommand('copy')` if available to copy + * the given text to the clipboard. + * + * @param {string} text - Text to copy to the browser clipboard. + * + * @returns {Promise} Copy successful. + */ + static async copyTextToClipboard(text) + { + if (typeof text !== 'string') + { + throw new TypeError(`FQL copyTextToClipboard error: 'text' is not a string.`); + } + + let success = false; + + if (navigator.clipboard) + { + try + { + await navigator.clipboard.writeText(text); + success = true; + } + catch (err) { /**/ } + } + else if (document.execCommand instanceof Function) + { + const textArea = document.createElement('textarea'); + + // Place in the top-left corner of screen regardless of scroll position. + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + + // Ensure it has a small width and height. Setting to 1px / 1em + // doesn't work as this gives a negative w/h on some browsers. + textArea.style.width = '2em'; + textArea.style.height = '2em'; + + // We don't need padding, reducing the size if it does flash render. + textArea.style.padding = '0'; + + // Clean up any borders. + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + + // Avoid flash of the white box if rendered for any reason. + textArea.style.background = 'transparent'; + + textArea.value = text; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try + { + success = document.execCommand('copy'); + } + catch (err) { /**/ } + + document.body.removeChild(textArea); + } + + return success; + } + + /** + * Creates a double click handler with a default delay of 400ms + * + * @param {object} [opts] - Optional parameters. + * + * @param {string} [opts.selector] - Data to pass to callbacks. + * + * @param {Function} [opts.singleCallback] - Single click callback. + * + * @param {Function} [opts.doubleCallback] - Double click callback. + * + * @param {number} [opts.delay=400] - Double click delay. + * + * @param {number} [opts._clicks] - Private data to track clicks. + * + * @param {number} [opts._timer] - Private data to track timer. + * + * @returns {JQuery} The JQuery element. + */ + static createJQueryDblClick({ selector, singleCallback, doubleCallback, delay = 400, _clicks = 0, + _timer = void 0 } = {}) + { + const elem = $(selector); + + elem.on(jquery.click, (event) => + { + _clicks++; + + if (_clicks === 1) + { + _timer = setTimeout(() => + { + if (typeof singleCallback === 'function') { singleCallback(event); } + _clicks = 0; + }, delay); + } + else + { + clearTimeout(_timer); + if (typeof doubleCallback === 'function') { doubleCallback(event); } + _clicks = 0; + } + }).on(jquery.dblclick, (event) => event.preventDefault()); + + return elem; + } + + /** + * A convenience method to return the module data object for FQL. + * + * This is a scoped location where we can store any FQL data. + * + * @returns {object} The FQL module data object. + */ + static getModuleData() + { + return game.modules.get(constants.moduleName); + } + + /** + * Parses a UUID and returns the component data parts. + * + * @param {string|object} data - The UUID as a string or object with UUID key as a string. + * + * @returns {{id: string, type: string}|{id: string, type: string, pack: string}|*} UUID data parts + */ + static getDataFromUUID(data) + { + const uuid = typeof data === 'string' ? data : data.uuid; + + if (typeof uuid !== 'string') { return void 0; } + + const match = uuid.match(/(\w+)/gm); + + switch (match.length) + { + case 2: + return { type: match[0], id: match[1] }; + case 4: + return { type: match[0], pack: `${match[1]}.${match[2]}`, id: match[3] }; + default: + return void 0; + } + } + + /** + * Gets a document for the given UUID. An error message will post if the UUID is invalid and a warning + * message will be posted if the current `game.user` does not have permission to view the document. + * + * @param {string|object} data - The UUID as a string or object with UUID key as a string. + * + * @param {boolean} [permissionCheck] - The UUID as a string or object with UUID key as a string. + * + * @returns {Promise} + */ + static async getDocumentFromUUID(data, { permissionCheck = true } = {}) + { + const uuid = typeof data === 'string' ? data : data.uuid; + + let document = null; + + try + { + const doc = await fromUuid(uuid); + + if (doc === null) + { + ui.notifications.error(game.i18n.format('ForienQuestLog.API.Utils.Notifications.NoDocument', { uuid })); + return null; + } + + const checkPerm = typeof permissionCheck === 'boolean' ? permissionCheck : true; + + if (checkPerm && !doc.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) + { + ui.notifications.warn('ForienQuestLog.API.Utils.Notifications.NoPermission', { localize: true }); + return null; + } + + document = doc; + } + catch (err) + { + ui.notifications.error(game.i18n.format('ForienQuestLog.API.Utils.Notifications.NoDocument', { uuid })); + console.error(err); + } + + return document; + } + + /** + * Returns the quest folder or initializes and returns the quest folder if it doesn't exist and `create` is true. + * + * @returns {Folder} The quest folder. + * @see https://foundryvtt.com/api/classes/client.Folder.html + */ + static getQuestFolder() + { + return game.journal.directory.folders.find((f) => f.name === this.#questDirName); + } + + /** + * Builds a UUID for the given actor / journal / item data. + * + * @param {object} data - document data + * + * @param {string[]|undefined} type - Provide a list of Document types to build a UUID from given data. If the type + * doesn't match the data undefined is returned. If type is undefined any document + * will match. + * + * @returns {string|undefined} UUID + */ + static getUUID(data, type = void 0) + { + // Verify data. + if (typeof data !== 'object' || data === null) { return void 0; } + + // 'type' doesn't match the data type. + if (Array.isArray(type) && !type.includes(data.type)) { return void 0; } + if (typeof type === 'string' && data.type !== type) { return void 0; } + + if (typeof data.uuid === 'string') + { + // Must verify that this is not an owned item from an actor. Search for multiple `.` + if (data.uuid.startsWith('Actor') && (data.uuid.match(/\./g) || []).length > 1) + { + return void 0; + } + + return data.uuid; + } + else + { + return void 0; + } + } + + /** + * Returns the quest folder or initializes and returns the quest folder if it doesn't exist and `create` is true. + * + * @returns {Promise} The quest folder. + * @see https://foundryvtt.com/api/classes/client.Folder.html + */ + static async initializeQuestFolder() + { + const folder = game.journal.directory.folders.find((f) => f.name === this.#questDirName); + if (folder !== void 0) { return folder; } + + if (game.user.isGM) + { + await Folder.create({ name: this.#questDirName, type: 'JournalEntry', parent: null }); + } + + return game.journal.directory.folders.find((f) => f.name === this.#questDirName); + } + + /** + * Returns whether the player is a trusted player and `trustedPlayerEdit` is enabled. + * + * @param {User} user - User to check for trusted status and `trustedPlayerEdit`. + * + * @returns {boolean} Is trusted player edit. + */ + static isTrustedPlayerEdit(user = game.user) + { + return user.isTrusted && game.settings.get(constants.moduleName, settings.trustedPlayerEdit); + } + + /** + * Returns true if FQL is hidden from players. This will always return false if the user is a GM. + * + * @returns {boolean} Is FQL hidden from players. + */ + static isFQLHiddenFromPlayers() + { + if (game.user.isGM) { return false; } + + return game.settings.get(constants.moduleName, settings.hideFQLFromPlayers); + } + + /** + * Sets an image based on boolean setting state for FQL macros. + * + * @param {string|string[]} setting - Setting name. + * + * @param {boolean} [value] - Current setting value. + * + * @returns {Promise} + */ + static async setMacroImage(setting, value = void 0) + { + const userID = game.user.id; + + const fqlSettings = Array.isArray(setting) ? setting : [setting]; + + for (const macroEntry of game.macros.contents) + { + for (const currentSetting of fqlSettings) + { + // Test if the FQL `macro-setting` flag value against the setting supplied. + const macroSetting = macroEntry.getFlag(constants.moduleName, 'macro-setting'); + if (macroSetting !== currentSetting) { continue; } + + // Only set macro image if the author of the macro matches the user and the user is an owner. + const macroAuthor = FVTTCompat.authorID(macroEntry); + if (macroAuthor !== userID || !macroEntry.isOwner) { continue; } + + const state = value ?? game.settings.get(constants.moduleName, currentSetting); + + // Pick the correct image for the current state. + const img = typeof state === 'boolean' && state ? + `modules/forien-quest-log/assets/icons/macros/${currentSetting}On.png` : + `modules/forien-quest-log/assets/icons/macros/${currentSetting}Off.png`; + + await macroEntry.update({ img }, { diff: false }); + } + } + } + + /** + * Shows a document sheet for the given UUID. An error message will post if the UUID is invalid and a warning + * message will be posted if the current `game.user` does not have permission to view the document. + * + * @param {string|object} data - The UUID as a string or object with UUID key as a string. + * + * @param {object} [opts] - Optional parameters. + * + * @param {boolean} [opts.permissionCheck=true] - Perform permission check. + * + * @param {...*} [opts.options] - Options to pass to sheet render method. + * + * @returns {Promise} The appId if rendered otherwise null. + */ + static async showSheetFromUUID(data, { permissionCheck = true, ...options } = {}) + { + const uuid = typeof data === 'string' ? data : data.uuid; + + try + { + const document = await fromUuid(uuid); + + if (document === null) + { + ui.notifications.error(game.i18n.format('ForienQuestLog.API.Utils.Notifications.NoDocument', { uuid })); + return null; + } + + if (permissionCheck && !document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER)) + { + ui.notifications.warn('ForienQuestLog.API.Utils.Notifications.NoPermission', { localize: true }); + return null; + } + + if (document?.sheet) + { + if (document.sheet.rendered) + { + document.sheet.bringToTop(); + return null; + } + else + { + document.sheet.render(true, options); + return document.sheet.appId; + } + } + } + catch (err) + { + ui.notifications.error(game.i18n.format('ForienQuestLog.API.Utils.Notifications.NoDocument', { uuid })); + console.error(err); + return null; + } + } + + /** + * Preloads templates for partials + */ + static preloadTemplates() + { + let templates = [ + "templates/partials/quest-log/tab.html", + "templates/partials/quest-preview/details.html", + "templates/partials/quest-preview/gmnotes.html", + "templates/partials/quest-preview/management.html", + "templates/partials/quest-preview/playernotes.html" + ]; + + templates = templates.map((t) => `modules/forien-quest-log/${t}`); + loadTemplates(templates); + } + + /** + * Register additional Handlebars helpers. `format` allows invoking `game.i18n.format` from a Handlebars template. + */ + static registerHandlebarsHelpers() + { + Handlebars.registerHelper('fql_format', (stringId, ...arrData) => + { + let objData; + if (typeof arrData[0] === 'object') + { + objData = arrData[0]; + } + else + { + objData = { ...arrData }; + } + + return game.i18n.format(stringId, objData); + }); + } + + /** + * Generates a UUID v4 compliant ID. This is used by Quest to attach a UUID to any data that isn't backed by a + * FoundryVTT document. Right now that is particularly {@link Task}. All GUI interaction and storage in Quest data + * that isn't based on an FVTT document must use a UUIDv4 to interact with this data. Lookups in Quest data must be + * by UUIDv4 to find an index in Quest data arrays before modifying data. FQL is potentially a multi-user module + * where many users could potentially be modifying Quest data that isn't backed by an FVTT document, so the Foundry + * core DB won't be synching or resolving this data. + * + * This code is an evolution of the following Gist. + * https://gist.github.com/jed/982883 + * + * There is a public domain / free copy license attached to it that is not a standard OSS license... + * https://gist.github.com/jed/982883#file-license-txt + * + * @returns {string} UUIDv4 + */ + static uuidv4() + { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (c ^ (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); + } +} diff --git a/src/control/util/index.js b/src/control/util/index.js new file mode 100644 index 00000000..e8521f7e --- /dev/null +++ b/src/control/util/index.js @@ -0,0 +1,2 @@ +export * from './FVTTCompat.js'; +export * from './Utils.js'; \ No newline at end of file diff --git a/src/init.js b/src/init.js new file mode 100644 index 00000000..461bd92c --- /dev/null +++ b/src/init.js @@ -0,0 +1,4 @@ +import { FQLHooks } from './control/index.js'; + +// Initialize all hooks. +FQLHooks.init(); \ No newline at end of file diff --git a/src/model/Quest.js b/src/model/Quest.js new file mode 100644 index 00000000..434516a9 --- /dev/null +++ b/src/model/Quest.js @@ -0,0 +1,1128 @@ +import { + FVTTCompat, + Utils } from '../control/index.js'; + +import { QuestPreviewShim } from '../view/index.js'; + +import { + constants, + questStatus, + settings } from './constants.js'; + +/** + * Stores and makes accessible the minimum amount of data that defines a quest. A Quest is loaded from the backing + * JournalEntry and has the JournalEntry stored for the ability to perform permissions checks. Please see QuestDB + * as when a Quest is loaded it is stored in a QuestEntry which also contains the enriched quest data for display + * in Handlebars templates along with caching of several of the methods available in Quest for fast sorting. + * + * {@link Quest.giverFromQuest} / {@link Quest.giverFromUUID} are used in {@link HandlerDetails} to look up + * and store the quest giver image / name in {@link Quest.giverData} when a quest giver is set. + * + * @see QuestDB + * @see QuestEntry + */ +export class Quest +{ + /** + * Stores the sheet class for Quest which is {@link QuestPreview}. This class / sheet is used to render Quest. + * While directly accessible from Quest the main way a QuestPreview is shown is through {@link QuestAPI.open} which + * provides the entry point for external API access and is also used internally when opening a quest. + * + * @type {typeof Application} + * @see Quest.sheet + */ + static #SheetClass; + + /** + * The backing JournalEntry document. + * + * @type {JournalEntry} + */ + #entry; + + /** @type {string | null} */ + #id; + + /** @type {string} */ + #name; + + /** + * Lookup the Quest giver by UUID and return the data stored in {@link Quest.giverData}. + * + * @param {Quest} quest - The quest to look up the quest giver. + * + * @returns {Promise} The image / name data associated with this Foundry UUID. + */ + static async giverFromQuest(quest) + { + let data = null; + + if (quest.giver === 'abstract') + { + data = { + name: quest.giverName, + img: quest.image, + hasTokenImg: false + }; + } + else if (typeof quest.giver === 'string') + { + data = Quest.giverFromUUID(quest.giver, quest.image); + } + + return data; + } + + /** + * @param {string} uuid - The Foundry UUID to lookup for image / name data. + * + * @param {string} [imageType] - The image type: 'actor' or 'token' + * + * @returns {Promise} The image / name data associated with this Foundry UUID. + */ + static async giverFromUUID(uuid, imageType = 'actor') + { + let data = null; + + if (typeof uuid === 'string') + { + const document = await fromUuid(uuid); + + if (document !== null) + { + switch (document.documentName) + { + case Actor.documentName: + { + const actorImage = document.img; + const tokenImage = FVTTCompat.tokenImg(document); + + const hasTokenImg = typeof tokenImage === 'string' && tokenImage !== actorImage; + + data = { + uuid, + name: document.name, + img: imageType === 'token' && hasTokenImg ? tokenImage : actorImage, + hasTokenImg + }; + break; + } + + case Item.documentName: + data = { + uuid, + name: document.name, + img: document.img, + hasTokenImg: false + }; + break; + + case JournalEntry.documentName: + data = { + uuid, + name: document.name, + img: FVTTCompat.journalImage(document), + hasTokenImg: false + }; + break; + } + } + } + + return data; + } + + /** + * @param {QuestData} data - The serialized quest data to set. + * + * @param {JournalEntry} entry - The associated Foundry JournalEntry. + */ + constructor(data = {}, entry = null) + { + this.#id = entry !== null ? entry.id : null; + + this.initData(data); + + this.#entry = entry; + + if (this.#entry && this.#id !== null) + { + this.#entry._sheet = new QuestPreviewShim(this.#id); + } + } + + /** + * @returns {boolean} Returns whether the current user can update the backing journal document. + */ + get canUserUpdate() + { + const entry = this.entry ? this.entry : game.journal.get(this.#id); + + return entry?.canUserModify?.(game.user, 'update') ?? false; + } + + /** + * @returns {JournalEntry} The associated backing journal entry document. + */ + get entry() + { + return this.#entry; + } + + /** + * Gets the Foundry ID associated with this Quest. + * + * @returns {string} The ID of the quest. + */ + get id() + { + return this.#id; + } + + /** + * Sets the associated backing journal entry document. + * + * @param {JournalEntry} entry - A journal entry document. + */ + set entry(entry) + { + this.#entry = entry; + } + + /** + * Sets the Foundry ID of the quest. + * + * @param {string} id - A Foundry ID. + */ + set id(id) + { + this.#id = id; + } + + /** + * @returns {boolean} Is the quest active / in progress. + */ + get isActive() + { + return questStatus.active === this.status; + } + + /** + * True when no players have OBSERVER or OWNER permissions for this quest. + * + * @returns {boolean} Quest is hidden. + */ + get isHidden() + { + let isHidden = true; + + if (this.entry && typeof FVTTCompat.ownership(this.entry) === 'object') + { + if (FVTTCompat.ownership(this.entry).default >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) { return false; } + + for (const [userId, permission] of Object.entries(FVTTCompat.ownership(this.entry))) + { + if (userId === 'default') { continue; } + + const user = game.users.get(userId); + + if (!user || user.isGM) { continue; } + + if (permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) + { + isHidden = false; + break; + } + } + } + + return isHidden; + } + + /** + * @returns {boolean} Is the quest in the inactive state. + */ + get isInactive() + { + return questStatus.inactive === this.status; + } + + /** + * Returns true if this quest is observable for the given player. For trusted player edit when the status is + * `inactive` the test is ownership instead of simply OBSERVER or higher. + * + * @returns {boolean} Is the quest observable. + */ + get isObservable() + { + if (game.user.isGM) { return true; } + + const isInactive = this.isInactive; + + // Special handling for trusted player edit who can only see owned quests in the hidden / inactive category. + if (Utils.isTrustedPlayerEdit() && isInactive) { return this.isOwner; } + + // Otherwise no one can see hidden / inactive quests; perform user permission check for observer. + return !isInactive && this.entry.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER); + } + + /** + * Gets whether the current user has owner permissions. + * + * @returns {boolean} Is owner. + */ + get isOwner() + { + return game.user.isGM || + (this.entry && this.entry.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)); + } + + /** + * Gets whether this quest is a personal quest. A personal quest has one or more players with OBSERVER or OWNER + * permissions. + * + * @returns {boolean} Is this quest personal. + */ + get isPersonal() + { + let isPersonal = false; + + if (this.entry && typeof FVTTCompat.ownership(this.entry) === 'object' && + FVTTCompat.ownership(this.entry).default < CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) + { + for (const [userId, permission] of Object.entries(FVTTCompat.ownership(this.entry))) + { + if (userId === 'default') { continue; } + + const user = game.users.get(userId); + + if (!user || user.isGM) { continue; } + + if (permission < CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) { continue; } + + isPersonal = true; + break; + } + } + + return isPersonal; + } + + /** + * Returns whether this quest is set as the primary quest. + * + * @returns {boolean} Primary quest state. + */ + get isPrimary() + { + return this.#id === game.settings.get(constants.moduleName, settings.primaryQuest); + } + + /** + * Gets the name of the quest. + * + * @returns {string} Quest name. + */ + get name() + { + return this.#name; + } + + /** + * Sets the name of the quest. + * + * @param {string} value - The new name. + */ + set name(value) + { + this.#name = typeof value === 'string' && value.length > 0 ? value : + game.i18n.localize('ForienQuestLog.API.QuestDB.Labels.NewQuest'); + } + + /** + * Creates a new Reward and pushes to the reward array. + * + * @param {object} data - The reward data. + */ + addReward(data = {}) + { + const reward = new Reward(data); + if (reward.type !== null) { this.rewards.push(reward); } + } + + /** + * Pushes a subquest ID to the subquest array. + * + * @param {string} questId - A Foundry ID + */ + addSubquest(questId) + { + if (!this.subquests.includes(questId)) + { + this.subquests.push(questId); + } + } + + /** + * Creates a new Task and pushes to the task array. + * + * @param {object} data - Task data. + */ + addTask(data = {}) + { + const task = new Task(data); + if (task.name && task.name.length) { this.tasks.push(task); } + } + + /** + * Gets all adjacent quest IDs including self. This includes any parent and subquests. + * + * @returns {string[]} All adjacent quests including self. + */ + getQuestIds() + { + return this.parent ? [this.parent, this.id, ...this.subquests] : [this.id, ...this.subquests]; + } + + /** + * Gets a Reward by Foundry VTT UUID or UUIDv4 for abstract Rewards. + * + * @param {string} uuidv4 - The FVTT UUID to find. + * + * @returns {Reward} The task or null. + */ + getReward(uuidv4) + { + const index = this.rewards.findIndex((t) => t.uuidv4 === uuidv4); + return index >= 0 ? this.rewards[index] : null; + } + + /** + * Returns a list of Actor data for whom this quest is personal. + * + * @returns {object[]} A list of actors who are assigned to this quest. + */ + getPersonalActors() + { + if (!this.isPersonal) { return []; } + + const users = []; + + if (this.entry && typeof FVTTCompat.ownership(this.entry) === 'object') + { + for (const [userId, permission] of Object.entries(FVTTCompat.ownership(this.entry))) + { + if (userId === 'default') { continue; } + + const user = game.users.get(userId); + + if (!user || user.isGM) { continue; } + + if (permission < CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER) { continue; } + + users.push(user); + } + } + + return users; + } + + /** + * Returns any stored Foundry sheet class. + * + * @returns {typeof Application} The associated sheet class. + */ + static getSheet() { return Quest.#SheetClass; } + + /** + * Gets a task by UUID v4. + * + * @param {string} uuidv4 - The UUID v4 to find. + * + * @returns {Task} The task or null. + */ + getTask(uuidv4) + { + const index = this.tasks.findIndex((t) => t.uuidv4 === uuidv4); + return index >= 0 ? this.tasks[index] : null; + } + + /** + * Normally would be in constructor(), but is extracted for usage in different methods as well + * + * @param {QuestData} data - The serialized Quest data to initialize. + */ + initData(data) + { + this.name = data.name || game.i18n.localize('ForienQuestLog.API.QuestDB.Labels.NewQuest'); + + /** + * @type {string} + */ + this.status = data.status || questStatus.inactive; + + /** + * @type {string|null} + */ + this.giver = data.giver || null; + + /** + * @type {object|null} + */ + this.giverData = data.giverData || null; + + /** + * @type {string} + */ + this.description = data.description || ''; + + /** + * @type {string} + */ + this.gmnotes = data.gmnotes || ''; + + /** + * @type {string} + */ + this.image = data.image || 'actor'; + + /** + * @type {string} + */ + this.giverName = data.giverName || 'actor'; + + /** + * @type {string} + */ + this.splash = data.splash || ''; + + /** + * @type {string} + */ + this.splashPos = data.splashPos || 'center'; + + /** + * @type {boolean} + */ + this.splashAsIcon = typeof data.splashAsIcon === 'boolean' ? data.splashAsIcon : false; + + /** + * @type {string|null} + */ + this.location = data.location || null; + + /** + * @type {string} + */ + this.playernotes = data.playernotes || ''; + + /** + * @type {number} + */ + this.priority = data.priority || 0; + + /** + * @type {string|null} + */ + this.type = data.type || null; + + /** + * @type {string|null} + */ + this.parent = data.parent || null; + + /** + * @type {string[]} + */ + this.subquests = data.subquests || []; + + /** + * @type {Task[]} + */ + this.tasks = Array.isArray(data.tasks) ? data.tasks.map((task) => new Task(task)) : []; + + /** + * @type {Reward[]} + */ + this.rewards = Array.isArray(data.rewards) ? data.rewards.map((reward) => new Reward(reward)) : []; + + // Sanity check. If status is incorrect set it to inactive. + if (!questStatus[this.status]) { this.status = questStatus.inactive; } + + if (typeof data.date === 'object') + { + /** + * Provides timestamps for quest create, start, end. + * + * @type {{start: (number|null), create: (number|null), end: (number|null)}} + */ + this.date = { + create: typeof data.date.create === 'number' ? data.date.create : null, + start: typeof data.date.start === 'number' ? data.date.start : null, + end: typeof data.date.end === 'number' ? data.date.end : null + }; + } + else + { + this.date = { + create: Date.now(), + }; + + switch (this.status) + { + case questStatus.active: + this.date.start = Date.now(); + this.date.end = null; + break; + + case questStatus.completed: + case questStatus.failed: + this.date.start = Date.now(); + this.date.end = Date.now(); + break; + + case questStatus.inactive: + case questStatus.available: + default: + this.date.start = null; + this.date.end = null; + break; + } + } + } + + /** + * Deletes Reward from Quest. + * + * @param {string} uuidv4 - The UUIDv4 associated with a Reward. + */ + removeReward(uuidv4) + { + const index = this.rewards.findIndex((t) => t.uuidv4 === uuidv4); + if (index >= 0) { this.rewards.splice(index, 1); } + } + + /** + * Removes subquest from Quest. + * + * @param {string} questId - The subquest ID to remove. + */ + removeSubquest(questId) + { + this.subquests = this.subquests.filter((id) => id !== questId); + } + + /** + * Removes the task from this quest by UUIDv4. + * + * @param {string} uuidv4 - The UUIDv4 associated with a Task. + * + * @see Utils.uuidv4 + */ + removeTask(uuidv4) + { + const index = this.tasks.findIndex((t) => t.uuidv4 === uuidv4); + if (index >= 0) { this.tasks.splice(index, 1); } + } + + /** + * Resets the quest giver. + */ + resetGiver() + { + this.giver = null; + this.image = 'actor'; + this.giverData = null; + this.giverName = 'actor'; + } + + /** + * Saves Quest to JournalEntry's content, and if needed, moves JournalEntry to different folder. + * Can also update JournalEntry's permissions. + * + * @returns {Promise} The ID of the quest saved or undefined if user couldn't save the quest. + */ + async save() + { + const entry = this.entry ? this.entry : game.journal.get(this.#id); + + // If the entry doesn't exist or the user can't update the journal entry via ownership then early out. + if (!entry || !this.canUserUpdate) { return; } + + // Save Quest JSON, but also potentially update the backing JournalEntry folder name. + const update = { + name: typeof this.#name === 'string' && this.#name.length > 0 ? this.#name : + game.i18n.localize('ForienQuestLog.API.QuestDB.Labels.NewQuest'), + flags: { + [constants.moduleName]: { json: this.toJSON() } + } + }; + + this.entry = await entry.update(update, { diff: false }); + + return this.#id; + } + + /** + * Sets any stored Foundry sheet class. + * + * @param {typeof Application} NewSheetClass - The sheet class. + */ + static setSheet(NewSheetClass) { Quest.#SheetClass = NewSheetClass; } + + /** + * Sets new status for the quest. Also updates any timestamp / date data depending on status set. + * + * @param {string} target - The target status to set. + * + * @returns {Promise} + */ + async setStatus(target) + { + if (!this.entry || !questStatus[target]) { return; } + + this.status = target; + + // Update the tracked date data based on status. + switch (this.status) + { + case questStatus.active: + this.date.start = Date.now(); + this.date.end = null; + break; + + case questStatus.completed: + case questStatus.failed: + this.date.end = Date.now(); + break; + + case questStatus.inactive: + case questStatus.available: + default: + this.date.start = null; + this.date.end = null; + break; + } + + // Potentially reset any tracked primary quest when the status is no longer active. + if (this.status !== questStatus.active) + { + const primaryQuestId = game.settings.get(constants.moduleName, settings.primaryQuest); + if (this.#id === primaryQuestId) + { + await game.settings.set(constants.moduleName, settings.primaryQuest, ''); + } + } + + await this.entry.update({ + flags: { + [constants.moduleName]: { json: this.toJSON() } + } + }); + + return this.#id; + } + + /** + * Locates and swaps the rewards indicated by the source and target UUIDv4s provided. + * + * @param {string} sourceUuidv4 - The source UUIDv4 + * + * @param {string} targetUuidv4 - The target UUIDv4 + */ + sortRewards(sourceUuidv4, targetUuidv4) + { + const index = this.rewards.findIndex((t) => t.uuidv4 === sourceUuidv4); + const targetIdx = this.rewards.findIndex((t) => t.uuidv4 === targetUuidv4); + + if (index >= 0 && targetIdx >= 0) + { + const entry = this.rewards.splice(index, 1)[0]; + this.rewards.splice(targetIdx, 0, entry); + } + } + + /** + * Locates and swaps the tasks indicated by the source and target UUIDv4s provided. + * + * @param {string} sourceUuidv4 - The source UUIDv4 + * + * @param {string} targetUuidv4 - The target UUIDv4 + */ + sortTasks(sourceUuidv4, targetUuidv4) + { + // If there are sub quests in the objectives above tasks then an undefined targetUuidv4 can occur. + if (!targetUuidv4) { return; } + + const index = this.tasks.findIndex((t) => t.uuidv4 === sourceUuidv4); + const targetIdx = this.tasks.findIndex((t) => t.uuidv4 === targetUuidv4); + + if (index >= 0 && targetIdx >= 0) + { + const entry = this.tasks.splice(index, 1)[0]; + this.tasks.splice(targetIdx, 0, entry); + } + } + + /** + * @returns {QuestData} The serialized JSON for this Quest. + */ + toJSON() + { + return { + name: this.#name, + status: this.status, + giver: this.giver, + giverData: this.giverData, + description: this.description, + gmnotes: this.gmnotes, + playernotes: this.playernotes, + image: this.image, + giverName: this.giverName, + splash: this.splash, + splashPos: this.splashPos, + splashAsIcon: this.splashAsIcon, + location: this.location, + priority: this.priority, + type: this.type, + parent: this.parent, + subquests: this.subquests, + tasks: this.tasks, + rewards: this.rewards, + date: this.date + }; + } + + /** + * Toggles Actor image between sheet's and token's images + */ + toggleImage() + { + this.image = this.image === 'actor' ? 'token' : 'actor'; + } + +// Document simulation ----------------------------------------------------------------------------------------------- + + /** + * The canonical name of this Document type, for example "Actor". + * + * @returns {string} The document name. + */ + static get documentName() + { + return 'Quest'; + } + + /** + * The canonical name of this Document type, for example "Actor". + * + * @returns {string} The document name. + */ + get documentName() + { + return 'Quest'; + } + + /** + * This mirrors document.sheet and constructs a new instance of the sheet class. + * + * @returns {Application} An associated sheet instance. + */ + get sheet() + { + const SheetClass = Quest.#SheetClass; + + return SheetClass ? new SheetClass(this) : void 0; + } +} + +/** + * Rewards can be either an item from a Foundry VTT compendium / world item or be an abstract reward. It should be + * noted that FVTT item data will have a Foundry VTT UUID, but abstract rewards entered by the user will have a UUIDv4 + * generated for them. This UUID regardless of type is accessible in `this.uuid`. + * + */ +export class Reward +{ + /** + * @param {object} data - Serialized reward data. + */ + constructor(data = {}) + { + /** + * @type {string|null} + */ + this.type = data.type || null; + + /** + * @type {object} + */ + this.data = data.data || {}; + + /** + * @type {boolean} + */ + this.hidden = typeof data.hidden === 'boolean' ? data.hidden : false; + + /** + * @type {boolean} + */ + this.locked = typeof data.locked === 'boolean' ? data.locked : true; + + /** + * @type {string} + */ + this.uuidv4 = data.uuidv4 || Utils.uuidv4(); + } + + /** + * Returns the name of the reward. + * + * @returns {string} Reward name. + */ + get name() { return this.data.name; } + + /** + * Returns the Foundry UUID associated with this reward. Abstract rewards do not have a Foundry UUID. + * + * @returns {string|void} The Foundry UUID. + */ + get uuid() { return this.data.uuid; } + + /** + * Serializes this reward. + * + * @returns {object} A JSON object. + */ + toJSON() + { + return JSON.parse(JSON.stringify({ + type: this.type, + data: this.data, + hidden: this.hidden, + locked: this.locked, + uuidv4: this.uuidv4 + })); + } + + /** + * Toggles the locked status. + * + * @returns {boolean} Current locked status. + */ + toggleLocked() + { + this.locked = !this.locked; + return this.locked; + } + + /** + * Toggles the hidden status. + * + * @returns {boolean} Current hidden status. + */ + toggleVisible() + { + this.hidden = !this.hidden; + return this.hidden; + } +} + +/** + * Encapsulates an objective / task. + */ +export class Task +{ + /** + * @param {object} data - The task data. + */ + constructor(data = {}) + { + /** + * @type {string|null} + */ + this.name = data.name || null; + + /** + * @type {boolean} + */ + this.completed = data.completed || false; + + /** + * @type {boolean} + */ + this.failed = data.failed || false; + + /** + * @type {boolean} + */ + this.hidden = data.hidden || false; + + /** + * @type {string} + */ + this.uuidv4 = data.uuidv4 || Utils.uuidv4(); + } + + /** + * Gets the current CSS class based on state. + * + * @returns {string} CSS class + */ + get state() + { + if (this.completed) + { + return 'check-square'; + } + else if (this.failed) + { + return 'minus-square'; + } + return 'square'; + } + + /** + * Serializes the task. + * + * @returns {object} JSON object. + */ + toJSON() + { + return JSON.parse(JSON.stringify({ + name: this.name, + completed: this.completed, + failed: this.failed, + hidden: this.hidden, + state: this.state, + uuidv4: this.uuidv4 + })); + } + + /** + * Toggles the task state between completed, failed, incomplete. + */ + toggle() + { + if (this.completed === false && this.failed === false) + { + this.completed = true; + } + else if (this.completed === true) + { + this.failed = true; + this.completed = false; + } + else + { + this.failed = false; + } + } + + /** + * Toggles the hidden state. + * + * @returns {boolean} Current hidden state. + */ + toggleVisible() + { + this.hidden = !this.hidden; + + return this.hidden; + } +} + +/** + * @typedef {object} QuestData + * + * @property {string} name - The quest name. + * + * @property {string} status - The quest status; one of {@link questStatus}. + * + * @property {string|null} giver - The Foundry UUID or 'abstract' for a custom source. + * + * @property {QuestImgNameData} giverData - The Foundry image / name data looked up by UUID. + * + * @property {string} description - The quest description. + * + * @property {string} gmnotes - The GM Notes. + * + * @property {string} image - `actor` or `token` for UUID based givers or the image link for custom source. + * + * @property {string} giverName - The name of the quest giver. + * + * @property {string} splash - The splash image. + * + * @property {string} splashPos - The splash position (top, center, bottom). + * + * @property {boolean} splashAsIcon - Use the splash image as the quest icon. + * + * @property {string|null} location - Unused / future use for quest location. + * + * @property {string} playernotes - The Player Notes. + * + * @property {number} priority - Unused / future use for quest priority sorting. + * + * @property {string|null} type - Unused / future use for sorting type of quest. + * + * @property {string|null} parent - The parent quest ID. + * + * @property {string[]} subquests - An array of quest IDs that are subquests. + * + * @property {QuestTaskData[]} tasks - An array of tasks. + * + * @property {QuestRewardData[]} rewards - An array of rewards. + * + * @property {QuestDateData} date - The create, end, start dates of the quest. + */ + +/** + * @typedef QuestDateData + * + * @property {number|null} create - Time ms since 1970 / Date.now() when quest was created. + * + * @property {number|null} end - Time ms since 1970 / Date.now() when quest ended (status: failed / complete). + * + * @property {number|null} start - Time ms since 1970 / Date.now() when quest was started (status: active). + */ + +/** + * @typedef QuestRewardData + * + * @property {string} type - Reward type. + * + * @property {QuestRewardAddData} data - Reward add data. + * + * @property {boolean} hidden - Reward hidden. + * + * @property {boolean} locked - Reward locked. + * + * @property {string} uuidv4 - The FQL UUIDv4 / unique ID. + */ + +/** + * @typedef QuestRewardAddData + * + * @property {string} type - Reward type. + * + * @property {QuestImgNameData} data - Reward image / name from {@link Enrich.giverFromUUID}. + * + * @property {boolean} hidden - Reward hidden. + */ + +/** + * @typedef QuestTaskData + * + * @property {string} name - Task name. + * + * @property {boolean} completed - Task completed. + * + * @property {boolean} failed - Task failed. + * + * @property {boolean} hidden - Task hidden. + * + * @property {string} state - Task state. + * + * @property {string} uuidv4 - The FQL UUIDv4 / unique ID. + */ + diff --git a/src/model/constants.js b/src/model/constants.js new file mode 100644 index 00000000..21f9cb29 --- /dev/null +++ b/src/model/constants.js @@ -0,0 +1,157 @@ +/** + * Defines the main FQL constants for module name and the DB flag. + * + * @type {{folderState: string, flagDB: string, moduleName: string, moduleLabel: string, primaryState: string}} + */ +const constants = { + moduleName: 'forien-quest-log', + moduleLabel: `Forien's Quest Log`, + flagDB: 'json' +}; + +/** + * Defines the {@link JQuery} events that are used in FQL. + * + * @type {{click: string, dblclick: string, dragstart: string, drop: string, focus: string, focusout: string, mousedown: string}} + */ +const jquery = { + click: 'click', + dblclick: 'dblclick', + dragenter: 'dragenter', + dragstart: 'dragstart', + drop: 'drop', + focus: 'focus', + focusout: 'focusout', + keydown: 'keydown', + mousedown: 'mousedown' +}; + +/** + * Stores strings for quest types (statuses) + * + * @returns {{active: string, available: string, completed: string, failed: string, inactive: string}} + */ +const questStatus = { + active: 'active', + available: 'available', + completed: 'completed', + failed: 'failed', + inactive: 'inactive' +}; + +/** + * Stores localization strings for quest types (statuses) + * + * @type {{active: string, available: string, completed: string, failed: string, inactive: string}} + */ +const questStatusI18n = { + active: 'ForienQuestLog.QuestTypes.Labels.Active', + available: 'ForienQuestLog.QuestTypes.Labels.Available', + completed: 'ForienQuestLog.QuestTypes.Labels.Completed', + failed: 'ForienQuestLog.QuestTypes.Labels.Failed', + inactive: 'ForienQuestLog.QuestTypes.Labels.InActive' +}; + +/** + * Stores the QuestLog tab indexes. This is used by QuestLog.setPosition to select the current table based on status + * name. + * + * @type {{inactive: number, available: number, active: number, completed: number, failed: number}} + */ +const questTabIndex = { + active: 1, + available: 0, + completed: 2, + failed: 3, + inactive: 4 +}; + +/** + * Stores the keys used with session storage. + * + * @type {FQLSessionConstants} + */ +const sessionConstants = { + currentPrimaryQuest: 'forien.questlog.currentPrimaryQuest', + trackerFolderState: 'forien.questtracker.folderState-', + trackerShowBackground: 'forien.questtracker.showBackground', + trackerShowPrimary: 'forien.questtracker.showPrimary' +}; + +/** + * @type {FQLSettings} Defines all the module settings for world and client. + */ +const settings = { + allowPlayersAccept: 'allowPlayersAccept', + allowPlayersCreate: 'allowPlayersCreate', + allowPlayersDrag: 'allowPlayersDrag', + countHidden: 'countHidden', + defaultAbstractRewardImage: 'defaultAbstractRewardImage', + defaultPermission: 'defaultPermission', + dynamicBookmarkBackground: 'dynamicBookmarkBackground', + hideFQLFromPlayers: 'hideFQLFromPlayers', + navStyle: 'navStyle', + notifyRewardDrop: 'notifyRewardDrop', + primaryQuest: 'primaryQuest', + questTrackerEnable: 'questTrackerEnable', + questTrackerPinned: 'questTrackerPinned', + questTrackerPosition: 'questTrackerPosition', + questTrackerResizable: 'questTrackerResizable', + showFolder: 'showFolder', + showTasks: 'showTasks', + trustedPlayerEdit: 'trustedPlayerEdit' +}; + +export { constants, jquery, questStatus, questStatusI18n, questTabIndex, sessionConstants, settings }; + +/** + * @typedef {object} FQLSessionConstants + * + * @property {string} currentPrimaryQuest - Stores current primary quest set from {@link FQLSettings.primaryQuest}. + * + * @property {string} trackerFolderState - Stores a boolean with tacked on quest ID for whether objectives are shown. + * + * @property {string} trackerShowBackground - Shows / hides the quest tracker background. + * + * @property {string} trackerShowPrimary - Stores a boolean if the tracker is showing the primary quest or all quests. + */ + +/** + * @typedef {object} FQLSettings + * + * @property {string} allowPlayersAccept - Allow players to accept quests. + * + * @property {string} allowPlayersCreate - Allow players to create quests. + * + * @property {string} allowPlayersDrag - Allow players to drag reward items to actor sheet. + * + * @property {string} countHidden - Count hidden objectives / subquests. + * + * @property {string} defaultAbstractRewardImage - Sets the default abstract reward image path. + * + * @property {string} defaultPermission - Sets the default permission level for new quests. + * + * @property {string} dynamicBookmarkBackground - Uses jQuery to dynamically set the tab background image. + * + * @property {string} hideFQLFromPlayers - Completely hides FQL from players. + * + * @property {string} navStyle - Navigation style / classic / or bookmark tabs. + * + * @property {string} notifyRewardDrop - Post a notification UI message when rewards are dropped in actor sheets. + * + * @property {string} primaryQuest - Stores the quest ID of a quest that is the current primary quest. + * + * @property {string} questTrackerEnable - Enables the quest tracker. + * + * @property {string} questTrackerPinned - Is the QuestTracker pinned to the side bar. + * + * @property {string} questTrackerPosition - Hidden setting to store current quest tracker position. + * + * @property {string} questTrackerResizable - Stores the current window handling mode ('auto' or 'resize'). + * + * @property {string} showFolder - Shows the `_fql_quests` directory in the journal entries sidebar. + * + * @property {string} showTasks - Determines if objective counts are rendered. + * + * @property {string} trustedPlayerEdit - Allows trusted players to have full quest editing capabilities. + */ diff --git a/src/model/index.js b/src/model/index.js new file mode 100644 index 00000000..8c5c4997 --- /dev/null +++ b/src/model/index.js @@ -0,0 +1 @@ +export * from './Quest.js'; \ No newline at end of file diff --git a/src/typedefs.js b/src/typedefs.js new file mode 100644 index 00000000..395d836d --- /dev/null +++ b/src/typedefs.js @@ -0,0 +1,55 @@ +/* eslint-disable jsdoc/valid-types */ // ESDoc uses a non-conformant external tag. + +/** + * @external {collect} https://collect.js.org/api.html + */ + +/** + * @external {CollectJS} https://collect.js.org/api.html + */ + +/** + * @external {Collection} https://collect.js.org/api.html + */ + +/** + * @external {Handlebars} https://handlebarsjs.com/api-reference/ + */ + +/** + * @external {JQuery} https://api.jquery.com/ + */ + +/** + * @external {JQuery.ClickEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {JQuery.DragEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {JQuery.DragStartEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {JQuery.DropEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {JQuery.FocusOutEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {JQuery.MouseDownEvent} https://api.jquery.com/category/events/event-object/ + */ + +/** + * @external {PointerEvent} https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent + */ + +/** + * @external {sessionStorage} https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage + */ + +/* eslint-enable jsdoc/valid-types */ diff --git a/src/view/index.js b/src/view/index.js new file mode 100644 index 00000000..11db5408 --- /dev/null +++ b/src/view/index.js @@ -0,0 +1,4 @@ +export * from './log/QuestLog.js'; +export * from './preview/QuestPreview.js'; +export * from './preview/QuestPreviewShim.js'; +export * from './tracker/QuestTracker.js'; \ No newline at end of file diff --git a/src/view/internal/FQLContextMenu.js b/src/view/internal/FQLContextMenu.js new file mode 100644 index 00000000..d07d6028 --- /dev/null +++ b/src/view/internal/FQLContextMenu.js @@ -0,0 +1,68 @@ +/** + * Provides a fixed / free placement context menu used in QuestLog. With a few modifications below the Foundry + * ContextMenu is converted into a free placement context menu. This is useful to free the context menu from being bound + * within the overflow constraints of a parent element and allow the context menu to display at the exact mouse point + * clicked in a larger element. Note: be mindful that CSS style `position: fixed` is used to make the context menu + * display relative to the main page viewport which defines the containing block, however if you use `filter`, + * `perspective`, or `transform` in styles then that element becomes the containing block higher up than the main + * window. FQLContextMenu does not reposition the inserted HTML which is relative to the element containing the context + * menu. + */ +export class FQLContextMenu extends ContextMenu +{ + /** + * Defines the default CSS styles for the context menu. + * + * @type {{"box-shadow": string, width: string, "font-size": string, "font-family": string, position: string}} + */ + static #defaultStyle = { + position: 'fixed', + width: 'fit-content', + 'font-family': '"Signika", sans-serif', + 'font-size': '14px', + 'box-shadow': '0 0 10px #000' + }; + + /** + * @type {{top: number, left: number}} + */ + #position; + + /** + * @inheritDoc + * @override + */ + constructor(element, selector, menuItems, options = {}) + { + super(element, selector, menuItems, options); + } + + /** + * Stores the pageX / pageY position from the the JQuery event to be applied in `_setPosition`. + * + * @inheritDoc + * @override + */ + bind() + { + this.element.on(this.eventName, this.selector, (event) => + { + event.preventDefault(); + + this.#position = { left: event.pageX, top: event.pageY }; + }); + super.bind(); + } + + /** + * Delegate to the parent `_setPosition` then apply the stored position from the callback in `bind`. + * + * @inheritDoc + * @override + */ + _setPosition(html, target) + { + super._setPosition(html, target); + html.css(foundry.utils.mergeObject(this.#position, FQLContextMenu.#defaultStyle)); + } +} \ No newline at end of file diff --git a/src/view/internal/FQLDialog.js b/src/view/internal/FQLDialog.js new file mode 100644 index 00000000..fc0e315e --- /dev/null +++ b/src/view/internal/FQLDialog.js @@ -0,0 +1,306 @@ +/** + * Provides a single dialog for confirming quest, task, & reward deletion. + * + * Note: You have been warned. This is tricky code. Please understand it before modifying. Feel free to ask questions: + * + * There presently is no modal dialog in Foundry and this dialog implementation repurposes a single dialog instance + * through potentially multiple cycles of obtaining and resolving Promises storing the resolve function in the dialog + * itself. There are four locations in the codebase where a delete confirmation dialog is invoked and awaited upon. Each + * time one of the static methods below is invoked the previous the current promise resolves with undefined / void 0 + * and then the same dialog instance is reconfigured with new information about a successive delete confirmation + * operation and brings the dialog to front and renders again. This provides reasonable semi-modal behavior from just a + * single dialog instance shared across confirmation to delete quests, tasks, and rewards. + */ +export class FQLDialog +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + + /** + * Stores any open FQLDialogImpl. + * + * @type {FQLDialogImpl} + */ + static #deleteDialog = void 0; + + /** + * Closes any open FQLDialogImpl that is associated with the questId or quest log. FQLDialogImpl gets associated + * with the last app that invoked the dialog. + * + * @param {object} [options] - Optional parameters. + * + * @param {string} [options.questId] - The quest ID associated with a QuestPreview app. + * + * @param {boolean} [options.isQuestLog] - Is the quest log closing. + */ + static closeDialogs({ questId, isQuestLog = false } = {}) + { + if (this.#deleteDialog && (this.#deleteDialog.fqlQuestId === questId || + this.#deleteDialog.fqlIsQuestLog === isQuestLog)) + { + this.#deleteDialog.close(); + this.#deleteDialog = void 0; + } + } + + /** + * Show a dialog to confirm quest deletion. + * + * @param {options} options - Optional parameters. + * + * @param {string} options.name - The name for the reward to delete. + * + * @param {string} options.result - The UUID of the reward to delete. + * + * @param {string|void} options.questId - The questId to track to auto-close the dialog when the QuestPreview closes. + * + * @returns {Promise} Result of the delete confirmation dialog. + */ + static async confirmDeleteQuest({ name, result, questId, isQuestLog = false }) + { + if (this.#deleteDialog && this.#deleteDialog.rendered) + { + return this.#deleteDialog.updateFQLData({ + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.Labels.Quest'), + body: 'ForienQuestLog.DeleteDialog.BodyQuest' + }); + } + + this.#deleteDialog = void 0; + + return new Promise((resolve) => + { + this.#deleteDialog = new FQLDialogImpl({ + resolve, + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.Labels.Quest'), + body: 'ForienQuestLog.DeleteDialog.BodyQuest' + }); + + this.#deleteDialog.render(true); + }); + } + + /** + * Show a dialog to confirm reward deletion. + * + * @param {options} options - Optional parameters. + * + * @param {string} options.name - The name for the reward to delete. + * + * @param {string} options.result - The UUID of the reward to delete. + * + * @param {string|void} options.questId - The questId to track to auto-close the dialog when the QuestPreview closes. + * + * @returns {Promise} Result of the delete confirmation dialog. + */ + static async confirmDeleteReward({ name, result, questId, isQuestLog = false }) + { + if (this.#deleteDialog && this.#deleteDialog.rendered) + { + return this.#deleteDialog.updateFQLData({ + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.QuestPreview.Labels.Reward'), + body: 'ForienQuestLog.DeleteDialog.BodyReward' + }); + } + + this.#deleteDialog = void 0; + + return new Promise((resolve) => + { + this.#deleteDialog = new FQLDialogImpl({ + resolve, + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.QuestPreview.Labels.Reward'), + body: 'ForienQuestLog.DeleteDialog.BodyReward' + }); + + this.#deleteDialog.render(true); + }); + } + + /** + * Show a dialog to confirm task deletion. + * + * @param {options} options - Optional parameters. + * + * @param {string} options.name - The name for the task to delete. + * + * @param {string} options.result - The UUIDv4 of the task to delete. + * + * @param {string|void} options.questId - The questId to track to auto-close the dialog when the QuestPreview closes. + * + * @returns {Promise} Result of the delete confirmation dialog. + */ + static async confirmDeleteTask({ name, result, questId, isQuestLog = false }) + { + if (this.#deleteDialog && this.#deleteDialog.rendered) + { + return this.#deleteDialog.updateFQLData({ + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.QuestPreview.Labels.Objective'), + body: 'ForienQuestLog.DeleteDialog.BodyObjective' + }); + } + + this.#deleteDialog = void 0; + + return new Promise((resolve) => + { + this.#deleteDialog = new FQLDialogImpl({ + resolve, + name, + result, + questId, + isQuestLog, + title: game.i18n.localize('ForienQuestLog.QuestPreview.Labels.Objective'), + body: 'ForienQuestLog.DeleteDialog.BodyObjective' + }); + + this.#deleteDialog.render(true); + }); + } +} + +/** + * Provides the FQL dialog implementation. + */ +class FQLDialogImpl extends Dialog +{ + /** + * Stores the options specific to the dialog + * + * @type {FQLDialogOptions} + */ + #fqlOptions; + + /** + * @param {FQLDialogOptions} options FQLDialogImpl Options + */ + constructor(options) + { + super(void 0, { minimizable: false, height: 'auto' }); + + this.#fqlOptions = options; + + /** + * The Dialog options to set. + * + * @type {object} + * @see https://foundryvtt.com/api/classes/client.Dialog.html + */ + this.data = { + title: game.i18n.format('ForienQuestLog.DeleteDialog.TitleDel', this.#fqlOptions), + content: `

    ${game.i18n.format('ForienQuestLog.DeleteDialog.HeaderDel', this.#fqlOptions)}

    ` + + `

    ${game.i18n.localize(this.#fqlOptions.body)}

    `, + buttons: { + yes: { + icon: '', + label: game.i18n.localize('ForienQuestLog.DeleteDialog.Delete'), + callback: () => this.#fqlOptions.resolve(this.#fqlOptions.result) + }, + no: { + icon: '', + label: game.i18n.localize('ForienQuestLog.DeleteDialog.Cancel'), + callback: () => this.#fqlOptions.resolve(void 0) + } + } + }; + } + + /** + * Overrides the close action to resolve the cached Promise with undefined. + * + * @returns {Promise} + */ + async close() + { + this.#fqlOptions.resolve(void 0); + return super.close(); + } + + /** + * @returns {boolean} Returns {@link FQLDialogOptions.isQuestLog} from options. + */ + get fqlIsQuestLog() { return this.#fqlOptions.isQuestLog; } + + /** + * @returns {string} Returns {@link FQLDialogOptions.questId} from options. + */ + get fqlQuestId() { return this.#fqlOptions.questId; } + + /** + * Updates the FQLDialogOptions when a dialog is already showing and a successive delete operation is initiated. + * + * Resolves the currently cached Promise with undefined and cache a new Promise which is returned. + * + * @param {FQLDialogOptions} options - The new options to set for Dialog rendering and success return value. + * + * @returns {Promise} The new Promise to await upon. + */ + updateFQLData(options) + { + // Resolve old promise with undefined + this.#fqlOptions.resolve(void 0); + + // Set new options + this.#fqlOptions = options; + + // Create a new Promise that will store the resolve function in this FQLDialogImpl. + const promise = new Promise((resolve) => { this.#fqlOptions.resolve = resolve; }); + + // Update title and content with new data. + this.data.title = game.i18n.format('ForienQuestLog.DeleteDialog.TitleDel', this.#fqlOptions); + this.data.content = `

    ${game.i18n.format('ForienQuestLog.DeleteDialog.HeaderDel', this.#fqlOptions)}

    ` + + `

    ${game.i18n.localize(this.#fqlOptions.body)}

    `; + + // Bring the dialog to top and render again. + this.bringToTop(); + this.render(true); + + // Return the new promise which is resolved from another update with undefined or the dialog confirmation action, + // or the dialog being closed. + return promise; + } +} + +/** + * @typedef FQLDialogOptions + * + * @property {Function} [resolve] - The cached resolve function of the Dialog promise. + * + * @property {string} name - The name of the data being deleted. + * + * @property {result} result - The result to resolve when `OK` is pressed. + * + * @property {string} questId - The associated QuestPreview by quest ID. + * + * @property {boolean} isQuestLog - boolean indicating that the QuestLog owns the dialog. + * + * @property {string} title - The title of the dialog. + * + * @property {string} body - The body language file ID to use for dialog rendering. + */ diff --git a/src/view/internal/FQLDocumentOwnershipConfig.js b/src/view/internal/FQLDocumentOwnershipConfig.js new file mode 100644 index 00000000..65153942 --- /dev/null +++ b/src/view/internal/FQLDocumentOwnershipConfig.js @@ -0,0 +1,33 @@ +/** + * Provides a custom override to DocumentOwnershipConfig enabling GM & trusted player w/ edit capabilities to alter + * quest ownership. The default DocumentOwnershipConfig only allows GM level users permission editing. + * + * When the underlying document / {@link JournalEntry} is updated the {@link QuestDB} will receive this update and + * fire {@link QuestDBHooks} that other parts of FQL can respond to handle as necessary. In particular + * {@link ViewManager} handles these hooks to update the GUI on local and remote clients when ownership change. + */ +export class FQLDocumentOwnershipConfig extends DocumentOwnershipConfig // eslint-disable-line no-undef +{ + /** @override */ + async _updateObject(event, formData) + { + event.preventDefault(); + + // Collect new ownership levels from the form data + const omit = CONST.DOCUMENT_META_OWNERSHIP_LEVELS.DEFAULT; + const ownershipLevels = {}; + + for (const [user, level] of Object.entries(formData)) + { + if (level === omit) + { + delete ownershipLevels[user]; + continue; + } + ownershipLevels[user] = level; + } + + // Update a single Document + return this.document.update({ ownership: ownershipLevels }, { diff: false, recursive: false, noHook: true }); + } +} diff --git a/src/view/internal/index.js b/src/view/internal/index.js new file mode 100644 index 00000000..8ba6c6ab --- /dev/null +++ b/src/view/internal/index.js @@ -0,0 +1,3 @@ +export * from './FQLContextMenu.js'; +export * from './FQLDialog.js'; +export * from './FQLDocumentOwnershipConfig.js'; \ No newline at end of file diff --git a/src/view/log/HandlerLog.js b/src/view/log/HandlerLog.js new file mode 100644 index 00000000..3b4a5681 --- /dev/null +++ b/src/view/log/HandlerLog.js @@ -0,0 +1,99 @@ +import { + QuestDB, + Socket, + ViewManager } from '../../control/index.js'; + +import { QuestAPI } from '../../control/public/index.js'; + +import { Quest } from '../../model/index.js'; + +import { FQLDialog } from '../internal/index.js'; + +/** + * Provides all {@link JQuery} callbacks for the {@link QuestLog}. + */ +export class HandlerLog +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Handles the quest add button. + * + * @returns {Promise} + */ + static async questAdd() + { + if (ViewManager.verifyQuestCanAdd()) + { + const quest = await QuestDB.createQuest(); + ViewManager.questAdded({ quest }); + } + } + + /** + * Handles deleting a quest. The trashcan icon. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @returns {Promise} + */ + static async questDelete(event) + { + const questId = $(event.target).data('quest-id'); + const name = $(event.target).data('quest-name'); + + const result = await FQLDialog.confirmDeleteQuest({ name, result: questId, questId, isQuestLog: true }); + if (result) + { + await QuestDB.deleteQuest({ questId: result }); + } + } + + /** + * Prepares the data transfer when a quest is dragged from the {@link QuestLog}. + * + * @param {JQuery.DragStartEvent} event - JQuery.DragStartEvent + */ + static questDragStart(event) + { + const dataTransfer = { + type: Quest.documentName, + id: $(event.target).data('quest-id') + }; + + event.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(dataTransfer)); + } + + /** + * Handles the quest open click via {@link QuestAPI.open}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + */ + static questOpen(event) + { + const questId = $(event.target)?.closest('.drag-quest')?.data('quest-id'); + QuestAPI.open({ questId }); + } + + /** + * Handles changing the quest status via {@link Socket.setQuestStatus}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @returns {Promise} + */ + static async questStatusSet(event) + { + const target = $(event.target).data('target'); + const questId = $(event.target).data('quest-id'); + + const quest = QuestDB.getQuest(questId); + if (quest) { await Socket.setQuestStatus({ quest, target }); } + } +} \ No newline at end of file diff --git a/src/view/log/QuestLog.js b/src/view/log/QuestLog.js new file mode 100644 index 00000000..ffdda860 --- /dev/null +++ b/src/view/log/QuestLog.js @@ -0,0 +1,320 @@ +import { + QuestDB, + Socket, + Utils } from '../../control/index.js'; + +import { + FQLContextMenu, + FQLDialog } from '../internal/index.js'; + +import { HandlerLog } from './HandlerLog.js'; + +import { + constants, + jquery, + questStatus, + questStatusI18n, + questTabIndex, + settings } from '../../model/constants.js'; + +/** + * Provides the main quest log app which shows the quests separated by status either with bookmark or classic tabs. + * + * In {@link QuestLog.getData} the {@link QuestsCollect} data is retrieved from {@link QuestDB.sortCollect} which + * provides automatic sorting of each quest status category by either {@link SortFunctions.ALPHA} or + * {@link SortFunctions.DATE_END} for status categories {@link questStatus.completed} and {@link questStatus.failed}. + * Several module settings and whether the current user is a GM is also passed back as data to be used in rendering the + * {@link Handlebars} template. + * + * {@link JQuery} control callbacks are setup in {@link QuestLog.activateListeners} and are located in a separate static + * control class {@link HandlerLog}. + */ +export class QuestLog extends Application +{ + /** + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.Application.html + */ + constructor(options = {}) + { + super(options); + } + + /** + * Default Application options + * + * @returns {object} options - Application options. + * @see https://foundryvtt.com/api/classes/client.Application.html#options + */ + static get defaultOptions() + { + return foundry.utils.mergeObject(super.defaultOptions, { + id: constants.moduleName, + classes: [constants.moduleName], + template: 'modules/forien-quest-log/templates/quest-log.html', + width: 700, + height: 480, + minimizable: true, + resizable: true, + title: game.i18n.localize('ForienQuestLog.QuestLog.Title'), + tabs: [{ navSelector: '.log-tabs', contentSelector: '.log-body', initial: 'active' }] + }); + } + + /** + * Specify the set of config buttons which should appear in the Application header. Buttons should be returned as an + * Array of objects. + * + * Provides an explicit override of Application._getHeaderButtons to add one additional buttons for the app header + * for showing the quest log to users via {@link Socket.showQuestLog} + * + * @returns {ApplicationHeaderButton[]} The app header buttons. + * @override + */ + _getHeaderButtons() + { + const buttons = super._getHeaderButtons(); + + // Share QuestLog w/ remote clients. + if (game.user.isGM) + { + buttons.unshift({ + label: game.i18n.localize('ForienQuestLog.Labels.AppHeader.ShowPlayers'), + class: 'share-quest', + icon: 'fas fa-eye', + onclick: () => + { + Socket.showQuestLog(this._tabs[0].active); + } + }); + } + + return buttons; + } + + + /** + * Defines all jQuery control callbacks with event listeners for click, drag, drop via various CSS selectors. + * + * @param {JQuery} html - The jQuery instance for the window content of this Application. + * + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#activateListeners + */ + activateListeners(html) + { + super.activateListeners(html); + + // Here we use a bit of jQuery to retrieve the background image of .window-content to match the game system + // background image for the bookmark tabs. This is only done if the module setting is checked which it is by + // default and the background image actually exists. The fallback is the default parchment image set in the + // FQL styles. + const navStyle = game.settings.get(constants.moduleName, settings.navStyle); + const dynamicBackground = game.settings.get(constants.moduleName, settings.dynamicBookmarkBackground); + if ('bookmarks' === navStyle && dynamicBackground) + { + const windowContent = $('#forien-quest-log .window-content'); + const fqlBookmarkItem = $('#forien-quest-log .item'); + + const backImage = windowContent.css('background-image'); + const backBlendMode = windowContent.css('background-blend-mode'); + const backColor = windowContent.css('background-color'); + + fqlBookmarkItem.css('background-image', backImage); + fqlBookmarkItem.css('background-color', backColor); + fqlBookmarkItem.css('background-blend-mode', backBlendMode); + } + + html.on(jquery.click, '.new-quest-btn', HandlerLog.questAdd); + + html.on(jquery.click, '.actions.quest-status i.delete', HandlerLog.questDelete); + + // This registers for any element and prevents the circle / slash icon displaying for not being a drag target. + html.on(jquery.dragenter, (event) => event.preventDefault()); + + html.on(jquery.dragstart, '.drag-quest', void 0, HandlerLog.questDragStart); + + html.on(jquery.click, '.open-quest', void 0, HandlerLog.questOpen); + + html.on(jquery.click, '.actions.quest-status i.move', HandlerLog.questStatusSet); + + this.#contextMenu(html); + } + + /** + * Handle closing any confirm delete quest dialog attached to QuestLog. + * + * @override + * @inheritDoc + */ + async close(options) + { + FQLDialog.closeDialogs({ isQuestLog: true }); + return super.close(options); + } + + /** + * Create the context menu. There are two separate context menus for the active / in progress tab and all other tabs. + * + * @param {JQuery} html - JQuery element for this application. + */ + #contextMenu(html) + { + const menuItemCopyLink = { + name: 'ForienQuestLog.QuestLog.ContextMenu.CopyEntityLink', + icon: '', + callback: async (menu) => + { + const questId = $(menu)?.closest('.drag-quest')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + + if (quest && await Utils.copyTextToClipboard(`@JournalEntry[${quest.id}]{${quest.name}}`)) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.LinkCopied')); + } + } + }; + + /** + * @type {object[]} + */ + const menuItemsOther = [menuItemCopyLink]; + + /** + * @type {object[]} + */ + const menuItemsActive = [menuItemCopyLink]; + + if (game.user.isGM) + { + const menuItemQuestID = { + name: 'ForienQuestLog.QuestLog.ContextMenu.CopyQuestID', + icon: '', + callback: async (menu) => + { + const questId = $(menu)?.closest('.drag-quest')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + + if (quest && await Utils.copyTextToClipboard(quest.id)) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestIDCopied')); + } + } + }; + + menuItemsActive.push(menuItemQuestID); + menuItemsOther.push(menuItemQuestID); + + menuItemsActive.push({ + name: 'ForienQuestLog.QuestLog.ContextMenu.PrimaryQuest', + icon: '', + callback: (menu) => + { + const questId = $(menu)?.closest('.drag-quest')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + if (quest) { Socket.setQuestPrimary({ quest }); } + } + }); + } + + // Must show two different context menus as only the active / in progress tab potentially has the menu option to + // allow the GM to set the primary quest. + new FQLContextMenu(html, '.tab:not([data-tab="active"]) .drag-quest', menuItemsOther); + new FQLContextMenu(html, '.tab[data-tab="active"] .drag-quest', menuItemsActive); + } + + /** + * Retrieves the sorted quest collection from the {@link QuestDB.sortCollect} and sets several state parameters for + * GM / player / trusted player edit along with several module settings: {@link FQLSettings.allowPlayersAccept}, + * {@link FQLSettings.allowPlayersCreate}, {@link FQLSettings.showTasks} and {@link FQLSettings.navStyle}. + * + * @override + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#getData + */ + async getData(options = {}) + { + return foundry.utils.mergeObject(super.getData(), { + options, + isGM: game.user.isGM, + isPlayer: !game.user.isGM, + isTrustedPlayerEdit: Utils.isTrustedPlayerEdit(), + canAccept: game.settings.get(constants.moduleName, settings.allowPlayersAccept), + canCreate: game.settings.get(constants.moduleName, settings.allowPlayersCreate), + showTasks: game.settings.get(constants.moduleName, settings.showTasks), + style: game.settings.get(constants.moduleName, settings.navStyle), + questStatusI18n, + quests: QuestDB.sortCollect() + }); + } + + /** + * Overrides the internal Application._render method to select the tab if the quest log is rendered with optional: + * `tabId` data that matches an entry in `constants.questStatus`. This comes into play as when a GM uses the + * `show to players` button in the app header as not only will the quest log open for players, but the specific tab + * selected by the GM will show. It is also possible to add `tabId` to the `ForienQuestLog.Open.QuestLog` hook to + * open a specific tab. + * + * @inheritDoc + */ + async _render(force = false, options = {}) + { + await super._render(force, options); + + if (this._state === Application.RENDER_STATES.RENDERED && typeof options.tabId === 'string' && + options.tabId in questStatus) + { + if (options.tabId === questStatus.inactive) + { + // Only switch to inactive tab if GM or trusted player w/ edit. + if (game.user.isGM || Utils.isTrustedPlayerEdit()) { this._tabs[0].activate(options.tabId); } + } + else + { + this._tabs[0].activate(options.tabId); + } + } + } + + /** + * Some game systems and custom UI theming modules provide hard overrides on overflow-x / overflow-y styles. Alas, we + * need to set these for '.window-content' to 'visible' which will cause an issue for very long tables. Thus, we must + * manually set the table max-heights based on the position / height of the {@link Application}. + * + * @param {object} opts - Optional parameters. + * + * @param {number|null} opts.left - The left offset position in pixels. + * + * @param {number|null} opts.top - The top offset position in pixels. + * + * @param {number|null} opts.width - The application width in pixels. + * + * @param {number|string|null} opts.height - The application height in pixels. + * + * @param {number|null} opts.scale - The application scale as a numeric factor where 1.0 is default. + * + * @returns {{left: number, top: number, width: number, height: number, scale:number}} + * The updated position object for the application containing the new values. + */ + setPosition(opts) + { + const currentPosition = super.setPosition(opts); + + // Retrieve all the table elements. + const tableElements = $('#forien-quest-log .table'); + + // Retrieve the active table. + const tabIndex = questTabIndex[this?._tabs[0]?.active]; + const table = tableElements[tabIndex]; + + if (table) + { + const fqlPosition = $('#forien-quest-log')[0].getBoundingClientRect(); + const tablePosition = table.getBoundingClientRect(); + + // Manually calculate the max height for the table based on the position of the main window div and table. + tableElements.css('max-height', `${currentPosition.height - (tablePosition.top - fqlPosition.top + 16)}px`); + } + + return currentPosition; + } +} diff --git a/src/view/preview/HandlerAny.js b/src/view/preview/HandlerAny.js new file mode 100644 index 00000000..e433397d --- /dev/null +++ b/src/view/preview/HandlerAny.js @@ -0,0 +1,70 @@ +import { + QuestDB, + Socket } from '../../control/index.js'; + +import { QuestAPI } from '../../control/public/index.js'; + +import { FQLDialog } from '../internal/index.js'; + +/** + * These handler {@link JQuery} callbacks can be called on any tab. + */ +export class HandlerAny +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Confirms deleting a quest with {@link FQLDialog.confirmDeleteQuest} before invoking {@link QuestDB.deleteQuest}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @returns {Promise} + */ + static async questDelete(event, quest) + { + const questId = $(event.target).data('quest-id'); + const name = $(event.target).data('quest-name'); + + const result = await FQLDialog.confirmDeleteQuest({ name, result: questId, questId: quest.id }); + if (result) + { + await QuestDB.deleteQuest({ questId: result }); + } + } + + /** + * Opens a {@link QuestPreview} via {@link QuestAPI.open}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent. + */ + static questOpen(event) + { + const questId = $(event.currentTarget).data('quest-id'); + QuestAPI.open({ questId }); + } + + /** + * Potentially sets a new {@link Quest.status} via {@link Socket.setQuestStatus}. If the current user is not a GM + * a GM level user must be logged in for a successful completion of the set status operation. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @returns {Promise} + */ + static async questStatusSet(event) + { + const target = $(event.target).data('target'); + const questId = $(event.target).data('quest-id'); + + const quest = QuestDB.getQuest(questId); + if (quest) { await Socket.setQuestStatus({ quest, target }); } + } +} \ No newline at end of file diff --git a/src/view/preview/HandlerDetails.js b/src/view/preview/HandlerDetails.js new file mode 100644 index 00000000..e6c6481f --- /dev/null +++ b/src/view/preview/HandlerDetails.js @@ -0,0 +1,1081 @@ +import { + Socket, + Utils } from '../../control/index.js'; + +import { Quest } from '../../model/index.js'; + +import { FQLDialog } from '../internal/index.js'; + +import { + constants, + jquery, + settings } from '../../model/constants.js'; + +/** + * Provides all {@link JQuery} callbacks for the `details` tab. + */ +export class HandlerDetails +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent. + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static questEditName(event, quest, questPreview) + { + const target = $(event.target).data('target'); + + let value = quest[target]; + + value = value.replace(/"/g, '"'); + + const input = $(``); + + const parent = $(event.target).closest('.actions-single').prev('.editable-container'); + + parent.html(''); + parent.append(input); + input.trigger(jquery.focus); + + // If the HTMLElement has setSelectionRange then set cursor to the end. + if (input[0]?.setSelectionRange) { input[0].setSelectionRange(value.length, value.length); } + + /** + * Store the input focus callback in the associated QuestPreview instance so that it can be invoked if the app is + * closed in {@link QuestPreview.close} while the input field is focused / being edited allowing any edits to be + * saved. Otherwise the callback is invoked normally below as part of the input focus out event. + * + * @param {JQuery.FocusOutEvent|void} event - JQuery.FocusOutEvent + * + * @param {object} saveOptions - Options to pass to `saveQuest`; used in {@link QuestPreview.close}. + * + * @returns {Promise} + * @package + * + * @see QuestPreview.close + * @see QuestPreview._activeFocusOutFunction + */ + questPreview._activeFocusOutFunction = async (event, saveOptions = void 0) => + { + const valueOut = input.val(); + questPreview._activeFocusOutFunction = void 0; + + switch (target) + { + case 'name': + quest.name = valueOut; + questPreview.options.title = game.i18n.format('ForienQuestLog.QuestPreview.Title', quest); + break; + } + await questPreview.saveQuest(saveOptions); + }; + + input.on(jquery.focusout, questPreview._activeFocusOutFunction); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview._activeFocusOutFunction = void 0; + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static questGiverCustomEditName(event, quest, questPreview) + { + const target = $(event.target).data('target'); + + let value = quest[target]; + + value = value.replace(/"/g, '"'); + + const input = $(``); + + const parent = $(event.target).closest('.actions-single').prev('.editable-container'); + + parent.css('flex', '0 0 230px'); + parent.html(''); + parent.append(input); + input.trigger(jquery.focus); + + // If the HTMLElement has setSelectionRange then set cursor to the end. + if (input[0]?.setSelectionRange) { input[0].setSelectionRange(value.length, value.length); } + + /** + * Store the input focus callback in the associated QuestPreview instance so that it can be invoked if the app is + * closed in {@link QuestPreview.close} while the input field is focused / being edited allowing any edits to be + * saved. Otherwise the callback is invoked normally below as part of the input focus out event. + * + * @param {JQuery.FocusOutEvent|void} event - JQuery.FocusOutEvent + * + * @param {object} saveOptions - Options to pass to `saveQuest`; used in {@link QuestPreview.close}. + * + * @returns {Promise} + * @package + * + * @see QuestPreview.close + * @see QuestPreview._activeFocusOutFunction + */ + questPreview._activeFocusOutFunction = async (event, saveOptions = void 0) => + { + const valueOut = input.val(); + questPreview._activeFocusOutFunction = void 0; + + switch (target) + { + case 'giverName': + quest.giverName = valueOut; + if (typeof quest.giverData === 'object') { quest.giverData.name = valueOut; } + questPreview.options.title = game.i18n.format('ForienQuestLog.QuestPreview.Title', quest); + await questPreview.saveQuest(saveOptions); + break; + } + }; + + input.on(jquery.focusout, questPreview._activeFocusOutFunction); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview._activeFocusOutFunction = void 0; + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static questGiverCustomSelectImage(quest, questPreview) + { + const currentPath = quest.giver === 'abstract' ? quest.image : void 0; + + new FilePicker({ + type: 'image', + current: currentPath, + callback: async (path) => + { + quest.giver = 'abstract'; + quest.image = path; + quest.giverName = game.i18n.localize('ForienQuestLog.QuestPreview.Labels.CustomSource'); + quest.giverData = await Quest.giverFromQuest(quest); + delete quest.giverData.uuid; + + await questPreview.saveQuest(); + }, + }).browse(currentPath); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async questGiverDelete(quest, questPreview) + { + quest.resetGiver(); + return questPreview.saveQuest(); + } + + /** + * @param {JQuery.DropEvent} event - JQuery.DropEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async questGiverDropDocument(event, quest, questPreview) + { + event.preventDefault(); + + let data; + + try + { + data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); + } + catch (err) + { + console.warn(`ForienQuestLog HandlerDetails.questGiverDropDocument warning: failed to parse data transfer`); + } + + const uuid = Utils.getUUID(data, ['Actor', 'Item', 'JournalEntry']); + + if (typeof uuid === 'string') + { + const giverData = await Quest.giverFromUUID(uuid); + if (giverData) + { + quest.giver = uuid; + quest.giverData = giverData; + await questPreview.saveQuest(); + } + else + { + ui.notifications.warn(game.i18n.format('ForienQuestLog.QuestPreview.Notifications.BadUUID', { uuid })); + } + } + else + { + // Slightly awkward as we need to check if this is an actor owned item specifically. + if (typeof data?.uuid === 'string' && + data.uuid.startsWith('Actor') && (data.uuid.match(/\./g) || []).length > 1) + { + ui.notifications.warn(game.i18n.localize('ForienQuestLog.QuestPreview.Notifications.WrongDocType')); + } + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async questGiverShowActorSheet(event, questPreview) + { + const uuid = $(event.target).data('actor-uuid'); + + if (typeof uuid === 'string' && uuid.length) + { + const appId = await Utils.showSheetFromUUID(uuid, { editable: false }); + + // If a new sheet is rendered push it to the opened appIds. + if (appId && !questPreview._openedAppIds.includes(appId)) { questPreview._openedAppIds.push(appId); } + } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async questGiverToggleImage(quest, questPreview) + { + quest.toggleImage(); + + const giverData = await Quest.giverFromQuest(quest); + if (giverData) + { + quest.giverData = giverData; + await questPreview.saveQuest(); + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static rewardAbstractEditName(event, quest, questPreview) + { + const target = $(event.target).data('target'); + + let value = quest[target]; + let uuidv4; + + if (target === 'reward.name') + { + uuidv4 = $(event.target).data('uuidv4'); + + const reward = quest.getReward(uuidv4); + if (!reward) { return; } + + value = reward.name; + } + + value = value.replace(/"/g, '"'); + + const input = $(``); + + // This consumes any clicks on the input element preventing the abstract reward image popup from showing when + // clicking on the input element. + input.on(jquery.click, (event) => { event.stopImmediatePropagation(); }); + + const parent = $(event.target).closest('.actions').prev('.editable-container'); + + parent.html(''); + parent.append(input); + input.trigger(jquery.focus); + + // If the HTMLElement has setSelectionRange then set cursor to the end. + if (input[0]?.setSelectionRange) { input[0].setSelectionRange(value.length, value.length); } + + /** + * Store the input focus callback in the associated QuestPreview instance so that it can be invoked if the app is + * closed in {@link QuestPreview.close} while the input field is focused / being edited allowing any edits to be + * saved. Otherwise the callback is invoked normally below as part of the input focus out event. + * + * @param {JQuery.FocusOutEvent|void} event - JQuery.FocusOutEvent + * + * @param {object} saveOptions - Options to pass to `saveQuest`; used in {@link QuestPreview.close}. + * + * @returns {Promise} + * @package + * + * @see QuestPreview.close + * @see QuestPreview._activeFocusOutFunction + */ + questPreview._activeFocusOutFunction = async (event, saveOptions = void 0) => + { + const valueOut = input.val(); + questPreview._activeFocusOutFunction = void 0; + + switch (target) + { + case 'reward.name': + { + uuidv4 = input.data('uuidv4'); + const reward = quest.getReward(uuidv4); + if (!reward) { return; } + + reward.data.name = valueOut; + await questPreview.saveQuest(saveOptions); + break; + } + } + }; + + input.on(jquery.focusout, questPreview._activeFocusOutFunction); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview._activeFocusOutFunction = void 0; + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * Creates a new abstract reward if the input entry is successful or contains data and a focus out event occurs. + * + * The module setting: {@link FQLSettings.defaultAbstractRewardImage} stores the default abstract reward image. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static rewardAddAbstract(event, quest, questPreview) + { + const li = $('
  • '); + + const input = $(``); + + const box = $(event.target).closest('.quest-rewards').find('.rewards-box ul'); + + li.append(input); + box.append(li); + + input.trigger(jquery.focus); + + input.on(jquery.focusout, async (event) => + { + const value = $(event.target).val(); + if (value !== void 0 && value.length) + { + quest.addReward({ + data: { + name: value, + img: game.settings.get(constants.moduleName, settings.defaultAbstractRewardImage) + }, + hidden: true, + type: 'Abstract' + }); + } + await questPreview.saveQuest(); + }); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardDelete(event, quest, questPreview) + { + const target = $(event.target); + const uuidv4 = target.data('uuidv4'); + const name = target.data('reward-name'); + + // Await a semi-modal dialog. + const result = await FQLDialog.confirmDeleteReward({ name, result: uuidv4, questId: quest.id }); + if (result) + { + quest.removeReward(result); + + await questPreview.saveQuest(); + } + } + + /** + * @param {JQuery.DragStartEvent} event - JQuery.DragStartEvent + * + * @param {Quest} quest - The current quest being manipulated. + */ + static async rewardDragStartItem(event, quest) + { + const data = $(event.target).data('transfer'); + + const document = await Utils.getDocumentFromUUID(data, { permissionCheck: false }); + if (document) + { + const uuidData = Utils.getDataFromUUID(data); + + /** + * @type {RewardDropData} + */ + const dataTransfer = { + _fqlData: { + type: 'reward', + questId: quest.id, + uuidv4: data.uuidv4, + itemName: data.name, + userName: game.user.name, + }, + type: 'Item', + uuid: data.uuid, + id: document.id + }; + + // Add compendium pack info if applicable. + if (uuidData && uuidData.pack) { dataTransfer.pack = uuidData.pack; } + + event.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(dataTransfer)); + } + } + + /** + * @param {JQuery.DragStartEvent} event - JQuery.DragStartEvent + */ + static rewardDragStartSort(event) + { + event.stopPropagation(); + + const li = event.target.closest('li') || null; + if (!li) { return; } + + const dataTransfer = { + type: 'Reward', + mode: 'Sort', + uuidv4: $(li).data('uuidv4') + }; + event.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(dataTransfer)); + } + + /** + * Handles an external item reward drop. Also handles the sort reward item drop. + * + * @param {JQuery.DropEvent} event - JQuery.DropEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardDropItem(event, quest, questPreview) + { + event.preventDefault(); + + let data; + try + { + data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); + } + catch (e) + { + return; + } + + if (data?.mode === 'Sort' && data?.type === 'Reward') + { + const dt = event.target.closest('li.reward') || null; + quest.sortRewards(data.uuidv4, dt?.dataset.uuidv4); + await quest.save(); + Socket.refreshQuestPreview({ questId: quest.id }); + } + + if (data?._fqlData !== void 0) { return; } + + if (data?.type === 'Actor') + { + const uuid = Utils.getUUID(data); + + if (typeof uuid === 'string') + { + const actor = await Quest.giverFromUUID(uuid); + if (actor) + { + quest.addReward({ type: 'Actor', data: actor, hidden: true }); + await questPreview.saveQuest(); + } + else + { + ui.notifications.warn(game.i18n.format('ForienQuestLog.QuestPreview.Notifications.BadUUID', { uuid })); + } + } + } + else if (data?.type === 'Item') + { + const uuid = Utils.getUUID(data); + + if (typeof uuid === 'string') + { + const item = await Quest.giverFromUUID(uuid); + if (item) + { + quest.addReward({ type: 'Item', data: item, hidden: true }); + await questPreview.saveQuest(); + } + else + { + ui.notifications.warn(game.i18n.format('ForienQuestLog.QuestPreview.Notifications.BadUUID', { uuid })); + } + } + else + { + // Slightly awkward as we need to check if this is an actor owned item specifically. + if (typeof data?.uuid === 'string' && + data.uuid.startsWith('Actor') && (data.uuid.match(/\./g) || []).length > 1) + { + ui.notifications.warn(game.i18n.localize('ForienQuestLog.QuestPreview.Notifications.WrongItemType')); + } + } + } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardsHideAll(quest, questPreview) + { + for (const reward of quest.rewards) { reward.hidden = true; } + if (quest.rewards.length) { await questPreview.saveQuest(); } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardsLockAll(quest, questPreview) + { + for (const reward of quest.rewards) { reward.locked = true; } + if (quest.rewards.length) { await questPreview.saveQuest(); } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardSelectImage(event, quest, questPreview) + { + const uuidv4 = $(event.target).data('uuidv4'); + + let reward = quest.getReward(uuidv4); + if (!reward) { return; } + + const currentPath = reward.data.img; + await new FilePicker({ + type: 'image', + current: currentPath, + callback: async (path) => + { + reward = quest.getReward(uuidv4); + if (reward) + { + reward.data.img = path; + await questPreview.saveQuest(); + } + }, + }).browse(currentPath); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardsShowAll(quest, questPreview) + { + for (const reward of quest.rewards) { reward.hidden = false; } + if (quest.rewards.length) { await questPreview.saveQuest(); } + } + + /** + * If an abstract reward has an image set then show an image popout. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent. + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardShowImagePopout(event, quest, questPreview) + { + // Check the event target and make sure it is `p.reward-name` otherwise early out. + if (event.target && !$(event.target).is('p.reward-name')) { return; } + + event.stopPropagation(); + + const uuidv4 = $(event.currentTarget).data('uuidv4'); + + const reward = quest.getReward(uuidv4); + + if (reward && (questPreview.canEdit || !reward.locked)) + { + if (questPreview._rewardImagePopup !== void 0 && questPreview._rewardImagePopup.rendered) + { + if (reward.data?.img?.length) + { + questPreview._rewardImagePopup.object = reward.data.img; + questPreview._rewardImagePopup.render(true); + questPreview._rewardImagePopup.bringToTop(); + } + } + else + { + if (reward.data?.img?.length) + { + questPreview._rewardImagePopup = new ImagePopout(reward.data.img, { shareable: true }); + questPreview._rewardImagePopup.render(true); + } + } + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent. + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardShowSheet(event, quest, questPreview) + { + event.stopPropagation(); + const data = $(event.currentTarget).data('transfer'); + const uuidv4 = $(event.currentTarget).data('uuidv4'); + + const reward = quest.getReward(uuidv4); + + if (reward && (questPreview.canEdit || !reward.locked)) + { + const appId = await Utils.showSheetFromUUID(data, { permissionCheck: false, editable: false }); + + // If a new sheet is rendered push it to the opened appIds. + if (appId && !questPreview._openedAppIds.includes(appId)) { questPreview._openedAppIds.push(appId); } + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardToggleHidden(event, quest, questPreview) + { + const uuidv4 = $(event.target).data('uuidv4'); + const reward = quest.getReward(uuidv4); + if (reward) + { + reward.toggleVisible(); + await questPreview.saveQuest(); + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardToggleLocked(event, quest, questPreview) + { + const uuidv4 = $(event.target).data('uuidv4'); + const reward = quest.getReward(uuidv4); + if (reward) + { + reward.toggleLocked(); + await questPreview.saveQuest(); + } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async rewardsUnlockAll(quest, questPreview) + { + for (const reward of quest.rewards) { reward.locked = false; } + if (quest.rewards.length) { await questPreview.saveQuest(); } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static async splashImagePopupShow(quest, questPreview) + { + if (questPreview._splashImagePopup !== void 0 && questPreview._splashImagePopup.rendered) + { + questPreview._splashImagePopup.bringToTop(); + } + else + { + questPreview._splashImagePopup = new ImagePopout(quest.splash, { shareable: true }); + questPreview._splashImagePopup.render(true); + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static taskAdd(event, quest, questPreview) + { + event.preventDefault(); + + const li = $('
  • '); + + const placeholder = $(''); + + const input = $(``); + + const box = $(event.target).closest('.quest-tasks').find('.tasks-box ul'); + + li.append(placeholder); + li.append(input); + box.append(li); + + input.trigger(jquery.focus); + + input.on(jquery.focusout, async (event) => + { + const value = $(event.target).val(); + if (value !== void 0 && value.length) + { + quest.addTask({ name: value, hidden: questPreview.canEdit }); + } + await questPreview.saveQuest(); + }); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async taskDelete(event, quest, questPreview) + { + const target = $(event.target); + const uuidv4 = target.data('uuidv4'); + const name = target.data('task-name'); + + const result = await FQLDialog.confirmDeleteTask({ name, result: uuidv4, questId: quest.id }); + if (result) + { + quest.removeTask(result); + + await questPreview.saveQuest(); + } + } + + /** + * @param {JQuery.DragStartEvent} event - JQuery.DragStartEvent + */ + static taskDragStartSort(event) + { + event.stopPropagation(); + + const li = event.target.closest('li') || null; + if (!li) { return; } + + const dataTransfer = { + type: 'Task', + mode: 'Sort', + uuidv4: $(li).data('uuidv4') + }; + + event.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(dataTransfer)); + } + + /** + * @param {JQuery.DropEvent} event - JQuery.DropEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @returns {Promise} + */ + static async taskDropItem(event, quest) + { + event.preventDefault(); + + let data; + try + { + data = JSON.parse(event.originalEvent.dataTransfer.getData('text/plain')); + } + catch (e) + { + return; + } + + if (data?.mode === 'Sort' && data?.type === 'Task') + { + const dt = event.target.closest('li.task') || null; + quest.sortTasks(data.uuidv4, dt?.dataset.uuidv4); + await quest.save(); + Socket.refreshQuestPreview({ questId: quest.id }); + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + */ + static taskEditName(event, quest, questPreview) + { + const target = $(event.target).data('target'); + let uuidv4 = $(event.target).data('uuidv4'); + let task = quest.getTask(uuidv4); + + // Early out conditional if the target isn't `task.name` or the task doesn't exist. + if (target === void 0 || target !== 'task.name' || !task) { return; } + + let value = task.name; + + value = value.replace(/"/g, '"'); + + const input = $(``); + + const parent = $(event.target).closest('.actions').prev('.editable-container'); + + parent.html(''); + parent.append(input); + input.trigger(jquery.focus); + + // If the HTMLElement has setSelectionRange then set cursor to the end. + if (input[0]?.setSelectionRange) { input[0].setSelectionRange(value.length, value.length); } + + /** + * Store the input focus callback in the associated QuestPreview instance so that it can be invoked if the app is + * closed in {@link QuestPreview.close} while the input field is focused / being edited allowing any edits to be + * saved. Otherwise the callback is invoked normally below as part of the input focus out event. + * + * @param {JQuery.FocusOutEvent|void} event -JQuery.FocusOutEvent + * + * @param {object} saveOptions - Options to pass to `saveQuest`; used in {@link QuestPreview.close}. + * + * @returns {Promise} + * @protected + * @see QuestPreview.close + * @see QuestPreview._activeFocusOutFunction + */ + questPreview._activeFocusOutFunction = async (event, saveOptions = void 0) => + { + const valueOut = input.val(); + questPreview._activeFocusOutFunction = void 0; + + switch (target) + { + case 'task.name': + { + uuidv4 = input.data('uuidv4'); + task = quest.getTask(uuidv4); + if (task) + { + task.name = valueOut; + await questPreview.saveQuest(saveOptions); + } + break; + } + } + }; + + input.on(jquery.focusout, questPreview._activeFocusOutFunction); + input.on(jquery.keydown, (event) => + { + // Handle `Esc` key down to cancel editing. + if (event.which === 27) + { + questPreview._activeFocusOutFunction = void 0; + questPreview.render(true, { focus: true }); + return false; + } + }); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async taskToggleHidden(event, quest, questPreview) + { + const uuidv4 = $(event.target).data('uuidv4'); + const task = quest.getTask(uuidv4); + if (task) + { + task.toggleVisible(); + await questPreview.saveQuest(); + } + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async taskToggleState(event, quest, questPreview) + { + const uuidv4 = $(event.target).data('uuidv4'); + + const task = quest.getTask(uuidv4); + if (task) + { + task.toggle(); + await questPreview.saveQuest(); + } + } +} + +/** + * @typedef {object} FQLDropData An object attached to drop data transfer which describes the FQL reward item and who + * is dropping it into an actor sheet. + * + * @property {string} type - The type of FQL drop data; one of: ['reward'] + * + * @property {string} questId - The Quest ID + * + * @property {string} uuidv4 - The associated UUIDv4 of a quest reward. + * + * @property {string} itemName - The reward item name. + * + * @property {string} userName - The username who is dropping the item. + */ + +/** + * @typedef {object} RewardDropData + * + * @property {FQLDropData} _fqlData - FQL drop data used to remove the reward from a quest. + * + * @property {string} type - Type of document. + * + * @property {object} [data] - Document data on V9 + * + * @property {string} uuid - The UUID of the document. + * + * @property {id} id - The ID of the document. + * + * @property {string} [pack] - Any associated compendium pack. + */ diff --git a/src/view/preview/HandlerManage.js b/src/view/preview/HandlerManage.js new file mode 100644 index 00000000..1d40e755 --- /dev/null +++ b/src/view/preview/HandlerManage.js @@ -0,0 +1,139 @@ +import { + QuestDB, + ViewManager } from '../../control/index.js'; + +import { FQLDocumentOwnershipConfig } from '../internal/index.js'; + +/** + * Provides all {@link JQuery} callbacks for the `management` tab. + */ +export class HandlerManage +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async addSubquest(quest, questPreview) + { + // If a permission control app / dialog is open close it. + if (questPreview._ownershipControl) + { + questPreview._ownershipControl.close(); + questPreview._ownershipControl = void 0; + } + + if (ViewManager.verifyQuestCanAdd()) + { + const subquest = await QuestDB.createQuest({ parentId: quest.id }); + ViewManager.questAdded({ quest: subquest }); + } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async configurePermissions(quest, questPreview) + { + if (quest.entry) + { + if (!questPreview._ownershipControl) + { + questPreview._ownershipControl = new FQLDocumentOwnershipConfig(quest.entry, { + top: Math.min(questPreview.position.top, window.innerHeight - 350), + left: questPreview.position.left + 125 + }).render(true, { focus: true }); + } + + questPreview._ownershipControl.render(true, { + top: Math.min(questPreview.position.top, window.innerHeight - 350), + left: questPreview.position.left + 125, + focus: true + }); + } + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async deleteSplashImage(quest, questPreview) + { + quest.splash = ''; + await questPreview.saveQuest(); + } + + /** + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async setSplashAsIcon(event, quest, questPreview) + { + quest.splashAsIcon = $(event.target).is(':checked'); + await questPreview.saveQuest(); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async setSplashImage(quest, questPreview) + { + const currentPath = quest.splash; + await new FilePicker({ + type: 'image', + current: currentPath, + callback: async (path) => + { + quest.splash = path; + await questPreview.saveQuest(); + }, + }).browse(currentPath); + } + + /** + * @param {Quest} quest - The current quest being manipulated. + * + * @param {QuestPreview} questPreview - The QuestPreview being manipulated. + * + * @returns {Promise} + */ + static async setSplashPos(quest, questPreview) + { + if (quest.splashPos === 'center') + { + quest.splashPos = 'top'; + } + else + { + quest.splashPos = quest.splashPos === 'top' ? 'bottom' : 'center'; + } + + await questPreview.saveQuest(); + } +} \ No newline at end of file diff --git a/src/view/preview/QuestPreview.js b/src/view/preview/QuestPreview.js new file mode 100644 index 00000000..82b43dc1 --- /dev/null +++ b/src/view/preview/QuestPreview.js @@ -0,0 +1,723 @@ +import { + FVTTCompat, + QuestDB, + Socket, + Utils } from '../../control/index.js'; + +import { FQLDialog } from '../internal/index.js'; + +import { HandlerAny } from './HandlerAny.js'; +import { HandlerDetails } from './HandlerDetails.js'; +import { HandlerManage } from './HandlerManage.js'; + +import { + constants, + jquery, + settings } from '../../model/constants.js'; + +/** + * QuestPreview is the main app / window of FQL for modifying individual Quest data. It appears reactive, but every + * single time a data value is manipulated in the quest it is saved and this app renders again. There are many cases + * when parent and subquests of the current quest also requires those QuestPreviews if visible and the {@link QuestLog} + * to be rendered again. Additionally, for remote clients socket events are broadcast to all users logged in to Foundry + * in the same world. This is facilitated through {@link Socket} which controls local rendering and remote rendering. + * In the future it will be possible to reduce reliance on {@link Socket} as the {@link QuestDB} has many lifecycle + * hooks, {@link QuestDBHooks} which can replace manual control aspects found in {@link Socket}. + * + * QuestPreview is the {@link Quest} sheet in Foundry parlance. In {@link FQLHooks.foundryInit} QuestPreview is set as + * the Quest sheet. All Quests are opened through this reference in Quest which is accessible by {@link Quest.sheet} + * + * The main source of QuestPreview creation is through {@link QuestAPI.open}. Both Socket, QuestLog and external + * API usage invokes `QuestAPI.open`. The constructor of QuestPreview requires a Quest and passes on options to t + * the FormApplication. + * + * The {@link JQuery} control handling of callbacks is facilitated through three separate static control classes and + * are setup in {@link QuestPreview.activateListeners}. Two of the control classes {@link HandlerDetails} and + * {@link HandlerManage} contain {@link JQuery} callbacks specific to the `details` and `management` tabs visible for GM + * users and trusted players with ownership permissions when the module setting {@link FQLSettings.trustedPlayerEdit} is + * enabled. {@link HandlerAny} contains callbacks utilized across both `details` and `management` tabs particularly + * around handling the action icons for manipulating the quest tasks. + * + * In {@link QuestPreview.getData} the cached {@link EnrichData} from {@link QuestDB} of the associated {@link Quest} + * is used in rendering the {@link Handlebars} template. + * + * It is worth noting that all internal array data such as tasks and rewards from {@link Quest} a separate + * `UUIDv4` identifier which provides a unique ID for each {@link Task} and {@link Reward}. Tasks and Rewards that are + * manipulated in Quest use this UUIDv4 value passed through the template via the enriched data of a quest. As part of + * the caching process of {@link QuestDB} {@link QuestEntry} instances are stored with both the Quest and enriched data + * from {@link Enrich.quest}. + * + * In {@link QuestPreview.getData} several local variables are set that are utilized both in the Handlebars template + * rendering process and in {@link QuestPreview.activateListeners} to assign certain capabilities that are accessible + * to the user. The GM and trusted players with edit capabilities have full access to editing all parameters of a quest + * except no players have access to the GM notes tab which is for private notes for the GM only. + * + * The general control of Foundry when {@link https://foundryvtt.com/api/classes/client.Application.html#render} is invoked goes as + * follows: + * - {@link QuestPreview.getData} prepares all data for the Handlebars template and sets the local user tracking + * variables. + * + * - {@link QuestPreview.activateListeners} Receives a jQuery element for the window content of the app and is where + * all the control callbacks are registered. + * + * In the handler callbacks for the delete action for quests, tasks, & rewards a special semi-modal dialog is invoked + * via {@link FQLDialog}. A single instance of it is rendered and reused across all delete actions. Please refer to the + * documentation. + * + * {@link ViewManager} responds to `closeQuestPreview` and `renderQuestPreview` tracking the opened QuestPreview + * instances. + * + * @see HandlerAny + * @see HandlerDetails + * @see HandlerManage + */ +export class QuestPreview extends FormApplication +{ + /** + * Stores the quest being displayed / edited. + * + * @type {Quest} + */ + #quest; + + /** + * Constructs a QuestPreview instance with a Quest and passes on options to FormApplication. + * + * @param {Quest} quest - The quest to preview / edit. + * + * @param {object} options - The FormApplication options. + * + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#options + */ + constructor(quest, options = {}) + { + super(void 0, options); + + this.#quest = quest; + + // Set the title of the FormApplication with the quest name. + this.options.title = game.i18n.format('ForienQuestLog.QuestPreview.Title', this.#quest); + + /** + * Set in `getData`. Determines if the player can accept quests which for non-GM / trusted players w/ edit allows + * a minimal set of options to set quests as `available` or `active`. + * + * @type {boolean} + * @package + * + * @see QuestPreview.getData + */ + this.canAccept = false; + + /** + * Set in `getData`. Determines if the current user can fully edit the Quest; a GM or trusted player w/ edit. + * + * @type {boolean} + * @package + * + * @see QuestPreview.getData + */ + this.canEdit = false; + + /** + * Set in `getData`. Determines if the player has ownership of the quest and thereby limited editing capabilities. + * + * @type {boolean} + * @package + * + * @see QuestPreview.getData + */ + this.playerEdit = false; + + /** + * Store the input focus callback in the associated QuestPreview instance so that it can be invoked if the app is + * closed in {@link QuestPreview.close} while the input field is focused / being edited allowing any edits to be + * saved. Otherwise the callback is invoked as part of the input focus out event in the jQuery handler. Please + * see the associated jQuery callback methods in {@link HandlerDetails} linked below. + * + * @param {JQuery.FocusOutEvent|void} event - JQuery.FocusOutEvent + * + * @param {object} [saveOptions] - Options to pass to `saveQuest`; used in {@link QuestPreview.close}. + * + * @returns {Promise} + * + * @type {Function} + * @package + * + * @see HandlerDetails.questEditName + * @see HandlerDetails.questGiverCustomEditName + * @see HandlerDetails.rewardAbstractEditName + * @see HandlerDetails.taskEditName + */ + this._activeFocusOutFunction = void 0; + + /** + * Tracks all opened sheets whether quest giver actor sheet or reward items. Close all sheets when QuestPreview + * closes. + * + * @type {number[]} + * @package + */ + this._openedAppIds = []; + + /** + * Tracks any open FQLPermissionControl dialog that can be opened from the management tab, so that it can be + * closed if this QuestPreview is closed or the tab is changed. + * + * @type {FQLDocumentOwnershipConfig} + * @package + * + * @see HandlerManage.configurePermissions + * @see QuestPreview.close + */ + this._ownershipControl = void 0; + + /** + * Stores a single instance of the ImagePopup for the abstract reward image opened in + * {@link HandlerDetails.rewardShowImagePopout} preventing multiple copies of reward images from being opened + * at the same time. If open this ImagePopup is also closed when this QuestPreview closes in + * {@link QuestPreview.close}. + * + * @type {ImagePopout} + * @package + * + * @see https://foundryvtt.com/api/classes/client.ImagePopout.html + */ + this._rewardImagePopup = void 0; + + /** + * Stores a single instance of the ImagePopup for the splash image opened in + * {@link HandlerDetails.splashImagePopupShow} preventing multiple copies of the splash image from being opened + * at the same time. If open this ImagePopup is also closed when this QuestPreview closes in + * {@link QuestPreview.close}. + * + * @type {ImagePopout} + * @package + * + * @see https://foundryvtt.com/api/classes/client.ImagePopout.html + */ + this._splashImagePopup = void 0; + } + + /** + * Default Application options + * + * @returns {object} options - FormApplication options. + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#options + */ + static get defaultOptions() + { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ['forien-quest-preview'], + template: 'modules/forien-quest-log/templates/quest-preview.html', + width: 1000, + height: 640, + minimizable: true, + resizable: true, + submitOnChange: false, + submitOnClose: false, + title: game.i18n.localize('ForienQuestLog.QuestPreview.Title'), + tabs: [{ navSelector: '.quest-tabs', contentSelector: '.quest-body', initial: 'details' }] + }); + } + + /** + * Returns the CSS application ID which uniquely references this UI element. + * + * @returns {string} The CSS app ID. + * @override + */ + get id() + { + return `quest-${this.#quest.id}`; + } + + /** + * Returns the associated Quest as the FormApplication target object. + * + * @returns {Quest} The FormApplication target object. + * @override + */ + get object() + { + return this.#quest; + } + + /** + * Prevent setting of the FormApplication target object. + * + * @param {object} value - Ignored + * + * @override + */ + set object(value) {} + + /** + * Specify the set of config buttons which should appear in the Application header. Buttons should be returned as an + * Array of objects. + * + * Provides an explicit override of Application._getHeaderButtons to add three additional buttons for the app header + * including copying the content link for the Quest, showing the quest to users via {@link Socket.showQuestPreview} + * and showing the splash image popup. + * + * @returns {ApplicationHeaderButton[]} The app header buttons. + * @override + */ + _getHeaderButtons() + { + const buttons = super._getHeaderButtons(); + + // Share QuestPreview w/ remote clients. + if (game.user.isGM) + { + buttons.unshift({ + label: game.i18n.localize('ForienQuestLog.Labels.AppHeader.ShowPlayers'), + class: 'share-quest', + icon: 'fas fa-eye', + onclick: () => Socket.showQuestPreview(this.#quest.id) + }); + } + + // Show splash image popup if splash image is defined. + if (this.#quest.splash.length) + { + buttons.unshift({ + label: '', + class: 'splash-image', + icon: 'far fa-image', + onclick: async () => + { + // Only show popup if a splash image is defined. + if (this.#quest.splash.length) + { + await HandlerDetails.splashImagePopupShow(this.#quest, this); + } + } + }); + } + + // Copy quest content link. + buttons.unshift({ + label: '', + class: 'copy-link', + icon: 'fas fa-link', + onclick: async () => + { + if (await Utils.copyTextToClipboard(`@JournalEntry[${this.#quest.id}]{${this.#quest.name}}`)) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.LinkCopied')); + } + } + }); + + return buttons; + } + + /** + * Close any tracked permission control app / dialog when tabs change. + * + * @protected + * @inheritDoc + */ + _onChangeTab(event, tabs, active) + { + if (this._ownershipControl) + { + this._ownershipControl.close(); + this._ownershipControl = void 0; + } + + super._onChangeTab(event, tabs, active); + } + + /** + * This might be a FormApplication, but we don't want the submit event to fire. + * + * @protected + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#_onSubmit + */ + async _onSubmit(event, options) // eslint-disable-line + { + event.preventDefault(); + return false; + } + + /** + * This method is called upon form submission after form data is validated. The default _updateObject workflow + * is prevented. + * + * @override + * @protected + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#_updateObject + */ + async _updateObject(event, formData) // eslint-disable-line no-unused-vars + { + event.preventDefault(); + } + + /** + * Returns the associated {@link Quest} + * + * @returns {Quest} Associated Quest. + */ + get quest() { return this.#quest; } + + /** + * Defines all jQuery control callbacks with event listeners for click, drag, drop via various CSS selectors. + * The callbacks are gated by several local variables defined in {@link QuestPreview.getData}. + * + * @param {JQuery} html - The jQuery instance for the window content of this Application. + * + * @see HandlerAny + * @see HandlerDetails + * @see HandlerManage + * @see QuestPreview.canAccept + * @see QuestPreview.canEdit + * @see QuestPreview.playerEdit + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#activateListeners + */ + activateListeners(html) + { + super.activateListeners(html); + + // Callbacks for any user. + + html.on(jquery.click, '.quest-giver-name .open-actor-sheet', async (event) => + await HandlerDetails.questGiverShowActorSheet(event, this)); + + // This CSS selector responds to any subquest attached to the details section or subquests listed in objectives. + html.on(jquery.click, '.quest-name-link', (event) => HandlerAny.questOpen(event)); + + // This registers for any element and prevents the circle / slash icon displaying for not being a drag target. + html.on(jquery.dragenter, (event) => event.preventDefault()); + + html.on(jquery.dragstart, '.item-reward .editable-container', async (event) => + await HandlerDetails.rewardDragStartItem(event, this.#quest)); + + html.on(jquery.dragstart, '.quest-rewards .fa-sort', (event) => HandlerDetails.rewardDragStartSort(event)); + + html.on(jquery.click, '.abstract-reward .editable-container', async (event) => + await HandlerDetails.rewardShowImagePopout(event, this.#quest, this)); + + html.on(jquery.click, '.actor-reward .editable-container', async (event) => + await HandlerDetails.rewardShowSheet(event, this.#quest, this)); + + html.on(jquery.click, '.item-reward .editable-container', async (event) => + await HandlerDetails.rewardShowSheet(event, this.#quest, this)); + + html.on(jquery.click, '.splash-image-link', () => HandlerDetails.splashImagePopupShow(this.#quest, this)); + + html.on(jquery.dragstart, '.quest-tasks .fa-sort', (event) => HandlerDetails.taskDragStartSort(event)); + + // Callbacks for GM, trusted player edit, and players with ownership + if (this.canEdit || this.playerEdit) + { + html.on(jquery.click, '.actions-single.quest-name .editable', (event) => + HandlerDetails.questEditName(event, this.#quest, this)); + + html.on(jquery.drop, '.quest-giver-gc', async (event) => + await HandlerDetails.questGiverDropDocument(event, this.#quest, this)); + + html.on(jquery.click, '.quest-giver-gc .toggleImage', async () => + await HandlerDetails.questGiverToggleImage(this.#quest, this)); + + html.on(jquery.click, '.quest-giver-gc .deleteQuestGiver', async () => + await HandlerDetails.questGiverDelete(this.#quest, this)); + + html.on(jquery.click, '.quest-tasks .add-new-task', + (event) => HandlerDetails.taskAdd(event, this.#quest, this)); + + html.on(jquery.click, '.actions.tasks .delete', async (event) => + await HandlerDetails.taskDelete(event, this.#quest, this)); + + html.on(jquery.drop, '.tasks-box', async (event) => await HandlerDetails.taskDropItem(event, this.#quest)); + + html.on(jquery.click, '.actions.tasks .editable', + (event) => HandlerDetails.taskEditName(event, this.#quest, this)); + + html.on(jquery.click, 'li.task .toggleState', async (event) => + await HandlerDetails.taskToggleState(event, this.#quest, this)); + } + + // Callbacks for GM, trusted player edit, or players who can accept quests. + if (this.canEdit || this.canAccept) + { + html.on(jquery.click, '.actions.quest-status i.delete', async (event) => + await HandlerAny.questDelete(event, this.#quest)); + + html.on(jquery.click, '.actions.quest-status i.move', async (event) => + { + await this.saveQuest({ refresh: false }); + await HandlerAny.questStatusSet(event); + }); + } + + // Callbacks only for the GM and trusted player edit. + if (this.canEdit) + { + html.on(jquery.click, '.quest-giver-name .actions-single .editable', (event) => + HandlerDetails.questGiverCustomEditName(event, this.#quest, this)); + + html.on(jquery.click, '.quest-giver-gc .drop-info', () => + HandlerDetails.questGiverCustomSelectImage(this.#quest, this)); + + html.on(jquery.click, '.quest-tabs .is-primary', () => Socket.setQuestPrimary({ quest: this.#quest })); + + html.on(jquery.click, '.quest-rewards .add-abstract', (event) => + HandlerDetails.rewardAddAbstract(event, this.#quest, this)); + + html.on(jquery.click, '.actions.rewards .editable', (event) => + HandlerDetails.rewardAbstractEditName(event, this.#quest, this)); + + html.on(jquery.click, '.actions.rewards .delete', async (event) => + await HandlerDetails.rewardDelete(event, this.#quest, this)); + + html.on(jquery.drop, '.rewards-box', + async (event) => await HandlerDetails.rewardDropItem(event, this.#quest, this)); + + html.on(jquery.click, '.quest-rewards .hide-all-rewards', async () => + await HandlerDetails.rewardsHideAll(this.#quest, this)); + + html.on(jquery.click, '.quest-rewards .lock-all-rewards', async () => + await HandlerDetails.rewardsLockAll(this.#quest, this)); + + html.on(jquery.click, '.reward-image', async (event) => + await HandlerDetails.rewardSelectImage(event, this.#quest, this)); + + html.on(jquery.click, '.quest-rewards .show-all-rewards', async () => + await HandlerDetails.rewardsShowAll(this.#quest, this)); + + html.on(jquery.click, '.actions.rewards .toggleHidden', async (event) => + await HandlerDetails.rewardToggleHidden(event, this.#quest, this)); + + html.on(jquery.click, '.actions.rewards .toggleLocked', async (event) => + await HandlerDetails.rewardToggleLocked(event, this.#quest, this)); + + html.on(jquery.click, '.quest-rewards .unlock-all-rewards', async () => + await HandlerDetails.rewardsUnlockAll(this.#quest, this)); + + html.on(jquery.click, '.actions.tasks .toggleHidden', async (event) => + await HandlerDetails.taskToggleHidden(event, this.#quest, this)); + + // Management view callbacks ------------------------------------------------------------------------------- + + html.on(jquery.click, '.add-subquest-btn', async () => await HandlerManage.addSubquest(this.#quest, this)); + + html.on(jquery.click, '.configure-perm-btn', () => HandlerManage.configurePermissions(this.#quest, this)); + + html.on(jquery.click, '.delete-splash', async () => await HandlerManage.deleteSplashImage(this.#quest, this)); + + html.on(jquery.click, `.quest-splash #splash-as-icon-${this.#quest.id}`, async (event) => + await HandlerManage.setSplashAsIcon(event, this.#quest, this)); + + html.on(jquery.click, '.quest-splash .drop-info', + async () => await HandlerManage.setSplashImage(this.#quest, this)); + + html.on(jquery.click, '.change-splash-pos', async () => await HandlerManage.setSplashPos(this.#quest, this)); + } + } + + /** + * When closing this Foundry app: + * - Close any associated dialogs via {@link FQLDialog.closeDialogs} + * - Close any associated {@link QuestPreview._ownershipControl} + * - Close any associated {@link QuestPreview._rewardImagePopup} + * - Close any associated {@link QuestPreview._splashImagePopup} + * - If set invoke {@link QuestPreview._activeFocusOutFunction} or {@link QuestPreview.saveQuest} if the current + * user is the owner of the quest and options `noSave` is false. + * + * Save the quest on close with no refresh of data. + * + * @param {object} opts - Optional params + * + * @param {boolean} [opts.noSave] - When true the quest is not saved on close otherwise save quest. + * + * @param {...*} [opts.options] - Options which are passed through to {@link FormApplication.close} + * + * @returns {Promise} + * @inheritDoc + * @see FormApplication.close + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#close + */ + async close({ noSave = false, ...options } = {}) + { + FQLDialog.closeDialogs({ questId: this.#quest.id }); + + // If a permission control app / dialog is open close it. + if (this._ownershipControl) + { + this._ownershipControl.close(); + this._ownershipControl = void 0; + } + + // Close any opened actor or reward item sheets. + for (const appId of this._openedAppIds) + { + const app = ui.windows[appId]; + if (app && app.rendered) { app.close(); } + } + + // If a reward ImagePopup is open close it. + if (this._rewardImagePopup) + { + this._rewardImagePopup.close(); + this._rewardImagePopup = void 0; + } + + // If a splash ImagePopup is open close it. + if (this._splashImagePopup) + { + this._splashImagePopup.close(); + this._splashImagePopup = void 0; + } + + // Only potentially save the quest if the user is the owner and noSave is false. + if (!noSave && this.#quest.isOwner) + { + // If there is an active input focus function set then invoke it so that the input field is saved. + if (typeof this._activeFocusOutFunction === 'function') + { + await this._activeFocusOutFunction(void 0, { refresh: false }); + + // Send a socket refresh event to all clients. This will also render all local apps as applicable. + // Must update parent and any subquests / children. + Socket.refreshQuestPreview({ + questId: this.#quest.parent ? [this.#quest.parent, this.#quest.id, ...this.#quest.subquests] : + [this.#quest.id, ...this.#quest.subquests], + focus: false, + }); + } + else + { + // Otherwise save the quest as normal. + await this.saveQuest({ refresh: false }); + } + } + + return super.close(options); + } + + /** + * Retrieves the cached enriched data from QuestDB to be used in the Handlebars template. Also sets the local + * variables used in {@link QuestPreview.activateListeners} to enable various control handling based on user + * permissions and module settings. + * + * @override + * @inheritDoc + * @see QuestPreview.canAccept + * @see QuestPreview.canEdit + * @see QuestPreview.playerEdit + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#getData + */ + async getData(options = {}) // eslint-disable-line no-unused-vars + { + const content = QuestDB.getQuestEntry(this.#quest.id).enrich; + + this.canAccept = game.settings.get(constants.moduleName, settings.allowPlayersAccept); + this.canEdit = game.user.isGM || (this.#quest.isOwner && Utils.isTrustedPlayerEdit()); + this.playerEdit = this.#quest.isOwner; + + // Player notes can be edited if current user is the owner of the journal document or there is an active GM + // online. + const canEditPlayerNotes = this.#quest.canUserUpdate || game.users.activeGM !== null; + + // By default, all normal players and trusted players without ownership of a quest are always on the default + // tab 'details' or 'playernotes'. In the case of a trusted player who has permissions revoked to access the + // quest and is on the 'management' the details tab needs to be activated. This is possible in 'getData' as it + // is fairly early in the render process. At this time the internal state of the application is '1' for + // 'RENDERING'. + if (!this.canEdit && this._tabs[0] && this._tabs[0].active !== 'details' && this._tabs[0].active !== 'playernotes') + { + this._tabs[0].activate('details'); + } + + const data = { + isGM: game.user.isGM, + isPlayer: !game.user.isGM, + + canAccept: this.canAccept, + canEdit: this.canEdit, + canEditPlayerNotes, + playerEdit: this.playerEdit + }; + + return foundry.utils.mergeObject(data, content); + } + + /** + * Refreshes the QuestPreview window and emits {@link Socket.refreshQuestPreview} so remote clients view of data is + * updated as well. Any rendered / visible parent and subquests of this quest are also refreshed. + * + * @returns {Promise} + */ + async refresh() + { + Socket.refreshQuestPreview({ + questId: this.#quest.parent ? [this.#quest.parent, this.#quest.id, ...this.#quest.subquests] : + [this.#quest.id, ...this.#quest.subquests], + focus: false, + }); + + this.render(true, { focus: true }); + } + + /** + * When the editor is saved we simply save the quest. The editor content if any is available is saved inside + * 'saveQuest'. + * + * @override + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#saveEditor + */ + async saveEditor(name) + { + // Any user regardless of ownership may edit player notes. If the user can't update the backing journal document + // Then send a socket request to a GM user who can perform the update. + if (name === 'playernotes' && !this.#quest.canUserUpdate && game.users.activeGM) + { + const playernotes = FVTTCompat.getEditorContent(this.editors?.playernotes); + + if (typeof playernotes === 'string') + { + Socket.savePlayerNotes({ quest: this.#quest, playernotes }); + } + + return super.saveEditor(name); + } + + return this.saveQuest(); + } + + /** + * Save the associated quest and refresh this app. + * + * @param {object} options - Optional parameters + * + * @param {boolean} options.refresh - Execute `QuestPreview.refresh` + * + * @returns {Promise} + * @see QuestPreview.refresh + */ + async saveQuest({ refresh = true } = {}) + { + // Save any altered content from the editors. + for (const key of Object.keys(this.editors)) + { + const editor = this.editors[key]; + + const content = FVTTCompat.getEditorContent(editor); + + if (content) + { + this.#quest[key] = content; + await super.saveEditor(key); + } + } + + await this.#quest.save(); + + return refresh ? this.refresh() : void 0; + } +} diff --git a/src/view/preview/QuestPreviewShim.js b/src/view/preview/QuestPreviewShim.js new file mode 100644 index 00000000..5db4ebe7 --- /dev/null +++ b/src/view/preview/QuestPreviewShim.js @@ -0,0 +1,65 @@ +import { QuestAPI } from '../../control/public/index.js'; + +/** + * Provides a very lightweight shim for {@link JournalEntry} documents that are FQL quests. It defers to + * opening the {@link Quest} to the {@link QuestAPI.open} method. Foundry will invoke this shim when a JournalEntry + * is clicked in the {@link JournalDirectory} via {@link DocumentDirectory._onClickEntryName}. This shim + * is set to {@link JournalEntry._sheet} in {@link QuestDB} when JE docs are created or loaded. + */ +export class QuestPreviewShim +{ + /** + * @type {string} + */ + #questId; + + /** + * Stores the associated JournalEntry / quest ID + * + * @param {string} questId - The quest ID to shim. + */ + constructor(questId) + { + this.#questId = questId; + } + + /** + * Always return false so `render` is invoked. + * + * @returns {boolean} False. + */ + get rendered() { return false; } + + /** + * Noop shim + */ + bringToTop() {} + + /** + * Noop shim + */ + close() {} + + /** + * Noop shim + */ + maximize() {} + + /** + * Defer to render as in some misuse cases by various modules _render can be invoked directly. + * + * @protected + */ + async _render() + { + this.render(); + } + + /** + * Defers to the {@link QuestAPI.open} to potentially open a quest. + */ + render() + { + QuestAPI.open({ questId: this.#questId }); + } +} \ No newline at end of file diff --git a/src/view/tracker/HandlerTracker.js b/src/view/tracker/HandlerTracker.js new file mode 100644 index 00000000..9617a8e0 --- /dev/null +++ b/src/view/tracker/HandlerTracker.js @@ -0,0 +1,174 @@ +import { + FoundryUIManager, + QuestDB, + Socket } from '../../control/index.js'; + +import { QuestAPI } from '../../control/public/index.js'; + +import { + constants, + sessionConstants, + settings } from '../../model/constants.js'; + +/** + * Provides all {@link JQuery} and {@link PointerEvent} callbacks for the {@link QuestTracker}. + */ +export class HandlerTracker +{ + /** + * @private + */ + constructor() + { + throw new Error('This is a static class that should not be instantiated.'); + } + + /** + * Handles the pointer down event from the header to reset the pinned state. + * + * @param {PointerEvent} event - PointerEvent + * + * @param {HTMLElement} header - The app header element. + * + * @param {QuestTracker} questTracker - The QuestTracker + */ + static async headerPointerDown(event, header, questTracker) + { + if (event.target.classList.contains('window-title') || + event.target.classList.contains('window-header')) + { + questTracker._dragHeader = true; + + questTracker._pinned = false; + + await game.settings.set(constants.moduleName, settings.questTrackerPinned, false); + + header.setPointerCapture(event.pointerId); + } + } + + /** + * Handles the pointer up event from the header to check for and set the pinned state. + * + * @param {PointerEvent} event - PointerEvent + * + * @param {HTMLElement} header - The app header element. + * + * @param {QuestTracker} questTracker - The QuestTracker + */ + static async headerPointerUp(event, header, questTracker) + { + header.releasePointerCapture(event.pointerId); + questTracker._dragHeader = false; + + if (questTracker._inPinDropRect) + { + questTracker._pinned = true; + await game.settings.set(constants.moduleName, settings.questTrackerPinned, true); + questTracker.element.css('animation', ''); + FoundryUIManager.updateTracker(); + } + } + + /** + * Handles the quest open click via {@link QuestAPI.open}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + */ + static questOpen(event) + { + const questId = event.currentTarget.dataset.questId; + QuestAPI.open({ questId }); + } + + /** + * Data for the quest folder open / close state is saved in {@link sessionStorage}. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + * + * @param {QuestTracker} questTracker - The QuestTracker. + */ + static questClick(event, questTracker) + { + const questId = event.currentTarget.dataset.questId; + + const questEntry = QuestDB.getQuestEntry(questId); + if (questEntry && questEntry.enrich.hasObjectives) + { + const folderState = sessionStorage.getItem(`${sessionConstants.trackerFolderState}${questId}`); + const collapsed = folderState !== 'false'; + sessionStorage.setItem(`${sessionConstants.trackerFolderState}${questId}`, (!collapsed).toString()); + + questTracker.render(); + } + } + + /** + * Handles the header button to show the primary quest or all quests. + * + * @param {QuestTracker} questTracker - The QuestTracker. + */ + static questPrimaryShow(questTracker) + { + const newPrimary = !(sessionStorage.getItem(sessionConstants.trackerShowPrimary) === 'true'); + sessionStorage.setItem(sessionConstants.trackerShowPrimary, (newPrimary).toString()); + + const showPrimaryIcon = $('#quest-tracker .header-button.show-primary i'); + showPrimaryIcon.attr('class', newPrimary ? 'fas fa-star' : 'far fa-star'); + showPrimaryIcon.attr('title', game.i18n.localize(newPrimary ? + 'ForienQuestLog.QuestTracker.Tooltips.PrimaryQuestShow' : + 'ForienQuestLog.QuestTracker.Tooltips.PrimaryQuestUnshow')); + + questTracker.render(); + } + + /** + * Handles toggling {@link Quest} tasks when clicked on by a user that is the GM or owner of quest. + * + * @param {JQuery.ClickEvent} event - JQuery.ClickEvent + */ + static async questTaskToggle(event) + { + // Don't handle any clicks of internal anchor elements such as entity content links. + if ($(event.target).is('.quest-tracker-task a')) { return; } + + const questId = event.currentTarget.dataset.questId; + const uuidv4 = event.currentTarget.dataset.uuidv4; + + const quest = QuestDB.getQuest(questId); + + if (quest) + { + const task = quest.getTask(uuidv4); + if (task) + { + task.toggle(); + await quest.save(); + + Socket.refreshQuestPreview({ + questId, + focus: false + }); + } + } + } + + /** + * Handles the header button to show the quest tracker background or hide it. + * + * @param {QuestTracker} questTracker - The QuestTracker. + */ + static showBackground(questTracker) + { + const newBackgroundState = !(sessionStorage.getItem(sessionConstants.trackerShowBackground) === 'true'); + sessionStorage.setItem(sessionConstants.trackerShowBackground, (newBackgroundState).toString()); + + const showBackgroundIcon = $('#quest-tracker .header-button.show-background i'); + showBackgroundIcon.attr('class', newBackgroundState ? 'fas fa-star' : 'far fa-star'); + showBackgroundIcon.attr('title', game.i18n.localize(newBackgroundState ? + 'ForienQuestLog.QuestTracker.Tooltips.BackgroundUnshow' : + 'ForienQuestLog.QuestTracker.Tooltips.BackgroundShow')); + + questTracker.render(); + } +} \ No newline at end of file diff --git a/src/view/tracker/QuestTracker.js b/src/view/tracker/QuestTracker.js new file mode 100644 index 00000000..c3541a4e --- /dev/null +++ b/src/view/tracker/QuestTracker.js @@ -0,0 +1,605 @@ +import { + FoundryUIManager, + QuestDB, + Socket, + Utils } from '../../control/index.js'; + +import { HandlerTracker } from './HandlerTracker.js'; + +import { FQLContextMenu } from '../internal/index.js'; + +import { collect } from '../../../external/index.js'; + +import { + constants, + jquery, + questStatus, + sessionConstants, + settings } from '../../model/constants.js'; + +/** + * Provides the quest tracker which provides an overview of active quests and objectives which can be opened / closed + * to show all objectives for a given quest. The folder / open state is stored in {@link sessionStorage}. + * + * In the {@link QuestTracker.getData} method {@link QuestTracker.prepareQuests} is invoked which gets all sorted + * {@link questStatus.active} via {@link QuestDB.sortCollect}. They are then mapped creating the specific data which is + * used in the {@link Handlebars} template. In the future this may be cached in a similar way that {@link Quest} data + * is cached for {@link QuestLog}. + */ +export class QuestTracker extends Application +{ + /** + * Provides the default width for the QuestTracker if not defined. + * + * @type {Readonly} + */ + static #DEFAULT_WIDTH = 296; + + /** + * Provides the default position for the QuestTracker if not defined. + * + * @type {Readonly<{top: number, width: number}>} + */ + static #DEFAULT_POSITION = { top: 80, width: QuestTracker.#DEFAULT_WIDTH }; + + /** + * Defines the timeout length to gate saving position to settings. + * + * @type {Readonly} + */ + static #TIMEOUT_POSITION = 1000; + + /** + * Stores the app / window extents from styles. + * + * @type {{minHeight: number, maxHeight: number, minWidth: number, maxWidth: number}} + */ + #appExtents; + + /** + * @type {JQuery} The window header element. + */ + #elemWindowHeader; + + /** + * @type {JQuery} The window content element. + */ + #elemWindowContent; + + /** + * @type {JQuery} The window resize handle. + */ + #elemResizeHandle; + + /** + * Stores whether the scroll bar is active. + * + * @type {boolean} + */ + #scrollbarActive; + + /** + * Stores the last call to setTimeout for {@link QuestTracker.setPosition} changes, so that they can be cancelled as + * new updates arrive gating the calls to saving position to settings. + * + * @type {number} + */ + #timeoutPosition = void 0; + + /** + * Stores the state of {@link FQLSettings.questTrackerResizable}. + * + * @type {boolean} + */ + #windowResizable; + + /** + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.Application.html + */ + constructor(options = {}) + { + super(options); + + try + { + /** + * Stores the current position of the quest tracker. + * + * @type {object} + * {@link Application.position} + */ + this.position = JSON.parse(game.settings.get(constants.moduleName, settings.questTrackerPosition)); + + // When upgrading to `v0.7.7` it is necessary to set the default width. + if (!this.position?.width) { this.position.width = QuestTracker.#DEFAULT_WIDTH; } + + } + catch (err) + { + this.position = QuestTracker.#DEFAULT_POSITION; + } + + /** + * Stores whether the header is being dragged. + * + * @type {boolean} + * @package + */ + this._dragHeader = false; + + /** + * Stores whether the QuestTracker is pinned to the sidebar. + * + * @type {boolean} + * @package + */ + this._pinned = game.settings.get(constants.moduleName, settings.questTrackerPinned); + + /** + * Stores whether the current position is in the sidebar pin drop rectangle. + * + * @type {boolean} + * @package + */ + this._inPinDropRect = false; + } + + /** + * Default {@link Application} options + * + * @returns {object} options - Application options. + * @see https://foundryvtt.com/api/classes/client.Application.html#options + */ + static get defaultOptions() + { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'quest-tracker', + template: 'modules/forien-quest-log/templates/quest-tracker.html', + minimizable: false, + resizable: true, + popOut: false, + width: 300, + height: 480, + title: game.i18n.localize('ForienQuestLog.QuestTracker.Title') + }); + } + + /** + * Create the context menu. There are two separate context menus for the active / in progress tab and all other tabs. + * + * @param {JQuery} html - JQuery element for this application. + */ + #contextMenu(html) + { + const menuItemCopyLink = { + name: 'ForienQuestLog.QuestLog.ContextMenu.CopyEntityLink', + icon: '', + callback: async (menu) => + { + const questId = $(menu)?.closest('.quest-tracker-header')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + + if (quest && await Utils.copyTextToClipboard(`@JournalEntry[${quest.id}]{${quest.name}}`)) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.LinkCopied')); + } + } + }; + + /** + * @type {object[]} + */ + const menuItems = [menuItemCopyLink]; + + if (game.user.isGM) + { + menuItems.push({ + name: 'ForienQuestLog.QuestLog.ContextMenu.CopyQuestID', + icon: '', + callback: async (menu) => + { + const questId = $(menu)?.closest('.quest-tracker-header')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + + if (quest && await Utils.copyTextToClipboard(quest.id)) + { + ui.notifications.info(game.i18n.format('ForienQuestLog.Notifications.QuestIDCopied')); + } + } + }); + + menuItems.push({ + name: 'ForienQuestLog.QuestLog.ContextMenu.PrimaryQuest', + icon: '', + callback: (menu) => + { + const questId = $(menu)?.closest('.quest-tracker-header')?.data('quest-id'); + const quest = QuestDB.getQuest(questId); + if (quest) { Socket.setQuestPrimary({ quest }); } + } + }); + } + + new FQLContextMenu(html, '.quest-tracker-header', menuItems); + } + + /** + * Specify the set of config buttons which should appear in the Application header. Buttons should be returned as an + * Array of objects. + * + * Provides an explicit override of Application._getHeaderButtons to add + * + * @returns {ApplicationHeaderButton[]} The app header buttons. + * @override + */ + _getHeaderButtons() + { + const buttons = super._getHeaderButtons(); + + // Remove default `Close` label for close button. + const closeButton = buttons.find((button) => button?.class === 'close'); + if (closeButton) { closeButton.label = void 0; } + + const showBackgroundState = sessionStorage.getItem(sessionConstants.trackerShowBackground) === 'true'; + const showBackgroundIcon = showBackgroundState ? 'fas fa-fill on' : 'fas fa-fill off'; + const showBackgroundTitle = showBackgroundState ? 'ForienQuestLog.QuestTracker.Tooltips.BackgroundUnshow' : + 'ForienQuestLog.QuestTracker.Tooltips.BackgroundShow'; + + buttons.unshift({ + title: showBackgroundTitle, + class: 'show-background', + icon: showBackgroundIcon + }); + + const primaryState = sessionStorage.getItem(sessionConstants.trackerShowPrimary) === 'true'; + const primaryIcon = primaryState ? 'fas fa-star' : 'far fa-star'; + const primaryTitle = primaryState ? 'ForienQuestLog.QuestTracker.Tooltips.PrimaryQuestUnshow' : + 'ForienQuestLog.QuestTracker.Tooltips.PrimaryQuestShow'; + + buttons.unshift({ + title: primaryTitle, + class: 'show-primary', + icon: primaryIcon + }); + + // Share QuestLog w/ remote clients. + if (game.user.isGM) + { + buttons.unshift({ + title: game.i18n.localize('ForienQuestLog.Labels.AppHeader.ShowPlayers'), + class: 'share-tracker', + icon: 'fas fa-eye' + }); + } + + return buttons; + } + + /** + * Gets the minimum width of this Application. + * + * @returns {number} Minimum width. + */ + get minWidth() { return this.#appExtents.minWidth || 275; } + + /** + * Is the QuestTracker pinned to the sidebar. + * + * @returns {boolean} QuestTracker pinned. + */ + get pinned() { return this._pinned; } + + /** + * Defines all {@link JQuery} control callbacks with event listeners for click, drag, drop via various CSS selectors. + * + * @param {JQuery} html - The jQuery instance for the window content of this Application. + * + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#activateListeners + */ + activateListeners(html) + { + super.activateListeners(html); + + const showBackgroundState = sessionStorage.getItem(sessionConstants.trackerShowBackground) === 'true'; + if (!showBackgroundState) + { + this.element[0].classList.add('no-background'); + } + + // Make the window draggable + const header = html.find('header'); + new Draggable(this, html, header[0], this.options.resizable); + + header[0].addEventListener('pointerdown', async (event) => + HandlerTracker.headerPointerDown(event, header[0], this)); + + header[0].addEventListener('pointerup', async (event) => + HandlerTracker.headerPointerUp(event, header[0], this)); + + html.on(jquery.click, '.header-button.close', void 0, this.close); + + if (game.user.isGM) + { + html.on(jquery.click, '.header-button.share-tracker i', void 0, () => Socket.showQuestTracker()); + } + + html.on(jquery.click, '.header-button.show-background i', void 0, () => HandlerTracker.showBackground(this)); + + html.on(jquery.click, '.header-button.show-primary i', void 0, () => HandlerTracker.questPrimaryShow(this)); + + // Add context menu. + this.#contextMenu(html); + + Utils.createJQueryDblClick({ + selector: '#quest-tracker .quest-tracker-header', + singleCallback: (event) => HandlerTracker.questClick(event, this), + doubleCallback: HandlerTracker.questOpen, + }); + + html.on(jquery.click, '.quest-tracker-link', void 0, HandlerTracker.questOpen); + + html.on(jquery.click, '.quest-tracker-task', void 0, async (event) => + await HandlerTracker.questTaskToggle(event)); + + this.#elemWindowHeader = $('#quest-tracker .window-header'); + this.#elemWindowContent = $('#quest-tracker .window-content'); + this.#elemResizeHandle = $('#quest-tracker .window-resizable-handle'); + + this.#appExtents = { + minWidth: parseInt(this.element.css('min-width')), + maxWidth: parseInt(this.element.css('max-width')), + minHeight: parseInt(this.element.css('min-height')), + maxHeight: parseInt(this.element.css('max-height')) + }; + + this.#windowResizable = game.settings.get(constants.moduleName, settings.questTrackerResizable); + + if (this.#windowResizable) + { + this.#elemResizeHandle.show(); + this.element.css('min-height', this.#appExtents.minHeight); + } + else + { + this.#elemResizeHandle.hide(); + this.element.css('min-height', this.#elemWindowHeader[0].scrollHeight); + + // A bit of a hack. We need to call the Application setPosition now to make sure the element parameters + // are correctly set as the exact height for the element is calculated in this.setPosition which is called + // by Application right after this method completes. + // Must set popOut temporarily to true as there is a gate in `Application.setPosition`. + this.options.popOut = true; + super.setPosition(this.position); + this.options.popOut = false; + } + + this.#scrollbarActive = this.#elemWindowContent[0].scrollHeight > this.#elemWindowContent[0].clientHeight; + + // Set current scrollbar active state and potentially set 'point-events' to 'auto'. + if (this.#scrollbarActive) { this.element.css('pointer-events', 'auto'); } + } + + /** + * Override default Application `bringToTop` to stop adjustment of z-index. + * + * @override + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.Application.html#bringToTop + */ + bringToTop() {} + + /** + * Sets `questTrackerEnable` to false. + * + * @param {object} [options] - Optional parameters. + * + * @param {boolean} [options.updateSetting=true] - If true then {@link settings.questTrackerEnable} is set to false. + * + * @returns {Promise} + */ + async close({ updateSetting = true } = {}) + { + await super.close(); + + if (updateSetting) + { + await game.settings.set(constants.moduleName, settings.questTrackerEnable, false); + } + } + + /** + * Parses quest data in {@link QuestTracker.prepareQuests}. + * + * @override + * @inheritDoc + * @see https://foundryvtt.com/api/classes/client.FormApplication.html#getData + */ + async getData(options = {}) + { + const showOnlyPrimary = sessionStorage.getItem(sessionConstants.trackerShowPrimary) === 'true'; + const primaryQuest = QuestDB.getQuestEntry(game.settings.get(constants.moduleName, settings.primaryQuest)); + + // Stores the primary quest ID when all in progress quests are shown so that the star icon is drawn for the + // primary quest. + const primaryQuestId = !showOnlyPrimary && primaryQuest ? primaryQuest.id : ''; + + const quests = await this.prepareQuests(showOnlyPrimary, primaryQuest); + + return foundry.utils.mergeObject(super.getData(options), { + title: this.options.title, + headerButtons: this._getHeaderButtons(), + hasQuests: quests.count() > 0, + primaryQuestId, + quests + }); + } + + /** + * Transforms the quest data from sorted active quests. In this case we need to determine which quests can be + * manipulated for trusted player edit. + * + * @param {boolean} showOnlyPrimary - Shows only the primary quest. + * + * @param {QuestEntry|void} primaryQuest - Any currently set primary quest. + * + * @returns {Promise>} Sorted active quests. + */ + async prepareQuests(showOnlyPrimary, primaryQuest) + { + /** + * If showOnlyPrimary and the primaryQuest exists then build a Collection with just the primary quest otherwise + * get all sorted in progress quests from the QuestDB. + * + * @type {Collection} + */ + const questEntries = showOnlyPrimary ? collect(primaryQuest ? [primaryQuest] : []) : + QuestDB.sortCollect({ status: questStatus.active }); + + const isGM = game.user.isGM; + const isTrustedPlayerEdit = Utils.isTrustedPlayerEdit(); + + return questEntries.transform((entry) => + { + const q = entry.enrich; + const collapsed = sessionStorage.getItem(`${sessionConstants.trackerFolderState}${q.id}`) === 'false'; + + const tasks = collapsed ? q.data_tasks : []; + const subquests = collapsed ? q.data_subquest : []; + + return { + id: q.id, + canEdit: isGM || (entry.isOwner && isTrustedPlayerEdit), + playerEdit: entry.isOwner, + source: q.giver, + name: q.name, + isGM, + isHidden: q.isHidden, + isInactive: q.isInactive, + isPersonal: q.isPersonal, + personalActors: q.personalActors, + hasObjectives: q.hasObjectives, + subquests, + tasks + }; + }); + } + + /** + * Some game systems and custom UI theming modules provide hard overrides on overflow-x / overflow-y styles. Alas we + * need to set these for '.window-content' to 'visible' which will cause an issue for very long tables. Thus we must + * manually set the table max-heights based on the position / height of the {@link Application}. + * + * @param {object} [opts] - Optional parameters. + * + * @param {number|null} [opts.left] - The left offset position in pixels. + * + * @param {number|null} [opts.top] - The top offset position in pixels. + * + * @param {number|null} [opts.width] - The application width in pixels. + * + * @param {number|string|null} [opts.height] - The application height in pixels. + * + * @param {number|null} [opts.scale] - The application scale as a numeric factor where 1.0 is default. + * + * @param {boolean} [opts.override] - Forces any manual pinned setting to take effect. + * + * @param {boolean} [opts.pinned] - Sets the pinned state. + * + * @returns {{left: number, top: number, width: number, height: number, scale:number}} + * The updated position object for the application containing the new values. + */ + setPosition({ override, pinned = this._pinned, ...opts } = {}) + { + // Potentially force override any pinned state. This is done from FQLHooks.openQuestTracker. + if (typeof override === 'boolean') + { + if (pinned) + { + this._pinned = true; + this._inPinDropRect = true; + game.settings.set(constants.moduleName, settings.questTrackerPinned, true); + FoundryUIManager.updateTracker(); + return opts; // Early out as updateTracker above calls setPosition again. + } + else + { + this._pinned = false; + this._inPinDropRect = false; + game.settings.set(constants.moduleName, settings.questTrackerPinned, false); + } + } + + const initialWidth = this.position.width; + const initialHeight = this.position.height; + + if (pinned) + { + if (typeof opts.left === 'number') { opts.left = this.position.left; } + if (typeof opts.top === 'number') { opts.top = this.position.top; } + if (typeof opts.width === 'number') { opts.width = this.position.width; } + } + + // Must set popOut temporarily to true as there is a gate in `Application.setPosition`. + this.options.popOut = true; + const currentPosition = super.setPosition(opts); + this.options.popOut = false; + + if (!this.#windowResizable) + { + // Add the extra `2` for small format (1080P and below screen size). + currentPosition.height = this.#elemWindowHeader[0].scrollHeight + this.#elemWindowContent[0].scrollHeight + 2; + } + + // Pin width / height to min / max styles if defined. + if (currentPosition.width < this.#appExtents.minWidth) { currentPosition.width = this.#appExtents.minWidth; } + if (currentPosition.width > this.#appExtents.maxWidth) { currentPosition.width = this.#appExtents.maxWidth; } + if (currentPosition.height < this.#appExtents.minHeight) { currentPosition.height = this.#appExtents.minHeight; } + if (currentPosition.height > this.#appExtents.maxHeight) { currentPosition.height = this.#appExtents.maxHeight; } + + const el = this.element[0]; + + currentPosition.resizeWidth = initialWidth < currentPosition.width; + currentPosition.resizeHeight = initialHeight < currentPosition.height; + + // Mutates `checkPosition` to set maximum left position. Must do this calculation after `super.setPosition` + // as in some cases `super.setPosition` will override the changes of `FoundryUIManager.checkPosition`. + const currentInPinDropRect = this._inPinDropRect; + this._inPinDropRect = FoundryUIManager.checkPosition(currentPosition); + + // Set the jiggle animation if the position movement is coming from dragging the header and the pin drop state + // has changed. + if (!this._pinned && this._dragHeader && currentInPinDropRect !== this._inPinDropRect) + { + this.element.css('animation', this._inPinDropRect ? 'fql-jiggle 0.3s infinite' : ''); + } + + el.style.top = `${currentPosition.top}px`; + el.style.left = `${currentPosition.left}px`; + el.style.width = `${currentPosition.width}px`; + el.style.height = `${currentPosition.height}px`; + + const scrollbarActive = this.#elemWindowContent[0].scrollHeight > this.#elemWindowContent[0].clientHeight; + + if (scrollbarActive !== this.#scrollbarActive) + { + this.#scrollbarActive = scrollbarActive; + this.element.css('pointer-events', scrollbarActive ? 'auto' : 'none'); + } + + if (currentPosition && currentPosition.width && currentPosition.height) + { + if (this.#timeoutPosition) + { + clearTimeout(this.#timeoutPosition); + } + + this.#timeoutPosition = setTimeout(() => + { + game.settings.set(constants.moduleName, settings.questTrackerPosition, JSON.stringify(currentPosition)); + }, QuestTracker.#TIMEOUT_POSITION); + } + + return currentPosition; + } +} \ No newline at end of file diff --git a/styles/basicapp.scss b/styles/basicapp.scss new file mode 100644 index 00000000..9cc9c08b --- /dev/null +++ b/styles/basicapp.scss @@ -0,0 +1,84 @@ +// Defines the styles for that mimics a popout Application. Used by QuestTracker to appear like a +// popout app, but be managed directly. +.fql-app { + max-height: 100%; + background: url(../../../ui/denim075.png) repeat; + border-radius: 5px; + box-shadow: 0 0 20px #000; + margin: 3px 0; + color: #f0f0e0; + position: absolute; +} + +.fql-window-app { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + padding: 0; + z-index: 99; + + .window-content { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; + padding: 8px; + color: #191813; + overflow-y: auto; + overflow-x: hidden; + } + + .window-header { + flex: 0 0 30px; + overflow: hidden; + padding: 0 8px; + line-height: 30px; + border-bottom: 1px solid #000; + pointer-events: auto; + + a { + flex: none; + margin: 0 0 0 8px; + } + + h4 { + font-family: Signika, sans-serif; + } + + i[class^=fa] { + margin-right: 3px; + } + + .window-title { + margin: 0; + word-break: break-all; + } + } + + .window-resizable-handle { + width: 20px; + height: 20px; + position: absolute; + bottom: -1px; + right: 0; + background: #444; + padding: 2px; + border: 1px solid #111; + border-radius: 4px 0 0 0; + + i.fas { + transform: rotate(45deg); + } + } + + &.minimized { + .window-header { + border: 1px solid #000; + } + + .window-resizable-handle { + display: none; + } + } +} \ No newline at end of file diff --git a/styles/global-mixin.scss b/styles/global-mixin.scss new file mode 100644 index 00000000..7162c56e --- /dev/null +++ b/styles/global-mixin.scss @@ -0,0 +1,89 @@ +@mixin button { + display: flex; + justify-content: center; + align-items: center; + background: $primary-color-bg-buttonspan; + border-radius: 5px; + width: 22px; + height: 22px; + transition: color .3s ease; + cursor: pointer; + + &:hover { + color: $primary-color-accent; + } + + i { + font-size: 16px; + border-radius: 50%; + line-height: 1; + } +} + +@mixin fonts { + /* almendra-regular - latin */ + @font-face { + font-family: 'Almendra'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/almendra-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/almendra-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* audiowide-regular - latin */ + @font-face { + font-family: 'Audiowide'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/audiowide-v9-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/audiowide-v9-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* bilbo-swash-caps-regular - latin */ + @font-face { + font-family: 'Bilbo Swash Caps'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/bilbo-swash-caps-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/bilbo-swash-caps-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* medievalsharp-regular - latin */ + @font-face { + font-family: 'MedievalSharp'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/medievalsharp-v14-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/medievalsharp-v14-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* metamorphous-regular - latin */ + @font-face { + font-family: 'Metamorphous'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/metamorphous-v13-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/metamorphous-v13-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } + + /* nova-square-regular - latin */ + @font-face { + font-family: 'Nova Square'; + font-style: normal; + font-weight: 400; + src: local(''), + url('../assets/fonts/nova-square-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ + url('../assets/fonts/nova-square-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + } +} + +@mixin header-buttons { + display: flex; + border-bottom: 2px solid $primary-color-borderheader; + border-block-end: 2px solid $primary-color-borderheader; +} \ No newline at end of file diff --git a/styles/global-variables.scss b/styles/global-variables.scss new file mode 100644 index 00000000..d1793d54 --- /dev/null +++ b/styles/global-variables.scss @@ -0,0 +1,44 @@ +// Support for Whetstone and nascent lib-themer (unfinished) +$background-color-light: rgba(255, 255, 255, .5); +$log-bookmark-image-background: var(--palette-app-background-image, url(../../../ui/parchment.jpg)) repeat; + +$primary-color-accent: var(--palette-primary, var(--default-primary-accent, #ff6400)); + +$primary-color-borderheader: var(--palette-primary, var(--default-primary-accent, #782e22)); + +$primary-color-bg-buttonspan: var(--default-secondary-accent, rgba(255, 255, 255, .75)); + +$primary-color-border-drop: rgba(0, 0, 0, .5); +$primary-color-bg-drop: rgba(0, 0, 0, .1); +$primary-color-hover-drop: rgba(0, 0, 0, .075); + +$primary-color-bg-li: rgba(255, 255, 255, .4); +$primary-color-bg-li-hidden: rgba(255, 255, 255, .2); + +$primary-color-icon: rgba(0,0,0,.75); + +$primary-color-bg-nav: rgba(255,255,255,.2); + +$primary-color-text: var(--default-primary-color, #EEE); +$primary-color-text-hidden: #888888; +$primary-color-text-hover: var(--default-primary-accent, red); + +$icon-color-primary-quest: gold; +$icon-color-show-background: lightblue; +$icon-color-completed: rgba(0, 175, 0, .8); +$icon-color-failed: rgba(200, 0, 0, .8); +$icon-color-trashcan: rgba(255, 0, 0, .6); + +// LibThemer support for QuestTracker +$tracker-color-background: var(--palette-fql-qt-color-background, #00000000); +$tracker-image-background: var(--palette-fql-qt-image-background, url(../../../ui/denim075.png)) repeat; +$tracker-image-background-blend-mode: var(--palette-fql-qt-image-background-blend-mode, normal); + +$tracker-color-text: var(--palette-fql-qt-text-color, #{$primary-color-text}); +$tracker-color-text-hidden: var(--palette-fql-qt-text-color-shaded-text, #{$primary-color-text-hidden}); + +$tracker-color-background-entitylink: var(--palette-fql-qt-color-background-entitylink, #ddd); +$tracker-color-text-entitylink: var(--palette-fql-qt-color-background-entitylink-contrast-text, black); + +// Doesn't work quite right as game systems can provide style overrides. +//$tracker-color-text-hover: var(--palette-fql-qt-text-color-light, #{$primary-color-text-hover}); diff --git a/styles/init.css b/styles/init.css deleted file mode 100644 index 4611368b..00000000 --- a/styles/init.css +++ /dev/null @@ -1,820 +0,0 @@ -#forien-quest-log .window-content, -#forien-quest-log-form .window-content, -.window-app.forien-quest-preview .window-content { - padding: 0; - height: 100%; } -#forien-quest-log .tab, -#forien-quest-log-form .tab, -.window-app.forien-quest-preview .tab { - height: 100%; - display: none; } - #forien-quest-log .tab.active, - #forien-quest-log-form .tab.active, - .window-app.forien-quest-preview .tab.active { - display: block; } -#forien-quest-log h1, -#forien-quest-log-form h1, -.window-app.forien-quest-preview h1 { - flex: 0 0 1px; - font-size: 22px; - line-height: 1; - font-weight: 700; - padding: 0 0 4px 0; - margin: 0 0 8px 0; } -#forien-quest-log h2, -#forien-quest-log-form h2, -.window-app.forien-quest-preview h2 { - font-size: 18px; - line-height: 1; - font-weight: 700; - padding: 0 0 2px 0; - margin: 0 0 4px 0; - border-width: 2px; } -#forien-quest-log label, -#forien-quest-log-form label, -.window-app.forien-quest-preview label { - display: block; - margin-bottom: 3px; } -#forien-quest-log input[type="text"], -#forien-quest-log-form input[type="text"], -.window-app.forien-quest-preview input[type="text"] { - border: none; - background: rgba(255, 255, 255, 0.5); - padding: 4px 8px; - box-shadow: 0 0 3px 1px transparent inset; - transition: box-shadow .3s ease; - height: 26px; } - #forien-quest-log input[type="text"]:hover, - #forien-quest-log-form input[type="text"]:hover, - .window-app.forien-quest-preview input[type="text"]:hover { - box-shadow: 0 0 0 1px #ff6400 inset; } -#forien-quest-log button, -#forien-quest-log-form button, -.window-app.forien-quest-preview button { - background: #F2F1EA; - height: 30px; - border: 1px solid #333; - border-radius: 5px; - margin: 0 0 0 8px; - transition: border-color .3s ease, background .3s ease, box-shadow .3s ease; - cursor: pointer; } - #forien-quest-log button:hover, - #forien-quest-log-form button:hover, - .window-app.forien-quest-preview button:hover { - box-shadow: 0 0 2px #ff6400 inset; - border-color: #ff6400; - background: #efefef; } - #forien-quest-log button:first-child, - #forien-quest-log-form button:first-child, - .window-app.forien-quest-preview button:first-child { - margin-left: 0; } -#forien-quest-log nav, -#forien-quest-log-form nav, -.window-app.forien-quest-preview nav { - flex: 0 0 40px; - background: rgba(255, 255, 255, 0.3); - justify-content: flex-start; - align-items: center; - padding: 0 16px; } - #forien-quest-log nav .item, - #forien-quest-log-form nav .item, - .window-app.forien-quest-preview nav .item { - text-align: left; - flex: 0 0 1px; - margin-left: 1rem; - white-space: nowrap; - transition: color .3s ease; } - #forien-quest-log nav .item:hover, - #forien-quest-log-form nav .item:hover, - .window-app.forien-quest-preview nav .item:hover { - text-shadow: none; - color: #ff6400; } - #forien-quest-log nav .item:first-child, - #forien-quest-log-form nav .item:first-child, - .window-app.forien-quest-preview nav .item:first-child { - margin-left: 0; } - #forien-quest-log nav .item.active, #forien-quest-log nav .item.active:hover, - #forien-quest-log-form nav .item.active, - #forien-quest-log-form nav .item.active:hover, - .window-app.forien-quest-preview nav .item.active, - .window-app.forien-quest-preview nav .item.active:hover { - font-weight: 700; - text-shadow: none; - color: inherit; } -#forien-quest-log .hidden, -#forien-quest-log-form .hidden, -.window-app.forien-quest-preview .hidden { - display: none; } -#forien-quest-log .editor, -#forien-quest-log-form .editor, -.window-app.forien-quest-preview .editor { - height: 100%; - padding: 8px; - background: rgba(255, 255, 255, 0.5); - border-radius: 5px; } - #forien-quest-log .editor .editor-content, - #forien-quest-log-form .editor .editor-content, - .window-app.forien-quest-preview .editor .editor-content { - height: 100%; - padding: 0; - padding: 0 4px 0 0; - overflow: auto; } -#forien-quest-log .actions, -#forien-quest-log-form .actions, -.window-app.forien-quest-preview .actions { - flex: 0 0 100px; - border-left: 1px solid rgba(0, 0, 0, 0.15); - height: 100%; - display: flex; - justify-content: center; - align-items: center; } - #forien-quest-log .actions i, - #forien-quest-log-form .actions i, - .window-app.forien-quest-preview .actions i { - font-size: 16px; - margin-left: 4px; - cursor: pointer; - color: rgba(0, 0, 0, 0.75); - transition: color .3s ease; } - #forien-quest-log .actions i.delete, - #forien-quest-log-form .actions i.delete, - .window-app.forien-quest-preview .actions i.delete { - color: rgba(255, 0, 0, 0.4); } - #forien-quest-log .actions i.fa-play, - #forien-quest-log-form .actions i.fa-play, - .window-app.forien-quest-preview .actions i.fa-play { - font-size: 14px; - padding-top: 2px; } - #forien-quest-log .actions i:hover, - #forien-quest-log-form .actions i:hover, - .window-app.forien-quest-preview .actions i:hover { - color: #ff6400; } - #forien-quest-log .actions i:first-child, - #forien-quest-log-form .actions i:first-child, - .window-app.forien-quest-preview .actions i:first-child { - margin: 0; } - -#forien-quest-log { - min-width: 500px; - min-height: 640px; } - #forien-quest-log .quest-log { - height: 100%; - overflow-y: auto; - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.1); - padding: 0 0 24px 0; } - #forien-quest-log .quest-log.bookmarks nav { - position: absolute; - left: 0; - transform: translateX(-100%); - flex-direction: column; - align-items: flex-end; - background: none; - padding: 0; - flex: 0; } - #forien-quest-log .quest-log.bookmarks nav .item { - background: url("/ui/parchment.jpg") repeat; - text-align: right; - margin: 0; - margin-bottom: 4px; - padding: 8px 16px; - width: 150px; - border-radius: 5px 0 0 5px; - position: relative; - z-index: 1; - box-shadow: -5px 0 5px -5px rgba(0, 0, 0, 0.25) inset, 0 5px 5px -5px rgba(0, 0, 0, 0.3), 0 -5px 5px -5px rgba(0, 0, 0, 0.3), -2px 0 5px -2px rgba(0, 0, 0, 0.3); - transition: padding .3s ease, width .3s ease, color .3s ease; } - #forien-quest-log .quest-log.bookmarks nav .item:hover { - padding-right: 32px; - width: 166px; } - #forien-quest-log .quest-log.bookmarks nav .item.active { - padding-right: 32px; - width: 166px; } - #forien-quest-log .quest-log.bookmarks nav .item.active::after { - content: ''; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - background: rgba(0, 0, 0, 0.1); - border-radius: 5px 0 0 5px; - z-index: -1; } - #forien-quest-log .quest-log .log-body { - flex: 1; - overflow-y: hidden; - padding: 0 16px; } - #forien-quest-log .quest-log .tab { - flex-direction: column; - padding: 16px 0 0 0; } - #forien-quest-log .quest-log .tab.active { - display: flex; } - #forien-quest-log .quest-log .tab .table { - flex: 1; - overflow-y: auto; } - #forien-quest-log .quest-log .table ul { - list-style: none; - margin: 0; - padding: 0; } - #forien-quest-log .quest-log .table ul li { - display: flex; - justify-content: flex-start; - align-items: center; - margin: 0 4px 2px 0; - background: rgba(255, 255, 255, 0.3); - border: 1px solid transparent; - border-radius: 5px; - height: 42px; - transition: border-color .3s ease, box-shadow .3s ease; } - #forien-quest-log .quest-log .table ul li:hover { - border-color: #ff6400; - box-shadow: 0 0 2px #ff6400 inset; } - #forien-quest-log .quest-log .table ul .img { - flex: 0 0 40px; - width: 40px; - height: 40px; - border-radius: 5px 0 0 5px; - background-size: cover; - background-position: center; } - #forien-quest-log .quest-log .table ul .personal-quest-icon { - margin-left: 8px; } - #forien-quest-log .quest-log .table ul .title { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - height: 100%; - padding: 0 8px; - cursor: pointer; } - #forien-quest-log .quest-log .table ul .title h2 { - margin: 0; - padding: 0; - line-height: 1; - border: none; - font-size: 16px; - font-weight: 700; } - #forien-quest-log .quest-log .table ul .title p { - margin: 0; - padding: 0; - font-size: 12px; - font-weight: 400; } - #forien-quest-log .quest-log .table ul .tasks { - flex: 0 0 60px; - border-left: 1px solid rgba(0, 0, 0, 0.15); - height: 100%; - display: flex; - justify-content: center; - align-items: center; } - #forien-quest-log .quest-log footer { - flex: 0 0 1px; - padding: 8px 16px 0 16px; - display: flex; } - -#forien-quest-log-form form { - padding: 1rem; - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.1); - height: 100%; } -#forien-quest-log-form form header { - flex: 0 0 1px; } - #forien-quest-log-form form header .source-details { - display: flex; } - #forien-quest-log-form form header .source-image { - flex: 0 0 100px; - height: 100px; - font-size: 12px; - line-height: 1.2; - font-weight: 700; - text-align: center; - margin-right: 8px; } - #forien-quest-log-form form header .source-image .giver-portrait { - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - border-radius: 5px; } - #forien-quest-log-form form header .source-image .hidden { - display: none; } - #forien-quest-log-form form header .source-image span { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - border: 2px dashed rgba(0, 0, 0, 0.5); - border-radius: 5px; - padding: 8px; } - #forien-quest-log-form form header .source-info { - flex: 1; - height: 100px; } - #forien-quest-log-form form header .quest-giver { - margin-bottom: 8px; } -#forien-quest-log-form .quest-title { - margin-top: 8px; } -#forien-quest-log-form .quest-text { - margin-top: 8px; - display: flex; - flex: 1; - overflow-y: hidden; } - #forien-quest-log-form .quest-text .quest-description, - #forien-quest-log-form .quest-text .quest-notes { - flex: 1; } - #forien-quest-log-form .quest-text .quest-notes { - margin-left: 8px; } - #forien-quest-log-form .quest-text .editor { - padding: 8px; - background: rgba(255, 255, 255, 0.5); - border-radius: 5px; - height: calc(100% - 30px); } - #forien-quest-log-form .quest-text .editor .tox .tox-toolbar-overlord { - background-color: transparent; - border-bottom: 1px solid #222; - padding-bottom: 4px; } - #forien-quest-log-form .quest-text .editor .tox .tox-toolbar, - #forien-quest-log-form .quest-text .editor .tox .tox-toolbar__overflow, - #forien-quest-log-form .quest-text .editor .tox .tox-toolbar__primary { - background: transparent; - background-color: transparent; } - #forien-quest-log-form .quest-text .editor .tox.tox-tinymce .tox-tbtn { - padding: 0; - margin: 0 0 0 4px; - width: 32px; } - #forien-quest-log-form .quest-text .editor .tox.tox-tinymce .tox-tbtn[title="Formats"] { - width: 90px; } - #forien-quest-log-form .quest-text .editor .editor-content { - height: 100%; - overflow-y: auto; - margin: 0; - padding: 0 12px 0 0; } -#forien-quest-log-form footer { - flex: 0 0 1px; - margin-top: 8px; } - -.window-app.forien-quest-preview { - min-width: 940px; - min-height: 640px; } - .window-app.forien-quest-preview .tab.active { - display: flex; - flex-direction: column; } - .window-app.forien-quest-preview .quest-preview { - height: 100%; - overflow-y: hidden; - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.1); - padding: 0 0 24px 0; } - .window-app.forien-quest-preview .quest-body { - height: 100%; - flex: 1; - overflow-y: auto; - padding: 16px 16px 0 16px; } - .window-app.forien-quest-preview .quest-body .details-header { - display: flex; - flex: 0 0 1px; - margin-bottom: 16px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc { - width: 100px; - height: 100px; - background-color: rgba(0, 0, 0, 0.1); - border-radius: 5px; - flex: 0 0 100px; - margin-right: 16px; - position: relative; } - .window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .quest-giver-image { - height: 100%; - width: 100%; - background-size: cover; - background-position: center; - cursor: pointer; - border-radius: 5px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage { - position: absolute; - top: 0; - left: 0; - display: flex; - justify-content: center; - align-items: center; - background: #efefef; - border-radius: 5px; - width: 22px; - height: 22px; - transition: color .3s ease; } - .window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .details-header .quest-giver-gc .toggleImage i { - font-size: 16px; - border-radius: 50%; - line-height: 1; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup { - flex: 1; - display: flex; - flex-direction: column; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-title { - display: flex; - justify-content: space-between; - align-items: center; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .editable-container { - flex: 1; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .editable-container input { - margin-bottom: 8px; - height: 28px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link { - flex: 0 0 100px; - background-size: cover; - background-position: center; - position: relative; - cursor: pointer; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link span { - position: absolute; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background: rgba(0, 0, 0, 0.3); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 28px; - color: rgba(255, 255, 255, 0.65); - opacity: 1; - transition: opacity .3s ease; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .splash-image-link span:hover { - opacity: 0; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .actions { - flex: 0 0 1px; - padding: 0 8px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .actions i { - font-size: 18px; - transition: color .3s ease; - cursor: pointer; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .actions i:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-title .actions { - border: none; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup section { - flex: 1; - display: flex; - background: rgba(255, 255, 255, 0.15); - border-radius: 5px; - overflow: hidden; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-details { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - padding: 8px 16px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name h2 { - display: inline-block; - margin: 0; - border: none; - cursor: pointer; - transition: color .3s ease; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-giver-name h2:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status { - display: flex; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p { - margin: 0 8px 0 0; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p::after { - content: '|'; - margin-left: 8px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p:last-child { - margin: 0; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status p:last-child::after { - content: none; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name { - transition: color .3s ease; - cursor: pointer; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name i { - font-size: 12px; } - .window-app.forien-quest-preview .quest-body .details-header .quest-setup .quest-status .quest-name:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .quest-info { - display: flex; - flex: 1; - overflow-y: hidden; } - .window-app.forien-quest-preview .quest-body .quest-info header { - display: flex; - justify-content: space-between; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right button { - flex: 0 0 1px; - white-space: nowrap; - height: 18px; - font-size: 12px; - line-height: 1; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right button i { - font-size: 10px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-description { - flex: 0 0 50%; - height: 100%; - overflow-y: hidden; - margin-right: 8px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-description .description { - height: calc(100% - 26px); - overflow: hidden; - background: rgba(255, 255, 255, 0.4); - border-radius: 5px; - padding: 8px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-description .description .description-content { - height: 100%; - overflow: auto; - padding: 0 4px 0 0; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right { - flex: 1; - display: flex; - flex-direction: column; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right h2 { - border: none; - margin: 0; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right header { - border-bottom: 2px solid #782e22; - margin-bottom: 4px; - flex: 0 0 1px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards { - flex: 0 0 calc(50% - 8px); - display: flex; - flex-direction: column; - overflow-y: hidden; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-box, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .quest-box { - flex: 1; - overflow-y: hidden; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks ul, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards ul { - height: 100%; - overflow-y: auto; - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks ul li, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards ul li { - display: flex; - border-radius: 5px; - background: rgba(255, 255, 255, 0.3); - margin: 0 4px 2px 0; - align-items: center; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions { - flex: 0 0 100px; - height: 100%; - cursor: default; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions i, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions i { - min-width: 16px; - text-align: center; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions .fa-sort, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions .fa-sort { - cursor: move; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions .del-btn, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions .del-btn { - color: rgba(255, 0, 0, 0.4); } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions .del-btn:hover, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions .del-btn:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .actions .fa-pen, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .actions .fa-pen { - font-size: 14px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container { - flex: 1; - padding: 4px 8px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container p, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container p { - margin: 0; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .editable-container input, - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .editable-container input { - padding: 0 4px; - line-height: 14px; - height: 16px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks { - margin-bottom: 16px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .toggleState { - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 32px; - height: 100%; - border-right: 1px solid rgba(0, 0, 0, 0.15); - font-size: 18px; - cursor: pointer; - transition: color .3s ease; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .toggleState:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container { - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 32px; - height: 100%; - border-right: 1px solid rgba(0, 0, 0, 0.15); - font-size: 18px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container .state-display { - background: rgba(0, 0, 0, 0.05); - border: 1px solid rgba(0, 0, 0, 0.3); - width: 16px; - height: 16px; - border-radius: 2px; - display: flex; - justify-content: center; - align-items: center; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .state-container .state-display i { - font-size: 11px; - line-height: 16px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name { - cursor: pointer; - transition: color .3s ease; - margin: 0; - padding: 4px 8px; - display: inline-block; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name:hover { - color: #ff6400; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .quest-name i { - font-size: 12px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .task-hidden { - background: rgba(0, 0, 0, 0.15); } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-tasks .task-hidden .task-name { - opacity: .5; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward { - flex: 0 0 25px; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .drop-info { - flex: 1 0 25px; - line-height: 20px; - border: 2px dashed rgba(0, 0, 0, 0.5); - border-radius: 5px; - padding: 0 16px; - text-align: center; - margin-right: 4px; - margin-bottom: 4px; - background: transparent; - justify-content: center; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden { - background: rgba(0, 0, 0, 0.15); } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden .reward-image { - opacity: .5; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-hidden .reward-name { - opacity: .5; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-image-container { - height: 100%; - flex: 0 0 25px; - display: flex; - align-items: center; - border-radius: 5px 0 0 5px; - overflow: hidden; - background-color: #222; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-image { - width: 25px; - height: 25px; - background-size: cover; - background-position: center; } - .window-app.forien-quest-preview .quest-body .quest-info .quest-col-right .quest-rewards .reward-name { - flex: 1; - font-size: 14px; - font-weight: 400; - margin: 0; - padding-right: 8px; } - .window-app.forien-quest-preview .quest-body .management .row { - display: flex; - flex: 0 0 1px; } - .window-app.forien-quest-preview .quest-body .management .quest-settings { - display: flex; - flex-direction: column; - flex: 1; - margin-right: 8px; - height: 226px; } - .window-app.forien-quest-preview .quest-body .management .quest-settings .setting-groups { - flex: 0 0 1px; } - .window-app.forien-quest-preview .quest-body .management .quest-settings .setting-groups .personal-quest-description { - font-size: 13px; - margin: 4px 0 2px 26px; } - .window-app.forien-quest-preview .quest-body .management .quest-settings .input-group { - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.4); - border-radius: 5px; - padding: 2px; - margin-bottom: 2px; } - .window-app.forien-quest-preview .quest-body .management .quest-settings input[type="checkbox"] { - flex: 0 0 20px; - width: 20px; - height: 20px; - margin: 0; } - .window-app.forien-quest-preview .quest-body .management .quest-settings label { - margin: 0 0 0 4px; - width: calc(100% - 20px); - overflow: hidden; - text-overflow: ellipsis; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings { - margin-left: 26px; - flex: 1; - overflow: hidden; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul { - margin: 0; - padding: 0 2px 0 0; - list-style: none; - display: flex; - flex-wrap: wrap; - width: calc(100% + 2px); - margin-left: -2px; - height: 100%; - overflow-y: auto; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul.disabled { - opacity: .5; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul.disabled li { - cursor: default; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul.disabled li:hover { - background: inherit; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul.disabled input, .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul.disabled label { - cursor: default; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul li { - cursor: pointer; - flex: 0 0 calc(100% / 4 - 4px); - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.4); - border-radius: 5px; - padding: 2px 8px 2px 2px; - margin: 2px; - white-space: nowrap; - overflow: hidden; } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul li:hover { - background: rgba(255, 255, 255, 0.6); } - .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul li input, .window-app.forien-quest-preview .quest-body .management .personal-quest-settings ul li label { - cursor: pointer; } - .window-app.forien-quest-preview .quest-body .management .quest-splash { - flex: 0 0 calc(100% / 3); } - .window-app.forien-quest-preview .quest-body .management .quest-splash .splash-image { - width: 100%; - height: 200px; - background-size: cover; - background-position: center; - background-color: rgba(255, 255, 255, 0.4); - border-radius: 5px; - cursor: pointer; } - .window-app.forien-quest-preview .quest-body .management .quest-splash .splash-image:hover { - background-color: rgba(255, 255, 255, 0.6); } - .window-app.forien-quest-preview .quest-body .management .subquests { - flex: 1; - display: flex; - flex-direction: column; - margin-top: 16px; - overflow: hidden; } - .window-app.forien-quest-preview .quest-body .management .subquests h2 { - flex: 0 0 1px; } - .window-app.forien-quest-preview .quest-body .management .subquests .subquests-box { - flex: 1; - overflow-y: auto; - margin: 0; - padding: 0; - list-style: none; } - .window-app.forien-quest-preview .quest-body .management .subquests .subquests-box li { - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.3); - height: 30px; - border-radius: 5px; - margin: 0 4px 2px 0; - border: 1px solid transparent; - transition: border-color .3s ease, box-shadow .3s ease; } - .window-app.forien-quest-preview .quest-body .management .subquests .subquests-box li:hover { - border-color: #ff6400; - box-shadow: 0 0 2px #ff6400 inset; } - .window-app.forien-quest-preview .quest-body .management .subquests .subquests-box h2 { - flex: 1; - border: none; - margin: 0 8px; - font-size: 14px; - line-height: 30px; - cursor: pointer; - transition: color .3s ease; } - .window-app.forien-quest-preview .quest-body .management .subquests .subquests-box .actions { - flex: 0 0 100px; - height: 100%; } - .window-app.forien-quest-preview .quest-body .management .subquests footer { - flex: 0 0 1px; - margin: 8px 0 0 0; } - .window-app.forien-quest-preview .quest-body .editor { - height: calc(100% - 26px); } - .window-app.forien-quest-preview .quest-body .gmnotes .editor { - height: calc(100% - 36px); } - .window-app.forien-quest-preview .quest-body .tox .tox-toolbar-overlord { - background-color: transparent; - border-bottom: 1px solid #222; - padding-bottom: 4px; } - .window-app.forien-quest-preview .quest-body .tox .tox-toolbar, - .window-app.forien-quest-preview .quest-body .tox .tox-toolbar__overflow, - .window-app.forien-quest-preview .quest-body .tox .tox-toolbar__primary { - background: transparent; - background-color: transparent; } - .window-app.forien-quest-preview .quest-body .tox.tox-tinymce .tox-tbtn { - padding: 0; - margin: 0 0 0 4px; - width: 32px; } - .window-app.forien-quest-preview .quest-body .tox.tox-tinymce .tox-tbtn[title="Formats"] { - width: 90px; } - -/*# sourceMappingURL=init.css.map */ diff --git a/styles/init.css.map b/styles/init.css.map deleted file mode 100644 index 234a97d1..00000000 --- a/styles/init.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "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", -"sources": ["quest-general.scss","init.scss","quest-log.scss","quest-form.scss","quest-preview.scss"], -"names": [], -"file": "init.css" -} diff --git a/styles/init.scss b/styles/init.scss index 784ee59d..d4477f38 100644 --- a/styles/init.scss +++ b/styles/init.scss @@ -1,11 +1,2 @@ -$background-parchment: url("/ui/parchment.jpg") repeat; -$background-parchment-dark: url("/modules/dnd5e-dark-mode/ui/parchment_dark.jpg") repeat; - -$background-color-light: rgba(255,255,255,.5); -$primary-accent-color: #ff6400; - -$button-background: #F2F1EA; -$button-hover: #efefef; -$button-border: #333; - -@import 'quest-general', 'quest-log', 'quest-form', 'quest-preview'; \ No newline at end of file +@import 'global-mixin', 'global-variables', 'basicapp', 'quest-general', 'quest-log', 'quest-preview', + 'quest-tracker'; diff --git a/styles/quest-form.scss b/styles/quest-form.scss deleted file mode 100644 index 4bf1e96a..00000000 --- a/styles/quest-form.scss +++ /dev/null @@ -1,125 +0,0 @@ -#forien-quest-log-form { - - form { - padding: 1rem; - display: flex; - flex-direction: column; - background: rgba(0,0,0,.1); - height: 100%; - // overflow-y: hidden; - } - - form header { - flex: 0 0 1px; - - .source-details { - display: flex; - } - - .source-image { - flex: 0 0 100px; - height: 100px; - font-size: 12px; - line-height: 1.2; - font-weight: 700; - text-align: center; - margin-right: 8px; - - .giver-portrait { - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - border-radius: 5px; - } - - .hidden { - display: none; - } - - span { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - border: 2px dashed rgba(0,0,0,.5); - border-radius: 5px; - padding: 8px; - } - } - - .source-info { - flex: 1; - height: 100px; - } - - .quest-giver { - margin-bottom: 8px; - } - - - } - - .quest-title { - margin-top: 8px; - } - - .quest-text { - margin-top: 8px; - display: flex; - flex: 1; - overflow-y: hidden; - - .quest-description, - .quest-notes { - flex: 1; - } - - .quest-notes { - margin-left: 8px; - } - - .editor { - padding: 8px; - background: $background-color-light; - border-radius: 5px; - height: calc(100% - 30px); - - .tox .tox-toolbar-overlord { - background-color: transparent; - border-bottom: 1px solid #222; - padding-bottom: 4px; - } - - .tox .tox-toolbar, - .tox .tox-toolbar__overflow, - .tox .tox-toolbar__primary { - background: transparent; - background-color: transparent; - } - - .tox.tox-tinymce .tox-tbtn { - padding: 0; - margin: 0 0 0 4px; - width: 32px; - } - - .tox.tox-tinymce .tox-tbtn[title="Formats"] { - width: 90px; - } - - .editor-content { - height: 100%; - overflow-y: auto; - margin: 0; - padding: 0 12px 0 0; - } - } - - } - - footer { - flex: 0 0 1px; - margin-top: 8px; - } -} diff --git a/styles/quest-general.scss b/styles/quest-general.scss index 0384a5de..dc1cd3e4 100644 --- a/styles/quest-general.scss +++ b/styles/quest-general.scss @@ -1,10 +1,46 @@ #forien-quest-log, -#forien-quest-log-form, +#quest-tracker, +.window-app.forien-quest-preview { + .pad-l-4 { + padding-left: 4px; + } + + .pad-l-8 { + padding-left: 8px; + } + + i { + flex: none; + + &.fas.fa-star { + color: $icon-color-primary-quest; + filter: drop-shadow(0 0 2px #000); + } + + &.fas.fa-fill { + color: $icon-color-show-background; + filter: drop-shadow(0 0 2px #000); + } + + &.fas.fa-fill.off { + color: white; + filter: drop-shadow(0 0 2px #000); + } + } +} + +#forien-quest-log, .window-app.forien-quest-preview { + // Some systems such as Blades In the Dark require this parameter. + .window-header { + margin: 0; + } + .window-content { padding: 0; - height: 100%; + margin: 0; + border-radius: 0 0 5px 5px; } .tab { @@ -27,7 +63,6 @@ h2 { font-size: 18px; - line-height: 1; font-weight: 700; padding: 0 0 2px 0; margin: 0 0 4px 0; @@ -48,39 +83,34 @@ height: 26px; &:hover { - box-shadow: 0 0 0 1px $primary-accent-color inset; + box-shadow: 0 0 0 1px $primary-color-accent inset; } } button { - background: $button-background; height: 30px; - border: 1px solid $button-border; border-radius: 5px; - margin: 0 0 0 8px; + margin-top: 2px; + margin-right: 4px; + margin-bottom: 2px; transition: border-color .3s ease, background .3s ease, box-shadow .3s ease; cursor: pointer; - &:hover { - box-shadow: 0 0 2px $primary-accent-color inset; - border-color: $primary-accent-color; - background: $button-hover; - } - &:first-child { margin-left: 0; } } - nav { + nav.tabs { flex: 0 0 40px; - background: rgba(255,255,255,.3); + background: $primary-color-bg-nav; justify-content: flex-start; align-items: center; padding: 0 16px; + font-size: larger; .item { text-align: left; @@ -91,7 +121,7 @@ &:hover { text-shadow: none; - color: $primary-accent-color; + color: $primary-color-accent; } &:first-child { @@ -111,6 +141,10 @@ display: none; } + .icon-button { + flex: none; + } + .editor { height: 100%; padding: 8px; @@ -120,43 +154,122 @@ .editor-content { height: 100%; padding: 0; - padding: 0 4px 0 0; overflow: auto; } } + // See `Enrich.statusActions` for building actions for quests. .actions { - flex: 0 0 100px; - border-left: 1px solid rgba(0,0,0,.15); - height: 100%; - display: flex; - justify-content: center; - align-items: center; - - i { - font-size: 16px; - margin-left: 4px; - cursor: pointer; - color: rgba(0,0,0,.75); - transition: color .3s ease; - - &.delete { - color: rgba(255,0,0,.4); - } - - &.fa-play { - font-size: 14px; - padding-top: 2px; - } - - &:hover { - color: $primary-accent-color; - } - - &:first-child { - margin: 0; - } + flex: 0 0 100px; + border-left: 1px solid rgba(0,0,0,.15); + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + // The flex size is reduced for players who have less actions available. + &.is-player { + flex: 0 0 60px; + } + + // Used to justify center tasks and rewards + span.justify-center { + flex: 1; + margin: 1px 1px 1px auto; + visibility: hidden; + } + + span.spacer { + flex: 0 0 4px; + } + + i { + flex: 0 0 18px; + font-size: 16px; + padding: 0; + text-align: center; + border: none; + cursor: pointer; + color: $primary-color-icon; + transition: color .3s ease; + + // Special handling to align sort icon to the left and use margin-right: auto to push rest of content right. + // In this case add span.justify-center to the right hand of the action div to center the content. + &.fa-sort { + flex: 0 0 18px; + cursor: move; + width: 14px; + border-right: 1px solid rgba(0, 0, 0, 0.15); + } + + // fa-eye-slash is slightly larger and offsets so provide specific flex settings and padding for fa-eye + &.fa-eye { + flex: 0 0 20px; + padding-left: 1px; + } + + &.fa-eye-slash { + flex: 0 0 20px; + } + + &.fa-check-circle { + color: $icon-color-completed; + } + + &.fa-times-circle { + color: $icon-color-failed; + } + + &.fa-trash { + color: $icon-color-trashcan; + } + + &.fa-play { + font-size: 14px; + padding-top: 2px; + } + + &:hover { + color: $primary-color-accent; + } + + &:hover.is-player { + color: $primary-color-icon; + cursor: default; + } + + &:first-child { + margin: 0; } } + } + + // Used for action divs that have a single icon (see: QuestPreview quest name / fa-pen) + .actions-single { + flex: 0 0 1px; + padding: 0 8px; + i { + font-size: 18px; + transition: color .3s ease; + cursor: pointer; + + &:hover { + color: $primary-color-accent; + } + } + } + + // Provides specific overrides to make sure the layout is consistent across game systems. + // In particular this is needed for Blades In the Dark `v3.3.0`. + section { + flex-direction: row; + display: block; + justify-content: flex-start; + } + + // Defines the corner of any scrollbars. + ::-webkit-scrollbar-corner { + background: rgba(0, 0, 0, 0.1); + } } \ No newline at end of file diff --git a/styles/quest-log.scss b/styles/quest-log.scss index e5922721..fb88c7a8 100644 --- a/styles/quest-log.scss +++ b/styles/quest-log.scss @@ -2,42 +2,49 @@ min-width: 500px; min-height: 640px; + // Necessary to ensure the bookmark tabs show. See `QuestLog.setPosition` for the manual positioning necessary. + // Some game systems and UI theming modules override `overflow`, so we must visible, but then handle very long + // list of quests / `.table` below manually based on the Application / setPosition. + &.window-app .window-content { + overflow: visible; + } + + $quest-min-height: 42px; // Defines the height of the quest
  • and children elements. + .quest-log { - height: 100%; - overflow-y: auto; display: flex; flex-direction: column; - background: rgba(0,0,0,.1); - padding: 0 0 24px 0; + padding: 0 0 16px 0; } - .quest-log.bookmarks nav { + .quest-log.bookmarks nav.log-tabs { position: absolute; left: 0; - transform: translateX(-100%); + transform: translateX(-97%); flex-direction: column; align-items: flex-end; background: none; padding: 0; flex: 0; + border-block-end: none; .item { - background: $background-parchment; + // The dynamic jQuery module setting overrides this with the window content background image. This is the fallback. + background: $log-bookmark-image-background; + text-align: right; - margin: 0; - margin-bottom: 4px; + margin: 0 0 4px 0; padding: 8px 16px; width: 150px; border-radius: 5px 0 0 5px; position: relative; z-index: 1; - box-shadow: - -5px 0 5px -5px rgba(0, 0, 0, 0.25) inset, + box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.3), - 0 -5px 5px -5px rgba(0, 0, 0, 0.3), - -2px 0 5px -2px rgba(0, 0, 0, 0.3); + 0 -5px 5px -5px rgba(0, 0, 0, 0.3); + transition: padding .3s ease, width .3s ease, color .3s ease; - + &:hover { padding-right: 32px; width: 166px; @@ -55,7 +62,6 @@ height: 100%; top: 0; left: 0; - background: rgba(0,0,0,.1); border-radius: 5px 0 0 5px; z-index: -1; } @@ -64,13 +70,31 @@ .quest-log .log-body { flex: 1; - overflow-y: hidden; padding: 0 16px; + + header { + @include header-buttons; + justify-content: space-between; + margin-top: 0; + padding-top: 0; + margin-bottom: 4px; + + h1 { + align-self: flex-end; + border: none; + margin: 0; + padding-bottom: 4px; + } + + button { + flex: 0 0 fit-content; + } + } } .quest-log .tab { flex-direction: column; - padding: 16px 0 0 0; + padding: 4px 0 0 0; &.active { display: flex; @@ -79,6 +103,9 @@ .table { flex: 1; overflow-y: auto; + + // For Firefox. + scrollbar-width: thin; } } @@ -87,7 +114,7 @@ margin: 0; padding: 0; - li { + li.drag-quest { display: flex; justify-content: flex-start; align-items: center; @@ -95,26 +122,25 @@ background: rgba(255, 255, 255, .3); border: 1px solid transparent; border-radius: 5px; - height: 42px; + min-height: $quest-min-height; transition: border-color .3s ease, box-shadow .3s ease; &:hover { - border-color: $primary-accent-color; - box-shadow: 0 0 2px $primary-accent-color inset; + border-color: $primary-color-accent; + box-shadow: 0 0 2px $primary-color-accent inset; } } + .open-quest { + cursor: pointer; + } + .img { flex: 0 0 40px; width: 40px; height: 40px; - border-radius: 5px 0 0 5px; + border-radius: 5px; background-size: cover; - background-position: center; - } - - .personal-quest-icon { - margin-left: 8px } .title { @@ -122,9 +148,9 @@ display: flex; flex-direction: column; justify-content: center; - height: 100%; - padding: 0 8px; - cursor: pointer; + padding-left: 2px; + padding-right: 8px; + min-height: $quest-min-height; h2 { margin: 0; @@ -143,20 +169,22 @@ } } + // The height parameter hard codes the height for the border to show, but in the future this may need to be + // adjustable. .tasks { - flex: 0 0 60px; + font-size: 18px; + flex: 0 0 50px; border-left: 1px solid rgba(0,0,0,.15); - height: 100%; + min-height: $quest-min-height; display: flex; justify-content: center; align-items: center; } - } - .quest-log footer { - flex: 0 0 1px; - padding: 8px 16px 0 16px; - display: flex; + // The height parameter hard codes the height for the border to show, but in the future this may need to be + // adjustable. + .actions { + min-height: $quest-min-height; + } } - } \ No newline at end of file diff --git a/styles/quest-preview.scss b/styles/quest-preview.scss index 7c3c7320..66f6731c 100644 --- a/styles/quest-preview.scss +++ b/styles/quest-preview.scss @@ -1,5 +1,5 @@ .window-app.forien-quest-preview { - min-width: 940px; + min-width: 1000px; min-height: 640px; .tab.active { @@ -13,35 +13,49 @@ display: flex; flex-direction: column; background: rgba(0, 0, 0, .1); - padding: 0 0 24px 0; + padding: 0; } .quest-body { height: 100%; flex: 1; + overflow-x: hidden; overflow-y: auto; - padding: 16px 16px 0 16px; + padding: 6px 14px 14px 14px; .details-header { display: flex; flex: 0 0 1px; - margin-bottom: 16px; + margin-bottom: 6px; .quest-giver-gc { - width: 100px; - height: 100px; - background-color: rgba(0, 0, 0, .1); + width: 116px; + height: 116px; + background-color: $primary-color-bg-drop; border-radius: 5px; - flex: 0 0 100px; - margin-right: 16px; + flex: 0 0 116px; + margin-right: 8px; position: relative; + font-size: 12px; + line-height: 1.2; + font-weight: 700; + text-align: center; + + .drop-info { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + height: 100%; + border: 2px dashed rgba(0,0,0,.5); + border-radius: 5px; + } + .quest-giver-image { height: 100%; width: 100%; background-size: cover; - background-position: center; - cursor: pointer; border-radius: 5px; } @@ -49,24 +63,14 @@ position: absolute; top: 0; left: 0; - display: flex; - justify-content: center; - align-items: center; - background: #efefef; - border-radius: 5px; - width: 22px; - height: 22px; - transition: color .3s ease; - - &:hover { - color: $primary-accent-color; - } + @include button; + } - i { - font-size: 16px; - border-radius: 50%; - line-height: 1; - } + .deleteQuestGiver { + position: absolute; + top: 0; + right: 0; + @include button; } } @@ -83,6 +87,7 @@ .editable-container { flex: 1; + padding-left: 6px; input { margin-bottom: 8px; @@ -93,7 +98,6 @@ .splash-image-link { flex: 0 0 100px; background-size: cover; - background-position: center; position: relative; cursor: pointer; @@ -119,31 +123,13 @@ } } - .actions { - flex: 0 0 1px; - padding: 0 8px; - - i { - font-size: 18px; - transition: color .3s ease; - cursor: pointer; - - &:hover { - color: $primary-accent-color; - } - } - } - - .quest-title .actions { - border: none; - } - section { flex: 1; display: flex; background: rgba(255, 255, 255, .15); border-radius: 5px; overflow: hidden; + margin-right: 4px; } .quest-details { @@ -151,7 +137,23 @@ display: flex; flex-direction: column; justify-content: center; - padding: 8px 16px; + padding-right: 16px; + } + + .quest-giver-name { + display: inline-flex; + justify-content: left; + flex-direction: row; + } + + .quest-giver-name .editable-container { + flex: 0 0 auto; + + input { + margin-bottom: 2px; + padding: 0; + height: 22px; + } } .quest-giver-name h2 { @@ -162,12 +164,17 @@ transition: color .3s ease; &:hover { - color: $primary-accent-color; + color: $primary-color-accent; } } + .quest-giver-name .action-single { + flex: 0 0 1px; + } + .quest-status { display: flex; + padding-left: 6px; p { margin: 0 8px 0 0; @@ -186,7 +193,7 @@ content: none; } - .quest-name { + .quest-name-link { transition: color .3s ease; cursor: pointer; @@ -195,7 +202,7 @@ } &:hover { - color: $primary-accent-color; + color: $primary-color-accent; } } } @@ -215,8 +222,8 @@ .quest-col-right button { flex: 0 0 1px; white-space: nowrap; - height: 18px; - font-size: 12px; + height: 20px; + font-size: 15px; line-height: 1; i { @@ -225,14 +232,14 @@ } .quest-description { - flex: 0 0 50%; + flex: 0 0 48%; height: 100%; overflow-y: hidden; margin-right: 8px; .description { - height: calc(100% - 26px); - overflow: hidden; + height: calc(100% - 30px); + overflow: auto; background: rgba(255, 255, 255, .4); border-radius: 5px; padding: 8px; @@ -246,21 +253,25 @@ } .quest-col-right { - flex: 1; + flex: 0 0 51%; display: flex; flex-direction: column; h2 { border: none; - margin: 0; + margin: 0 auto 0 0; } header { - border-bottom: 2px solid #782e22; + @include header-buttons; margin-bottom: 4px; flex: 0 0 1px; } + span.spacer-edit { + flex: 0 0 18px; + } + .quest-tasks, .quest-rewards { flex: 0 0 calc(50% - 8px); @@ -285,45 +296,40 @@ li { display: flex; border-radius: 5px; - background: rgba(255, 255, 255, .3); + background: $primary-color-bg-li; margin: 0 4px 2px 0; align-items: center; } - } - .actions { - flex: 0 0 100px; - height: 100%; - cursor: default; - - i { - min-width: 16px; - text-align: center; - } - - .fa-sort { - cursor: move; - } - - .del-btn { - color: rgba(255, 0, 0, .4); - - &:hover { - color: $primary-accent-color; - } + li:last-of-type { + margin-bottom: 0; } + } - .fa-pen { - font-size: 14px; - } + .is-link { + cursor: pointer; } .editable-container { flex: 1; - padding: 4px 8px; + padding: 4px 4px; p { margin: 0; + max-width: 290px; + word-wrap: break-word; + + &.can-edit { + max-width: 290px; + } + + &.player-edit { + max-width: 330px; + } + + &.player { + max-width: 400px; + } } input { @@ -349,7 +355,7 @@ transition: color .3s ease; &:hover { - color: $primary-accent-color; + color: $primary-color-accent; } } @@ -379,15 +385,28 @@ } } - .quest-name { + .quest-name-link { cursor: pointer; transition: color .3s ease; margin: 0; - padding: 4px 8px; - display: inline-block; + padding: 4px 4px; + flex: 1; + word-wrap: break-word; + + .can-edit { + max-width: 290px; + } + + .player-edit { + max-width: 330px; + } + + .player { + max-width: 390px; + } &:hover { - color: $primary-accent-color; + color: $primary-color-accent; } i { @@ -396,7 +415,7 @@ } .task-hidden { - background: rgba(0, 0, 0, .15); + background: $primary-color-bg-li-hidden; .task-name { opacity: .5; @@ -405,26 +424,38 @@ } .quest-rewards { + button { + &.hide-all-rewards, &.show-all-rewards { + flex: 0 0 90px; + } + + &.lock-all-rewards, &.unlock-all-rewards { + flex: 0 0 98px; + } + } .reward { flex: 0 0 25px; } + span.spacer-edit { + flex: 0 0 18px; + } + .drop-info { flex: 1 0 25px; line-height: 20px; - border: 2px dashed rgba(0, 0, 0, .5); + border: 2px dashed $primary-color-border-drop; border-radius: 5px; padding: 0 16px; text-align: center; margin-right: 4px; - margin-bottom: 4px; background: transparent; justify-content: center; } .reward-hidden { - background: rgba(0, 0, 0, .15); + background: $primary-color-bg-li-hidden; .reward-image { opacity: .5; @@ -443,6 +474,10 @@ border-radius: 5px 0 0 5px; overflow: hidden; background-color: #222; + + &.can-edit { + cursor: pointer; + } } .reward-image { @@ -461,11 +496,23 @@ } } } + } + .playernotes { + .quest-playernotes { + height: 100%; + overflow-y: hidden; + } } - .management { + .gmnotes { + .quest-gmnotes { + height: 100%; + overflow-y: hidden; + } + } + .management { .row { display: flex; flex: 0 0 1px; @@ -480,27 +527,6 @@ .setting-groups { flex: 0 0 1px; - - .personal-quest-description { - font-size: 13px; - margin: 4px 0 2px 26px; - } - } - - .input-group { - display: flex; - align-items: center; - background: rgba(255, 255, 255, .4); - border-radius: 5px; - padding: 2px; - margin-bottom: 2px; - } - - input[type="checkbox"] { - flex: 0 0 20px; - width: 20px; - height: 20px; - margin: 0; } label { @@ -511,76 +537,73 @@ } } - .personal-quest-settings { - margin-left: 26px; - flex: 1; - overflow: hidden; - - ul { - margin: 0; - padding: 0 2px 0 0; - list-style: none; - display: flex; - flex-wrap: wrap; - width: calc(100% + 2px); - margin-left: -2px; - height: 100%; - overflow-y: auto; - - &.disabled { - opacity: .5; - - li { - cursor: default; + .quest-splash { + flex: 0 0 calc(100% / 2.5); + position: relative; - &:hover { - background: inherit; - } - } + .splash-image { + width: 100%; + height: 200px; + background-size: cover; + background-color: $primary-color-bg-drop; + border-radius: 5px; - input, label { - cursor: default; - } + &:hover { + background-color: $primary-color-hover-drop; } + } - li { - cursor: pointer; - flex: 0 0 calc(100% / 4 - 4px); - display: flex; - align-items: center; - background: rgba(255, 255, 255, .4); - border-radius: 5px; - padding: 2px 8px 2px 2px; - margin: 2px; - white-space: nowrap; - overflow: hidden; + .state-container { + margin-left: 12px; + position: relative; - &:hover { - background: rgba(255, 255, 255, .6); - } + input[type="checkbox"] { + vertical-align: center; + display: inline; + position: absolute; + top: 1px; + width: 18px; + height: 18px; + margin: 0; + cursor: pointer; + } - input, label { - cursor: pointer; - } + label { + position: absolute; + left: 22px; + width: 260px; + display: inline; + font-weight: lighter; } } - } - .quest-splash { - flex: 0 0 calc(100% / 3); - - .splash-image { - width: 100%; - height: 200px; - background-size: cover; - background-position: center; - background-color: rgba(255, 255, 255, .4); + .drop-info { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + border: 2px dashed $primary-color-border-drop; border-radius: 5px; cursor: pointer; + } - &:hover { - background-color: rgba(255, 255, 255, .6); - } + .splash-border { + border: 2px dashed $primary-color-border-drop; + } + + .delete-splash { + position: relative; + top: 0; + left: calc(100% - 22px); + @include button; + } + + .change-splash-pos { + position: relative; + top: calc(100% - 44px); + left: calc(100% - 22px); + right: 0; + @include button; } } @@ -591,8 +614,23 @@ margin-top: 16px; overflow: hidden; + header { + @include header-buttons; + justify-content: space-between; + margin-top: 0; + padding-top: 0; + margin-bottom: 4px; + } + h2 { - flex: 0 0 1px; + align-self: flex-end; + border: none; + margin: 0; + padding-bottom: 0; + } + + button { + flex: 0 0 fit-content; } .subquests-box { @@ -613,65 +651,31 @@ transition: border-color .3s ease, box-shadow .3s ease; &:hover { - border-color: $primary-accent-color; - box-shadow: 0 0 2px $primary-accent-color inset; + border-color: $primary-color-accent; + box-shadow: 0 0 2px $primary-color-accent inset; } } h2 { flex: 1; border: none; - margin: 0 8px; + margin: 0 4px; + align-self: center; font-size: 14px; line-height: 30px; cursor: pointer; transition: color .3s ease; } - - .actions { - flex: 0 0 100px; - height: 100%; - } - } - - footer { - flex: 0 0 1px; - margin: 8px 0 0 0; } } - } .editor { - height: calc(100% - 26px); + height: calc(100% - 30px); } .gmnotes .editor { height: calc(100% - 36px); } - - .tox .tox-toolbar-overlord { - background-color: transparent; - border-bottom: 1px solid #222; - padding-bottom: 4px; - } - - .tox .tox-toolbar, - .tox .tox-toolbar__overflow, - .tox .tox-toolbar__primary { - background: transparent; - background-color: transparent; - } - - .tox.tox-tinymce .tox-tbtn { - padding: 0; - margin: 0 0 0 4px; - width: 32px; - } - - .tox.tox-tinymce .tox-tbtn[title="Formats"] { - width: 90px; - } - } } diff --git a/styles/quest-tracker.scss b/styles/quest-tracker.scss new file mode 100644 index 00000000..5e1b1bfc --- /dev/null +++ b/styles/quest-tracker.scss @@ -0,0 +1,246 @@ +#quest-tracker { + pointer-events: none; + min-width: 275px; + min-height: 72px; + max-width: 400px; + max-height: 750px; + + @keyframes fql-jiggle { + 0% { + transform: rotate(-0.25deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(0.5deg); + animation-timing-function: ease-out; + } + } + + .window-content { + // For Firefox. + scrollbar-width: thin; + } + + a.content-link { + background: $tracker-color-background-entitylink; + border: none; + color: $tracker-color-text-entitylink; + } + + &.fql-app { + background: $tracker-image-background; + background-color: $tracker-color-background; + background-blend-mode: $tracker-image-background-blend-mode; + box-shadow: 0 0 12px #000; + + // The no-background class is added by QuestTrackerShell.svelte removing the background and setting the scrollbar + // and window resizable transparent. + &.no-background { + background: none; + box-shadow: none; + + // For Firefox. + scrollbar-color: rgba(80, 80, 80, 0.7) rgba(60, 60, 60, 0.5); + + ::-webkit-scrollbar-thumb { + background: rgba(60, 60, 60, 0.5); + border: rgba(80, 80, 80, 0.7); + } + + .window-resizable-handle { + opacity: 0.4; + } + } + + // Doesn't work quite right as game systems can provide specific colors. + //a:hover { + // text-shadow: 0 0 8px $tracker-color-text-hover; + //} + } + + .window-content { + padding: 0 8px 0 8px; + color: $tracker-color-text; + font-family: "Open Sans", Lato, Signika, sans-serif; + overflow-y: auto; + } + + .window-header { + color: $tracker-color-text; + pointer-events: auto; + + h4 { + font-family: Signika, sans-serif; + } + } + + .window-resizable-handle { + pointer-events: auto; + } + + * { + box-sizing: border-box; + } + + #hidden { + color: $tracker-color-text-hidden; + } + + .quest:not(:last-child) { + margin-bottom: 16px; + } + + .quests { + flex: none; + padding: 8px 0 8px 0; + } + + .no-quests { + flex: none; + padding: 12px 0 8px 0; + + // Must specifically target elements to support context menu. + filter: drop-shadow(1px 1px 1px #000); + } + + .quest { + overflow-x: hidden; + overflow-y: hidden; + height: auto; + + display: flex; + flex-direction: column; + flex: 1; + + .title { + a, i { + // Must specifically target elements to support context menu. + filter: drop-shadow(1px 1px 1px #000); + } + } + + i { + cursor: pointer; + pointer-events: auto; + + // Add additional width to last icon such that there is space between any previous icon. + &:last-of-type { + flex: 0 0 26px; + text-align: right; + } + } + + .title { + display: flex; + flex-direction: row; + margin: 0; + font-size: 18px; + align-items: center; + } + + .quest-tracker-header { + cursor: pointer; + pointer-events: auto; + width: fit-content; + height:auto; + } + + .quest-tracker-span { + flex: 1; + } + + .quest-tracker-link { + pointer-events: auto; + } + + .tasks { + margin: 3px 0 0 0; + list-style: none; + padding-left: 4px; + + i { + // Add additional width to last icon such that there is space between any previous icon. + &:last-of-type { + flex: 0 0 21px; + text-align: right; + } + } + + .subquest { + // Must specifically target elements to support context menu. + filter: drop-shadow(1px 1px 1px #000); + } + + .quest-tracker-task { + cursor: pointer; + width: fit-content; + + // Must specifically target elements to support context menu. + filter: drop-shadow(1px 1px 1px #000); + + span { + cursor: pointer; + pointer-events: auto; + &.check-square, &.minus-square { + text-decoration: line-through; + } + } + } + + .subquest-separator { + margin-top: 3px; + margin-bottom: 4px; + width: 50px; + height: 1px; + background-color: rgba(255, 255, 255, .5); + } + + .task { + margin: 2px 0 0 0; + display: flex; + align-items: center; + + span { + cursor: pointer; + &.check-square, &.minus-square { + text-decoration: line-through; + } + } + + &::before { + content: "\f0c8"; + display: inline-block; + padding-right: 4px; + font-weight: 400; + font-family: "Font Awesome 5 Free"; + min-width: 14px; + width: fit-content; + pointer-events: auto; + align-self: flex-start; + } + + &.minus-square { + &::before { + content: "\f00d"; + display: inline-block; + font-weight: 900; + padding-left: 1px; + min-width: 13px; + width: fit-content; + } + } + + &.check-square { + &::before { + display: inline-block; + content: "\f00c"; + font-weight: 900; + min-width: 14px; + width: fit-content; + } + } + } + } + } +} \ No newline at end of file diff --git a/templates/partials/quest-form/task.html b/templates/partials/quest-form/task.html deleted file mode 100644 index b3c443ea..00000000 --- a/templates/partials/quest-form/task.html +++ /dev/null @@ -1,4 +0,0 @@ -
    - - -
    diff --git a/templates/partials/quest-log/tab.html b/templates/partials/quest-log/tab.html index a25eb723..e5357fde 100644 --- a/templates/partials/quest-log/tab.html +++ b/templates/partials/quest-log/tab.html @@ -1,45 +1,39 @@ -

    {{format 'ForienQuestLog.Quests' (localize (lookup questTypes tab))}}

    +
    +

    {{fql_format 'ForienQuestLog.QuestLog.Labels.TableHeader' (localize (lookup questStatusI18n tab))}}

    + {{#if (or isGM canCreate)}} + + {{/if}} +
      {{#each quests}} + {{#with enrich}}
    • - {{#if giver.img}}
      {{/if}} - {{#if (and personal ../isGM)}} - + {{#if (eq questIconType 'quest-giver')}} +
      {{/if}} -
      - -

      {{title}}

      + {{#if (eq questIconType 'splash-image')}} +
      + {{/if}} + {{#if isPrimary}}{{/if}} + {{#if canEdit}} + {{#if isHidden}}{{/if}} + {{#if isPersonal}}{{/if}} + {{/if}} +
      +

      {{name}}

      {{#if isSubquest}} -

      {{format 'ForienQuestLog.QuestPreview.SubTitle' parent.name}}

      +

      {{fql_format 'ForienQuestLog.QuestLog.Labels.SubTitle' data_parent.name}}

      {{/if}}
      - {{#unless (eq ../showTasks 'no')}} -
      {{checkedTasks}}{{#unless (eq ../showTasks 'onlyCurrent')}}/{{totalTasks}}{{/unless}}
      + {{#unless (eq ../../showTasks 'no')}} +
      {{checkedTasks}}{{#unless (eq ../../showTasks 'onlyCurrent')}}/{{totalTasks}}{{/unless}}
      {{/unless}} - {{#if (or ../isGM ../canAccept)}} -
      - {{#if (or (eq ../tab 'hidden') (eq ../tab 'available'))}} - - {{/if}} - {{#if ../isGM}} - {{#if (and (eq ../tab 'hidden') ../availableTab)}} - - {{/if}} - {{/if}} - {{#if ../isGM}} - {{#if (eq ../tab 'active')}} - - - {{/if}} - {{#unless (eq ../tab 'hidden')}} - - {{/unless}} - - {{/if}} -
      + {{#if statusActions.length}} + {{{statusActions}}} {{/if}}
    • + {{/with}} {{/each}}
    diff --git a/templates/partials/quest-preview/details.html b/templates/partials/quest-preview/details.html index e7e9d3d4..e4ba3706 100644 --- a/templates/partials/quest-preview/details.html +++ b/templates/partials/quest-preview/details.html @@ -1,24 +1,38 @@
    - {{#with giver}} -
    - {{#if (and ../canEdit ../image)}} - + {{#if (eq giver null)}} + {{#if (or canEdit playerEdit)}} + + {{#if canEdit}} + {{localize 'ForienQuestLog.QuestPreview.Labels.DragDropActor'}} + {{else}} + {{localize 'ForienQuestLog.QuestPreview.Labels.DragDropActorPlayer'}} + {{/if}} + + {{/if}} + {{else}} + {{#with giverData}} +
    + {{#if (and (or ../canEdit ../playerEdit) ../image img)}} + {{#if hasTokenImg}} + + {{/if}} + + {{/if}} + {{/with}} {{/if}} - - {{/with}}

    - {{title}} + {{name}}

    - {{#if canEdit}} -
    - + {{#if (or canEdit playerEdit)}} +
    +
    {{/if}}
    @@ -26,42 +40,38 @@

    - {{#with giver}} -

    {{name}}

    + {{#with giverData}} +
    +

    {{name}}

    +
    {{/with}} + {{#if (and canEdit (eq giver 'abstract'))}} +
    + +
    + {{/if}}
    -
    +

    - Quest is {{this.statusLabel}} + {{statusLabel}}

    {{#if isSubquest}} -

    {{format 'ForienQuestLog.QuestPreview.SubTitle' parent.name}}

    + {{/if}}
    {{#if splash.length}} -