diff --git a/.editorconfig b/.editorconfig index adaaa329eb..01c20e5bc6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,611 +14,13 @@ ij_visual_guides = none ij_wrap_on_typing = false [*.{kt,kts}] -ktlint_code_style = official +ktlint_code_style = ktlint_official twitter_compose_allowed_composition_locals = LocalTypographySettings,LocalDimens,LocalWindowSize,LocalFoldableHinge -[*.java] -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = true -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = true -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_annotation_parameter_wrap = off -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = off -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = off -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = off -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_at_first_column = true -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 99 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_while_brace_force = never -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = off -ij_java_extends_list_wrap = off -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = false -ij_java_for_brace_force = never -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = off -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = $android.**,$androidx.**,$com.**,$junit.**,$net.**,$org.**,$java.**,$javax.**,$*,|,android.**,|,androidx.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,| -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 2 -ij_java_keep_blank_lines_between_package_declaration_and_header = 2 -ij_java_keep_blank_lines_in_code = 2 -ij_java_keep_blank_lines_in_declarations = 2 -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = true -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_at_first_column = true -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = off -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = off -ij_java_modifier_list_wrap = false -ij_java_names_count_to_use_import_on_demand = 99 -ij_java_new_line_after_lparen_in_record_header = false -ij_java_parameter_annotation_wrap = off -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = off -ij_java_rparen_on_new_line_in_record_header = false -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = off -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = off -ij_java_throws_list_wrap = off -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = off -ij_java_visibility = public -ij_java_while_brace_force = never -ij_java_while_on_new_line = false -ij_java_wrap_comments = false -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.properties] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals -ij_properties_spaces_around_key_value_delimiter = false - -[.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 - [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.opml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,rss_kuketz,rss_morningpaper}] indent_size = 2 tab_width = 2 -ij_continuation_indent_size = 2 -ij_xml_align_attributes = false -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = false -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = true -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = true [{*.bash,*.sh,*.zsh}] indent_size = 2 tab_width = 2 -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false - -[{*.gant,*.gradle,*.groovy,*.gy}] -indent_size = 2 -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true -ij_groovy_align_multiline_list_or_map = true -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = true -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 5 -ij_groovy_do_while_brace_force = never -ij_groovy_else_on_new_line = false -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_if_brace_force = never -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 3 -ij_groovy_parameter_annotation_wrap = off -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = true -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never -ij_groovy_while_on_new_line = false -ij_groovy_wrap_long_lines = false - -[{*.gradle.kts,*.kt,*.kts,*.main.kts}] -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = true -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = true -ij_kotlin_call_parameters_right_paren_on_new_line = true -ij_kotlin_call_parameters_wrap = on_every_item -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL -ij_kotlin_continuation_indent_for_chained_calls = false -ij_kotlin_continuation_indent_for_expression_bodies = false -ij_kotlin_continuation_indent_in_argument_lists = false -ij_kotlin_continuation_indent_in_elvis = false -ij_kotlin_continuation_indent_in_if_conditions = false -ij_kotlin_continuation_indent_in_parameter_lists = false -ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = split_into_lines -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = true -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.*,javax.*,kotlin.* -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 2 -ij_kotlin_keep_blank_lines_in_code = 2 -ij_kotlin_keep_blank_lines_in_declarations = 2 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = true -ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 99 -ij_kotlin_name_count_to_use_star_import_for_members = 99 -ij_kotlin_packages_to_use_import_on_demand = kotlinx.android.synthetic.* -ij_kotlin_parameter_annotation_wrap = off -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_use_custom_formatting_for_modifiers = true -ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.har,*.json}] -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,*.sht,*.shtm,*.shtml}] -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 - -[{*.yaml,*.yml}] -indent_size = 2 -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..a5a4eb22fc --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000..79ee123c2b --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5dc3a1d0f..131046cf07 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -273,12 +273,13 @@ tasks { doLast { val langs = getListOfSupportedLocales() - val localesConfig = """ + val localesConfig = + """ -${langs.joinToString("\n") { " " }} + ${langs.joinToString(" ") { "" }} - """.trimIndent() + """.trimIndent() localesConfigFile.bufferedWriter().use { writer -> writer.write(localesConfig) @@ -290,9 +291,8 @@ ${langs.joinToString("\n") { " " class RoomSchemaArgProvider( @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - val schemaDir: File + val schemaDir: File, ) : CommandLineArgumentProvider { - override fun asArguments(): Iterable { // Note: If you're using KSP, change the line below to return return listOf("room.schemaLocation=${schemaDir.path}") diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrityTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrityTest.kt index fe0ee89b4f..b36d71b457 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrityTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrityTest.kt @@ -1,7 +1,7 @@ package com.nononsenseapps.feeder.crypto -import kotlin.test.assertEquals import org.junit.Test +import kotlin.test.assertEquals class AesCbcWithIntegrityTest { @Test diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/legacy/LegacyDatabaseHandler.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/legacy/LegacyDatabaseHandler.kt index 896c41e712..4deb124b83 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/legacy/LegacyDatabaseHandler.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/legacy/LegacyDatabaseHandler.kt @@ -11,13 +11,16 @@ const val LEGACY_DATABASE_NAME = DATABASE_NAME class LegacyDatabaseHandler constructor( context: Context, name: String = LEGACY_DATABASE_NAME, - version: Int = LEGACY_DATABASE_VERSION + version: Int = LEGACY_DATABASE_VERSION, ) : SQLiteOpenHelper(context, name, null, version) { - override fun onCreate(db: SQLiteDatabase) { } - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) { } override fun onOpen(db: SQLiteDatabase) { @@ -46,6 +49,7 @@ const val FEED_ITEM_TABLE_NAME = "FeedItem" // Naming the id column with an underscore is good to be consistent // with other Android things. This is ALWAYS needed const val COL_ID = "_id" + // These fields can be anything you want. const val COL_TITLE = "title" const val COL_CUSTOM_TITLE = "customtitle" @@ -63,6 +67,7 @@ const val COL_AUTHOR = "author" const val COL_PUBDATE = "pubdate" const val COL_UNREAD = "unread" const val COL_NOTIFIED = "notified" + // These fields corresponds to columns in Feed table const val COL_FEED = "feed" const val COL_FEEDTITLE = "feedtitle" diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom10To11.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom10To11.kt index 58b8ce13a7..8fa85b1ded 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom10To11.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom10To11.kt @@ -17,12 +17,13 @@ class MigrationFrom10To11 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate10to11() { @@ -31,15 +32,15 @@ class MigrationFrom10To11 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom11To12.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom11To12.kt index e1f2438444..c07f6ee6bb 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom11To12.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom11To12.kt @@ -17,12 +17,13 @@ class MigrationFrom11To12 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate11to12() { @@ -31,15 +32,15 @@ class MigrationFrom11To12 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom12To13.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom12To13.kt index bb147ed8b9..98f1a5f021 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom12To13.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom12To13.kt @@ -17,12 +17,13 @@ class MigrationFrom12To13 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate12to13() { @@ -31,15 +32,15 @@ class MigrationFrom12To13 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom13To14.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom13To14.kt index 626e3c385d..9335fd5778 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom13To14.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom13To14.kt @@ -17,12 +17,13 @@ class MigrationFrom13To14 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate13to14() { @@ -31,15 +32,15 @@ class MigrationFrom13To14 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom14To15.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom14To15.kt index a9891148b9..ae2d290ce3 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom14To15.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom14To15.kt @@ -17,12 +17,13 @@ class MigrationFrom14To15 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate14to15() { @@ -31,15 +32,15 @@ class MigrationFrom14To15 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '') + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '') """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom15To16.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom15To16.kt index c6158872b3..6a631553f2 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom15To16.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom15To16.kt @@ -17,12 +17,13 @@ class MigrationFrom15To16 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate15to16() { @@ -31,15 +32,15 @@ class MigrationFrom15To16 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with, alternate_id) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '', 0) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash, fulltext_by_default, open_articles_with, alternate_id) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666, 0, '', 0) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom16To17.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom16To17.kt index ab52cb4bdb..2ffe086712 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom16To17.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom16To17.kt @@ -16,12 +16,13 @@ class MigrationFrom16To17 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate15to16() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom17To18.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom17To18.kt index 58083eeb50..0ca4b4a853 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom17To18.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom17To18.kt @@ -16,12 +16,13 @@ class MigrationFrom17To18 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate15to16() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom18To19.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom18To19.kt index 748dd3d2d5..f5557ceb66 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom18To19.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom18To19.kt @@ -16,12 +16,13 @@ class MigrationFrom18To19 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate15to16() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom19To20.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom19To20.kt index ef1dbd6968..6e645262f1 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom19To20.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom19To20.kt @@ -17,12 +17,13 @@ class MigrationFrom19To20 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom20To21.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom20To21.kt index 371f88b633..2640b6db67 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom20To21.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom20To21.kt @@ -5,11 +5,11 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @LargeTest @@ -18,12 +18,13 @@ class MigrationFrom20To21 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom21To22.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom21To22.kt index f6f0cc9784..073cf5be24 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom21To22.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom21To22.kt @@ -5,10 +5,10 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -17,12 +17,13 @@ class MigrationFrom21To22 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -35,8 +36,8 @@ class MigrationFrom21To22 { ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom22To23.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom22To23.kt index 85081c538b..39e7998a7f 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom22To23.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom22To23.kt @@ -5,10 +5,10 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -17,12 +17,13 @@ class MigrationFrom22To23 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -35,8 +36,8 @@ class MigrationFrom22To23 { ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom7To8.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom7To8.kt index 7fb7b7864b..732a67c4b1 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom7To8.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom7To8.kt @@ -17,12 +17,13 @@ class MigrationFrom7To8 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate7to8() { @@ -31,8 +32,8 @@ class MigrationFrom7To8 { db.use { db.execSQL( """ - INSERT INTO feeds(title, url, custom_title, tag, notify) - VALUES('feed', 'http://url', '', '', 0) + INSERT INTO feeds(title, url, custom_title, tag, notify) + VALUES('feed', 'http://url', '', '', 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom8To9.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom8To9.kt index fa9393acda..b655e4f78e 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom8To9.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom8To9.kt @@ -17,12 +17,13 @@ class MigrationFrom8To9 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate8to9() { @@ -31,8 +32,8 @@ class MigrationFrom8To9 { db.use { db.execSQL( """ - INSERT INTO feeds(title, url, custom_title, tag, notify, last_sync) - VALUES('feed', 'http://url', '', '', 0, 0) + INSERT INTO feeds(title, url, custom_title, tag, notify, last_sync) + VALUES('feed', 'http://url', '', '', 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom9To10.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom9To10.kt index 778e3d82c8..3a923d2d3c 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom9To10.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFrom9To10.kt @@ -20,12 +20,13 @@ class MigrationFrom9To10 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate9to10() { @@ -34,15 +35,15 @@ class MigrationFrom9To10 { db.use { db.execSQL( """ - INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) - VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) + INSERT INTO feeds(id, title, url, custom_title, tag, notify, last_sync, response_hash) + VALUES(1, 'feed', 'http://url', '', '', 0, 0, 666) """.trimIndent(), ) db.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, description) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, '$bigBody') + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, description) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, '$bigBody') """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy5ToLatest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy5ToLatest.kt index dfac2d9a44..d0ec33ea03 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy5ToLatest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy5ToLatest.kt @@ -38,10 +38,6 @@ import com.nononsenseapps.feeder.util.contentValues import com.nononsenseapps.feeder.util.setInt import com.nononsenseapps.feeder.util.setLong import com.nononsenseapps.feeder.util.setString -import java.net.URL -import java.time.Instant -import java.time.ZoneOffset -import java.time.ZonedDateTime import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals @@ -54,31 +50,36 @@ import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.android.closestDI +import java.net.URL +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime @RunWith(AndroidJUnit4::class) @LargeTest class MigrationFromLegacy5ToLatest { - private val feederApplication: FeederApplication = getApplicationContext() private val di: DI by closestDI(feederApplication) @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) private val testDbName = "TestingDatabase" private val legacyDb: LegacyDatabaseHandler - get() = LegacyDatabaseHandler( - context = feederApplication, - name = testDbName, - version = 5, - ) + get() = + LegacyDatabaseHandler( + context = feederApplication, + name = testDbName, + version = 5, + ) private val roomDb: AppDatabase get() = @@ -121,29 +122,31 @@ class MigrationFromLegacy5ToLatest { db.execSQL(CREATE_TAGS_VIEW) // Bare minimum non-null feeds - val idA = db.insert( - FEED_TABLE_NAME, - null, - contentValues { - setString(COL_TITLE to "feedA") - setString(COL_CUSTOM_TITLE to "feedACustom") - setString(COL_URL to "https://feedA") - setString(COL_TAG to "") - }, - ) + val idA = + db.insert( + FEED_TABLE_NAME, + null, + contentValues { + setString(COL_TITLE to "feedA") + setString(COL_CUSTOM_TITLE to "feedACustom") + setString(COL_URL to "https://feedA") + setString(COL_TAG to "") + }, + ) // All fields filled - val idB = db.insert( - FEED_TABLE_NAME, - null, - contentValues { - setString(COL_TITLE to "feedB") - setString(COL_CUSTOM_TITLE to "feedBCustom") - setString(COL_URL to "https://feedB") - setString(COL_TAG to "tag") - setInt(COL_NOTIFY to 1) - }, - ) + val idB = + db.insert( + FEED_TABLE_NAME, + null, + contentValues { + setString(COL_TITLE to "feedB") + setString(COL_CUSTOM_TITLE to "feedBCustom") + setString(COL_URL to "https://feedB") + setString(COL_TAG to "tag") + setInt(COL_NOTIFY to 1) + }, + ) IntRange(0, 1).forEach { index -> db.insert( @@ -194,128 +197,132 @@ class MigrationFromLegacy5ToLatest { } @Test - fun legacyMigrationTo7MinimalFeed() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_5_7, - MIGRATION_7_8, - ) + fun legacyMigrationTo7MinimalFeed() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_5_7, + MIGRATION_7_8, + ) - roomDb.let { db -> - val feeds = db.feedDao().loadFeeds() + roomDb.let { db -> + val feeds = db.feedDao().loadFeeds() - assertEquals("Wrong number of feeds", 2, feeds.size) + assertEquals("Wrong number of feeds", 2, feeds.size) - val feedA = feeds[0] + val feedA = feeds[0] - assertEquals("feedA", feedA.title) - assertEquals("feedACustom", feedA.customTitle) - assertEquals(URL("https://feedA"), feedA.url) - assertEquals("", feedA.tag) - assertEquals(Instant.EPOCH, feedA.lastSync) - assertFalse(feedA.notify) - assertNull(feedA.imageUrl) + assertEquals("feedA", feedA.title) + assertEquals("feedACustom", feedA.customTitle) + assertEquals(URL("https://feedA"), feedA.url) + assertEquals("", feedA.tag) + assertEquals(Instant.EPOCH, feedA.lastSync) + assertFalse(feedA.notify) + assertNull(feedA.imageUrl) + } } - } @Test - fun legacyMigrationTo7CompleteFeed() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_5_7, - MIGRATION_7_8, - ) + fun legacyMigrationTo7CompleteFeed() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_5_7, + MIGRATION_7_8, + ) - roomDb.let { db -> - val feeds = db.feedDao().loadFeeds() + roomDb.let { db -> + val feeds = db.feedDao().loadFeeds() - assertEquals("Wrong number of feeds", 2, feeds.size) + assertEquals("Wrong number of feeds", 2, feeds.size) - val feedB = feeds[1] + val feedB = feeds[1] - assertEquals("feedB", feedB.title) - assertEquals("feedBCustom", feedB.customTitle) - assertEquals(URL("https://feedB"), feedB.url) - assertEquals("tag", feedB.tag) - assertEquals(Instant.EPOCH, feedB.lastSync) - assertTrue(feedB.notify) - assertNull(feedB.imageUrl) + assertEquals("feedB", feedB.title) + assertEquals("feedBCustom", feedB.customTitle) + assertEquals(URL("https://feedB"), feedB.url) + assertEquals("tag", feedB.tag) + assertEquals(Instant.EPOCH, feedB.lastSync) + assertTrue(feedB.notify) + assertNull(feedB.imageUrl) + } } - } @Test - fun legacyMigrationTo7MinimalFeedItem() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_5_7, - MIGRATION_7_8, - ) + fun legacyMigrationTo7MinimalFeedItem() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_5_7, + MIGRATION_7_8, + ) - roomDb.let { db -> - val feed = db.feedDao().loadFeeds()[0] - assertEquals("feedA", feed.title) - @Suppress("DEPRECATION") - val items = - db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) + roomDb.let { db -> + val feed = db.feedDao().loadFeeds()[0] + assertEquals("feedA", feed.title) + @Suppress("DEPRECATION") + val items = + db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) - assertEquals(2, items.size) + assertEquals(2, items.size) - items.forEachIndexed { index, it -> - assertEquals(feed.id, it.feedId) - assertEquals("guid$index", it.guid) - assertEquals("plain$index", it.plainTitle) - assertEquals("plain$index", it.plainTitle) - assertEquals("snippet$index", it.plainSnippet) - assertTrue(it.unread) - assertNull(it.author) - assertNull(it.enclosureLink) - assertNull(it.imageUrl) - assertNull(it.pubDate) - assertNull(it.link) - assertFalse(it.notified) + items.forEachIndexed { index, it -> + assertEquals(feed.id, it.feedId) + assertEquals("guid$index", it.guid) + assertEquals("plain$index", it.plainTitle) + assertEquals("plain$index", it.plainTitle) + assertEquals("snippet$index", it.plainSnippet) + assertTrue(it.unread) + assertNull(it.author) + assertNull(it.enclosureLink) + assertNull(it.imageUrl) + assertNull(it.pubDate) + assertNull(it.link) + assertFalse(it.notified) + } } } - } @Test - fun legacyMigrationTo7CompleteFeedItem() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_5_7, - MIGRATION_7_8, - ) + fun legacyMigrationTo7CompleteFeedItem() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_5_7, + MIGRATION_7_8, + ) - roomDb.let { db -> - val feed = db.feedDao().loadFeeds()[1] - assertEquals("feedB", feed.title) - @Suppress("DEPRECATION") - val items = - db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) + roomDb.let { db -> + val feed = db.feedDao().loadFeeds()[1] + assertEquals("feedB", feed.title) + @Suppress("DEPRECATION") + val items = + db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) - assertEquals(2, items.size) + assertEquals(2, items.size) - items.forEachIndexed { index, it -> - assertEquals(feed.id, it.feedId) - assertEquals("guid$index", it.guid) - assertEquals("plain$index", it.plainTitle) - assertEquals("plain$index", it.plainTitle) - assertEquals("snippet$index", it.plainSnippet) - assertFalse(it.unread) - assertEquals("author$index", it.author) - assertEquals("https://enclosure$index", it.enclosureLink) - assertEquals("https://image$index", it.imageUrl) - assertEquals(ZonedDateTime.of(2018, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC), it.pubDate) - assertEquals("https://link$index", it.link) - assertTrue(it.notified) + items.forEachIndexed { index, it -> + assertEquals(feed.id, it.feedId) + assertEquals("guid$index", it.guid) + assertEquals("plain$index", it.plainTitle) + assertEquals("plain$index", it.plainTitle) + assertEquals("snippet$index", it.plainSnippet) + assertFalse(it.unread) + assertEquals("author$index", it.author) + assertEquals("https://enclosure$index", it.enclosureLink) + assertEquals("https://image$index", it.imageUrl) + assertEquals(ZonedDateTime.of(2018, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC), it.pubDate) + assertEquals("https://link$index", it.link) + assertTrue(it.notified) + } } } - } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy6ToLatest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy6ToLatest.kt index 72105c2197..e1bdb9ee95 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy6ToLatest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/MigrationFromLegacy6ToLatest.kt @@ -37,10 +37,6 @@ import com.nononsenseapps.feeder.util.contentValues import com.nononsenseapps.feeder.util.setInt import com.nononsenseapps.feeder.util.setLong import com.nononsenseapps.feeder.util.setString -import java.net.URL -import java.time.Instant -import java.time.ZoneOffset -import java.time.ZonedDateTime import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert.assertEquals @@ -53,31 +49,36 @@ import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.android.closestDI +import java.net.URL +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime @RunWith(AndroidJUnit4::class) @LargeTest class MigrationFromLegacy6ToLatest { - private val feederApplication: FeederApplication = getApplicationContext() private val di: DI by closestDI(feederApplication) @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) private val testDbName = "TestingDatabase" private val legacyDb: LegacyDatabaseHandler - get() = LegacyDatabaseHandler( - context = feederApplication, - name = testDbName, - version = 6, - ) + get() = + LegacyDatabaseHandler( + context = feederApplication, + name = testDbName, + version = 6, + ) private val roomDb: AppDatabase get() = @@ -97,30 +98,32 @@ class MigrationFromLegacy6ToLatest { createViewsAndTriggers(db) // Bare minimum non-null feeds - val idA = db.insert( - FEED_TABLE_NAME, - null, - contentValues { - setString(COL_TITLE to "feedA") - setString(COL_CUSTOM_TITLE to "feedACustom") - setString(COL_URL to "https://feedA") - setString(COL_TAG to "") - }, - ) + val idA = + db.insert( + FEED_TABLE_NAME, + null, + contentValues { + setString(COL_TITLE to "feedA") + setString(COL_CUSTOM_TITLE to "feedACustom") + setString(COL_URL to "https://feedA") + setString(COL_TAG to "") + }, + ) // All fields filled - val idB = db.insert( - FEED_TABLE_NAME, - null, - contentValues { - setString(COL_TITLE to "feedB") - setString(COL_CUSTOM_TITLE to "feedBCustom") - setString(COL_URL to "https://feedB") - setString(COL_TAG to "tag") - setString(COL_IMAGEURL to "https://image") - setInt(COL_NOTIFY to 1) - }, - ) + val idB = + db.insert( + FEED_TABLE_NAME, + null, + contentValues { + setString(COL_TITLE to "feedB") + setString(COL_CUSTOM_TITLE to "feedBCustom") + setString(COL_URL to "https://feedB") + setString(COL_TAG to "tag") + setString(COL_IMAGEURL to "https://image") + setInt(COL_NOTIFY to 1) + }, + ) IntRange(0, 1).forEach { index -> db.insert( @@ -171,122 +174,126 @@ class MigrationFromLegacy6ToLatest { } @Test - fun legacyMigrationTo7MinimalFeed() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_6_7, - ) + fun legacyMigrationTo7MinimalFeed() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_6_7, + ) - roomDb.let { db -> - val feeds = db.feedDao().loadFeeds() + roomDb.let { db -> + val feeds = db.feedDao().loadFeeds() - assertEquals("Wrong number of feeds", 2, feeds.size) + assertEquals("Wrong number of feeds", 2, feeds.size) - val feedA = feeds[0] + val feedA = feeds[0] - assertEquals("feedA", feedA.title) - assertEquals("feedACustom", feedA.customTitle) - assertEquals(URL("https://feedA"), feedA.url) - assertEquals("", feedA.tag) - assertEquals(Instant.EPOCH, feedA.lastSync) - assertFalse(feedA.notify) - assertNull(feedA.imageUrl) + assertEquals("feedA", feedA.title) + assertEquals("feedACustom", feedA.customTitle) + assertEquals(URL("https://feedA"), feedA.url) + assertEquals("", feedA.tag) + assertEquals(Instant.EPOCH, feedA.lastSync) + assertFalse(feedA.notify) + assertNull(feedA.imageUrl) + } } - } @Test - fun legacyMigrationTo7CompleteFeed() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_6_7, - ) + fun legacyMigrationTo7CompleteFeed() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_6_7, + ) - roomDb.let { db -> - val feeds = db.feedDao().loadFeeds() + roomDb.let { db -> + val feeds = db.feedDao().loadFeeds() - assertEquals("Wrong number of feeds", 2, feeds.size) + assertEquals("Wrong number of feeds", 2, feeds.size) - val feedB = feeds[1] + val feedB = feeds[1] - assertEquals("feedB", feedB.title) - assertEquals("feedBCustom", feedB.customTitle) - assertEquals(URL("https://feedB"), feedB.url) - assertEquals("tag", feedB.tag) - assertEquals(Instant.EPOCH, feedB.lastSync) - assertTrue(feedB.notify) - assertEquals(URL("https://image"), feedB.imageUrl) + assertEquals("feedB", feedB.title) + assertEquals("feedBCustom", feedB.customTitle) + assertEquals(URL("https://feedB"), feedB.url) + assertEquals("tag", feedB.tag) + assertEquals(Instant.EPOCH, feedB.lastSync) + assertTrue(feedB.notify) + assertEquals(URL("https://image"), feedB.imageUrl) + } } - } @Test - fun legacyMigrationTo7MinimalFeedItem() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_6_7, - ) + fun legacyMigrationTo7MinimalFeedItem() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_6_7, + ) - roomDb.let { db -> - val feed = db.feedDao().loadFeeds()[0] - assertEquals("feedA", feed.title) - @Suppress("DEPRECATION") - val items = db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) + roomDb.let { db -> + val feed = db.feedDao().loadFeeds()[0] + assertEquals("feedA", feed.title) + @Suppress("DEPRECATION") + val items = db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) - assertEquals(2, items.size) + assertEquals(2, items.size) - items.forEachIndexed { index, it -> - assertEquals(feed.id, it.feedId) - assertEquals("guid$index", it.guid) - assertEquals("plain$index", it.plainTitle) - assertEquals("plain$index", it.plainTitle) - assertEquals("snippet$index", it.plainSnippet) - assertTrue(it.unread) - assertNull(it.author) - assertNull(it.enclosureLink) - assertNull(it.imageUrl) - assertNull(it.pubDate) - assertNull(it.link) - assertFalse(it.notified) + items.forEachIndexed { index, it -> + assertEquals(feed.id, it.feedId) + assertEquals("guid$index", it.guid) + assertEquals("plain$index", it.plainTitle) + assertEquals("plain$index", it.plainTitle) + assertEquals("snippet$index", it.plainSnippet) + assertTrue(it.unread) + assertNull(it.author) + assertNull(it.enclosureLink) + assertNull(it.imageUrl) + assertNull(it.pubDate) + assertNull(it.link) + assertFalse(it.notified) + } } } - } @Test - fun legacyMigrationTo7CompleteFeedItem() = runBlocking { - testHelper.runMigrationsAndValidate( - testDbName, - 7, - true, - MIGRATION_6_7, - ) + fun legacyMigrationTo7CompleteFeedItem() = + runBlocking { + testHelper.runMigrationsAndValidate( + testDbName, + 7, + true, + MIGRATION_6_7, + ) - roomDb.let { db -> - val feed = db.feedDao().loadFeeds()[1] - assertEquals("feedB", feed.title) - @Suppress("DEPRECATION") - val items = db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) + roomDb.let { db -> + val feed = db.feedDao().loadFeeds()[1] + assertEquals("feedB", feed.title) + @Suppress("DEPRECATION") + val items = db.feedItemDao().loadFeedItemsInFeedDesc(feedId = feed.id) - assertEquals(2, items.size) + assertEquals(2, items.size) - items.forEachIndexed { index, it -> - assertEquals(feed.id, it.feedId) - assertEquals("guid$index", it.guid) - assertEquals("plain$index", it.plainTitle) - assertEquals("plain$index", it.plainTitle) - assertEquals("snippet$index", it.plainSnippet) - assertFalse(it.unread) - assertEquals("author$index", it.author) - assertEquals("https://enclosure$index", it.enclosureLink) - assertEquals("https://image$index", it.imageUrl) - assertEquals(ZonedDateTime.of(2018, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC), it.pubDate) - assertEquals("https://link$index", it.link) - assertTrue(it.notified) + items.forEachIndexed { index, it -> + assertEquals(feed.id, it.feedId) + assertEquals("guid$index", it.guid) + assertEquals("plain$index", it.plainTitle) + assertEquals("plain$index", it.plainTitle) + assertEquals("snippet$index", it.plainSnippet) + assertFalse(it.unread) + assertEquals("author$index", it.author) + assertEquals("https://enclosure$index", it.enclosureLink) + assertEquals("https://image$index", it.imageUrl) + assertEquals(ZonedDateTime.of(2018, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC), it.pubDate) + assertEquals("https://link$index", it.link) + assertTrue(it.notified) + } } } - } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom23To24.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom23To24.kt index 60b59f8b70..9afad8acfb 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom23To24.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom23To24.kt @@ -8,13 +8,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.android.closestDI import org.kodein.di.instance +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @LargeTest @@ -26,12 +26,13 @@ class TestMigrationFrom23To24 { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -77,8 +78,9 @@ class TestMigrationFrom23To24 { } } - val blocks = sharedPrefs.getStringSet("pref_block_list_values", null) - ?: emptySet() + val blocks = + sharedPrefs.getStringSet("pref_block_list_values", null) + ?: emptySet() assertTrue { blocks.isEmpty() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom24To25.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom24To25.kt index 91a0c7fa5b..397f78289a 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom24To25.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom24To25.kt @@ -7,15 +7,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @LargeTest @@ -26,12 +26,13 @@ class TestMigrationFrom24To25 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -44,8 +45,8 @@ class TestMigrationFrom24To25 : DIAware { ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom25To26.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom25To26.kt index 527c6eb543..5bf7764dbf 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom25To26.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom25To26.kt @@ -7,13 +7,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -24,12 +24,13 @@ class TestMigrationFrom25To26 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -42,8 +43,8 @@ class TestMigrationFrom25To26 : DIAware { ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) - VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) + VALUES(8, 'http://item', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom26To27.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom26To27.kt index f152c07207..c22b1ada48 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom26To27.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom26To27.kt @@ -7,14 +7,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @LargeTest @@ -25,12 +25,13 @@ class TestMigrationFrom26To27 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -43,14 +44,14 @@ class TestMigrationFrom26To27 : DIAware { ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) - VALUES(8, 'http://item1', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) + VALUES(8, 'http://item1', 'title', 'ptitle', 'psnippet', 1, 0, 1, 0, 0, 1, 0, 0) """.trimIndent(), ) oldDB.execSQL( """ - INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) - VALUES(9, 'http://item2', 'title', 'ptitle', 'psnippet', 0, 0, 1, 0, 0, 1, 0, 0) + INSERT INTO feed_items(id, guid, title, plain_title, plain_snippet, unread, notified, feed_id, first_synced_time, primary_sort_time, pinned, bookmarked, fulltext_downloaded) + VALUES(9, 'http://item2', 'title', 'ptitle', 'psnippet', 0, 0, 1, 0, 0, 1, 0, 0) """.trimIndent(), ) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom27To28.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom27To28.kt index 21f4d281ee..741f502ed7 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom27To28.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom27To28.kt @@ -7,13 +7,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -24,12 +24,13 @@ class TestMigrationFrom27To28 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -42,12 +43,13 @@ class TestMigrationFrom27To28 : DIAware { """.trimIndent(), ) } - val db = testHelper.runMigrationsAndValidate( - dbName, - TO_VERSION, - true, - MigrationFrom27To28(di), - ) + val db = + testHelper.runMigrationsAndValidate( + dbName, + TO_VERSION, + true, + MigrationFrom27To28(di), + ) db.query( """ diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom28To29.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom28To29.kt index 900e545757..48ae6f11e8 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom28To29.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom28To29.kt @@ -8,13 +8,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertNull import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertNull @RunWith(AndroidJUnit4::class) @LargeTest @@ -25,12 +25,13 @@ class TestMigrationFrom28To29 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -49,12 +50,13 @@ class TestMigrationFrom28To29 : DIAware { """.trimIndent(), ) } - val db = testHelper.runMigrationsAndValidate( - dbName, - TO_VERSION, - true, - MigrationFrom28To29(di), - ) + val db = + testHelper.runMigrationsAndValidate( + dbName, + TO_VERSION, + true, + MigrationFrom28To29(di), + ) db.query( """ diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom29To30.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom29To30.kt index 46c7581200..169103bdb3 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom29To30.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom29To30.kt @@ -7,13 +7,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -24,12 +24,13 @@ class TestMigrationFrom29To30 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -48,12 +49,13 @@ class TestMigrationFrom29To30 : DIAware { """.trimIndent(), ) } - val db = testHelper.runMigrationsAndValidate( - dbName, - TO_VERSION, - true, - MigrationFrom29To30(di), - ) + val db = + testHelper.runMigrationsAndValidate( + dbName, + TO_VERSION, + true, + MigrationFrom29To30(di), + ) db.query( """ diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom30To31.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom30To31.kt index ff8b5584a6..7e22c8a9e1 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom30To31.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/db/room/TestMigrationFrom30To31.kt @@ -7,13 +7,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import com.nononsenseapps.feeder.FeederApplication -import kotlin.test.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @LargeTest @@ -24,12 +24,13 @@ class TestMigrationFrom30To31 : DIAware { @Rule @JvmField - val testHelper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java, - emptyList(), - FrameworkSQLiteOpenHelperFactory(), - ) + val testHelper: MigrationTestHelper = + MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) @Test fun migrate() { @@ -48,12 +49,13 @@ class TestMigrationFrom30To31 : DIAware { """.trimIndent(), ) } - val db = testHelper.runMigrationsAndValidate( - dbName, - TO_VERSION, - true, - MigrationFrom30To31(di), - ) + val db = + testHelper.runMigrationsAndValidate( + dbName, + TO_VERSION, + true, + MigrationFrom30To31(di), + ) db.query( """ diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt index 2c543d1ba0..869ba09f76 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/Feeds.kt @@ -1,24 +1,25 @@ package com.nononsenseapps.feeder.model -import java.io.InputStream import org.intellij.lang.annotations.Language +import java.io.InputStream class Feeds { - companion object { val nixosRss: InputStream get() = Companion::class.java.getResourceAsStream("rss_nixos.xml")!! val cowboyJson: String - get() = String( - Companion::class.java.getResourceAsStream("cowboyprogrammer_feed.json")!! - .use { it.readBytes() }, - ) + get() = + String( + Companion::class.java.getResourceAsStream("cowboyprogrammer_feed.json")!! + .use { it.readBytes() }, + ) val cowboyAtom: String - get() = String( - Companion::class.java.getResourceAsStream("cowboyprogrammer_atom.xml")!!.use { it.readBytes() }, - ) + get() = + String( + Companion::class.java.getResourceAsStream("cowboyprogrammer_atom.xml")!!.use { it.readBytes() }, + ) /** * Reported in https://gitlab.com/spacecowboy/Feeder/-/issues/410 diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/FeedsToSyncTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/FeedsToSyncTest.kt index 0091649f49..1728335f03 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/FeedsToSyncTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/FeedsToSyncTest.kt @@ -11,8 +11,6 @@ import com.nononsenseapps.feeder.db.room.FeedDao import com.nononsenseapps.feeder.db.room.ID_UNSET import com.nononsenseapps.feeder.ui.TestDatabaseRule import com.nononsenseapps.feeder.util.minusMinutes -import java.net.URL -import java.time.Instant import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Rule @@ -24,6 +22,8 @@ import org.kodein.di.android.subDI import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.net.URL +import java.time.Instant @RunWith(AndroidJUnit4::class) class FeedsToSyncTest : DIAware { @@ -41,101 +41,115 @@ class FeedsToSyncTest : DIAware { private val rssLocalSync: RssLocalSync by instance() @Test - fun returnsStaleFeed() = runBlocking { - // with stale feed - val feed = withFeed() + fun returnsStaleFeed() = + runBlocking { + // with stale feed + val feed = withFeed() - // when - val result = rssLocalSync.feedsToSync(feedId = feed.id, tag = "") + // when + val result = rssLocalSync.feedsToSync(feedId = feed.id, tag = "") - // then - assertEquals(listOf(feed), result) - } + // then + assertEquals(listOf(feed), result) + } @Test - fun doesNotReturnFreshFeed() = runBlocking { - val now = Instant.now() - val feed = withFeed(lastSync = now.minusMinutes(1)) - - // when - val result = rssLocalSync.feedsToSync( - feedId = feed.id, - tag = "", - staleTime = now.minusMinutes(2).toEpochMilli(), - ) - - // then - assertEquals(emptyList(), result) - } + fun doesNotReturnFreshFeed() = + runBlocking { + val now = Instant.now() + val feed = withFeed(lastSync = now.minusMinutes(1)) + + // when + val result = + rssLocalSync.feedsToSync( + feedId = feed.id, + tag = "", + staleTime = now.minusMinutes(2).toEpochMilli(), + ) + + // then + assertEquals(emptyList(), result) + } @Test - fun returnsAllStaleFeeds() = runBlocking { - val items = listOf( - withFeed(url = URL("http://one")), - withFeed(url = URL("http://two")), - ) + fun returnsAllStaleFeeds() = + runBlocking { + val items = + listOf( + withFeed(url = URL("http://one")), + withFeed(url = URL("http://two")), + ) - val result = rssLocalSync.feedsToSync(feedId = ID_UNSET, tag = "") + val result = rssLocalSync.feedsToSync(feedId = ID_UNSET, tag = "") - assertEquals(items, result) - } + assertEquals(items, result) + } @Test - fun doesNotReturnAllFreshFeeds() = runBlocking { - val now = Instant.now() - val items = listOf( - withFeed(url = URL("http://one"), lastSync = now.minusMinutes(1)), - withFeed(url = URL("http://two"), lastSync = now.minusMinutes(3)), - ) - - val result = rssLocalSync.feedsToSync( - feedId = ID_UNSET, - tag = "", - staleTime = now.minusMinutes(2).toEpochMilli(), - ) - - assertEquals(listOf(items[1]), result) - } + fun doesNotReturnAllFreshFeeds() = + runBlocking { + val now = Instant.now() + val items = + listOf( + withFeed(url = URL("http://one"), lastSync = now.minusMinutes(1)), + withFeed(url = URL("http://two"), lastSync = now.minusMinutes(3)), + ) + + val result = + rssLocalSync.feedsToSync( + feedId = ID_UNSET, + tag = "", + staleTime = now.minusMinutes(2).toEpochMilli(), + ) + + assertEquals(listOf(items[1]), result) + } @Test - fun returnsTaggedStaleFeeds() = runBlocking { - val items = listOf( - withFeed(url = URL("http://one"), tag = "tag"), - withFeed(url = URL("http://two"), tag = "tag"), - ) + fun returnsTaggedStaleFeeds() = + runBlocking { + val items = + listOf( + withFeed(url = URL("http://one"), tag = "tag"), + withFeed(url = URL("http://two"), tag = "tag"), + ) - val result = rssLocalSync.feedsToSync(feedId = ID_UNSET, tag = "") + val result = rssLocalSync.feedsToSync(feedId = ID_UNSET, tag = "") - assertEquals(items, result) - } + assertEquals(items, result) + } @Test - fun doesNotReturnTaggedFreshFeeds() = runBlocking { - val now = Instant.now() - val items = listOf( - withFeed(url = URL("http://one"), lastSync = now.minusMinutes(1), tag = "tag"), - withFeed(url = URL("http://two"), lastSync = now.minusMinutes(3), tag = "tag"), - ) - - val result = rssLocalSync.feedsToSync( - feedId = ID_UNSET, - tag = "tag", - staleTime = now.minusMinutes(2).toEpochMilli(), - ) - - assertEquals(listOf(items[1]), result) - } + fun doesNotReturnTaggedFreshFeeds() = + runBlocking { + val now = Instant.now() + val items = + listOf( + withFeed(url = URL("http://one"), lastSync = now.minusMinutes(1), tag = "tag"), + withFeed(url = URL("http://two"), lastSync = now.minusMinutes(3), tag = "tag"), + ) + + val result = + rssLocalSync.feedsToSync( + feedId = ID_UNSET, + tag = "tag", + staleTime = now.minusMinutes(2).toEpochMilli(), + ) + + assertEquals(listOf(items[1]), result) + } private suspend fun withFeed( lastSync: Instant = Instant.ofEpochMilli(0), url: URL = URL("http://url"), tag: String = "", ): Feed { - val feed = Feed( - lastSync = lastSync, - url = url, - tag = tag, - ) + val feed = + Feed( + lastSync = lastSync, + url = url, + tag = tag, + ) val id = testDb.db.feedDao().insertFeed(feed) diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt index 16855fdcac..6f845ac5d5 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssLocalSyncKtTest.kt @@ -19,11 +19,6 @@ import com.nononsenseapps.feeder.ui.TestDatabaseRule import com.nononsenseapps.feeder.util.FilePathProvider import com.nononsenseapps.feeder.util.filePathProvider import com.nononsenseapps.feeder.util.minusMinutes -import java.net.URL -import java.time.Instant -import java.util.concurrent.TimeUnit -import kotlin.test.Ignore -import kotlin.test.assertTrue import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse @@ -44,6 +39,11 @@ import org.kodein.di.android.subDI import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.net.URL +import java.time.Instant +import java.util.concurrent.TimeUnit +import kotlin.test.Ignore +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @MediumTest @@ -58,12 +58,13 @@ class RssLocalSyncKtTest : DIAware { bind(overrides = true) with singleton { FeedStore(di) } bind(overrides = true) with singleton { Repository(di) } bind(overrides = true) with singleton { RssLocalSync(di) } - bind(overrides = true) with singleton { - filePathProvider( - cacheDir = getApplicationContext().cacheDir, - filesDir = getApplicationContext().filesDir, - ) - } + bind(overrides = true) with + singleton { + filePathProvider( + cacheDir = getApplicationContext().cacheDir, + filesDir = getApplicationContext().filesDir, + ) + } } val server = MockWebServer() @@ -89,534 +90,567 @@ class RssLocalSyncKtTest : DIAware { isJson: Boolean = true, useAlternateId: Boolean = false, ): Long { - val id = testDb.db.feedDao().insertFeed( - Feed( - title = title, - url = url, - tag = "", - alternateId = useAlternateId, - ), - ) - - server.dispatcher = object : Dispatcher() { - override fun dispatch(request: RecordedRequest): MockResponse { - return responses.getOrDefault( - request.requestUrl?.toUrl(), - MockResponse().setResponseCode(404), - ) + val id = + testDb.db.feedDao().insertFeed( + Feed( + title = title, + url = url, + tag = "", + alternateId = useAlternateId, + ), + ) + + server.dispatcher = + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return responses.getOrDefault( + request.requestUrl?.toUrl(), + MockResponse().setResponseCode(404), + ) + } } - } - responses[url] = MockResponse().apply { - setResponseCode(200) - if (isJson) { - setHeader("Content-Type", "application/json") + responses[url] = + MockResponse().apply { + setResponseCode(200) + if (isJson) { + setHeader("Content-Type", "application/json") + } + setBody(raw) } - setBody(raw) - } return id } @Test - fun syncCowboyJsonWorks() = runBlocking { - val cowboyJsonId = insertFeed( - "cowboyjson", - server.url("/feed.json").toUrl(), - cowboyJson, - ) - + fun syncCowboyJsonWorks() = runBlocking { - rssLocalSync.syncFeeds( - feedId = cowboyJsonId, + val cowboyJsonId = + insertFeed( + "cowboyjson", + server.url("/feed.json").toUrl(), + cowboyJson, + ) + + runBlocking { + rssLocalSync.syncFeeds( + feedId = cowboyJsonId, + ) + } + + @Suppress("DEPRECATION") + assertEquals( + "Unexpected number of items in feed", + 10, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyJsonId).size, ) } - @Suppress("DEPRECATION") - assertEquals( - "Unexpected number of items in feed", - 10, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyJsonId).size, - ) - } - @Test - fun syncCowboyAtomWorks() = runBlocking { - val cowboyAtomId = insertFeed( - "cowboyatom", - server.url("/atom.xml").toUrl(), - cowboyAtom, - isJson = false, - ) - + fun syncCowboyAtomWorks() = runBlocking { - rssLocalSync.syncFeeds( - feedId = cowboyAtomId, + val cowboyAtomId = + insertFeed( + "cowboyatom", + server.url("/atom.xml").toUrl(), + cowboyAtom, + isJson = false, + ) + + runBlocking { + rssLocalSync.syncFeeds( + feedId = cowboyAtomId, + ) + } + + @Suppress("DEPRECATION") + assertEquals( + "Unexpected number of items in feed", + 15, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, ) } - @Suppress("DEPRECATION") - assertEquals( - "Unexpected number of items in feed", - 15, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, - ) - } - @Test - fun alternateIdMitigatesRssFeedsWithNonUniqueGuids() = runBlocking { - val duplicateIdRss = insertFeed( - "aussieWeather", - server.url("/IDZ00059.warnings_vic.xml").toUrl(), - rssWithDuplicateGuids, - isJson = false, - useAlternateId = true, - ) - + fun alternateIdMitigatesRssFeedsWithNonUniqueGuids() = runBlocking { - rssLocalSync.syncFeeds( - feedId = duplicateIdRss, + val duplicateIdRss = + insertFeed( + "aussieWeather", + server.url("/IDZ00059.warnings_vic.xml").toUrl(), + rssWithDuplicateGuids, + isJson = false, + useAlternateId = true, + ) + + runBlocking { + rssLocalSync.syncFeeds( + feedId = duplicateIdRss, + ) + } + + @Suppress("DEPRECATION") + assertEquals( + "Expected duplicate guids to be mitigated by alternate id", + 13, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(duplicateIdRss).size, ) } - @Suppress("DEPRECATION") - assertEquals( - "Expected duplicate guids to be mitigated by alternate id", - 13, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(duplicateIdRss).size, - ) - } - @Test - fun syncAllWorks() = runBlocking { - val cowboyJsonId = insertFeed( - "cowboyjson", - server.url("/feed.json").toUrl(), - cowboyJson, - ) - val cowboyAtomId = insertFeed( - "cowboyatom", - server.url("/atom.xml").toUrl(), - cowboyAtom, - isJson = false, - ) - + fun syncAllWorks() = runBlocking { - rssLocalSync.syncFeeds( - feedId = ID_UNSET, + val cowboyJsonId = + insertFeed( + "cowboyjson", + server.url("/feed.json").toUrl(), + cowboyJson, + ) + val cowboyAtomId = + insertFeed( + "cowboyatom", + server.url("/atom.xml").toUrl(), + cowboyAtom, + isJson = false, + ) + + runBlocking { + rssLocalSync.syncFeeds( + feedId = ID_UNSET, + ) + } + + @Suppress("DEPRECATION") + assertEquals( + "Unexpected number of items in feed", + 10, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyJsonId).size, ) - } - @Suppress("DEPRECATION") - assertEquals( - "Unexpected number of items in feed", - 10, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyJsonId).size, - ) - - @Suppress("DEPRECATION") - assertEquals( - "Unexpected number of items in feed", - 15, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, - ) - } + @Suppress("DEPRECATION") + assertEquals( + "Unexpected number of items in feed", + 15, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, + ) + } @Test - fun responsesAreNotParsedUnlessFeedHashHasChanged() = runBlocking { - val cowboyJsonId = insertFeed( - "cowboyjson", - server.url("/feed.json").toUrl(), - cowboyJson, - ) - + fun responsesAreNotParsedUnlessFeedHashHasChanged() = runBlocking { - rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> - assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) - assertTrue("Feed should have a valid response hash", feed.responseHash > 0) - // "Long time" ago, but not unset - testDb.db.feedDao().updateFeed(feed.copy(lastSync = Instant.ofEpochMilli(999L))) + val cowboyJsonId = + insertFeed( + "cowboyjson", + server.url("/feed.json").toUrl(), + cowboyJson, + ) + + runBlocking { + rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> + assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) + assertTrue("Feed should have a valid response hash", feed.responseHash > 0) + // "Long time" ago, but not unset + testDb.db.feedDao().updateFeed(feed.copy(lastSync = Instant.ofEpochMilli(999L))) + } + rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) } - rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) - } - assertEquals("Feed should have been fetched twice", 2, server.requestCount) + assertEquals("Feed should have been fetched twice", 2, server.requestCount) - assertNotEquals( - "Cached response should still have updated feed last sync", - 999L, - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync.toEpochMilli(), - ) - } + assertNotEquals( + "Cached response should still have updated feed last sync", + 999L, + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync.toEpochMilli(), + ) + } @Test - fun feedsSyncedWithin15MinAreIgnored() = runBlocking { - val cowboyJsonId = insertFeed( - "cowboyjson", - server.url("/feed.json").toUrl(), - cowboyJson, - ) - - val fourteenMinsAgo = Instant.now().minusMinutes(14) + fun feedsSyncedWithin15MinAreIgnored() = runBlocking { - rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> - assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) - assertTrue("Feed should have a valid response hash", feed.responseHash > 0) + val cowboyJsonId = + insertFeed( + "cowboyjson", + server.url("/feed.json").toUrl(), + cowboyJson, + ) - testDb.db.feedDao().updateFeed(feed.copy(lastSync = fourteenMinsAgo)) + val fourteenMinsAgo = Instant.now().minusMinutes(14) + runBlocking { + rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> + assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) + assertTrue("Feed should have a valid response hash", feed.responseHash > 0) + + testDb.db.feedDao().updateFeed(feed.copy(lastSync = fourteenMinsAgo)) + } + rssLocalSync.syncFeeds( + feedId = cowboyJsonId, + forceNetwork = false, + minFeedAgeMinutes = 15, + ) } - rssLocalSync.syncFeeds( - feedId = cowboyJsonId, - forceNetwork = false, - minFeedAgeMinutes = 15, + + assertEquals( + "Recently synced feed should not get a second network request", + 1, + server.requestCount, ) - } - assertEquals( - "Recently synced feed should not get a second network request", - 1, - server.requestCount, - ) - - assertEquals( - "Last sync should not have changed", - fourteenMinsAgo, - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync, - ) - } + assertEquals( + "Last sync should not have changed", + fourteenMinsAgo, + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync, + ) + } @Test - fun feedsSyncedWithin15MinAreNotIgnoredWhenForcingNetwork() = runBlocking { - val cowboyJsonId = insertFeed( - "cowboyjson", - server.url("/feed.json").toUrl(), - cowboyJson, - ) - - val fourteenMinsAgo = Instant.now().minusMinutes(14) + fun feedsSyncedWithin15MinAreNotIgnoredWhenForcingNetwork() = runBlocking { - rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> - assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) - assertTrue("Feed should have a valid response hash", feed.responseHash > 0) + val cowboyJsonId = + insertFeed( + "cowboyjson", + server.url("/feed.json").toUrl(), + cowboyJson, + ) - testDb.db.feedDao().updateFeed(feed.copy(lastSync = fourteenMinsAgo)) + val fourteenMinsAgo = Instant.now().minusMinutes(14) + runBlocking { + rssLocalSync.syncFeeds(feedId = cowboyJsonId, forceNetwork = true) + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.let { feed -> + assertTrue("Feed should have been synced", feed.lastSync.toEpochMilli() > 0) + assertTrue("Feed should have a valid response hash", feed.responseHash > 0) + + testDb.db.feedDao().updateFeed(feed.copy(lastSync = fourteenMinsAgo)) + } + rssLocalSync.syncFeeds( + feedId = cowboyJsonId, + forceNetwork = true, + minFeedAgeMinutes = 15, + ) } - rssLocalSync.syncFeeds( - feedId = cowboyJsonId, - forceNetwork = true, - minFeedAgeMinutes = 15, + + assertEquals("Request should have been sent due to forced network", 2, server.requestCount) + + assertNotEquals( + "Last sync should have changed", + fourteenMinsAgo, + testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync, ) } - assertEquals("Request should have been sent due to forced network", 2, server.requestCount) + @Test + fun feedShouldNotBeUpdatedIfRequestFails() = + runBlocking { + val response = + MockResponse().also { + it.setResponseCode(500) + } + server.enqueue(response) + + val url = server.url("/feed.json") + + val failingJsonId = + testDb.db.feedDao().insertFeed( + Feed( + title = "failJson", + url = URL("$url"), + tag = "", + ), + ) - assertNotEquals( - "Last sync should have changed", - fourteenMinsAgo, - testDb.db.feedDao().loadFeed(cowboyJsonId)!!.lastSync, - ) - } + runBlocking { + rssLocalSync.syncFeeds(feedId = failingJsonId) + } - @Test - fun feedShouldNotBeUpdatedIfRequestFails() = runBlocking { - val response = MockResponse().also { - it.setResponseCode(500) + assertEquals( + "Last sync should not have been updated", + Instant.EPOCH, + testDb.db.feedDao().loadFeed(failingJsonId)!!.lastSync, + ) + + // Assert the feed was retrieved + assertEquals("/feed.json", server.takeRequest().path) } - server.enqueue(response) - val url = server.url("/feed.json") + @Test + fun feedWithNoUniqueLinksGetsSomeGeneratedGUIDsFromTitles() = + runBlocking { + val response = + MockResponse().also { + it.setResponseCode(200) + it.setBody(String(nixosRss.readBytes())) + } + server.enqueue(response) + + val url = server.url("/news-rss.xml") + + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + title = "NixOS", + url = URL("$url"), + tag = "", + ), + ) - val failingJsonId = testDb.db.feedDao().insertFeed( - Feed( - title = "failJson", - url = URL("$url"), - tag = "", - ), - ) + runBlocking { + rssLocalSync.syncFeeds( + feedId = feedId, + ) + } - runBlocking { - rssLocalSync.syncFeeds(feedId = failingJsonId) - } + // Assert the feed was retrieved + assertEquals("/news-rss.xml", server.takeRequest().path) - assertEquals( - "Last sync should not have been updated", - Instant.EPOCH, - testDb.db.feedDao().loadFeed(failingJsonId)!!.lastSync, - ) + @Suppress("DEPRECATION") + val items = testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId) + assertEquals( + "Unique IDs should have been generated for items", + 99, + items.size, + ) - // Assert the feed was retrieved - assertEquals("/feed.json", server.takeRequest().path) - } + // Should be unique to item so that it stays the same after updates + assertTrue { + items.first().guid.startsWith("https://nixos.org/news.html|") + } + } @Test - fun feedWithNoUniqueLinksGetsSomeGeneratedGUIDsFromTitles() = runBlocking { - val response = MockResponse().also { - it.setResponseCode(200) - it.setBody(String(nixosRss.readBytes())) - } - server.enqueue(response) + fun feedWithNoDatesShouldGetSomeGenerated() = + runBlocking { + val response = + MockResponse().also { + it.setResponseCode(200) + it.setBody(fooRss(2)) + } + server.enqueue(response) + + val url = server.url("/rss") + + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + url = URL("$url"), + ), + ) - val url = server.url("/news-rss.xml") + val beforeSyncTime = Instant.now() - val feedId = testDb.db.feedDao().insertFeed( - Feed( - title = "NixOS", - url = URL("$url"), - tag = "", - ), - ) + runBlocking { + rssLocalSync.syncFeeds(feedId = feedId) + } - runBlocking { - rssLocalSync.syncFeeds( - feedId = feedId, + // Assert the feed was retrieved + assertEquals("/rss", server.takeRequest().path) + + @Suppress("DEPRECATION") + val items = testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId) + + assertNotNull( + "Item should have gotten a pubDate generated", + items[0].pubDate, ) - } - // Assert the feed was retrieved - assertEquals("/news-rss.xml", server.takeRequest().path) + assertNotEquals( + "Items should have distinct pubDates", + items[0].pubDate, + items[1].pubDate, + ) - @Suppress("DEPRECATION") - val items = testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId) - assertEquals( - "Unique IDs should have been generated for items", - 99, - items.size, - ) + assertTrue( + "The pubDate should be after 'before sync time'", + items[0].pubDate!!.toInstant() > beforeSyncTime, + ) - // Should be unique to item so that it stays the same after updates - assertTrue { - items.first().guid.startsWith("https://nixos.org/news.html|") + // Compare ID to compare insertion order (and thus pubdate compared to raw feed) + assertTrue("The pubDates' magnitude should match descending iteration order") { + items[0].guid == "https://foo.bar/1" && + items[1].guid == "https://foo.bar/2" && + items[0].pubDate!! > items[1].pubDate!! + } } - } @Test - fun feedWithNoDatesShouldGetSomeGenerated() = runBlocking { - val response = MockResponse().also { - it.setResponseCode(200) - it.setBody(fooRss(2)) - } - server.enqueue(response) + fun feedWithNoDatesShouldNotGetOverriddenDatesNextSync() = + runBlocking { + server.enqueue( + MockResponse().also { + it.setResponseCode(200) + it.setBody(fooRss(1)) + }, + ) + server.enqueue( + MockResponse().also { + it.setResponseCode(200) + it.setBody(fooRss(2)) + }, + ) - val url = server.url("/rss") + val url = server.url("/rss") - val feedId = testDb.db.feedDao().insertFeed( - Feed( - url = URL("$url"), - ), - ) + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + url = URL("$url"), + ), + ) - val beforeSyncTime = Instant.now() + // Sync first time + runBlocking { + rssLocalSync.syncFeeds(feedId = feedId) + } - runBlocking { - rssLocalSync.syncFeeds(feedId = feedId) - } + // Assert the feed was retrieved + assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) - // Assert the feed was retrieved - assertEquals("/rss", server.takeRequest().path) - - @Suppress("DEPRECATION") - val items = testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId) - - assertNotNull( - "Item should have gotten a pubDate generated", - items[0].pubDate, - ) - - assertNotEquals( - "Items should have distinct pubDates", - items[0].pubDate, - items[1].pubDate, - ) - - assertTrue( - "The pubDate should be after 'before sync time'", - items[0].pubDate!!.toInstant() > beforeSyncTime, - ) - - // Compare ID to compare insertion order (and thus pubdate compared to raw feed) - assertTrue("The pubDates' magnitude should match descending iteration order") { - items[0].guid == "https://foo.bar/1" && - items[1].guid == "https://foo.bar/2" && - items[0].pubDate!! > items[1].pubDate!! - } - } + @Suppress("DEPRECATION") + val firstItem = + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId).let { items -> + assertNotNull( + "Item should have gotten a pubDate generated", + items[0].pubDate, + ) - @Test - fun feedWithNoDatesShouldNotGetOverriddenDatesNextSync() = runBlocking { - server.enqueue( - MockResponse().also { - it.setResponseCode(200) - it.setBody(fooRss(1)) - }, - ) - server.enqueue( - MockResponse().also { - it.setResponseCode(200) - it.setBody(fooRss(2)) - }, - ) - - val url = server.url("/rss") - - val feedId = testDb.db.feedDao().insertFeed( - Feed( - url = URL("$url"), - ), - ) - - // Sync first time - runBlocking { - rssLocalSync.syncFeeds(feedId = feedId) - } + items[0] + } - // Assert the feed was retrieved - assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) + // Sync second time + runBlocking { + rssLocalSync.syncFeeds(feedId = feedId, forceNetwork = true) + } - @Suppress("DEPRECATION") - val firstItem = + // Assert the feed was retrieved + assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) + + @Suppress("DEPRECATION") testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId).let { items -> - assertNotNull( - "Item should have gotten a pubDate generated", - items[0].pubDate, + assertEquals( + "Should be 2 items in feed", + 2, + items.size, ) - items[0] + val item = items.last() + + assertEquals( + "Making sure we are comparing the same item", + firstItem.id, + item.id, + ) + + assertEquals( + "Pubdate should not have changed", + firstItem.pubDate, + item.pubDate, + ) } + } - // Sync second time + @Test + fun feedShouldNotBeCleanedToHaveLessItemsThanActualFeed() = runBlocking { - rssLocalSync.syncFeeds(feedId = feedId, forceNetwork = true) - } + val feedItemCount = 9 + server.enqueue( + MockResponse().also { + it.setResponseCode(200) + it.setBody(fooRss(feedItemCount)) + }, + ) - // Assert the feed was retrieved - assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) + val url = server.url("/rss") - @Suppress("DEPRECATION") - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId).let { items -> - assertEquals( - "Should be 2 items in feed", - 2, - items.size, - ) + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + url = URL("$url"), + ), + ) - val item = items.last() + val maxFeedItemCount = 5 - assertEquals( - "Making sure we are comparing the same item", - firstItem.id, - item.id, - ) + // Sync first time + runBlocking { + rssLocalSync.syncFeeds( + feedId = feedId, + maxFeedItemCount = maxFeedItemCount, + ) + } - assertEquals( - "Pubdate should not have changed", - firstItem.pubDate, - item.pubDate, - ) + // Assert the feed was retrieved + assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) + + @Suppress("DEPRECATION") + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId).let { items -> + assertEquals( + "Feed should have no less items than in the raw feed even if that's more than cleanup count", + feedItemCount, + items.size, + ) + } } - } @Test - fun feedShouldNotBeCleanedToHaveLessItemsThanActualFeed() = runBlocking { - val feedItemCount = 9 - server.enqueue( - MockResponse().also { - it.setResponseCode(200) - it.setBody(fooRss(feedItemCount)) - }, - ) - - val url = server.url("/rss") - - val feedId = testDb.db.feedDao().insertFeed( - Feed( - url = URL("$url"), - ), - ) - - val maxFeedItemCount = 5 - - // Sync first time + fun slowResponseShouldBeOk() = runBlocking { - rssLocalSync.syncFeeds( - feedId = feedId, - maxFeedItemCount = maxFeedItemCount, - ) - } + val url = server.url("/atom.xml").toUrl() + val cowboyAtomId = insertFeed("cowboy", url, cowboyAtom, isJson = false) + responses[url]!!.throttleBody(1024 * 100, 29, TimeUnit.SECONDS) - // Assert the feed was retrieved - assertEquals("/rss", server.takeRequest(100, TimeUnit.MILLISECONDS)!!.path) + runBlocking { + rssLocalSync.syncFeeds(feedId = cowboyAtomId) + } - @Suppress("DEPRECATION") - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(feedId).let { items -> + @Suppress("DEPRECATION") assertEquals( - "Feed should have no less items than in the raw feed even if that's more than cleanup count", - feedItemCount, - items.size, + "Feed should have been parsed from slow response", + 15, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, ) } - } @Test - fun slowResponseShouldBeOk() = runBlocking { - val url = server.url("/atom.xml").toUrl() - val cowboyAtomId = insertFeed("cowboy", url, cowboyAtom, isJson = false) - responses[url]!!.throttleBody(1024 * 100, 29, TimeUnit.SECONDS) - + fun verySlowResponseShouldBeCancelled() = runBlocking { - rssLocalSync.syncFeeds(feedId = cowboyAtomId) - } + val url = server.url("/atom.xml").toUrl() + val cowboyAtomId = insertFeed("cowboy", url, cowboyAtom, isJson = false) + responses[url]!!.throttleBody(1024 * 100, 31, TimeUnit.SECONDS) - @Suppress("DEPRECATION") - assertEquals( - "Feed should have been parsed from slow response", - 15, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, - ) - } - - @Test - fun verySlowResponseShouldBeCancelled() = runBlocking { - val url = server.url("/atom.xml").toUrl() - val cowboyAtomId = insertFeed("cowboy", url, cowboyAtom, isJson = false) - responses[url]!!.throttleBody(1024 * 100, 31, TimeUnit.SECONDS) + runBlocking { + rssLocalSync.syncFeeds(feedId = cowboyAtomId) + } - runBlocking { - rssLocalSync.syncFeeds(feedId = cowboyAtomId) + @Suppress("DEPRECATION") + assertEquals( + "Feed should not have been parsed from extremely slow response", + 0, + testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, + ) } - @Suppress("DEPRECATION") - assertEquals( - "Feed should not have been parsed from extremely slow response", - 0, - testDb.db.feedItemDao().loadFeedItemsInFeedDesc(cowboyAtomId).size, - ) - } - fun fooRss(itemsCount: Int = 1): String { return """ - - - - Foo Feed - https://foo.bar - ${ - (1..itemsCount).map { - """ + + + + Foo Feed + https://foo.bar + ${ + (1..itemsCount).map { + """ Foo Item $it https://foo.bar/$it Woop woop $it + """.trimIndent() + }.fold("") { acc, s -> + "$acc\n$s" + } + } + + """.trimIndent() - }.fold("") { acc, s -> - "$acc\n$s" - } - } - - - """.trimIndent() } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt index c45e3e1896..237d54c821 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/RssNotificationsKtTest.kt @@ -5,12 +5,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.nononsenseapps.feeder.db.COL_LINK import com.nononsenseapps.feeder.db.room.FeedItem -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull @RunWith(AndroidJUnit4::class) class RssNotificationsKtTest { @@ -20,7 +20,7 @@ class RssNotificationsKtTest { assertEquals( "com.nononsenseapps.feeder.ui.OpenLinkInDefaultActivity", - intent.component?.className + intent.component?.className, ) assertEquals("99", intent.data?.lastPathSegment) assertEquals("http://foo", intent.data?.getQueryParameter(COL_LINK)) @@ -28,11 +28,12 @@ class RssNotificationsKtTest { @Test fun openInDefaultActivityIntentsAreConsideredDifferentForSameItem() { - val feedItem = FeedItem( - id = 5, - link = "http://foo", - enclosureLink = "ftp://bar" - ) + val feedItem = + FeedItem( + id = 5, + link = "http://foo", + enclosureLink = "ftp://bar", + ) val linkIntent = getOpenInDefaultActivityIntent(getInstrumentation().context, feedItem.id, link = feedItem.link) val enclosureIntent = getOpenInDefaultActivityIntent(getInstrumentation().context, feedItem.id, link = feedItem.enclosureLink) @@ -40,17 +41,17 @@ class RssNotificationsKtTest { assertFalse( linkIntent.filterEquals(enclosureIntent), - message = "linkIntent should not be considered equal to enclosureIntent" + message = "linkIntent should not be considered equal to enclosureIntent", ) assertFalse( linkIntent.filterEquals(markAsReadIntent), - message = "linkIntent should not be considered equal to markAsReadIntent" + message = "linkIntent should not be considered equal to markAsReadIntent", ) assertFalse( enclosureIntent.filterEquals(markAsReadIntent), - message = "enclosureIntent should not be considered equal to markAsReadIntent" + message = "enclosureIntent should not be considered equal to markAsReadIntent", ) } @@ -61,8 +62,9 @@ class RssNotificationsKtTest { val enclosureIntent = getOpenInDefaultActivityIntent(getInstrumentation().context, 5, link = magnetLink) assertEquals( - magnetLink, enclosureIntent.data?.getQueryParameter(COL_LINK), - message = "Expected link to not get garbled as query parameter" + magnetLink, + enclosureIntent.data?.getQueryParameter(COL_LINK), + message = "Expected link to not get garbled as query parameter", ) } @@ -72,7 +74,7 @@ class RssNotificationsKtTest { assertNull( enclosureIntent.data?.getQueryParameter(COL_LINK), - message = "Expected a null query parameter" + message = "Expected a null query parameter", ) } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/model/opml/OPMLTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/model/opml/OPMLTest.kt index b0a0ed3d60..23e080bb0c 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/model/opml/OPMLTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/model/opml/OPMLTest.kt @@ -23,10 +23,6 @@ import com.nononsenseapps.feeder.db.room.OPEN_ARTICLE_WITH_APPLICATION_DEFAULT import com.nononsenseapps.feeder.model.OPMLParserHandler import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.ToastMaker -import java.io.File -import java.io.IOException -import java.net.URL -import kotlin.random.Random import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.After @@ -43,32 +39,40 @@ import org.kodein.di.bind import org.kodein.di.compose.instance import org.kodein.di.instance import org.kodein.di.singleton +import java.io.File +import java.io.IOException +import java.net.URL +import kotlin.random.Random @RunWith(AndroidJUnit4::class) class OPMLTest : DIAware { private val context: Context = getApplicationContext() lateinit var db: AppDatabase - override val di = DI.lazy { - bind() with singleton { - PreferenceManager.getDefaultSharedPreferences( - this@OPMLTest.context, - ) + override val di = + DI.lazy { + bind() with + singleton { + PreferenceManager.getDefaultSharedPreferences( + this@OPMLTest.context, + ) + } + bind() with instance(db) + bind() with singleton { db.feedDao() } + bind() with singleton { db.blocklistDao() } + bind() with singleton { SettingsStore(di) } + bind() with singleton { FeedStore(di) } + bind() with singleton { OPMLImporter(di) } + bind() with singleton { WorkManager.getInstance(this@OPMLTest.context) } + bind() with + instance( + object : ToastMaker { + override suspend fun makeToast(text: String) {} + + override suspend fun makeToast(resId: Int) {} + }, + ) + bind() with singleton { this@OPMLTest.context.contentResolver } } - bind() with instance(db) - bind() with singleton { db.feedDao() } - bind() with singleton { db.blocklistDao() } - bind() with singleton { SettingsStore(di) } - bind() with singleton { FeedStore(di) } - bind() with singleton { OPMLImporter(di) } - bind() with singleton { WorkManager.getInstance(this@OPMLTest.context) } - bind() with instance( - object : ToastMaker { - override suspend fun makeToast(text: String) {} - override suspend fun makeToast(resId: Int) {} - }, - ) - bind() with singleton { this@OPMLTest.context.contentResolver } - } private var dir: File? = null private var path: File? = null @@ -94,629 +98,648 @@ class OPMLTest : DIAware { @MediumTest @Test - fun testWrite() = runBlocking { - // Create some feeds - createSampleFeeds() - - writeFile( - path = path!!.absolutePath, - settings = ALL_SETTINGS_WITH_VALUES, - blockedPatterns = BLOCKED_PATTERNS, - tags = getTags(), - ) { tag -> - db.feedDao().loadFeeds(tag = tag) - } + fun testWrite() = + runBlocking { + // Create some feeds + createSampleFeeds() + + writeFile( + path = path!!.absolutePath, + settings = ALL_SETTINGS_WITH_VALUES, + blockedPatterns = BLOCKED_PATTERNS, + tags = getTags(), + ) { tag -> + db.feedDao().loadFeeds(tag = tag) + } - // check contents of file - path!!.bufferedReader().useLines { lines -> - lines.forEachIndexed { i, line -> - assertEquals("line $i differed", sampleFile[i], line) + // check contents of file + path!!.bufferedReader().useLines { lines -> + lines.forEachIndexed { i, line -> + assertEquals("line $i differed", sampleFile[i], line) + } } } - } @MediumTest @Test - fun testReadSettings() = runBlocking { - writeSampleFile() + fun testReadSettings() = + runBlocking { + writeSampleFile() - val parser = OpmlPullParser(opmlParserHandler) - parser.parseFile(path!!.canonicalPath) + val parser = OpmlPullParser(opmlParserHandler) + parser.parseFile(path!!.canonicalPath) - // Verify database is correct - val actual = settingsStore.getAllSettings() + // Verify database is correct + val actual = settingsStore.getAllSettings() - ALL_SETTINGS_WITH_VALUES.forEach { (key, expected) -> - assertEquals(expected, actual[key].toString()) - } + ALL_SETTINGS_WITH_VALUES.forEach { (key, expected) -> + assertEquals(expected, actual[key].toString()) + } - val actualBlocked = settingsStore.blockListPreference.first() + val actualBlocked = settingsStore.blockListPreference.first() - assertEquals(1, actualBlocked.size) - assertEquals("foo", actualBlocked.first()) - } + assertEquals(1, actualBlocked.size) + assertEquals("foo", actualBlocked.first()) + } @MediumTest @Test - fun testRead() = runBlocking { - writeSampleFile() - - val parser = OpmlPullParser(opmlParserHandler) - parser.parseFile(path!!.canonicalPath) - - // Verify database is correct - val seen = ArrayList() - val feeds = db.feedDao().loadFeeds() - assertFalse("No feeds in DB!", feeds.isEmpty()) - for (feed in feeds) { - val i = Integer.parseInt(feed.title.replace("[custom \"]".toRegex(), "")) - seen.add(i) - assertEquals("URL doesn't match", URL("http://example.com/$i/rss.xml"), feed.url) - - when (i) { - 0 -> { - assertEquals("title should be the same", "\"$i\"", feed.title) - assertEquals( - "custom title should have been set to title", - "\"$i\"", - feed.customTitle, - ) - } - - else -> { - assertEquals( - "custom title should have overridden title", - "custom \"$i\"", - feed.title, - ) - assertEquals( - "title and custom title should match", - feed.customTitle, - feed.title, - ) + fun testRead() = + runBlocking { + writeSampleFile() + + val parser = OpmlPullParser(opmlParserHandler) + parser.parseFile(path!!.canonicalPath) + + // Verify database is correct + val seen = ArrayList() + val feeds = db.feedDao().loadFeeds() + assertFalse("No feeds in DB!", feeds.isEmpty()) + for (feed in feeds) { + val i = Integer.parseInt(feed.title.replace("[custom \"]".toRegex(), "")) + seen.add(i) + assertEquals("URL doesn't match", URL("http://example.com/$i/rss.xml"), feed.url) + + when (i) { + 0 -> { + assertEquals("title should be the same", "\"$i\"", feed.title) + assertEquals( + "custom title should have been set to title", + "\"$i\"", + feed.customTitle, + ) + } + + else -> { + assertEquals( + "custom title should have overridden title", + "custom \"$i\"", + feed.title, + ) + assertEquals( + "title and custom title should match", + feed.customTitle, + feed.title, + ) + } + } + + when { + i % 3 == 1 -> assertEquals("tag1", feed.tag) + i % 3 == 2 -> assertEquals("tag2", feed.tag) + else -> assertEquals("", feed.tag) } } - - when { - i % 3 == 1 -> assertEquals("tag1", feed.tag) - i % 3 == 2 -> assertEquals("tag2", feed.tag) - else -> assertEquals("", feed.tag) + for (i in 0..9) { + assertTrue("Missing $i", seen.contains(i)) } } - for (i in 0..9) { - assertTrue("Missing $i", seen.contains(i)) - } - } @MediumTest @Test - fun testReadExisting() = runBlocking { - writeSampleFile() - - // Create something that does not exist - var feednew = Feed( - url = URL("http://example.com/20/rss.xml"), - title = "\"20\"", - tag = "kapow", - ) - var id = db.feedDao().insertFeed(feednew) - feednew = feednew.copy(id = id) - // Create something that will exist - var feedold = Feed( - url = URL("http://example.com/0/rss.xml"), - title = "\"0\"", - ) - id = db.feedDao().insertFeed(feedold) - - feedold = feedold.copy(id = id) - - // Read file - val parser = OpmlPullParser(opmlParserHandler) - parser.parseFile(path!!.canonicalPath) - - // should not kill the existing stuff - val seen = ArrayList() - val feeds = db.feedDao().loadFeeds() - assertFalse("No feeds in DB!", feeds.isEmpty()) - for (feed in feeds) { - val i = Integer.parseInt(feed.title.replace("[custom \"]".toRegex(), "")) - seen.add(i) - assertEquals(URL("http://example.com/$i/rss.xml"), feed.url) - - when { - i == 20 -> { - assertEquals("Should not have changed", feednew.id, feed.id) - assertEquals("Should not have changed", feednew.url, feed.url) - assertEquals("Should not have changed", feednew.tag, feed.tag) - } - - i % 3 == 1 -> assertEquals("tag1", feed.tag) - i % 3 == 2 -> assertEquals("tag2", feed.tag) - else -> assertEquals("", feed.tag) - } - - // Ensure titles are correct - when (i) { - 0 -> { - assertEquals("title should be the same", feedold.title, feed.title) - assertEquals( - "custom title should have been set to title", - feedold.title, - feed.customTitle, - ) - } - - 20 -> { - assertEquals( - "feed not present in OPML should not have changed", - feednew.title, - feed.title, - ) - assertEquals( - "feed not present in OPML should not have changed", - feednew.customTitle, - feednew.customTitle, - ) - } - - else -> { - assertEquals( - "custom title should have overridden title", - "custom \"$i\"", - feed.title, - ) + fun testReadExisting() = + runBlocking { + writeSampleFile() + + // Create something that does not exist + var feednew = + Feed( + url = URL("http://example.com/20/rss.xml"), + title = "\"20\"", + tag = "kapow", + ) + var id = db.feedDao().insertFeed(feednew) + feednew = feednew.copy(id = id) + // Create something that will exist + var feedold = + Feed( + url = URL("http://example.com/0/rss.xml"), + title = "\"0\"", + ) + id = db.feedDao().insertFeed(feedold) + + feedold = feedold.copy(id = id) + + // Read file + val parser = OpmlPullParser(opmlParserHandler) + parser.parseFile(path!!.canonicalPath) + + // should not kill the existing stuff + val seen = ArrayList() + val feeds = db.feedDao().loadFeeds() + assertFalse("No feeds in DB!", feeds.isEmpty()) + for (feed in feeds) { + val i = Integer.parseInt(feed.title.replace("[custom \"]".toRegex(), "")) + seen.add(i) + assertEquals(URL("http://example.com/$i/rss.xml"), feed.url) + + when { + i == 20 -> { + assertEquals("Should not have changed", feednew.id, feed.id) + assertEquals("Should not have changed", feednew.url, feed.url) + assertEquals("Should not have changed", feednew.tag, feed.tag) + } + + i % 3 == 1 -> assertEquals("tag1", feed.tag) + i % 3 == 2 -> assertEquals("tag2", feed.tag) + else -> assertEquals("", feed.tag) + } + + // Ensure titles are correct + when (i) { + 0 -> { + assertEquals("title should be the same", feedold.title, feed.title) + assertEquals( + "custom title should have been set to title", + feedold.title, + feed.customTitle, + ) + } + + 20 -> { + assertEquals( + "feed not present in OPML should not have changed", + feednew.title, + feed.title, + ) + assertEquals( + "feed not present in OPML should not have changed", + feednew.customTitle, + feednew.customTitle, + ) + } + + else -> { + assertEquals( + "custom title should have overridden title", + "custom \"$i\"", + feed.title, + ) + assertEquals( + "title and custom title should match", + feed.customTitle, + feed.title, + ) + } + } + + if (i == 0) { + // Make sure id is same as old + assertEquals("Id should be same still", feedold.id, feed.id) + + assertTrue("Notify is wrong", feed.notify) + assertTrue("AlternateId is wrong", feed.alternateId) + assertTrue("FullTextByDefault is wrong", feed.fullTextByDefault) + assertEquals("OpenArticlesWith is wrong", "reader", feed.openArticlesWith) assertEquals( - "title and custom title should match", - feed.customTitle, - feed.title, + "ImageURL is wrong", + URL("https://example.com/feedImage.png"), + feed.imageUrl, ) } } - - if (i == 0) { - // Make sure id is same as old - assertEquals("Id should be same still", feedold.id, feed.id) - - assertTrue("Notify is wrong", feed.notify) - assertTrue("AlternateId is wrong", feed.alternateId) - assertTrue("FullTextByDefault is wrong", feed.fullTextByDefault) - assertEquals("OpenArticlesWith is wrong", "reader", feed.openArticlesWith) - assertEquals( - "ImageURL is wrong", - URL("https://example.com/feedImage.png"), - feed.imageUrl, - ) + assertTrue("Missing 20", seen.contains(20)) + for (i in 0..9) { + assertTrue("Missing $i", seen.contains(i)) } } - assertTrue("Missing 20", seen.contains(20)) - for (i in 0..9) { - assertTrue("Missing $i", seen.contains(i)) - } - } @MediumTest @Test - fun testReadBadFile() = runBlocking { - path!!.bufferedWriter().use { - it.write("This is just some bullshit in the file\n") - } + fun testReadBadFile() = + runBlocking { + path!!.bufferedWriter().use { + it.write("This is just some bullshit in the file\n") + } - // Read file - val parser = OpmlPullParser(opmlParserHandler) - parser.parseFile(path!!.absolutePath) + // Read file + val parser = OpmlPullParser(opmlParserHandler) + parser.parseFile(path!!.absolutePath) - val feeds = db.feedDao().loadFeeds() - assertTrue("Expected no feeds and no exception", feeds.isEmpty()) - } + val feeds = db.feedDao().loadFeeds() + assertTrue("Expected no feeds and no exception", feeds.isEmpty()) + } @SmallTest @Test - fun testReadMissingFile() = runBlocking { - val path = File(dir, "lsadflibaslsdfa.opml") - // Read file - val parser = OpmlPullParser(opmlParserHandler) - val result = parser.parseFile(path.absolutePath) - - assertTrue(result.isLeft()) - } + fun testReadMissingFile() = + runBlocking { + val path = File(dir, "lsadflibaslsdfa.opml") + // Read file + val parser = OpmlPullParser(opmlParserHandler) + val result = parser.parseFile(path.absolutePath) + + assertTrue(result.isLeft()) + } @Throws(IOException::class) - private fun writeSampleFile() = runBlocking { - // Use test write to write the sample file - testWrite() - // Then delete all feeds again - db.runInTransaction { - runBlocking { - db.feedDao().loadFeeds().forEach { - db.feedDao().deleteFeed(it) + private fun writeSampleFile() = + runBlocking { + // Use test write to write the sample file + testWrite() + // Then delete all feeds again + db.runInTransaction { + runBlocking { + db.feedDao().loadFeeds().forEach { + db.feedDao().deleteFeed(it) + } } } } - } private suspend fun createSampleFeeds() { for (i in 0..9) { - val feed = Feed( - url = URL("http://example.com/$i/rss.xml"), - title = "\"$i\"", - customTitle = if (i == 0) "" else "custom \"$i\"", - tag = when (i % 3) { - 1 -> "tag1" - 2 -> "tag2" - else -> "" - }, - notify = i == 0, - alternateId = i == 0, - fullTextByDefault = i == 0, - imageUrl = if (i == 0) { - URL("https://example.com/feedImage.png") - } else { - null - }, - openArticlesWith = if (i == 0) { - "reader" - } else { - OPEN_ARTICLE_WITH_APPLICATION_DEFAULT - }, - ) + val feed = + Feed( + url = URL("http://example.com/$i/rss.xml"), + title = "\"$i\"", + customTitle = if (i == 0) "" else "custom \"$i\"", + tag = + when (i % 3) { + 1 -> "tag1" + 2 -> "tag2" + else -> "" + }, + notify = i == 0, + alternateId = i == 0, + fullTextByDefault = i == 0, + imageUrl = + if (i == 0) { + URL("https://example.com/feedImage.png") + } else { + null + }, + openArticlesWith = + if (i == 0) { + "reader" + } else { + OPEN_ARTICLE_WITH_APPLICATION_DEFAULT + }, + ) db.feedDao().insertFeed(feed) } } - private suspend fun getTags(): List = - db.feedDao().loadTags() + private suspend fun getTags(): List = db.feedDao().loadTags() @Test @MediumTest - fun antennaPodOPMLImports() = runBlocking { - // given - val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("antennapod-feeds.opml")!! - - // when - val parser = OpmlPullParser(opmlParserHandler) - parser.parseInputStreamWithFallback(opmlStream) - - // then - val feeds = db.feedDao().loadFeeds() - val tags = db.feedDao().loadTags() - assertEquals("Expecting 8 feeds", 8, feeds.size) - assertEquals("Expecting 1 tags (incl empty)", 1, tags.size) - - feeds.forEach { feed -> - assertEquals("No tag expected", "", feed.tag) - when (feed.url) { - URL("http://aliceisntdead.libsyn.com/rss") -> { - assertEquals("Alice Isn't Dead", feed.title) - } - - URL("http://feeds.soundcloud.com/users/soundcloud:users:154104768/sounds.rss") -> { - assertEquals("Invisible City", feed.title) - } - - URL("http://feeds.feedburner.com/PodCastle_Main") -> { - assertEquals("PodCastle", feed.title) - } - - URL("http://www.artofstorytellingshow.com/podcast/storycast.xml") -> { - assertEquals("The Art of Storytelling with Brother Wolf", feed.title) + fun antennaPodOPMLImports() = + runBlocking { + // given + val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("antennapod-feeds.opml")!! + + // when + val parser = OpmlPullParser(opmlParserHandler) + parser.parseInputStreamWithFallback(opmlStream) + + // then + val feeds = db.feedDao().loadFeeds() + val tags = db.feedDao().loadTags() + assertEquals("Expecting 8 feeds", 8, feeds.size) + assertEquals("Expecting 1 tags (incl empty)", 1, tags.size) + + feeds.forEach { feed -> + assertEquals("No tag expected", "", feed.tag) + when (feed.url) { + URL("http://aliceisntdead.libsyn.com/rss") -> { + assertEquals("Alice Isn't Dead", feed.title) + } + + URL("http://feeds.soundcloud.com/users/soundcloud:users:154104768/sounds.rss") -> { + assertEquals("Invisible City", feed.title) + } + + URL("http://feeds.feedburner.com/PodCastle_Main") -> { + assertEquals("PodCastle", feed.title) + } + + URL("http://www.artofstorytellingshow.com/podcast/storycast.xml") -> { + assertEquals("The Art of Storytelling with Brother Wolf", feed.title) + } + + URL("http://feeds.feedburner.com/TheCleansed") -> { + assertEquals("The Cleansed: A Post-Apocalyptic Saga", feed.title) + } + + URL("http://media.signumuniversity.org/tolkienprof/feed") -> { + assertEquals("The Tolkien Professor", feed.title) + } + + URL("http://nightvale.libsyn.com/rss") -> { + assertEquals("Welcome to Night Vale", feed.title) + } + + URL("http://withinthewires.libsyn.com/rss") -> { + assertEquals("Within the Wires", feed.title) + } + + else -> fail("Unexpected URI. Feed: $feed") } - - URL("http://feeds.feedburner.com/TheCleansed") -> { - assertEquals("The Cleansed: A Post-Apocalyptic Saga", feed.title) - } - - URL("http://media.signumuniversity.org/tolkienprof/feed") -> { - assertEquals("The Tolkien Professor", feed.title) - } - - URL("http://nightvale.libsyn.com/rss") -> { - assertEquals("Welcome to Night Vale", feed.title) - } - - URL("http://withinthewires.libsyn.com/rss") -> { - assertEquals("Within the Wires", feed.title) - } - - else -> fail("Unexpected URI. Feed: $feed") } } - } @Test @MediumTest - fun flymOPMLImports() = runBlocking { - // given - val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("Flym_auto_backup.opml")!! - - // when - val parser = OpmlPullParser(opmlParserHandler) - parser.parseInputStreamWithFallback(opmlStream) - - // then - val feeds = db.feedDao().loadFeeds() - val tags = db.feedDao().loadTags() - assertEquals("Expecting 11 feeds", 11, feeds.size) - assertEquals("Expecting 4 tags (incl empty)", 4, tags.size) - - feeds.forEach { feed -> - when (feed.url) { - URL("http://www.smbc-comics.com/rss.php") -> { - assertEquals("black humor", feed.tag) - assertEquals("SMBC", feed.customTitle) - assertFalse(feed.fullTextByDefault) - } - - URL("http://www.deathbulge.com/rss.xml") -> { - assertEquals("black humor", feed.tag) - assertEquals("Deathbulge", feed.customTitle) - assertTrue(feed.fullTextByDefault) - } - - URL("http://www.sandraandwoo.com/gaia/feed/") -> { - assertEquals("comics", feed.tag) - assertEquals("Gaia", feed.customTitle) - assertFalse(feed.fullTextByDefault) - } - - URL("http://replaycomic.com/feed/") -> { - assertEquals("comics", feed.tag) - assertEquals("Replay", feed.customTitle) - assertTrue(feed.fullTextByDefault) - } - - URL("http://www.cuttimecomic.com/rss.php") -> { - assertEquals("comics", feed.tag) - assertEquals("Cut Time", feed.customTitle) - assertFalse(feed.fullTextByDefault) + fun flymOPMLImports() = + runBlocking { + // given + val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("Flym_auto_backup.opml")!! + + // when + val parser = OpmlPullParser(opmlParserHandler) + parser.parseInputStreamWithFallback(opmlStream) + + // then + val feeds = db.feedDao().loadFeeds() + val tags = db.feedDao().loadTags() + assertEquals("Expecting 11 feeds", 11, feeds.size) + assertEquals("Expecting 4 tags (incl empty)", 4, tags.size) + + feeds.forEach { feed -> + when (feed.url) { + URL("http://www.smbc-comics.com/rss.php") -> { + assertEquals("black humor", feed.tag) + assertEquals("SMBC", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("http://www.deathbulge.com/rss.xml") -> { + assertEquals("black humor", feed.tag) + assertEquals("Deathbulge", feed.customTitle) + assertTrue(feed.fullTextByDefault) + } + + URL("http://www.sandraandwoo.com/gaia/feed/") -> { + assertEquals("comics", feed.tag) + assertEquals("Gaia", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("http://replaycomic.com/feed/") -> { + assertEquals("comics", feed.tag) + assertEquals("Replay", feed.customTitle) + assertTrue(feed.fullTextByDefault) + } + + URL("http://www.cuttimecomic.com/rss.php") -> { + assertEquals("comics", feed.tag) + assertEquals("Cut Time", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("http://www.commitstrip.com/feed/") -> { + assertEquals("comics", feed.tag) + assertEquals("Commit strip", feed.customTitle) + assertTrue(feed.fullTextByDefault) + } + + URL("http://www.sandraandwoo.com/feed/") -> { + assertEquals("comics", feed.tag) + assertEquals("Sandra and Woo", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("http://www.awakencomic.com/rss.php") -> { + assertEquals("comics", feed.tag) + assertEquals("Awaken", feed.customTitle) + assertTrue(feed.fullTextByDefault) + } + + URL("http://www.questionablecontent.net/QCRSS.xml") -> { + assertEquals("comics", feed.tag) + assertEquals("Questionable Content", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("https://www.archlinux.org/feeds/news/") -> { + assertEquals("Tech", feed.tag) + assertEquals("Arch news", feed.customTitle) + assertFalse(feed.fullTextByDefault) + } + + URL("https://grisebouille.net/feed/") -> { + assertEquals("Political humour", feed.tag) + assertEquals("Grisebouille", feed.customTitle) + assertTrue(feed.fullTextByDefault) + } + + else -> fail("Unexpected URI. Feed: $feed") } - - URL("http://www.commitstrip.com/feed/") -> { - assertEquals("comics", feed.tag) - assertEquals("Commit strip", feed.customTitle) - assertTrue(feed.fullTextByDefault) - } - - URL("http://www.sandraandwoo.com/feed/") -> { - assertEquals("comics", feed.tag) - assertEquals("Sandra and Woo", feed.customTitle) - assertFalse(feed.fullTextByDefault) - } - - URL("http://www.awakencomic.com/rss.php") -> { - assertEquals("comics", feed.tag) - assertEquals("Awaken", feed.customTitle) - assertTrue(feed.fullTextByDefault) - } - - URL("http://www.questionablecontent.net/QCRSS.xml") -> { - assertEquals("comics", feed.tag) - assertEquals("Questionable Content", feed.customTitle) - assertFalse(feed.fullTextByDefault) - } - - URL("https://www.archlinux.org/feeds/news/") -> { - assertEquals("Tech", feed.tag) - assertEquals("Arch news", feed.customTitle) - assertFalse(feed.fullTextByDefault) - } - - URL("https://grisebouille.net/feed/") -> { - assertEquals("Political humour", feed.tag) - assertEquals("Grisebouille", feed.customTitle) - assertTrue(feed.fullTextByDefault) - } - - else -> fail("Unexpected URI. Feed: $feed") } } - } @Test @MediumTest - fun rssGuardOPMLImports1() = runBlocking { - // given - val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("rssguard_1.opml")!! - - // when - val parser = OpmlPullParser(opmlParserHandler) - parser.parseInputStreamWithFallback(opmlStream) - - // then - val feeds = db.feedDao().loadFeeds() - val tags = db.feedDao().loadTags() - assertEquals("Expecting 30 feeds", 30, feeds.size) - assertEquals("Expecting 6 tags (incl empty)", 6, tags.size) - - feeds.forEach { feed -> - when (feed.url) { - URL("http://www.les-trois-sagesses.org/rss-articles.xml") -> { - assertEquals("Religion", feed.tag) - assertEquals("Les trois sagesses", feed.customTitle) - } - - URL("http://www.avrildeperthuis.com/feed/") -> { - assertEquals("Amis", feed.tag) - assertEquals("avril de perthuis", feed.customTitle) - } - - URL("http://www.fashioningtech.com/profiles/blog/feed?xn_auth=no") -> { - assertEquals("Actu Geek", feed.tag) - assertEquals("Everyone's Blog Posts - Fashioning Technology", feed.customTitle) - } - - URL("http://feeds2.feedburner.com/ChartPorn") -> { - assertEquals("Graphs", feed.tag) - assertEquals("Chart Porn", feed.customTitle) - } - - URL("http://www.mosqueedeparis.net/index.php?format=feed&type=atom") -> { - assertEquals("Religion", feed.tag) - assertEquals("Mosquee de Paris", feed.customTitle) - } - - URL("http://sourceforge.net/projects/stuntrally/rss") -> { - assertEquals("Mainstream update", feed.tag) - assertEquals("Stunt Rally", feed.customTitle) - } - - URL("http://www.mairie6.lyon.fr/cs/Satellite?Thematique=&TypeContenu=Actualite&pagename=RSSFeed&site=Mairie6") -> { - assertEquals("", feed.tag) - assertEquals("Actualités", feed.customTitle) + fun rssGuardOPMLImports1() = + runBlocking { + // given + val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("rssguard_1.opml")!! + + // when + val parser = OpmlPullParser(opmlParserHandler) + parser.parseInputStreamWithFallback(opmlStream) + + // then + val feeds = db.feedDao().loadFeeds() + val tags = db.feedDao().loadTags() + assertEquals("Expecting 30 feeds", 30, feeds.size) + assertEquals("Expecting 6 tags (incl empty)", 6, tags.size) + + feeds.forEach { feed -> + when (feed.url) { + URL("http://www.les-trois-sagesses.org/rss-articles.xml") -> { + assertEquals("Religion", feed.tag) + assertEquals("Les trois sagesses", feed.customTitle) + } + + URL("http://www.avrildeperthuis.com/feed/") -> { + assertEquals("Amis", feed.tag) + assertEquals("avril de perthuis", feed.customTitle) + } + + URL("http://www.fashioningtech.com/profiles/blog/feed?xn_auth=no") -> { + assertEquals("Actu Geek", feed.tag) + assertEquals("Everyone's Blog Posts - Fashioning Technology", feed.customTitle) + } + + URL("http://feeds2.feedburner.com/ChartPorn") -> { + assertEquals("Graphs", feed.tag) + assertEquals("Chart Porn", feed.customTitle) + } + + URL("http://www.mosqueedeparis.net/index.php?format=feed&type=atom") -> { + assertEquals("Religion", feed.tag) + assertEquals("Mosquee de Paris", feed.customTitle) + } + + URL("http://sourceforge.net/projects/stuntrally/rss") -> { + assertEquals("Mainstream update", feed.tag) + assertEquals("Stunt Rally", feed.customTitle) + } + + URL("http://www.mairie6.lyon.fr/cs/Satellite?Thematique=&TypeContenu=Actualite&pagename=RSSFeed&site=Mairie6") -> { + assertEquals("", feed.tag) + assertEquals("Actualités", feed.customTitle) + } } } } - } @MediumTest @Test - fun testExportThenImport(): Unit = runBlocking { - val fileUri = context.cacheDir.resolve("exporttest.opml").toUri() - val feedIds = mutableSetOf() - feedStore.saveFeed( - Feed( - title = "Ampersands are & the worst", - url = URL("https://example.com/ampersands"), - ), - ).also { feedIds.add(it) } - feedStore.saveFeed( - Feed( - title = "So are > brackets", - url = URL("https://example.com/lt"), - ), - ).also { feedIds.add(it) } - feedStore.saveFeed( - Feed( - title = "So are < brackets", - url = URL("https://example.com/gt"), - ), - ).also { feedIds.add(it) } - - assertEquals(3, feedIds.size) - - val exportResult = exportOpml(di, fileUri) - - exportResult.leftOrNull()?.let { e -> - throw e.throwable!! - } + fun testExportThenImport(): Unit = + runBlocking { + val fileUri = context.cacheDir.resolve("exporttest.opml").toUri() + val feedIds = mutableSetOf() + feedStore.saveFeed( + Feed( + title = "Ampersands are & the worst", + url = URL("https://example.com/ampersands"), + ), + ).also { feedIds.add(it) } + feedStore.saveFeed( + Feed( + title = "So are > brackets", + url = URL("https://example.com/lt"), + ), + ).also { feedIds.add(it) } + feedStore.saveFeed( + Feed( + title = "So are < brackets", + url = URL("https://example.com/gt"), + ), + ).also { feedIds.add(it) } + + assertEquals(3, feedIds.size) + + val exportResult = exportOpml(di, fileUri) + + exportResult.leftOrNull()?.let { e -> + throw e.throwable!! + } - val opmlFeedList = OpmlFeedList() - val parser = OpmlPullParser(opmlFeedList) - val result = parser.parseFile(fileUri.path!!) + val opmlFeedList = OpmlFeedList() + val parser = OpmlPullParser(opmlFeedList) + val result = parser.parseFile(fileUri.path!!) - result.leftOrNull()?.let { e -> - throw e.throwable!! - } + result.leftOrNull()?.let { e -> + throw e.throwable!! + } - assertEquals(3, opmlFeedList.feeds.size) - } + assertEquals(3, opmlFeedList.feeds.size) + } @Test @MediumTest - fun importPlenaryProgramming(): Unit = runBlocking { - // given - val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("Programming.opml")!! - - // when - val opmlFeedList = OpmlFeedList() - val parser = OpmlPullParser(opmlFeedList) - val result = parser.parseInputStreamWithFallback(opmlStream) + fun importPlenaryProgramming(): Unit = + runBlocking { + // given + val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("Programming.opml")!! + + // when + val opmlFeedList = OpmlFeedList() + val parser = OpmlPullParser(opmlFeedList) + val result = parser.parseInputStreamWithFallback(opmlStream) + + result.leftOrNull()?.let { + throw it.throwable!! + } - result.leftOrNull()?.let { - throw it.throwable!! + // then + assertEquals("Expecting feeds", 50, opmlFeedList.feeds.size) } - // then - assertEquals("Expecting feeds", 50, opmlFeedList.feeds.size) - } - @Test @MediumTest - fun rssGuardOPMLImports2() = runBlocking { - // given - val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("rssguard_2.opml")!! - - // when - val parser = OpmlPullParser(opmlParserHandler) - parser.parseInputStreamWithFallback(opmlStream) - - // then - val feeds = db.feedDao().loadFeeds() - val tags = db.feedDao().loadTags() - assertEquals("Expecting 30 feeds", 30, feeds.size) - assertEquals("Expecting 6 tags (incl empty)", 6, tags.size) - - feeds.forEach { feed -> - when (feed.url) { - URL("http://www.les-trois-sagesses.org/rss-articles.xml") -> { - assertEquals("Religion", feed.tag) - assertEquals("Les trois sagesses", feed.customTitle) - } - - URL("http://www.avrildeperthuis.com/feed/") -> { - assertEquals("Amis", feed.tag) - assertEquals("avril de perthuis", feed.customTitle) - } - - URL("http://www.fashioningtech.com/profiles/blog/feed?xn_auth=no") -> { - assertEquals("Actu Geek", feed.tag) - assertEquals("Everyone's Blog Posts - Fashioning Technology", feed.customTitle) - } - - URL("http://feeds2.feedburner.com/ChartPorn") -> { - assertEquals("Graphs", feed.tag) - assertEquals("Chart Porn", feed.customTitle) - } - - URL("http://www.mosqueedeparis.net/index.php?format=feed&type=atom") -> { - assertEquals("Religion", feed.tag) - assertEquals("Mosquee de Paris", feed.customTitle) - } - - URL("http://sourceforge.net/projects/stuntrally/rss") -> { - assertEquals("Mainstream update", feed.tag) - assertEquals("Stunt Rally", feed.customTitle) - } - - URL("http://www.mairie6.lyon.fr/cs/Satellite?Thematique=&TypeContenu=Actualite&pagename=RSSFeed&site=Mairie6") -> { - assertEquals("", feed.tag) - assertEquals("Actualités", feed.customTitle) + fun rssGuardOPMLImports2() = + runBlocking { + // given + val opmlStream = this@OPMLTest.javaClass.getResourceAsStream("rssguard_2.opml")!! + + // when + val parser = OpmlPullParser(opmlParserHandler) + parser.parseInputStreamWithFallback(opmlStream) + + // then + val feeds = db.feedDao().loadFeeds() + val tags = db.feedDao().loadTags() + assertEquals("Expecting 30 feeds", 30, feeds.size) + assertEquals("Expecting 6 tags (incl empty)", 6, tags.size) + + feeds.forEach { feed -> + when (feed.url) { + URL("http://www.les-trois-sagesses.org/rss-articles.xml") -> { + assertEquals("Religion", feed.tag) + assertEquals("Les trois sagesses", feed.customTitle) + } + + URL("http://www.avrildeperthuis.com/feed/") -> { + assertEquals("Amis", feed.tag) + assertEquals("avril de perthuis", feed.customTitle) + } + + URL("http://www.fashioningtech.com/profiles/blog/feed?xn_auth=no") -> { + assertEquals("Actu Geek", feed.tag) + assertEquals("Everyone's Blog Posts - Fashioning Technology", feed.customTitle) + } + + URL("http://feeds2.feedburner.com/ChartPorn") -> { + assertEquals("Graphs", feed.tag) + assertEquals("Chart Porn", feed.customTitle) + } + + URL("http://www.mosqueedeparis.net/index.php?format=feed&type=atom") -> { + assertEquals("Religion", feed.tag) + assertEquals("Mosquee de Paris", feed.customTitle) + } + + URL("http://sourceforge.net/projects/stuntrally/rss") -> { + assertEquals("Mainstream update", feed.tag) + assertEquals("Stunt Rally", feed.customTitle) + } + + URL("http://www.mairie6.lyon.fr/cs/Satellite?Thematique=&TypeContenu=Actualite&pagename=RSSFeed&site=Mairie6") -> { + assertEquals("", feed.tag) + assertEquals("Actualités", feed.customTitle) + } } } } - } companion object { private val BLOCKED_PATTERNS: List = listOf("foo") private val ALL_SETTINGS_WITH_VALUES: Map = UserSettings.values().associate { userSetting -> - userSetting.key to when (userSetting) { - UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB - UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true" - UserSettings.SETTING_THEME -> "night" - UserSettings.SETTING_DARK_THEME -> "dark" - UserSettings.SETTING_DYNAMIC_THEME -> "false" - UserSettings.SETTING_SORT -> "oldest_first" - UserSettings.SETTING_SHOW_FAB -> "false" - UserSettings.SETTING_FEED_ITEM_STYLE -> "SUPER_COMPACT" - UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED" - UserSettings.SETTING_SYNC_ON_RESUME -> "true" - UserSettings.SETTING_SYNC_ONLY_WIFI -> "false" - UserSettings.SETTING_IMG_ONLY_WIFI -> "true" - UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false" - UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB - UserSettings.SETTING_TEXT_SCALE -> "1.6" - UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true" - UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true" - UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true" - UserSettings.SETTING_SYNC_FREQ -> "720" - UserSettings.SETTING_MAX_LINES -> "6" - UserSettings.SETTINGS_FILTER_SAVED -> "true" - UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true" - UserSettings.SETTINGS_FILTER_READ -> "false" - UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true" - UserSettings.SETTING_OPEN_ADJACENT -> "true" - } + userSetting.key to + when (userSetting) { + UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB + UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true" + UserSettings.SETTING_THEME -> "night" + UserSettings.SETTING_DARK_THEME -> "dark" + UserSettings.SETTING_DYNAMIC_THEME -> "false" + UserSettings.SETTING_SORT -> "oldest_first" + UserSettings.SETTING_SHOW_FAB -> "false" + UserSettings.SETTING_FEED_ITEM_STYLE -> "SUPER_COMPACT" + UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED" + UserSettings.SETTING_SYNC_ON_RESUME -> "true" + UserSettings.SETTING_SYNC_ONLY_WIFI -> "false" + UserSettings.SETTING_IMG_ONLY_WIFI -> "true" + UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false" + UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB + UserSettings.SETTING_TEXT_SCALE -> "1.6" + UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true" + UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true" + UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true" + UserSettings.SETTING_SYNC_FREQ -> "720" + UserSettings.SETTING_MAX_LINES -> "6" + UserSettings.SETTINGS_FILTER_SAVED -> "true" + UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true" + UserSettings.SETTINGS_FILTER_READ -> "false" + UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true" + UserSettings.SETTING_OPEN_ADJACENT -> "true" + } } } } @@ -731,7 +754,8 @@ suspend fun OpmlPullParser.parseFile(path: String): Either { } } -private val sampleFile: List = """ +private val sampleFile: List = + """ @@ -784,18 +808,22 @@ private val sampleFile: List = """ -""".trimIndent() - .split("\n") + """.trimIndent() + .split("\n") class OpmlFeedList : OPMLParserHandler { val feeds = mutableMapOf() val settings = mutableMapOf() val blockList = mutableListOf() + override suspend fun saveFeed(feed: Feed) { feeds[feed.url] = feed } - override suspend fun saveSetting(key: String, value: String) { + override suspend fun saveSetting( + key: String, + value: String, + ) { settings.put(key, value) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/Helpers.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/Helpers.kt index ec7d091724..9f631bc660 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/Helpers.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/Helpers.kt @@ -10,7 +10,7 @@ suspend fun whileNotEq( other: Any?, timeoutMillis: Long = 500, sleepMillis: Long = 50, - body: (suspend () -> T) + body: (suspend () -> T), ): T = withTimeout(timeoutMillis) { var item = body.invoke() @@ -28,7 +28,7 @@ suspend fun whileEq( other: Any?, timeoutMillis: Long = 500, sleepMillis: Long = 50, - body: (suspend () -> T) + body: (suspend () -> T), ): T = withTimeout(timeoutMillis) { var item = body.invoke() @@ -46,13 +46,13 @@ suspend fun untilNotEq( other: Any?, timeoutMillis: Long = 500, sleepMillis: Long = 50, - body: (suspend () -> T) + body: (suspend () -> T), ): T = whileEq( other = other, timeoutMillis = timeoutMillis, sleepMillis = sleepMillis, - body = body + body = body, ) /** @@ -62,11 +62,11 @@ suspend fun untilEq( other: Any?, timeoutMillis: Long = 500, sleepMillis: Long = 50, - body: (suspend () -> T) + body: (suspend () -> T), ): T = whileNotEq( other = other, timeoutMillis = timeoutMillis, sleepMillis = sleepMillis, - body = body + body = body, ) diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/NotificationClearingTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/NotificationClearingTest.kt index 91246ca590..3be6899cd6 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/NotificationClearingTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/NotificationClearingTest.kt @@ -8,7 +8,6 @@ import com.nononsenseapps.feeder.db.room.FeedItemWithFeed import com.nononsenseapps.feeder.model.RssNotificationBroadcastReceiver import com.nononsenseapps.feeder.model.getDeleteIntent import com.nononsenseapps.feeder.model.notify -import java.net.URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -19,84 +18,95 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.net.URL // This can be flaky @RunWith(AndroidJUnit4::class) @Ignore class NotificationClearingTest { private val receiver: RssNotificationBroadcastReceiver = RssNotificationBroadcastReceiver() + @get:Rule val testDb = TestDatabaseRule(getApplicationContext()) @Test - fun clearingNotificationMarksAsNotified() = runBlocking { - val feedId = testDb.db.feedDao().insertFeed( - Feed( - title = "testFeed", - url = URL("http://testfeed"), - tag = "testTag" - ) - ) + fun clearingNotificationMarksAsNotified() = + runBlocking { + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + title = "testFeed", + url = URL("http://testfeed"), + tag = "testTag", + ), + ) - val item1Id = testDb.db.feedItemDao().insertFeedItem( - FeedItem( - feedId = feedId, - guid = "item1", - title = "item1", - notified = false - ) - ) + val item1Id = + testDb.db.feedItemDao().insertFeedItem( + FeedItem( + feedId = feedId, + guid = "item1", + title = "item1", + notified = false, + ), + ) - val di = getDeleteIntent( - getApplicationContext(), - FeedItemWithFeed( - id = item1Id, feedId = feedId, guid = "item1", title = "item1" - ) - ) + val di = + getDeleteIntent( + getApplicationContext(), + FeedItemWithFeed( + id = item1Id, + feedId = feedId, + guid = "item1", + title = "item1", + ), + ) - runBlocking { - // Receiver runs on main thread - withContext(Dispatchers.Main) { - receiver.onReceive(getApplicationContext(), di) - } + runBlocking { + // Receiver runs on main thread + withContext(Dispatchers.Main) { + receiver.onReceive(getApplicationContext(), di) + } - delay(50) + delay(50) - val item = testDb.db.feedItemDao().loadFeedItem(guid = "item1", feedId = feedId) - assertTrue(item!!.notified) + val item = testDb.db.feedItemDao().loadFeedItem(guid = "item1", feedId = feedId) + assertTrue(item!!.notified) + } } - } @Test - fun notifyWorksOnMainThread() = runBlocking { - val feedId = testDb.db.feedDao().insertFeed( - Feed( - title = "testFeed", - url = URL("http://testfeed"), - tag = "testTag" - ) - ) + fun notifyWorksOnMainThread() = + runBlocking { + val feedId = + testDb.db.feedDao().insertFeed( + Feed( + title = "testFeed", + url = URL("http://testfeed"), + tag = "testTag", + ), + ) - testDb.db.feedItemDao().insertFeedItem( - FeedItem( - feedId = feedId, - guid = "item1", - title = "item1", - notified = false + testDb.db.feedItemDao().insertFeedItem( + FeedItem( + feedId = feedId, + guid = "item1", + title = "item1", + notified = false, + ), ) - ) - runBlocking { - // Try to notify on main thread - withContext(Dispatchers.Main) { - notify(getApplicationContext()) - } + runBlocking { + // Try to notify on main thread + withContext(Dispatchers.Main) { + notify(getApplicationContext()) + } - delay(50) + delay(50) - // Only care that the above call didn't crash because we ran on the main thread - val item = testDb.db.feedItemDao().loadFeedItem(guid = "item1", feedId = feedId) - assertFalse(item!!.notified) + // Only care that the above call didn't crash because we ran on the main thread + val item = testDb.db.feedItemDao().loadFeedItem(guid = "item1", feedId = feedId) + assertFalse(item!!.notified) + } } - } } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/TestDatabaseRule.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/TestDatabaseRule.kt index 5e237f84eb..c3f1eb32d0 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/TestDatabaseRule.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/TestDatabaseRule.kt @@ -9,13 +9,14 @@ class TestDatabaseRule(val context: Context) : ExternalResource() { lateinit var db: AppDatabase override fun before() { - db = Room.inMemoryDatabaseBuilder( - context, - AppDatabase::class.java - ).build().also { - // Ensure all classes use test database - AppDatabase.setInstance(it) - } + db = + Room.inMemoryDatabaseBuilder( + context, + AppDatabase::class.java, + ).build().also { + // Ensure all classes use test database + AppDatabase.setInstance(it) + } } override fun after() { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/AddFeedFromShareActivityTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/AddFeedFromShareActivityTest.kt index 71b8ae0205..539300f9f7 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/AddFeedFromShareActivityTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/AddFeedFromShareActivityTest.kt @@ -6,18 +6,19 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.launchActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nononsenseapps.feeder.ui.AddFeedFromShareActivity -import kotlin.test.assertEquals import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) class AddFeedFromShareActivityTest { @Test fun activityShouldStart() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - AddFeedFromShareActivity::class.java - ) + val intent = + Intent( + ApplicationProvider.getApplicationContext(), + AddFeedFromShareActivity::class.java, + ) launchActivity(intent).use { scenario -> assertEquals(Lifecycle.State.RESUMED, scenario.state) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/MainActivityTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/MainActivityTest.kt index 0f5361d6a1..f45ae60d34 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/MainActivityTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/MainActivityTest.kt @@ -6,18 +6,19 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.launchActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nononsenseapps.feeder.ui.MainActivity -import kotlin.test.assertEquals import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) class MainActivityTest { @Test fun activityShouldStart() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - MainActivity::class.java - ) + val intent = + Intent( + ApplicationProvider.getApplicationContext(), + MainActivity::class.java, + ) launchActivity(intent).use { scenario -> assertEquals(Lifecycle.State.RESUMED, scenario.state) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/ManageSettingsActivityTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/ManageSettingsActivityTest.kt index 783c8758b9..b77681a738 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/ManageSettingsActivityTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/activity/ManageSettingsActivityTest.kt @@ -6,18 +6,19 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.launchActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nononsenseapps.feeder.ui.ManageSettingsActivity -import kotlin.test.assertEquals import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) class ManageSettingsActivityTest { @Test fun activityShouldStart() { - val intent = Intent( - ApplicationProvider.getApplicationContext(), - ManageSettingsActivity::class.java - ) + val intent = + Intent( + ApplicationProvider.getApplicationContext(), + ManageSettingsActivity::class.java, + ) launchActivity(intent).use { scenario -> assertEquals(Lifecycle.State.RESUMED, scenario.state) } diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/EndToEndTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/EndToEndTest.kt index 2c29b696e4..d8215d3b63 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/EndToEndTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/EndToEndTest.kt @@ -12,7 +12,6 @@ import org.kodein.di.compose.withDI @Ignore class EndToEndTest : BaseComposeTest { - @get:Rule override val composeTestRule = createAndroidComposeRule() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt index 4958faffd6..dbf460d7d3 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/ReaderScreenScrollingTest.kt @@ -6,7 +6,6 @@ import org.junit.Rule @Ignore class ReaderScreenScrollingTest { - @get:Rule val composeTestRule = createComposeRule() // createAndroidComposeRule() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/StartingNavigationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/StartingNavigationTest.kt index 361f1ce822..bd50f00345 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/StartingNavigationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/StartingNavigationTest.kt @@ -4,16 +4,15 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import com.nononsenseapps.feeder.ui.MainActivity import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.robots.feedScreen -import kotlin.test.assertFalse import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.kodein.di.compose.withDI +import kotlin.test.assertFalse @Ignore class StartingNavigationTest : BaseComposeTest { - @get:Rule override val composeTestRule = createAndroidComposeRule() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/SyncSetupTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/SyncSetupTest.kt index a3a6a826e1..4abaf59c8c 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/SyncSetupTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/SyncSetupTest.kt @@ -16,7 +16,6 @@ import org.kodein.di.compose.withDI @Ignore class SyncSetupTest : BaseComposeTest { - @get:Rule override val composeTestRule = createAndroidComposeRule() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/AddFeedDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/AddFeedDestinationTest.kt index 6021f1ca2c..5d03f39818 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/AddFeedDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/AddFeedDestinationTest.kt @@ -4,10 +4,10 @@ import androidx.navigation.NavController import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class AddFeedDestinationTest { @@ -23,7 +23,7 @@ class AddFeedDestinationTest { fun addFeedHasCorrectRoute() { assertEquals( "add/feed/{feedUrl}?feedTitle={feedTitle}", - AddFeedDestination.route + AddFeedDestination.route, ) } @@ -31,7 +31,7 @@ class AddFeedDestinationTest { fun addFeedNavigateNoTitle() { AddFeedDestination.navigate( navController, - "https://cowboyprogrammer.org" + "https://cowboyprogrammer.org", ) verify { @@ -44,7 +44,7 @@ class AddFeedDestinationTest { AddFeedDestination.navigate( navController, "https://cowboyprogrammer.org", - "" + "", ) verify { @@ -57,7 +57,7 @@ class AddFeedDestinationTest { AddFeedDestination.navigate( navController, "https://cowboyprogrammer.org", - " " + " ", ) verify { @@ -70,7 +70,7 @@ class AddFeedDestinationTest { AddFeedDestination.navigate( navController, "https://cowboyprogrammer.org", - "A feed" + "A feed", ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/ArticleDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/ArticleDestinationTest.kt index 8478e71d91..3da0d61e27 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/ArticleDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/ArticleDestinationTest.kt @@ -5,10 +5,10 @@ import com.nononsenseapps.feeder.util.DEEP_LINK_BASE_URI import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class ArticleDestinationTest { @@ -24,7 +24,7 @@ class ArticleDestinationTest { fun readerHasCorrectRoute() { assertEquals( "reader/{itemId}", - ArticleDestination.route + ArticleDestination.route, ) } @@ -32,9 +32,9 @@ class ArticleDestinationTest { fun readerHasCorrectDeeplinks() { assertEquals( listOf( - "$DEEP_LINK_BASE_URI/article/{itemId}" + "$DEEP_LINK_BASE_URI/article/{itemId}", ), - ArticleDestination.deepLinks.map { it.uriPattern } + ArticleDestination.deepLinks.map { it.uriPattern }, ) } @@ -42,7 +42,7 @@ class ArticleDestinationTest { fun readerNavigate() { ArticleDestination.navigate( navController, - 55L + 55L, ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/EditFeedDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/EditFeedDestinationTest.kt index b2dbd5cf77..e0ceb0c439 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/EditFeedDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/EditFeedDestinationTest.kt @@ -4,10 +4,10 @@ import androidx.navigation.NavController import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class EditFeedDestinationTest { @@ -23,7 +23,7 @@ class EditFeedDestinationTest { fun editFeedHasCorrectRoute() { assertEquals( "edit/feed/{feedId}", - EditFeedDestination.route + EditFeedDestination.route, ) } @@ -31,7 +31,7 @@ class EditFeedDestinationTest { fun editFeedNavigate() { EditFeedDestination.navigate( navController, - 99L + 99L, ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/FeedDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/FeedDestinationTest.kt index 2565f1fbe0..c9ffd62cf7 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/FeedDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/FeedDestinationTest.kt @@ -6,10 +6,10 @@ import com.nononsenseapps.feeder.util.DEEP_LINK_BASE_URI import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class FeedDestinationTest { @@ -25,7 +25,7 @@ class FeedDestinationTest { fun feedHasCorrectRoute() { assertEquals( "feed?id={id}&tag={tag}", - FeedDestination.route + FeedDestination.route, ) } @@ -33,16 +33,16 @@ class FeedDestinationTest { fun feedHasCorrectDeeplinks() { assertEquals( listOf( - "$DEEP_LINK_BASE_URI/feed?id={id}&tag={tag}" + "$DEEP_LINK_BASE_URI/feed?id={id}&tag={tag}", ), - FeedDestination.deepLinks.map { it.uriPattern } + FeedDestination.deepLinks.map { it.uriPattern }, ) } @Test fun feedNavigateDefaults() { FeedDestination.navigate( - navController + navController, ) verify { @@ -54,7 +54,7 @@ class FeedDestinationTest { fun feedNavigateId() { FeedDestination.navigate( navController, - feedId = 6L + feedId = 6L, ) verify { @@ -66,7 +66,7 @@ class FeedDestinationTest { fun feedNavigateTag() { FeedDestination.navigate( navController, - tag = "foo bar+cop" + tag = "foo bar+cop", ) verify { @@ -79,7 +79,7 @@ class FeedDestinationTest { FeedDestination.navigate( navController, feedId = ID_ALL_FEEDS, - tag = "foo bar+cop" + tag = "foo bar+cop", ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SearchFeedDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SearchFeedDestinationTest.kt index 62d32c3782..bc3f5a0a08 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SearchFeedDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SearchFeedDestinationTest.kt @@ -4,10 +4,10 @@ import androidx.navigation.NavController import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class SearchFeedDestinationTest { @@ -23,14 +23,14 @@ class SearchFeedDestinationTest { fun searchFeedHasCorrectRoute() { assertEquals( "search/feed?feedUrl={feedUrl}", - SearchFeedDestination.route + SearchFeedDestination.route, ) } @Test fun searchFeedNavigateDefaults() { SearchFeedDestination.navigate( - navController + navController, ) verify { @@ -42,7 +42,7 @@ class SearchFeedDestinationTest { fun searchFeedNavigateFeed() { SearchFeedDestination.navigate( navController, - "https://cowboyprogrammer.org" + "https://cowboyprogrammer.org", ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SettingsDestinationTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SettingsDestinationTest.kt index d986c82404..076055ac2b 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SettingsDestinationTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/compose/navigation/SettingsDestinationTest.kt @@ -4,10 +4,10 @@ import androidx.navigation.NavController import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.mockk.verify -import kotlin.test.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test +import kotlin.test.assertEquals @Ignore class SettingsDestinationTest { @@ -23,14 +23,14 @@ class SettingsDestinationTest { fun settingsHasCorrectRoute() { assertEquals( "settings", - SettingsDestination.route + SettingsDestination.route, ) } @Test fun settingsNavigateDefaults() { SettingsDestination.navigate( - navController + navController, ) verify { diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/FeedScreenRobot.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/FeedScreenRobot.kt index d082388f72..1b3e73712a 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/FeedScreenRobot.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/ui/robots/FeedScreenRobot.kt @@ -8,8 +8,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import com.nononsenseapps.feeder.ui.compose.BaseComposeTest -fun BaseComposeTest.feedScreen(block: FeedScreenRobot.() -> Unit) = - FeedScreenRobot(composeTestRule).apply { block() } +fun BaseComposeTest.feedScreen(block: FeedScreenRobot.() -> Unit) = FeedScreenRobot(composeTestRule).apply { block() } class FeedScreenRobot( private val testRule: ComposeTestRule, diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/util/BugReportKTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/util/BugReportKTest.kt index 956b5639ec..2b7a7130c9 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/util/BugReportKTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/util/BugReportKTest.kt @@ -8,15 +8,14 @@ import android.content.Intent.EXTRA_TEXT import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @MediumTest class BugReportKTest { - @Test fun bugIntentIsCorrect() { val intent = emailBugReportIntent() diff --git a/app/src/androidTest/java/com/nononsenseapps/feeder/util/IcoDecoderTest.kt b/app/src/androidTest/java/com/nononsenseapps/feeder/util/IcoDecoderTest.kt index 3de5e70d6c..c1a779ea4b 100644 --- a/app/src/androidTest/java/com/nononsenseapps/feeder/util/IcoDecoderTest.kt +++ b/app/src/androidTest/java/com/nononsenseapps/feeder/util/IcoDecoderTest.kt @@ -9,12 +9,12 @@ import coil.decode.ImageSource import coil.fetch.SourceResult import coil.request.Options import com.danielrampelt.coil.ico.IcoDecoder -import kotlin.test.assertNotNull import kotlinx.coroutines.runBlocking import okio.buffer import okio.source import org.junit.Test import org.junit.runner.RunWith +import kotlin.test.assertNotNull @RunWith(AndroidJUnit4::class) @SmallTest @@ -23,45 +23,51 @@ class IcoDecoderTest { @Test fun testPngFavicon() { - val decoder = factory.create( - pngIco, - Options(ApplicationProvider.getApplicationContext()), - ImageLoader(ApplicationProvider.getApplicationContext()), - ) + val decoder = + factory.create( + pngIco, + Options(ApplicationProvider.getApplicationContext()), + ImageLoader(ApplicationProvider.getApplicationContext()), + ) assertNotNull(decoder) - val result = runBlocking { - decoder.decode() - } + val result = + runBlocking { + decoder.decode() + } assertNotNull(result) } @Test fun testGitlabIco() { - val decoder = factory.create( - pngIco, - Options(ApplicationProvider.getApplicationContext()), - ImageLoader(ApplicationProvider.getApplicationContext()), - ) + val decoder = + factory.create( + pngIco, + Options(ApplicationProvider.getApplicationContext()), + ImageLoader(ApplicationProvider.getApplicationContext()), + ) assertNotNull(decoder) - val result = runBlocking { - decoder.decode() - } + val result = + runBlocking { + decoder.decode() + } assertNotNull(result) } companion object { private val gitlabIco: SourceResult get() { - val buf = Companion::class.java.getResourceAsStream("gitlab.ico")!! - .source() - .buffer() + val buf = + Companion::class.java.getResourceAsStream("gitlab.ico")!! + .source() + .buffer() - val imageSource = ImageSource( - source = buf, - context = ApplicationProvider.getApplicationContext(), - ) + val imageSource = + ImageSource( + source = buf, + context = ApplicationProvider.getApplicationContext(), + ) return SourceResult( source = imageSource, @@ -72,14 +78,16 @@ class IcoDecoderTest { private val pngIco: SourceResult get() { - val buf = Companion::class.java.getResourceAsStream("png.ico")!! - .source() - .buffer() + val buf = + Companion::class.java.getResourceAsStream("png.ico")!! + .source() + .buffer() - val imageSource = ImageSource( - source = buf, - context = ApplicationProvider.getApplicationContext(), - ) + val imageSource = + ImageSource( + source = buf, + context = ApplicationProvider.getApplicationContext(), + ) return SourceResult( source = imageSource, diff --git a/app/src/main/java/com/danielrampelt/coil/ico/IcoDecoder.kt b/app/src/main/java/com/danielrampelt/coil/ico/IcoDecoder.kt index 569a0c4694..f02b98fefd 100644 --- a/app/src/main/java/com/danielrampelt/coil/ico/IcoDecoder.kt +++ b/app/src/main/java/com/danielrampelt/coil/ico/IcoDecoder.kt @@ -38,9 +38,7 @@ class IcoDecoder( } } - private fun BitmapFactory.Options.decode( - bufferedSource: BufferedSource, - ): DecodeResult { + private fun BitmapFactory.Options.decode(bufferedSource: BufferedSource): DecodeResult { // Read the image's dimensions. // inJustDecodeBounds = true // val peek = bufferedSource.peek() @@ -57,9 +55,10 @@ class IcoDecoder( inPremultiplied = options.premultipliedAlpha // Decode the bitmap. - val outBitmap: Bitmap? = bufferedSource.use { - BitmapFactory.decodeStream(it.inputStream(), null, this) - } + val outBitmap: Bitmap? = + bufferedSource.use { + BitmapFactory.decodeStream(it.inputStream(), null, this) + } if (outBitmap == null) { Log.w( diff --git a/app/src/main/java/com/nononsenseapps/feeder/FeederApplication.kt b/app/src/main/java/com/nononsenseapps/feeder/FeederApplication.kt index 79e201119b..950cd4c802 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/FeederApplication.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/FeederApplication.kt @@ -39,9 +39,6 @@ import com.nononsenseapps.feeder.util.currentlyUnmetered import com.nononsenseapps.feeder.util.filePathProvider import com.nononsenseapps.feeder.util.logDebug import com.nononsenseapps.jsonfeed.cachingHttpClient -import java.io.File -import java.security.Security -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext @@ -55,6 +52,9 @@ import org.kodein.di.bind import org.kodein.di.direct import org.kodein.di.instance import org.kodein.di.singleton +import java.io.File +import java.security.Security +import java.util.concurrent.TimeUnit class FeederApplication : Application(), DIAware, ImageLoaderFactory { private val applicationCoroutineScope = ApplicationCoroutineScope() @@ -63,9 +63,10 @@ class FeederApplication : Application(), DIAware, ImageLoaderFactory { override val di by DI.lazy { // import(androidXModule(this@FeederApplication)) - bind() with singleton { - filePathProvider(cacheDir = cacheDir, filesDir = filesDir) - } + bind() with + singleton { + filePathProvider(cacheDir = cacheDir, filesDir = filesDir) + } bind() with singleton { this@FeederApplication } bind() with singleton { AppDatabase.getInstance(this@FeederApplication) } bind() with singleton { instance().feedDao() } @@ -83,96 +84,103 @@ class FeederApplication : Application(), DIAware, ImageLoaderFactory { bind() with singleton { WorkManager.getInstance(this@FeederApplication) } bind() with singleton { contentResolver } - bind() with singleton { - object : ToastMaker { - override suspend fun makeToast(text: String) = withContext(Dispatchers.Main) { - Toast.makeText(this@FeederApplication, text, Toast.LENGTH_SHORT).show() - } + bind() with + singleton { + object : ToastMaker { + override suspend fun makeToast(text: String) = + withContext(Dispatchers.Main) { + Toast.makeText(this@FeederApplication, text, Toast.LENGTH_SHORT).show() + } - override suspend fun makeToast(resId: Int) = withContext(Dispatchers.Main) { - Toast.makeText(this@FeederApplication, resId, Toast.LENGTH_SHORT).show() + override suspend fun makeToast(resId: Int) = + withContext(Dispatchers.Main) { + Toast.makeText(this@FeederApplication, resId, Toast.LENGTH_SHORT).show() + } } } - } bind() with singleton { NotificationManagerCompat.from(this@FeederApplication) } - bind() with singleton { - PreferenceManager.getDefaultSharedPreferences( - this@FeederApplication, - ) - } + bind() with + singleton { + PreferenceManager.getDefaultSharedPreferences( + this@FeederApplication, + ) + } - bind() with singleton { - val filePathProvider = instance() - cachingHttpClient( - cacheDirectory = (filePathProvider.httpCacheDir), - ) { - addNetworkInterceptor(UserAgentInterceptor) - if (BuildConfig.DEBUG) { - addInterceptor { chain -> - val request = chain.request() - logDebug( - "FEEDER", - "Request ${request.url} headers [${request.headers}]", - ) - - chain.proceed(request).also { + bind() with + singleton { + val filePathProvider = instance() + cachingHttpClient( + cacheDirectory = (filePathProvider.httpCacheDir), + ) { + addNetworkInterceptor(UserAgentInterceptor) + if (BuildConfig.DEBUG) { + addInterceptor { chain -> + val request = chain.request() logDebug( "FEEDER", - "Response ${it.request.url} code ${it.networkResponse?.code} cached ${it.cacheResponse != null}", + "Request ${request.url} headers [${request.headers}]", ) + + chain.proceed(request).also { + logDebug( + "FEEDER", + "Response ${it.request.url} code ${it.networkResponse?.code} cached ${it.cacheResponse != null}", + ) + } } } } } - } - bind() with singleton { - val filePathProvider = instance() - val repository = instance() - val okHttpClient = instance() - .newBuilder() - // This is not used by Coil but no need to risk evicting the real cache - .cache(Cache(filePathProvider.cacheDir.resolve("dummy_img"), 1024L)) - .addInterceptor { chain -> - chain.proceed( - when (!repository.loadImageOnlyOnWifi.value || currentlyUnmetered(this@FeederApplication)) { - true -> chain.request() - false -> { - // Forces only cached responses to be used - if no cache then 504 is thrown - chain.request().newBuilder() - .cacheControl( - CacheControl.Builder() - .onlyIfCached() - .maxStale(Int.MAX_VALUE, TimeUnit.SECONDS) - .maxAge(Int.MAX_VALUE, TimeUnit.SECONDS) - .build(), - ) - .build() - } - }, + bind() with + singleton { + val filePathProvider = instance() + val repository = instance() + val okHttpClient = + instance() + .newBuilder() + // This is not used by Coil but no need to risk evicting the real cache + .cache(Cache(filePathProvider.cacheDir.resolve("dummy_img"), 1024L)) + .addInterceptor { chain -> + chain.proceed( + when (!repository.loadImageOnlyOnWifi.value || currentlyUnmetered(this@FeederApplication)) { + true -> chain.request() + false -> { + // Forces only cached responses to be used - if no cache then 504 is thrown + chain.request().newBuilder() + .cacheControl( + CacheControl.Builder() + .onlyIfCached() + .maxStale(Int.MAX_VALUE, TimeUnit.SECONDS) + .maxAge(Int.MAX_VALUE, TimeUnit.SECONDS) + .build(), + ) + .build() + } + }, + ) + } + .build() + + ImageLoader.Builder(instance()) + .okHttpClient(okHttpClient = okHttpClient) + .diskCache( + DiskCache.Builder() + .directory(filePathProvider.imageCacheDir) + .maxSizeBytes(250L * 1024 * 1024) + .build(), ) - } - .build() - - ImageLoader.Builder(instance()) - .okHttpClient(okHttpClient = okHttpClient) - .diskCache( - DiskCache.Builder() - .directory(filePathProvider.imageCacheDir) - .maxSizeBytes(250L * 1024 * 1024) - .build(), - ) - .components { - add(TooLargeImageInterceptor()) - add(SvgDecoder.Factory()) - if (SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) + .components { + add(TooLargeImageInterceptor()) + add(SvgDecoder.Factory()) + if (SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + add(IcoDecoder.Factory(this@FeederApplication)) } - add(IcoDecoder.Factory(this@FeederApplication)) - } - .build() - } + .build() + } bind() with instance(applicationCoroutineScope) import(networkModule) bind() with instance(ttsStateHolder) diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedItemStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedItemStore.kt index c390d20079..ee4e650589 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedItemStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedItemStore.kt @@ -20,13 +20,6 @@ import com.nononsenseapps.feeder.model.PreviewItem import com.nononsenseapps.feeder.model.previewColumns import com.nononsenseapps.feeder.ui.compose.feed.FeedListItem import com.nononsenseapps.feeder.ui.compose.feedarticle.FeedListFilter -import java.net.URL -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -34,6 +27,13 @@ import kotlinx.coroutines.withContext import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.net.URL +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale class FeedItemStore(override val di: DI) : DIAware { private val dao: FeedItemDao by instance() @@ -67,10 +67,11 @@ class FeedItemStore(override val di: DI) : DIAware { filter: FeedListFilter, ): Flow> = Pager( - config = PagingConfig( - pageSize = PAGE_SIZE, - enablePlaceholders = false, - ), + config = + PagingConfig( + pageSize = PAGE_SIZE, + enablePlaceholders = false, + ), ) { val queryString = StringBuilder() val args = mutableListOf() @@ -209,14 +210,20 @@ class FeedItemStore(override val di: DI) : DIAware { dao.markAsRead(itemIds) } - suspend fun markAsReadAndNotified(itemId: Long, readTime: Instant = Instant.now()) { + suspend fun markAsReadAndNotified( + itemId: Long, + readTime: Instant = Instant.now(), + ) { dao.markAsReadAndNotified( id = itemId, readTime = readTime.coerceAtLeast(Instant.EPOCH), ) } - suspend fun markAsReadAndNotifiedAndOverwriteReadTime(itemId: Long, readTime: Instant) { + suspend fun markAsReadAndNotifiedAndOverwriteReadTime( + itemId: Long, + readTime: Instant, + ) { dao.markAsReadAndNotifiedAndOverwriteReadTime( id = itemId, readTime = readTime.coerceAtLeast(Instant.EPOCH), @@ -227,7 +234,10 @@ class FeedItemStore(override val di: DI) : DIAware { dao.markAsUnread(itemId) } - suspend fun setBookmarked(itemId: Long, bookmarked: Boolean) { + suspend fun setBookmarked( + itemId: Long, + bookmarked: Boolean, + ) { dao.setBookmarked(itemId, bookmarked) } @@ -239,7 +249,10 @@ class FeedItemStore(override val di: DI) : DIAware { return dao.loadFeedItemFlow(itemId) } - suspend fun getFeedItemId(feedUrl: URL, articleGuid: String): Long? { + suspend fun getFeedItemId( + feedUrl: URL, + articleGuid: String, + ): Long? { return dao.getItemWith(feedUrl = feedUrl, articleGuid = articleGuid) } @@ -266,15 +279,16 @@ class FeedItemStore(override val di: DI) : DIAware { fun getFeedsItemsWithDefaultFullTextNeedingDownload(): Flow> = dao.getFeedsItemsWithDefaultFullTextNeedingDownload() - suspend fun markAsFullTextDownloaded(feedItemId: Long) = - dao.markAsFullTextDownloaded(feedItemId) + suspend fun markAsFullTextDownloaded(feedItemId: Long) = dao.markAsFullTextDownloaded(feedItemId) fun getFeedItemsNeedingNotifying(): Flow> { return dao.getFeedItemsNeedingNotifying() } - suspend fun loadFeedItem(guid: String, feedId: Long): FeedItem? = - dao.loadFeedItem(guid = guid, feedId = feedId) + suspend fun loadFeedItem( + guid: String, + feedId: Long, + ): FeedItem? = dao.loadFeedItem(guid = guid, feedId = feedId) suspend fun upsertFeedItems( itemsWithText: List>, @@ -283,14 +297,19 @@ class FeedItemStore(override val di: DI) : DIAware { dao.upsertFeedItems(itemsWithText = itemsWithText, block = block) } - suspend fun getItemsToBeCleanedFromFeed(feedId: Long, keepCount: Int) = - dao.getItemsToBeCleanedFromFeed(feedId = feedId, keepCount = keepCount) + suspend fun getItemsToBeCleanedFromFeed( + feedId: Long, + keepCount: Int, + ) = dao.getItemsToBeCleanedFromFeed(feedId = feedId, keepCount = keepCount) suspend fun deleteFeedItems(ids: List) { dao.deleteFeedItems(ids) } - suspend fun updateWordCountFull(id: Long, wordCount: Int) { + suspend fun updateWordCountFull( + id: Long, + wordCount: Int, + ) { dao.updateWordCountFull(id, wordCount) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedStore.kt index 3f76eff76a..d53f895b2e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/FeedStore.kt @@ -11,14 +11,14 @@ import com.nononsenseapps.feeder.ui.compose.navdrawer.DrawerFeed import com.nononsenseapps.feeder.ui.compose.navdrawer.DrawerItemWithUnreadCount import com.nononsenseapps.feeder.ui.compose.navdrawer.DrawerTag import com.nononsenseapps.feeder.ui.compose.navdrawer.DrawerTop -import java.net.URL -import java.time.Instant import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.net.URL +import java.time.Instant class FeedStore(override val di: DI) : DIAware { private val feedDao: FeedDao by instance() @@ -49,11 +49,12 @@ class FeedStore(override val di: DI) : DIAware { } } - suspend fun toggleNotifications(feedId: Long, value: Boolean) = - feedDao.setNotify(id = feedId, notify = value) + suspend fun toggleNotifications( + feedId: Long, + value: Boolean, + ) = feedDao.setNotify(id = feedId, notify = value) - suspend fun getDisplayTitle(feedId: Long): String? = - feedDao.getFeedTitle(feedId)?.displayTitle + suspend fun getDisplayTitle(feedId: Long): String? = feedDao.getFeedTitle(feedId)?.displayTitle suspend fun deleteFeeds(feedIds: List) { feedDao.deleteFeeds(feedIds) @@ -73,39 +74,41 @@ class FeedStore(override val di: DI) : DIAware { mapFeedsToSortedDrawerItems(feeds) } - private fun mapFeedsToSortedDrawerItems( - feeds: List, - ): List { + private fun mapFeedsToSortedDrawerItems(feeds: List): List { var topTag = DrawerTop(unreadCount = 0, totalChildren = 0) val tags: MutableMap = mutableMapOf() val data: MutableList = mutableListOf() for (feedDbo in feeds) { - val feed = DrawerFeed( - unreadCount = feedDbo.unreadCount, - tag = feedDbo.tag, - id = feedDbo.id, - displayTitle = feedDbo.displayTitle, - imageUrl = feedDbo.imageUrl, - ) + val feed = + DrawerFeed( + unreadCount = feedDbo.unreadCount, + tag = feedDbo.tag, + id = feedDbo.id, + displayTitle = feedDbo.displayTitle, + imageUrl = feedDbo.imageUrl, + ) data.add(feed) - topTag = topTag.copy( - unreadCount = topTag.unreadCount + feed.unreadCount, - totalChildren = topTag.totalChildren + 1, - ) + topTag = + topTag.copy( + unreadCount = topTag.unreadCount + feed.unreadCount, + totalChildren = topTag.totalChildren + 1, + ) if (feed.tag.isNotEmpty()) { - val tag = tags[feed.tag] ?: DrawerTag( - tag = feed.tag, - unreadCount = 0, - uiId = getTagUiId(feed.tag), - totalChildren = 0, - ) - tags[feed.tag] = tag.copy( - unreadCount = tag.unreadCount + feed.unreadCount, - totalChildren = tag.totalChildren + 1, - ) + val tag = + tags[feed.tag] ?: DrawerTag( + tag = feed.tag, + unreadCount = 0, + uiId = getTagUiId(feed.tag), + totalChildren = 0, + ) + tags[feed.tag] = + tag.copy( + unreadCount = tag.unreadCount + feed.unreadCount, + totalChildren = tag.totalChildren + 1, + ) } } @@ -115,17 +118,23 @@ class FeedStore(override val di: DI) : DIAware { return data.sorted() } - fun getFeedTitles(feedId: Long, tag: String): Flow> = + fun getFeedTitles( + feedId: Long, + tag: String, + ): Flow> = when { feedId > ID_UNSET -> feedDao.getFeedTitlesWithId(feedId) tag.isNotBlank() -> feedDao.getFeedTitlesWithTag(tag) else -> feedDao.getAllFeedTitles() } - fun getCurrentlySyncingLatestTimestamp(): Flow = - feedDao.getCurrentlySyncingLatestTimestamp() + fun getCurrentlySyncingLatestTimestamp(): Flow = feedDao.getCurrentlySyncingLatestTimestamp() - suspend fun setCurrentlySyncingOn(feedId: Long, syncing: Boolean, lastSync: Instant? = null) { + suspend fun setCurrentlySyncingOn( + feedId: Long, + syncing: Boolean, + lastSync: Instant? = null, + ) { if (lastSync != null) { feedDao.setCurrentlySyncingOn(feedId = feedId, syncing = syncing, lastSync = lastSync) } else { @@ -164,21 +173,21 @@ class FeedStore(override val di: DI) : DIAware { feedDao.deleteFeedWithUrl(url) } - suspend fun loadFeedIfStale(feedId: Long, staleTime: Long) = - feedDao.loadFeedIfStale(feedId = feedId, staleTime = staleTime) + suspend fun loadFeedIfStale( + feedId: Long, + staleTime: Long, + ) = feedDao.loadFeedIfStale(feedId = feedId, staleTime = staleTime) - suspend fun loadFeed(feedId: Long): Feed? = - feedDao.loadFeed(feedId = feedId) + suspend fun loadFeed(feedId: Long): Feed? = feedDao.loadFeed(feedId = feedId) - suspend fun loadFeedsIfStale(tag: String, staleTime: Long) = - feedDao.loadFeedsIfStale(tag = tag, staleTime = staleTime) + suspend fun loadFeedsIfStale( + tag: String, + staleTime: Long, + ) = feedDao.loadFeedsIfStale(tag = tag, staleTime = staleTime) - suspend fun loadFeedsIfStale(staleTime: Long) = - feedDao.loadFeedsIfStale(staleTime = staleTime) + suspend fun loadFeedsIfStale(staleTime: Long) = feedDao.loadFeedsIfStale(staleTime = staleTime) - suspend fun loadFeeds(tag: String) = - feedDao.loadFeeds(tag = tag) + suspend fun loadFeeds(tag: String) = feedDao.loadFeeds(tag = tag) - suspend fun loadFeeds() = - feedDao.loadFeeds() + suspend fun loadFeeds() = feedDao.loadFeeds() } diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt index 61f3857807..a3a80b156d 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/Repository.kt @@ -37,10 +37,6 @@ import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.addDynamicShortcutToFeed import com.nononsenseapps.feeder.util.logDebug import com.nononsenseapps.feeder.util.reportShortcutToFeedUsed -import java.net.URL -import java.time.Instant -import java.time.ZonedDateTime -import java.util.concurrent.TimeUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -52,6 +48,10 @@ import kotlinx.coroutines.launch import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.net.URL +import java.time.Instant +import java.time.ZonedDateTime +import java.util.concurrent.TimeUnit @OptIn(ExperimentalCoroutinesApi::class) class Repository(override val di: DI) : DIAware { @@ -73,12 +73,13 @@ class Repository(override val di: DI) : DIAware { private fun addFeederNewsIfInitialStart() { if (!settingsStore.addedFeederNews.value) { applicationCoroutineScope.launch { - val feedId = feedStore.upsertFeed( - Feed( - title = "Feeder News", - url = URL("https://news.nononsenseapps.com/index.atom"), - ), - ) + val feedId = + feedStore.upsertFeed( + Feed( + title = "Feeder News", + url = URL("https://news.nononsenseapps.com/index.atom"), + ), + ) settingsStore.setAddedFeederNews(true) requestFeedSync( di = di, @@ -89,10 +90,15 @@ class Repository(override val di: DI) : DIAware { } val minReadTime: StateFlow = settingsStore.minReadTime + fun setMinReadTime(value: Instant) = settingsStore.setMinReadTime(value) val currentFeedAndTag: StateFlow> = settingsStore.currentFeedAndTag - fun setCurrentFeedAndTag(feedId: Long, tag: String) { + + fun setCurrentFeedAndTag( + feedId: Long, + tag: String, + ) { if (feedId > ID_UNSET) { applicationCoroutineScope.launch { application.apply { @@ -112,21 +118,25 @@ class Repository(override val di: DI) : DIAware { } val isArticleOpen: StateFlow = settingsStore.isArticleOpen + fun setIsArticleOpen(open: Boolean) { settingsStore.setIsArticleOpen(open) } val isMarkAsReadOnScroll: StateFlow = settingsStore.isMarkAsReadOnScroll + fun setIsMarkAsReadOnScroll(value: Boolean) { settingsStore.setIsMarkAsReadOnScroll(value) } val maxLines: StateFlow = settingsStore.maxLines + fun setMaxLines(value: Int) { settingsStore.setMaxLines(value.coerceAtLeast(1)) } val showOnlyTitle: StateFlow = settingsStore.showOnlyTitle + fun setShowOnlyTitles(value: Boolean) { settingsStore.setShowOnlyTitles(value) } @@ -154,62 +164,73 @@ class Repository(override val di: DI) : DIAware { } val currentArticleId: StateFlow = settingsStore.currentArticleId - fun setCurrentArticle(articleId: Long) = - settingsStore.setCurrentArticle(articleId) + + fun setCurrentArticle(articleId: Long) = settingsStore.setCurrentArticle(articleId) val currentTheme: StateFlow = settingsStore.currentTheme + fun setCurrentTheme(value: ThemeOptions) = settingsStore.setCurrentTheme(value) val preferredDarkTheme: StateFlow = settingsStore.darkThemePreference - fun setPreferredDarkTheme(value: DarkThemePreferences) = - settingsStore.setDarkThemePreference(value) + + fun setPreferredDarkTheme(value: DarkThemePreferences) = settingsStore.setDarkThemePreference(value) val blockList: Flow> = settingsStore.blockListPreference - suspend fun addBlocklistPattern(pattern: String) = - settingsStore.addBlocklistPattern(pattern) + suspend fun addBlocklistPattern(pattern: String) = settingsStore.addBlocklistPattern(pattern) - suspend fun removeBlocklistPattern(pattern: String) = - settingsStore.removeBlocklistPattern(pattern) + suspend fun removeBlocklistPattern(pattern: String) = settingsStore.removeBlocklistPattern(pattern) val currentSorting: StateFlow = settingsStore.currentSorting + fun setCurrentSorting(value: SortingOptions) = settingsStore.setCurrentSorting(value) val showFab: StateFlow = settingsStore.showFab + fun setShowFab(value: Boolean) = settingsStore.setShowFab(value) val feedItemStyle: StateFlow = settingsStore.feedItemStyle + fun setFeedItemStyle(value: FeedItemStyle) = settingsStore.setFeedItemStyle(value) val swipeAsRead: StateFlow = settingsStore.swipeAsRead + fun setSwipeAsRead(value: SwipeAsRead) = settingsStore.setSwipeAsRead(value) val syncOnResume: StateFlow = settingsStore.syncOnResume + fun setSyncOnResume(value: Boolean) = settingsStore.setSyncOnResume(value) val syncOnlyOnWifi: StateFlow = settingsStore.syncOnlyOnWifi + fun setSyncOnlyOnWifi(value: Boolean) = settingsStore.setSyncOnlyOnWifi(value) val syncOnlyWhenCharging: StateFlow = settingsStore.syncOnlyWhenCharging - fun setSyncOnlyWhenCharging(value: Boolean) = - settingsStore.setSyncOnlyWhenCharging(value) + + fun setSyncOnlyWhenCharging(value: Boolean) = settingsStore.setSyncOnlyWhenCharging(value) val loadImageOnlyOnWifi = settingsStore.loadImageOnlyOnWifi + fun setLoadImageOnlyOnWifi(value: Boolean) = settingsStore.setLoadImageOnlyOnWifi(value) val showThumbnails = settingsStore.showThumbnails + fun setShowThumbnails(value: Boolean) = settingsStore.setShowThumbnails(value) val useDetectLanguage = settingsStore.useDetectLanguage + fun setUseDetectLanguage(value: Boolean) = settingsStore.setUseDetectLanguage(value) val useDynamicTheme = settingsStore.useDynamicTheme + fun setUseDynamicTheme(value: Boolean) = settingsStore.setUseDynamicTheme(value) val textScale = settingsStore.textScale + fun setTextScale(value: Float) = settingsStore.setTextScale(value) val maximumCountPerFeed = settingsStore.maximumCountPerFeed + fun setMaxCountPerFeed(value: Int) = settingsStore.setMaxCountPerFeed(value) val itemOpener @@ -218,12 +239,15 @@ class Repository(override val di: DI) : DIAware { fun setItemOpener(value: ItemOpener) = settingsStore.setItemOpener(value) val linkOpener = settingsStore.linkOpener + fun setLinkOpener(value: LinkOpener) = settingsStore.setLinkOpener(value) val syncFrequency = settingsStore.syncFrequency + fun setSyncFrequency(value: SyncFrequency) = settingsStore.setSyncFrequency(value) val resumeTime: StateFlow = sessionStore.resumeTime + fun setResumeTime(value: Instant) { sessionStore.setResumeTime(value) } @@ -239,84 +263,96 @@ class Repository(override val di: DI) : DIAware { } @OptIn(ExperimentalCoroutinesApi::class) - fun getCurrentFeedListItems(): Flow> = combine( - currentFeedAndTag, - minReadTime, - currentSorting, - feedListFilter, - ) { feedAndTag, minReadTime, currentSorting, feedListFilter -> - val (feedId, tag) = feedAndTag - FeedListArgs( - feedId = feedId, - tag = tag, - minReadTime = when (feedId) { - ID_SAVED_ARTICLES -> Instant.EPOCH - else -> minReadTime - }, - newestFirst = currentSorting == SortingOptions.NEWEST_FIRST, - filter = feedListFilter, - ) - }.flatMapLatest { - feedItemStore.getPagedFeedItemsRaw( - feedId = it.feedId, - tag = it.tag, - minReadTime = it.minReadTime, - newestFirst = it.newestFirst, - filter = it.filter, - ) - } + fun getCurrentFeedListItems(): Flow> = + combine( + currentFeedAndTag, + minReadTime, + currentSorting, + feedListFilter, + ) { feedAndTag, minReadTime, currentSorting, feedListFilter -> + val (feedId, tag) = feedAndTag + FeedListArgs( + feedId = feedId, + tag = tag, + minReadTime = + when (feedId) { + ID_SAVED_ARTICLES -> Instant.EPOCH + else -> minReadTime + }, + newestFirst = currentSorting == SortingOptions.NEWEST_FIRST, + filter = feedListFilter, + ) + }.flatMapLatest { + feedItemStore.getPagedFeedItemsRaw( + feedId = it.feedId, + tag = it.tag, + minReadTime = it.minReadTime, + newestFirst = it.newestFirst, + filter = it.filter, + ) + } @OptIn(ExperimentalCoroutinesApi::class) - fun getCurrentFeedListVisibleItemCount(): Flow = combine( - currentFeedAndTag, - minReadTime, - feedListFilter, - ) { feedAndTag, minReadTime, feedListFilter -> - val (feedId, tag) = feedAndTag - FeedListArgs( - feedId = feedId, - tag = tag, - minReadTime = when (feedId) { - ID_SAVED_ARTICLES -> Instant.EPOCH - else -> minReadTime - }, - newestFirst = false, - filter = feedListFilter, - ) - }.flatMapLatest { - feedItemStore.getFeedItemCountRaw( - feedId = it.feedId, - tag = it.tag, - minReadTime = it.minReadTime, - filter = it.filter, - ) - } - - val currentArticle: Flow
= currentArticleId - .flatMapLatest { itemId -> - feedItemStore.getFeedItem(itemId) - } - .mapLatest { item -> - Article(item = item) + fun getCurrentFeedListVisibleItemCount(): Flow = + combine( + currentFeedAndTag, + minReadTime, + feedListFilter, + ) { feedAndTag, minReadTime, feedListFilter -> + val (feedId, tag) = feedAndTag + FeedListArgs( + feedId = feedId, + tag = tag, + minReadTime = + when (feedId) { + ID_SAVED_ARTICLES -> Instant.EPOCH + else -> minReadTime + }, + newestFirst = false, + filter = feedListFilter, + ) + }.flatMapLatest { + feedItemStore.getFeedItemCountRaw( + feedId = it.feedId, + tag = it.tag, + minReadTime = it.minReadTime, + filter = it.filter, + ) } + val currentArticle: Flow
= + currentArticleId + .flatMapLatest { itemId -> + feedItemStore.getFeedItem(itemId) + } + .mapLatest { item -> + Article(item = item) + } + suspend fun getFeed(feedId: Long): Feed? = feedStore.getFeed(feedId) suspend fun getFeed(url: URL): Feed? = feedStore.getFeed(url) suspend fun saveFeed(feed: Feed): Long = feedStore.saveFeed(feed) - suspend fun setBookmarked(itemId: Long, bookmarked: Boolean) = - feedItemStore.setBookmarked(itemId = itemId, bookmarked = bookmarked) + suspend fun setBookmarked( + itemId: Long, + bookmarked: Boolean, + ) = feedItemStore.setBookmarked(itemId = itemId, bookmarked = bookmarked) suspend fun markAsNotified(itemIds: List) = feedItemStore.markAsNotified(itemIds) - suspend fun toggleNotifications(feedId: Long, value: Boolean) = - feedStore.toggleNotifications(feedId, value) + suspend fun toggleNotifications( + feedId: Long, + value: Boolean, + ) = feedStore.toggleNotifications(feedId, value) val feedNotificationSettings: Flow> = feedStore.feedForSettings - suspend fun markAsReadAndNotified(itemId: Long, readTimeBeforeMinReadTime: Boolean = false) { + suspend fun markAsReadAndNotified( + itemId: Long, + readTimeBeforeMinReadTime: Boolean = false, + ) { minReadTime.value.let { minReadTimeValue -> if (readTimeBeforeMinReadTime && minReadTimeValue.isAfter(Instant.EPOCH)) { // If read time is not EPOCH, one second before so swipe can get rid of it @@ -352,20 +388,25 @@ class Repository(override val di: DI) : DIAware { else -> itemOpener.value // Global default } - fun getScreenTitleForFeedOrTag(feedId: Long, tag: String) = flow { + fun getScreenTitleForFeedOrTag( + feedId: Long, + tag: String, + ) = flow { emit( ScreenTitle( - title = when { - feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) - tag.isNotBlank() -> tag - else -> null - }, - type = when (feedId) { - ID_UNSET -> FeedType.TAG - ID_ALL_FEEDS -> FeedType.ALL_FEEDS - ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES - else -> FeedType.FEED - }, + title = + when { + feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) + tag.isNotBlank() -> tag + else -> null + }, + type = + when (feedId) { + ID_UNSET -> FeedType.TAG + ID_ALL_FEEDS -> FeedType.ALL_FEEDS + ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES + else -> FeedType.FEED + }, ), ) } @@ -374,17 +415,19 @@ class Repository(override val di: DI) : DIAware { fun getScreenTitleForCurrentFeedOrTag(): Flow = currentFeedAndTag.mapLatest { (feedId, tag) -> ScreenTitle( - title = when { - feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) - tag.isNotBlank() -> tag - else -> null - }, - type = when (feedId) { - ID_UNSET -> FeedType.TAG - ID_ALL_FEEDS -> FeedType.ALL_FEEDS - ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES - else -> FeedType.FEED - }, + title = + when { + feedId > ID_UNSET -> feedStore.getDisplayTitle(feedId) + tag.isNotBlank() -> tag + else -> null + }, + type = + when (feedId) { + ID_UNSET -> FeedType.TAG + ID_ALL_FEEDS -> FeedType.ALL_FEEDS + ID_SAVED_ARTICLES -> FeedType.SAVED_ARTICLES + else -> FeedType.FEED + }, ) } @@ -396,7 +439,10 @@ class Repository(override val di: DI) : DIAware { } } - suspend fun markAllAsReadInFeedOrTag(feedId: Long, tag: String) { + suspend fun markAllAsReadInFeedOrTag( + feedId: Long, + tag: String, + ) { when { feedId > ID_UNSET -> feedItemStore.markAllAsReadInFeed(feedId) tag.isNotBlank() -> feedItemStore.markAllAsReadInTag(tag) @@ -406,7 +452,11 @@ class Repository(override val di: DI) : DIAware { setMinReadTime(Instant.now()) } - suspend fun markBeforeAsRead(cursor: FeedItemCursor, feedId: Long, tag: String) { + suspend fun markBeforeAsRead( + cursor: FeedItemCursor, + feedId: Long, + tag: String, + ) { feedItemStore.markAsReadRaw( feedId = feedId, tag = tag, @@ -418,7 +468,11 @@ class Repository(override val di: DI) : DIAware { scheduleSendRead() } - suspend fun markAfterAsRead(cursor: FeedItemCursor, feedId: Long, tag: String) { + suspend fun markAfterAsRead( + cursor: FeedItemCursor, + feedId: Long, + tag: String, + ) { feedItemStore.markAsReadRaw( feedId = feedId, tag = tag, @@ -436,12 +490,13 @@ class Repository(override val di: DI) : DIAware { feedStore.drawerItemsWithUnreadCounts val getUnreadBookmarksCount - get() = feedItemStore.getFeedItemCountRaw( - feedId = ID_SAVED_ARTICLES, - tag = "", - minReadTime = Instant.EPOCH, - filter = emptyFeedListFilter, - ) + get() = + feedItemStore.getFeedItemCountRaw( + feedId = ID_SAVED_ARTICLES, + tag = "", + minReadTime = Instant.EPOCH, + filter = emptyFeedListFilter, + ) @OptIn(ExperimentalCoroutinesApi::class) fun getCurrentlyVisibleFeedTitles(): Flow> = @@ -453,20 +508,21 @@ class Repository(override val di: DI) : DIAware { fun toggleTagExpansion(tag: String) = sessionStore.toggleTagExpansion(tag) - fun ensurePeriodicSyncConfigured() = - settingsStore.configurePeriodicSync(replace = false) + fun ensurePeriodicSyncConfigured() = settingsStore.configurePeriodicSync(replace = false) fun getFeedsItemsWithDefaultFullTextNeedingDownload(): Flow> = feedItemStore.getFeedsItemsWithDefaultFullTextNeedingDownload() - suspend fun markAsFullTextDownloaded(feedItemId: Long) = - feedItemStore.markAsFullTextDownloaded(feedItemId) + suspend fun markAsFullTextDownloaded(feedItemId: Long) = feedItemStore.markAsFullTextDownloaded(feedItemId) fun getFeedItemsNeedingNotifying(): Flow> { return feedItemStore.getFeedItemsNeedingNotifying() } - suspend fun remoteMarkAsRead(feedUrl: URL, articleGuid: String) { + suspend fun remoteMarkAsRead( + feedUrl: URL, + articleGuid: String, + ) { // Always write a remoteReadMark - this is part of concurrency mitigation syncRemoteStore.addRemoteReadMark(feedUrl = feedUrl, articleGuid = articleGuid) // But also try to get an existing ID and set @@ -500,11 +556,12 @@ class Repository(override val di: DI) : DIAware { syncRemoteStore.setSynced(feedItemId) } - suspend fun upsertFeed(feedSql: Feed) = - feedStore.upsertFeed(feedSql) + suspend fun upsertFeed(feedSql: Feed) = feedStore.upsertFeed(feedSql) - suspend fun loadFeedItem(guid: String, feedId: Long): FeedItem? = - feedItemStore.loadFeedItem(guid = guid, feedId = feedId) + suspend fun loadFeedItem( + guid: String, + feedId: Long, + ): FeedItem? = feedItemStore.loadFeedItem(guid = guid, feedId = feedId) suspend fun upsertFeedItems( itemsWithText: List>, @@ -513,8 +570,10 @@ class Repository(override val di: DI) : DIAware { feedItemStore.upsertFeedItems(itemsWithText, block) } - suspend fun getItemsToBeCleanedFromFeed(feedId: Long, keepCount: Int) = - feedItemStore.getItemsToBeCleanedFromFeed(feedId = feedId, keepCount = keepCount) + suspend fun getItemsToBeCleanedFromFeed( + feedId: Long, + keepCount: Int, + ) = feedItemStore.getItemsToBeCleanedFromFeed(feedId = feedId, keepCount = keepCount) suspend fun deleteFeedItems(ids: List) { feedItemStore.deleteFeedItems(ids) @@ -524,8 +583,7 @@ class Repository(override val di: DI) : DIAware { syncRemoteStore.deleteStaleRemoteReadMarks(Instant.now()) } - suspend fun getGuidsWhichAreSyncedAsReadInFeed(feed: Feed) = - syncRemoteStore.getGuidsWhichAreSyncedAsReadInFeed(feed.url) + suspend fun getGuidsWhichAreSyncedAsReadInFeed(feed: Feed) = syncRemoteStore.getGuidsWhichAreSyncedAsReadInFeed(feed.url) suspend fun applyRemoteReadMarks() { val toBeApplied = syncRemoteStore.getRemoteReadMarksReadyToBeApplied() @@ -569,7 +627,10 @@ class Repository(override val di: DI) : DIAware { return syncClient.getDevices() } - suspend fun joinSyncChain(syncCode: String, secretKey: String): Either { + suspend fun joinSyncChain( + syncCode: String, + secretKey: String, + ): Either { return syncClient.join(syncCode = syncCode, remoteSecretKey = secretKey) .onRight { syncClient.getDevices() @@ -603,9 +664,10 @@ class Repository(override val di: DI) : DIAware { private fun scheduleSendRead() { logDebug(LOG_TAG, "Scheduling work") - val constraints = Constraints.Builder() - // This prevents expedited if true - .setRequiresCharging(syncOnlyWhenCharging.value) + val constraints = + Constraints.Builder() + // This prevents expedited if true + .setRequiresCharging(syncOnlyWhenCharging.value) if (syncOnlyOnWifi.value) { constraints.setRequiredNetworkType(NetworkType.UNMETERED) @@ -613,11 +675,12 @@ class Repository(override val di: DI) : DIAware { constraints.setRequiredNetworkType(NetworkType.CONNECTED) } - val workRequest = OneTimeWorkRequestBuilder() - .addTag("feeder") - .keepResultsForAtLeast(5, TimeUnit.MINUTES) - .setConstraints(constraints.build()) - .setInitialDelay(10, TimeUnit.SECONDS) + val workRequest = + OneTimeWorkRequestBuilder() + .addTag("feeder") + .keepResultsForAtLeast(5, TimeUnit.MINUTES) + .setConstraints(constraints.build()) + .setInitialDelay(10, TimeUnit.SECONDS) workManager.enqueueUniqueWork( SyncServiceSendReadWorker.UNIQUE_SENDREAD_NAME, @@ -626,38 +689,46 @@ class Repository(override val di: DI) : DIAware { ) } - suspend fun loadFeedIfStale(feedId: Long, staleTime: Long) = - feedStore.loadFeedIfStale(feedId = feedId, staleTime = staleTime) + suspend fun loadFeedIfStale( + feedId: Long, + staleTime: Long, + ) = feedStore.loadFeedIfStale(feedId = feedId, staleTime = staleTime) - suspend fun loadFeed(feedId: Long): Feed? = - feedStore.loadFeed(feedId = feedId) + suspend fun loadFeed(feedId: Long): Feed? = feedStore.loadFeed(feedId = feedId) - suspend fun loadFeedsIfStale(tag: String, staleTime: Long) = - feedStore.loadFeedsIfStale(tag = tag, staleTime = staleTime) + suspend fun loadFeedsIfStale( + tag: String, + staleTime: Long, + ) = feedStore.loadFeedsIfStale(tag = tag, staleTime = staleTime) - suspend fun loadFeedsIfStale(staleTime: Long) = - feedStore.loadFeedsIfStale(staleTime = staleTime) + suspend fun loadFeedsIfStale(staleTime: Long) = feedStore.loadFeedsIfStale(staleTime = staleTime) - suspend fun loadFeeds(tag: String): List = - feedStore.loadFeeds(tag = tag) + suspend fun loadFeeds(tag: String): List = feedStore.loadFeeds(tag = tag) - suspend fun loadFeeds(): List = - feedStore.loadFeeds() + suspend fun loadFeeds(): List = feedStore.loadFeeds() - suspend fun setCurrentlySyncingOn(feedId: Long, syncing: Boolean, lastSync: Instant? = null) = - feedStore.setCurrentlySyncingOn(feedId = feedId, syncing = syncing, lastSync = lastSync) + suspend fun setCurrentlySyncingOn( + feedId: Long, + syncing: Boolean, + lastSync: Instant? = null, + ) = feedStore.setCurrentlySyncingOn(feedId = feedId, syncing = syncing, lastSync = lastSync) val isOpenAdjacent: StateFlow = settingsStore.openAdjacent + fun setOpenAdjacent(value: Boolean) { settingsStore.setOpenAdjacent(value) } val showReadingTime: StateFlow = settingsStore.showReadingTime + fun setShowReadingTime(value: Boolean) { settingsStore.setShowReadingTime(value) } - suspend fun updateWordCountFull(id: Long, wordCount: Int) { + suspend fun updateWordCountFull( + id: Long, + wordCount: Int, + ) { feedItemStore.updateWordCountFull(id, wordCount) } @@ -707,16 +778,17 @@ data class Article( val link: String? = item?.link val feedDisplayTitle: String = item?.feedDisplayTitle ?: "" val title: String = item?.plainTitle ?: "" - val enclosure: Enclosure = item?.enclosureLink?.let { link -> - Enclosure( - present = true, - link = link, - name = item.enclosureFilename ?: "", - type = item.enclosureType ?: "", + val enclosure: Enclosure = + item?.enclosureLink?.let { link -> + Enclosure( + present = true, + link = link, + name = item.enclosureFilename ?: "", + type = item.enclosureType ?: "", + ) + } ?: Enclosure( + present = false, ) - } ?: Enclosure( - present = false, - ) val author: String? = item?.author val pubDate: ZonedDateTime? = item?.pubDate val feedId: Long = item?.feedId ?: ID_UNSET diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SessionStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SessionStore.kt index e192a097b8..3884182138 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SessionStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SessionStore.kt @@ -1,10 +1,10 @@ package com.nononsenseapps.feeder.archmodel -import java.time.Instant import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import java.time.Instant class SessionStore { private val _resumeTime = MutableStateFlow(Instant.EPOCH) @@ -14,6 +14,7 @@ class SessionStore { * activity returns to the foreground */ val resumeTime: StateFlow = _resumeTime.asStateFlow() + fun setResumeTime(instant: Instant) { _resumeTime.update { instant @@ -22,6 +23,7 @@ class SessionStore { private val _expandedTags = MutableStateFlow(emptySet()) val expandedTags: StateFlow> = _expandedTags.asStateFlow() + fun toggleTagExpansion(tag: String) { _expandedTags.update { if (tag in expandedTags.value) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt index aeaab64245..35233b9ed4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SettingsStore.kt @@ -18,8 +18,6 @@ import com.nononsenseapps.feeder.model.workmanager.oldPeriodics import com.nononsenseapps.feeder.ui.compose.feedarticle.FeedListFilter import com.nononsenseapps.feeder.util.PREF_MAX_ITEM_COUNT_PER_FEED import com.nononsenseapps.feeder.util.getStringNonNull -import java.time.Instant -import java.util.concurrent.TimeUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +28,8 @@ import kotlinx.coroutines.flow.update import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.time.Instant +import java.util.concurrent.TimeUnit @OptIn(ExperimentalCoroutinesApi::class) class SettingsStore(override val di: DI) : DIAware { @@ -38,29 +38,36 @@ class SettingsStore(override val di: DI) : DIAware { private val _addedFeederNews = MutableStateFlow(sp.getBoolean(PREF_ADDED_FEEDER_NEWS, false)) val addedFeederNews: StateFlow = _addedFeederNews.asStateFlow() + fun setAddedFeederNews(value: Boolean) { sp.edit().putBoolean(PREF_ADDED_FEEDER_NEWS, value).apply() _addedFeederNews.value = value } - private val _minReadTime: MutableStateFlow = MutableStateFlow( - // by design - min read time is never written to disk - Instant.now(), - ) + private val _minReadTime: MutableStateFlow = + MutableStateFlow( + // by design - min read time is never written to disk + Instant.now(), + ) val minReadTime: StateFlow = _minReadTime.asStateFlow() + fun setMinReadTime(value: Instant) { _minReadTime.value = value } - private val _currentFeedAndTag = MutableStateFlow( - sp.getLong(PREF_LAST_FEED_ID, ID_UNSET) to (sp.getString(PREF_LAST_FEED_TAG, null) ?: ""), - ) + private val _currentFeedAndTag = + MutableStateFlow( + sp.getLong(PREF_LAST_FEED_ID, ID_UNSET) to (sp.getString(PREF_LAST_FEED_TAG, null) ?: ""), + ) val currentFeedAndTag = _currentFeedAndTag.asStateFlow() /** * Returns true if the parameters were different from current state */ - fun setCurrentFeedAndTag(feedId: Long, tag: String): Boolean = + fun setCurrentFeedAndTag( + feedId: Long, + tag: String, + ): Boolean = if (_currentFeedAndTag.value.first != feedId || _currentFeedAndTag.value.second != tag ) { @@ -72,37 +79,45 @@ class SettingsStore(override val di: DI) : DIAware { false } - private val _currentArticle = MutableStateFlow( - sp.getLong(PREF_LAST_ARTICLE_ID, ID_UNSET), - ) + private val _currentArticle = + MutableStateFlow( + sp.getLong(PREF_LAST_ARTICLE_ID, ID_UNSET), + ) val currentArticleId = _currentArticle.asStateFlow() + fun setCurrentArticle(articleId: Long) { _currentArticle.value = articleId sp.edit().putLong(PREF_LAST_ARTICLE_ID, articleId).apply() } - private val _isArticleOpen = MutableStateFlow( - sp.getBoolean(PREF_IS_ARTICLE_OPEN, false), - ) + private val _isArticleOpen = + MutableStateFlow( + sp.getBoolean(PREF_IS_ARTICLE_OPEN, false), + ) val isArticleOpen: StateFlow = _isArticleOpen.asStateFlow() + fun setIsArticleOpen(open: Boolean) { _isArticleOpen.update { open } sp.edit().putBoolean(PREF_IS_ARTICLE_OPEN, open).apply() } - private val _isMarkAsReadOnScroll = MutableStateFlow( - sp.getBoolean(PREF_IS_MARK_AS_READ_ON_SCROLL, false), - ) + private val _isMarkAsReadOnScroll = + MutableStateFlow( + sp.getBoolean(PREF_IS_MARK_AS_READ_ON_SCROLL, false), + ) val isMarkAsReadOnScroll: StateFlow = _isMarkAsReadOnScroll.asStateFlow() + fun setIsMarkAsReadOnScroll(open: Boolean) { _isMarkAsReadOnScroll.update { open } sp.edit().putBoolean(PREF_IS_MARK_AS_READ_ON_SCROLL, open).apply() } - private val _maxLines = MutableStateFlow( - sp.getInt(PREF_MAX_LINES, 2), - ) + private val _maxLines = + MutableStateFlow( + sp.getInt(PREF_MAX_LINES, 2), + ) val maxLines: StateFlow = _maxLines.asStateFlow() + fun setMaxLines(value: Int) { if (value > 0) { _maxLines.update { value } @@ -110,27 +125,32 @@ class SettingsStore(override val di: DI) : DIAware { } } - private val _showOnlyTitle = MutableStateFlow( - sp.getBoolean(PREF_LIST_SHOW_ONLY_TITLES, false), - ) + private val _showOnlyTitle = + MutableStateFlow( + sp.getBoolean(PREF_LIST_SHOW_ONLY_TITLES, false), + ) val showOnlyTitle: StateFlow = _showOnlyTitle.asStateFlow() + fun setShowOnlyTitles(value: Boolean) { _showOnlyTitle.update { value } sp.edit().putBoolean(PREF_LIST_SHOW_ONLY_TITLES, value).apply() } - private val _feedListFilter = MutableStateFlow( - PrefsFeedListFilter( - saved = sp.getBoolean(PREFS_FILTER_SAVED, false), - recentlyRead = sp.getBoolean(PREFS_FILTER_RECENTLY_READ, true), - read = sp.getBoolean( - PREFS_FILTER_READ, - // Migration - !sp.getBoolean("pref_show_only_unread", true), + private val _feedListFilter = + MutableStateFlow( + PrefsFeedListFilter( + saved = sp.getBoolean(PREFS_FILTER_SAVED, false), + recentlyRead = sp.getBoolean(PREFS_FILTER_RECENTLY_READ, true), + read = + sp.getBoolean( + PREFS_FILTER_READ, + // Migration + !sp.getBoolean("pref_show_only_unread", true), + ), ), - ), - ) + ) val feedListFilter: StateFlow = _feedListFilter.asStateFlow() + fun setFeedListFilterSaved(value: Boolean) { _feedListFilter.update { it.copy(saved = value) } sp.edit().putBoolean(PREFS_FILTER_SAVED, value).apply() @@ -146,37 +166,43 @@ class SettingsStore(override val di: DI) : DIAware { sp.edit().putBoolean(PREFS_FILTER_READ, value).apply() } - private val _currentTheme = MutableStateFlow( - themeOptionsFromString( - sp.getString(PREF_THEME, null)?.uppercase() - ?: ThemeOptions.SYSTEM.name, - ), - ) + private val _currentTheme = + MutableStateFlow( + themeOptionsFromString( + sp.getString(PREF_THEME, null)?.uppercase() + ?: ThemeOptions.SYSTEM.name, + ), + ) val currentTheme = _currentTheme.asStateFlow() + fun setCurrentTheme(value: ThemeOptions) { _currentTheme.value = value sp.edit().putString(PREF_THEME, value.name.lowercase()).apply() } - private val _darkThemePreference = MutableStateFlow( - darkThemePreferenceFromString( - sp.getString(PREF_DARK_THEME, null)?.uppercase() - ?: DarkThemePreferences.BLACK.name, - ), - ) + private val _darkThemePreference = + MutableStateFlow( + darkThemePreferenceFromString( + sp.getString(PREF_DARK_THEME, null)?.uppercase() + ?: DarkThemePreferences.BLACK.name, + ), + ) val darkThemePreference = _darkThemePreference.asStateFlow() + fun setDarkThemePreference(value: DarkThemePreferences) { _darkThemePreference.value = value sp.edit().putString(PREF_DARK_THEME, value.name.lowercase()).apply() } - private val _currentSorting = MutableStateFlow( - sortingOptionsFromString( - sp.getString(PREF_SORT, null)?.uppercase() - ?: SortingOptions.NEWEST_FIRST.name, - ), - ) + private val _currentSorting = + MutableStateFlow( + sortingOptionsFromString( + sp.getString(PREF_SORT, null)?.uppercase() + ?: SortingOptions.NEWEST_FIRST.name, + ), + ) val currentSorting = _currentSorting.asStateFlow() + fun setCurrentSorting(value: SortingOptions) { _currentSorting.value = value sp.edit().putString(PREF_SORT, value.name.lowercase()).apply() @@ -184,6 +210,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _showFab = MutableStateFlow(sp.getBoolean(PREF_SHOW_FAB, true)) val showFab = _showFab.asStateFlow() + fun setShowFab(value: Boolean) { _showFab.value = value sp.edit().putBoolean(PREF_SHOW_FAB, value).apply() @@ -191,6 +218,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _syncOnResume = MutableStateFlow(sp.getBoolean(PREF_SYNC_ON_RESUME, false)) val syncOnResume = _syncOnResume.asStateFlow() + fun setSyncOnResume(value: Boolean) { _syncOnResume.value = value sp.edit().putBoolean(PREF_SYNC_ON_RESUME, value).apply() @@ -198,6 +226,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _syncOnlyOnWifi = MutableStateFlow(sp.getBoolean(PREF_SYNC_ONLY_WIFI, false)) val syncOnlyOnWifi = _syncOnlyOnWifi.asStateFlow() + fun setSyncOnlyOnWifi(value: Boolean) { _syncOnlyOnWifi.value = value sp.edit().putBoolean(PREF_SYNC_ONLY_WIFI, value).apply() @@ -207,6 +236,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _syncOnlyWhenCharging = MutableStateFlow(sp.getBoolean(PREF_SYNC_ONLY_CHARGING, false)) val syncOnlyWhenCharging = _syncOnlyWhenCharging.asStateFlow() + fun setSyncOnlyWhenCharging(value: Boolean) { _syncOnlyWhenCharging.value = value sp.edit().putBoolean(PREF_SYNC_ONLY_CHARGING, value).apply() @@ -215,6 +245,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _loadImageOnlyOnWifi = MutableStateFlow(sp.getBoolean(PREF_IMG_ONLY_WIFI, false)) val loadImageOnlyOnWifi = _loadImageOnlyOnWifi.asStateFlow() + fun setLoadImageOnlyOnWifi(value: Boolean) { _loadImageOnlyOnWifi.value = value sp.edit().putBoolean(PREF_IMG_ONLY_WIFI, value).apply() @@ -222,6 +253,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _showThumbnails = MutableStateFlow(sp.getBoolean(PREF_IMG_SHOW_THUMBNAILS, true)) val showThumbnails = _showThumbnails.asStateFlow() + fun setShowThumbnails(value: Boolean) { _showThumbnails.value = value sp.edit().putBoolean(PREF_IMG_SHOW_THUMBNAILS, value).apply() @@ -230,6 +262,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _useDetectLanguage = MutableStateFlow(sp.getBoolean(PREF_READALOUD_USE_DETECT_LANGUAGE, true)) val useDetectLanguage = _useDetectLanguage.asStateFlow() + fun setUseDetectLanguage(value: Boolean) { _useDetectLanguage.value = value sp.edit().putBoolean(PREF_READALOUD_USE_DETECT_LANGUAGE, value).apply() @@ -237,12 +270,14 @@ class SettingsStore(override val di: DI) : DIAware { private val _useDynamicTheme = MutableStateFlow( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && sp.getBoolean( - PREF_DYNAMIC_THEME, - true, - ), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + sp.getBoolean( + PREF_DYNAMIC_THEME, + true, + ), ) val useDynamicTheme = _useDynamicTheme.asStateFlow() + fun setUseDynamicTheme(value: Boolean) { _useDynamicTheme.value = value && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S sp.edit().putBoolean(PREF_DYNAMIC_THEME, value).apply() @@ -256,6 +291,7 @@ class SettingsStore(override val di: DI) : DIAware { ), ) val textScale = _textScale.asStateFlow() + fun setTextScale(value: Float) { _textScale.value = value sp.edit().putFloat(PREF_TEXT_SCALE, value).apply() @@ -264,20 +300,23 @@ class SettingsStore(override val di: DI) : DIAware { private val _maximumCountPerFeed = MutableStateFlow(sp.getStringNonNull(PREF_MAX_ITEM_COUNT_PER_FEED, "100").toInt()) val maximumCountPerFeed = _maximumCountPerFeed.asStateFlow() + fun setMaxCountPerFeed(value: Int) { _maximumCountPerFeed.value = value sp.edit().putString(PREF_MAX_ITEM_COUNT_PER_FEED, "$value").apply() } - private val _itemOpener = MutableStateFlow( - itemOpenerFromString( - sp.getStringNonNull( - PREF_DEFAULT_OPEN_ITEM_WITH, - PREF_VAL_OPEN_WITH_READER, + private val _itemOpener = + MutableStateFlow( + itemOpenerFromString( + sp.getStringNonNull( + PREF_DEFAULT_OPEN_ITEM_WITH, + PREF_VAL_OPEN_WITH_READER, + ), ), - ), - ) + ) val itemOpener = _itemOpener.asStateFlow() + fun setItemOpener(value: ItemOpener) { _itemOpener.value = value sp.edit().putString( @@ -290,12 +329,14 @@ class SettingsStore(override val di: DI) : DIAware { ).apply() } - private val _linkOpener = MutableStateFlow( - linkOpenerFromString( - sp.getStringNonNull(PREF_OPEN_LINKS_WITH, PREF_VAL_OPEN_WITH_CUSTOM_TAB), - ), - ) + private val _linkOpener = + MutableStateFlow( + linkOpenerFromString( + sp.getStringNonNull(PREF_OPEN_LINKS_WITH, PREF_VAL_OPEN_WITH_CUSTOM_TAB), + ), + ) val linkOpener = _linkOpener.asStateFlow() + fun setLinkOpener(value: LinkOpener) { _linkOpener.value = value sp.edit().putString( @@ -309,6 +350,7 @@ class SettingsStore(override val di: DI) : DIAware { private val _openAdjacent = MutableStateFlow(sp.getBoolean(PREF_OPEN_ADJACENT, true)) val openAdjacent = _openAdjacent.asStateFlow() + fun setOpenAdjacent(value: Boolean) { _openAdjacent.value = value sp.edit().putBoolean(PREF_OPEN_ADJACENT, value).apply() @@ -316,15 +358,18 @@ class SettingsStore(override val di: DI) : DIAware { private val _showReadingTime = MutableStateFlow(sp.getBoolean(PREF_LIST_SHOW_READING_TIME, false)) val showReadingTime = _showReadingTime.asStateFlow() + fun setShowReadingTime(value: Boolean) { _showReadingTime.value = value sp.edit().putBoolean(PREF_LIST_SHOW_READING_TIME, value).apply() } - private val _feedItemStyle = MutableStateFlow( - feedItemStyleFromString(sp.getStringNonNull(PREF_FEED_ITEM_STYLE, FeedItemStyle.CARD.name)), - ) + private val _feedItemStyle = + MutableStateFlow( + feedItemStyleFromString(sp.getStringNonNull(PREF_FEED_ITEM_STYLE, FeedItemStyle.CARD.name)), + ) val feedItemStyle = _feedItemStyle.asStateFlow() + fun setFeedItemStyle(value: FeedItemStyle) { _feedItemStyle.value = value sp.edit().putString( @@ -333,12 +378,14 @@ class SettingsStore(override val di: DI) : DIAware { ).apply() } - private val _swipeAsRead = MutableStateFlow( - swipeAsReadFromString( - sp.getStringNonNull(PREF_SWIPE_AS_READ, SwipeAsRead.ONLY_FROM_END.name), - ), - ) + private val _swipeAsRead = + MutableStateFlow( + swipeAsReadFromString( + sp.getStringNonNull(PREF_SWIPE_AS_READ, SwipeAsRead.ONLY_FROM_END.name), + ), + ) val swipeAsRead = _swipeAsRead.asStateFlow() + fun setSwipeAsRead(value: SwipeAsRead) { _swipeAsRead.value = value sp.edit().putString( @@ -348,13 +395,14 @@ class SettingsStore(override val di: DI) : DIAware { } val blockListPreference: Flow> - get() = blocklistDao.getGlobPatterns() - .mapLatest { patterns -> - patterns.map { pattern -> - // Remove start and ending * - pattern.dropEnds(1, 1) + get() = + blocklistDao.getGlobPatterns() + .mapLatest { patterns -> + patterns.map { pattern -> + // Remove start and ending * + pattern.dropEnds(1, 1) + } } - } suspend fun removeBlocklistPattern(value: String) { blocklistDao.deletePattern(value) @@ -377,6 +425,7 @@ class SettingsStore(override val di: DI) : DIAware { ) } val syncFrequency = _syncFrequency.asStateFlow() + fun setSyncFrequency(value: SyncFrequency) { _syncFrequency.value = value sp.edit().putString(PREF_SYNC_FREQ, "${value.minutes}").apply() @@ -393,8 +442,9 @@ class SettingsStore(override val di: DI) : DIAware { } if (shouldSync) { - val constraints = Constraints.Builder() - .setRequiresCharging(syncOnlyWhenCharging.value) + val constraints = + Constraints.Builder() + .setRequiresCharging(syncOnlyWhenCharging.value) if (syncOnlyOnWifi.value) { constraints.setRequiredNetworkType(NetworkType.UNMETERED) @@ -404,15 +454,17 @@ class SettingsStore(override val di: DI) : DIAware { val timeInterval = syncFrequency.value.minutes - val workRequestBuilder = PeriodicWorkRequestBuilder( - timeInterval, - TimeUnit.MINUTES, - ) + val workRequestBuilder = + PeriodicWorkRequestBuilder( + timeInterval, + TimeUnit.MINUTES, + ) - val syncWork = workRequestBuilder - .setConstraints(constraints.build()) - .addTag("feeder") - .build() + val syncWork = + workRequestBuilder + .setConstraints(constraints.build()) + .addTag("feeder") + .build() workManager.enqueueUniquePeriodicWork( UNIQUE_PERIODIC_NAME, @@ -646,49 +698,56 @@ fun String.dropEnds( ) } -fun linkOpenerFromString(value: String): LinkOpener = when (value) { - PREF_VAL_OPEN_WITH_BROWSER -> LinkOpener.DEFAULT_BROWSER - else -> LinkOpener.CUSTOM_TAB -} +fun linkOpenerFromString(value: String): LinkOpener = + when (value) { + PREF_VAL_OPEN_WITH_BROWSER -> LinkOpener.DEFAULT_BROWSER + else -> LinkOpener.CUSTOM_TAB + } -fun itemOpenerFromString(value: String) = when (value) { - PREF_VAL_OPEN_WITH_BROWSER -> ItemOpener.DEFAULT_BROWSER - PREF_VAL_OPEN_WITH_WEBVIEW, - PREF_VAL_OPEN_WITH_CUSTOM_TAB, - -> ItemOpener.CUSTOM_TAB +fun itemOpenerFromString(value: String) = + when (value) { + PREF_VAL_OPEN_WITH_BROWSER -> ItemOpener.DEFAULT_BROWSER + PREF_VAL_OPEN_WITH_WEBVIEW, + PREF_VAL_OPEN_WITH_CUSTOM_TAB, + -> ItemOpener.CUSTOM_TAB - else -> ItemOpener.READER -} + else -> ItemOpener.READER + } -fun sortingOptionsFromString(value: String): SortingOptions = try { - SortingOptions.valueOf(value.uppercase()) -} catch (_: Exception) { - SortingOptions.NEWEST_FIRST -} +fun sortingOptionsFromString(value: String): SortingOptions = + try { + SortingOptions.valueOf(value.uppercase()) + } catch (_: Exception) { + SortingOptions.NEWEST_FIRST + } -fun themeOptionsFromString(value: String) = try { - ThemeOptions.valueOf(value.uppercase()) -} catch (_: Exception) { - ThemeOptions.SYSTEM -} +fun themeOptionsFromString(value: String) = + try { + ThemeOptions.valueOf(value.uppercase()) + } catch (_: Exception) { + ThemeOptions.SYSTEM + } -fun darkThemePreferenceFromString(value: String): DarkThemePreferences = try { - DarkThemePreferences.valueOf(value.uppercase()) -} catch (_: Exception) { - DarkThemePreferences.BLACK -} +fun darkThemePreferenceFromString(value: String): DarkThemePreferences = + try { + DarkThemePreferences.valueOf(value.uppercase()) + } catch (_: Exception) { + DarkThemePreferences.BLACK + } -fun swipeAsReadFromString(value: String): SwipeAsRead = try { - SwipeAsRead.valueOf(value.uppercase()) -} catch (_: Exception) { - SwipeAsRead.ONLY_FROM_END -} +fun swipeAsReadFromString(value: String): SwipeAsRead = + try { + SwipeAsRead.valueOf(value.uppercase()) + } catch (_: Exception) { + SwipeAsRead.ONLY_FROM_END + } -fun feedItemStyleFromString(value: String) = try { - FeedItemStyle.valueOf(value.uppercase()) -} catch (_: Exception) { - FeedItemStyle.CARD -} +fun feedItemStyleFromString(value: String) = + try { + FeedItemStyle.valueOf(value.uppercase()) + } catch (_: Exception) { + FeedItemStyle.CARD + } fun syncFrequencyFromString(value: String) = SyncFrequency.values() diff --git a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStore.kt b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStore.kt index 6d646ff37b..8dabc35552 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStore.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStore.kt @@ -13,13 +13,13 @@ import com.nononsenseapps.feeder.db.room.SyncDeviceDao import com.nononsenseapps.feeder.db.room.SyncRemote import com.nononsenseapps.feeder.db.room.SyncRemoteDao import com.nononsenseapps.feeder.db.room.generateDeviceName -import java.net.URL -import java.time.Instant -import java.time.temporal.ChronoUnit import kotlinx.coroutines.flow.Flow import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.net.URL +import java.time.Instant +import java.time.temporal.ChronoUnit class SyncRemoteStore(override val di: DI) : DIAware { private val dao: SyncRemoteDao by instance() @@ -83,7 +83,10 @@ class SyncRemoteStore(override val di: DI) : DIAware { readStatusDao.deleteReadStatusSyncForItem(feedItemId) } - suspend fun addRemoteReadMark(feedUrl: URL, articleGuid: String) { + suspend fun addRemoteReadMark( + feedUrl: URL, + articleGuid: String, + ) { // Ignores duplicates remoteReadMarkDao.insert( RemoteReadMark( @@ -100,22 +103,21 @@ class SyncRemoteStore(override val di: DI) : DIAware { remoteReadMarkDao.deleteStaleRemoteReadMarks(now.minus(7, ChronoUnit.DAYS)) } - suspend fun getRemoteReadMarksReadyToBeApplied() = - remoteReadMarkDao.getRemoteReadMarksReadyToBeApplied() + suspend fun getRemoteReadMarksReadyToBeApplied() = remoteReadMarkDao.getRemoteReadMarksReadyToBeApplied() - suspend fun getGuidsWhichAreSyncedAsReadInFeed(feedUrl: URL) = - remoteReadMarkDao.getGuidsWhichAreSyncedAsReadInFeed(feedUrl = feedUrl) + suspend fun getGuidsWhichAreSyncedAsReadInFeed(feedUrl: URL) = remoteReadMarkDao.getGuidsWhichAreSyncedAsReadInFeed(feedUrl = feedUrl) suspend fun replaceWithDefaultSyncRemote() { dao.replaceWithDefaultSyncRemote() } private suspend fun createDefaultSyncRemote(): SyncRemote { - val remote = SyncRemote( - id = 1L, - deviceName = generateDeviceName(), - secretKey = AesCbcWithIntegrity.generateKey().toString(), - ) + val remote = + SyncRemote( + id = 1L, + deviceName = generateDeviceName(), + secretKey = AesCbcWithIntegrity.generateKey().toString(), + ) dao.insert(remote) return remote } diff --git a/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareComponentActivity.kt b/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareComponentActivity.kt index 09debda53d..e14cb9c071 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareComponentActivity.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareComponentActivity.kt @@ -18,11 +18,12 @@ abstract class DIAwareComponentActivity : ComponentActivity(), DIAware { extend(parentDI) bind() with provider { menuInflater } bind() with instance(this@DIAwareComponentActivity) - bind() with singleton { - ActivityLauncher( - this@DIAwareComponentActivity, - di.direct.instance(), - ) - } + bind() with + singleton { + ActivityLauncher( + this@DIAwareComponentActivity, + di.direct.instance(), + ) + } } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareViewModel.kt index d0515b0aaa..e4586e40d8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/base/DIAwareViewModel.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.savedstate.SavedStateRegistryOwner -import java.lang.reflect.InvocationTargetException import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.bind @@ -18,6 +17,7 @@ import org.kodein.di.compose.LocalDI import org.kodein.di.direct import org.kodein.di.factory import org.kodein.di.instance +import java.lang.reflect.InvocationTargetException /** * A view model which is also kodein aware. Construct any deriving class by using the getViewModel() @@ -49,19 +49,21 @@ class DIAwareViewModelFactory( } inline fun DI.Builder.bindWithActivityViewModelScope() { - bind() with factory { activity: DIAwareComponentActivity -> - val factory = DIAwareViewModelFactory(activity.di) + bind() with + factory { activity: DIAwareComponentActivity -> + val factory = DIAwareViewModelFactory(activity.di) - ViewModelProvider(activity, factory).get(T::class.java) - } + ViewModelProvider(activity, factory).get(T::class.java) + } } inline fun DI.Builder.bindWithComposableViewModelScope() { - bind() with factory { activity: DIAwareComponentActivity -> - val factory = DIAwareSavedStateViewModelFactory(activity.di, activity) + bind() with + factory { activity: DIAwareComponentActivity -> + val factory = DIAwareSavedStateViewModelFactory(activity.di, activity) - ViewModelProvider(activity, factory).get(T::class.java) - } + ViewModelProvider(activity, factory).get(T::class.java) + } } class DIAwareSavedStateViewModelFactory( @@ -92,9 +94,7 @@ class DIAwareSavedStateViewModelFactory( } @Composable -inline fun SavedStateRegistryOwner.diAwareViewModel( - key: String? = null, -): T { +inline fun SavedStateRegistryOwner.diAwareViewModel(key: String? = null): T { val factory = DIAwareSavedStateViewModelFactory(LocalDI.current, this) return viewModel( @@ -105,9 +105,7 @@ inline fun SavedStateRegistryOwner.diAwareViewMod } @Composable -inline fun NavBackStackEntry.diAwareViewModel( - key: String? = null, -): T { +inline fun NavBackStackEntry.diAwareViewModel(key: String? = null): T { val factory = DIAwareSavedStateViewModelFactory(LocalDI.current, this, arguments) return viewModel( diff --git a/app/src/main/java/com/nononsenseapps/feeder/blob/Blob.kt b/app/src/main/java/com/nononsenseapps/feeder/blob/Blob.kt index 9ab37bff3b..b6e9141c40 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/blob/Blob.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/blob/Blob.kt @@ -7,24 +7,36 @@ import java.io.OutputStream import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream -fun blobFile(itemId: Long, filesDir: File): File = - File(filesDir, "$itemId.txt.gz") +fun blobFile( + itemId: Long, + filesDir: File, +): File = File(filesDir, "$itemId.txt.gz") @Throws(IOException::class) -fun blobInputStream(itemId: Long, filesDir: File): InputStream = - GZIPInputStream(blobFile(itemId = itemId, filesDir = filesDir).inputStream()) +fun blobInputStream( + itemId: Long, + filesDir: File, +): InputStream = GZIPInputStream(blobFile(itemId = itemId, filesDir = filesDir).inputStream()) @Throws(IOException::class) -fun blobOutputStream(itemId: Long, filesDir: File): OutputStream = - GZIPOutputStream(blobFile(itemId = itemId, filesDir = filesDir).outputStream()) +fun blobOutputStream( + itemId: Long, + filesDir: File, +): OutputStream = GZIPOutputStream(blobFile(itemId = itemId, filesDir = filesDir).outputStream()) -fun blobFullFile(itemId: Long, filesDir: File): File = - File(filesDir, "$itemId.full.html.gz") +fun blobFullFile( + itemId: Long, + filesDir: File, +): File = File(filesDir, "$itemId.full.html.gz") @Throws(IOException::class) -fun blobFullInputStream(itemId: Long, filesDir: File): InputStream = - GZIPInputStream(blobFullFile(itemId = itemId, filesDir = filesDir).inputStream()) +fun blobFullInputStream( + itemId: Long, + filesDir: File, +): InputStream = GZIPInputStream(blobFullFile(itemId = itemId, filesDir = filesDir).inputStream()) @Throws(IOException::class) -fun blobFullOutputStream(itemId: Long, filesDir: File): OutputStream = - GZIPOutputStream(blobFullFile(itemId = itemId, filesDir = filesDir).outputStream()) +fun blobFullOutputStream( + itemId: Long, + filesDir: File, +): OutputStream = GZIPOutputStream(blobFullFile(itemId = itemId, filesDir = filesDir).outputStream()) diff --git a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt index ae65d26f4f..04f870d1c3 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RSSContentProvider.kt @@ -22,36 +22,45 @@ class RSSContentProvider : ContentProvider(), DIAware { private val feedDao: FeedDao by instance() private val feedItemDao: FeedItemDao by instance() - private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { - addURI(AUTHORITY, RssContentProviderContract.feedsUriPathList, URI_FEED_LIST) - addURI( - AUTHORITY, - RssContentProviderContract.articlesUriPathList, - URI_ARTICLE_LIST, - ) - addURI( - AUTHORITY, - RssContentProviderContract.articlesUriPathItem, - URI_ARTICLE_IN_FEED_LIST, - ) - } + private val uriMatcher = + UriMatcher(UriMatcher.NO_MATCH).apply { + addURI(AUTHORITY, RssContentProviderContract.feedsUriPathList, URI_FEED_LIST) + addURI( + AUTHORITY, + RssContentProviderContract.articlesUriPathList, + URI_ARTICLE_LIST, + ) + addURI( + AUTHORITY, + RssContentProviderContract.articlesUriPathItem, + URI_ARTICLE_IN_FEED_LIST, + ) + } override fun onCreate(): Boolean { return true } - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + override fun delete( + uri: Uri, + selection: String?, + selectionArgs: Array?, + ): Int { throw UnsupportedOperationException("Not implemented") } - override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) { - URI_FEED_LIST -> RssContentProviderContract.feedsMimeTypeList - URI_ARTICLE_LIST -> RssContentProviderContract.articlesMimeTypeList - URI_ARTICLE_IN_FEED_LIST -> RssContentProviderContract.articlesMimeTypeItem - else -> null - } + override fun getType(uri: Uri): String? = + when (uriMatcher.match(uri)) { + URI_FEED_LIST -> RssContentProviderContract.feedsMimeTypeList + URI_ARTICLE_LIST -> RssContentProviderContract.articlesMimeTypeList + URI_ARTICLE_IN_FEED_LIST -> RssContentProviderContract.articlesMimeTypeItem + else -> null + } - override fun insert(uri: Uri, values: ContentValues?): Uri? { + override fun insert( + uri: Uri, + values: ContentValues?, + ): Uri? { throw UnsupportedOperationException("Not implemented") } diff --git a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt index 68b53e6f09..5111717d80 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/contentprovider/RssContentProviderContract.kt @@ -7,10 +7,11 @@ object RssContentProviderContract { /** * Columns available via the content provider */ - val feedsColumns = listOf( - "id", - "title", - ) + val feedsColumns = + listOf( + "id", + "title", + ) const val articlesMimeTypeList = "vnd.android.cursor.dir/vnd.rssprovider.items" const val articlesUriPathList = "articles" @@ -20,9 +21,10 @@ object RssContentProviderContract { /** * Columns available via the content provider */ - val articlesColumns = listOf( - "id", - "title", - "text", - ) + val articlesColumns = + listOf( + "id", + "title", + "text", + ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrity.kt b/app/src/main/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrity.kt index 9fea9d2494..ba41b407e4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrity.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/crypto/AesCbcWithIntegrity.kt @@ -145,24 +145,30 @@ object AesCbcWithIntegrity { * or a suitable RNG is not available */ @Throws(GeneralSecurityException::class) - fun generateKeyFromPassword(password: String, salt: ByteArray): SecretKeys { + fun generateKeyFromPassword( + password: String, + salt: ByteArray, + ): SecretKeys { // Get enough random bytes for both the AES key and the HMAC key: - val keySpec: KeySpec = PBEKeySpec( - password.toCharArray(), - salt, - PBE_ITERATION_COUNT, - AES_KEY_LENGTH_BITS + HMAC_KEY_LENGTH_BITS, - ) - val keyFactory = SecretKeyFactory - .getInstance(PBE_ALGORITHM) + val keySpec: KeySpec = + PBEKeySpec( + password.toCharArray(), + salt, + PBE_ITERATION_COUNT, + AES_KEY_LENGTH_BITS + HMAC_KEY_LENGTH_BITS, + ) + val keyFactory = + SecretKeyFactory + .getInstance(PBE_ALGORITHM) val keyBytes = keyFactory.generateSecret(keySpec).encoded // Split the random bytes into two parts: val confidentialityKeyBytes = keyBytes.copyOfRange(0, AES_KEY_LENGTH_BITS / 8) - val integrityKeyBytes = keyBytes.copyOfRange( - AES_KEY_LENGTH_BITS / 8, - AES_KEY_LENGTH_BITS / 8 + HMAC_KEY_LENGTH_BITS / 8, - ) + val integrityKeyBytes = + keyBytes.copyOfRange( + AES_KEY_LENGTH_BITS / 8, + AES_KEY_LENGTH_BITS / 8 + HMAC_KEY_LENGTH_BITS / 8, + ) // Generate the AES key val confidentialityKey: SecretKey = SecretKeySpec(confidentialityKeyBytes, CIPHER) @@ -180,7 +186,10 @@ object AesCbcWithIntegrity { * @throws GeneralSecurityException */ @Throws(GeneralSecurityException::class) - fun generateKeyFromPassword(password: String, salt: String): SecretKeys { + fun generateKeyFromPassword( + password: String, + salt: String, + ): SecretKeys { return generateKeyFromPassword(password, Base64.decode(salt, BASE64_FLAGS)) } @@ -228,6 +237,7 @@ object AesCbcWithIntegrity { * Encryption * ----------------------------------------------------------------- */ + /** * Generates a random IV and encrypts this plain text with the given key. Then attaches * a hashed MAC, which is contained in the CipherTextIvMac class. @@ -245,11 +255,12 @@ object AesCbcWithIntegrity { plaintext: String, secretKeys: SecretKeys, encoding: Charset = Charsets.UTF_8, - ): String = encrypt( - plaintext = plaintext, - secretKeys = secretKeys, - encoding = encoding, - ).toString() + ): String = + encrypt( + plaintext = plaintext, + secretKeys = secretKeys, + encoding = encoding, + ).toString() /** * Generates a random IV and encrypts this plain text with the given key. Then attaches @@ -282,7 +293,10 @@ object AesCbcWithIntegrity { * @throws GeneralSecurityException if AES is not implemented on this system */ @Throws(GeneralSecurityException::class) - fun encrypt(plaintext: ByteArray, secretKeys: SecretKeys): CipherTextIvMac { + fun encrypt( + plaintext: ByteArray, + secretKeys: SecretKeys, + ): CipherTextIvMac { var iv = generateIv() val aesCipherForEncryption = Cipher.getInstance(CIPHER_TRANSFORMATION) aesCipherForEncryption.init( @@ -307,6 +321,7 @@ object AesCbcWithIntegrity { * Decryption * ----------------------------------------------------------------- */ + /** * AES CBC decrypt. * @@ -354,7 +369,10 @@ object AesCbcWithIntegrity { * @throws GeneralSecurityException if MACs don't match or AES is not implemented */ @Throws(GeneralSecurityException::class) - fun decrypt(civ: CipherTextIvMac, secretKeys: SecretKeys): ByteArray { + fun decrypt( + civ: CipherTextIvMac, + secretKeys: SecretKeys, + ): ByteArray { val ivCipherConcat = CipherTextIvMac.ivCipherConcat(civ.iv, civ.cipherText) val computedMac = generateMac(ivCipherConcat, secretKeys.integrityKey) return if (constantTimeEq(computedMac, civ.mac)) { @@ -375,6 +393,7 @@ object AesCbcWithIntegrity { * Helper Code * ----------------------------------------------------------------- */ + /** * Generate the mac based on HMAC_ALGORITHM * @param integrityKey The key used for hmac @@ -384,7 +403,10 @@ object AesCbcWithIntegrity { * @throws InvalidKeyException */ @Throws(NoSuchAlgorithmException::class, InvalidKeyException::class) - fun generateMac(byteCipherText: ByteArray, integrityKey: SecretKey): ByteArray { + fun generateMac( + byteCipherText: ByteArray, + integrityKey: SecretKey, + ): ByteArray { // Now compute the mac for later integrity checking val sha256HMAC = Mac.getInstance(HMAC_ALGORITHM) sha256HMAC.init(integrityKey) @@ -397,7 +419,10 @@ object AesCbcWithIntegrity { * @param b * @return true iff the arrays are exactly equal. */ - private fun constantTimeEq(a: ByteArray, b: ByteArray): Boolean { + private fun constantTimeEq( + a: ByteArray, + b: ByteArray, + ): Boolean { if (a.size != b.size) { return false } @@ -422,14 +447,16 @@ class SecretKeys( * @return base64(confidentialityKey):base64(integrityKey) */ override fun toString(): String { - val a = Base64.encodeToString( - confidentialityKey.encoded, - AesCbcWithIntegrity.BASE64_FLAGS, - ) - val b = Base64.encodeToString( - integrityKey.encoded, - AesCbcWithIntegrity.BASE64_FLAGS, - ) + val a = + Base64.encodeToString( + confidentialityKey.encoded, + AesCbcWithIntegrity.BASE64_FLAGS, + ) + val b = + Base64.encodeToString( + integrityKey.encoded, + AesCbcWithIntegrity.BASE64_FLAGS, + ) return "$a:$b" } @@ -528,7 +555,10 @@ class CipherTextIvMac { * @param cipherText the cipherText to append * @return iv:cipherText, a new byte array. */ - fun ivCipherConcat(iv: ByteArray, cipherText: ByteArray): ByteArray { + fun ivCipherConcat( + iv: ByteArray, + cipherText: ByteArray, + ): ByteArray { val combined = ByteArray(iv.size + cipherText.size) System.arraycopy(iv, 0, combined, 0, iv.size) System.arraycopy(cipherText, 0, combined, iv.size, cipherText.size) diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt index 5084efd925..00904acc62 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/AppDatabase.kt @@ -55,12 +55,19 @@ private const val LOG_TAG = "FEEDER_APPDB" @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun feedDao(): FeedDao + abstract fun feedItemDao(): FeedItemDao + abstract fun blocklistDao(): BlocklistDao + abstract fun syncRemoteDao(): SyncRemoteDao + abstract fun readStatusSyncedDao(): ReadStatusSyncedDao + abstract fun remoteReadMarkDao(): RemoteReadMarkDao + abstract fun remoteFeedDao(): RemoteFeedDao + abstract fun syncDeviceDao(): SyncDeviceDao companion object { @@ -91,34 +98,35 @@ abstract class AppDatabase : RoomDatabase() { } // 17-20 were never part of any release, just made for easier testing -fun getAllMigrations(di: DI) = arrayOf( - MIGRATION_5_7, - MIGRATION_6_7, - MIGRATION_7_8, - MIGRATION_8_9, - MIGRATION_9_10, - MIGRATION_10_11, - MIGRATION_11_12, - MIGRATION_12_13, - MIGRATION_13_14, - MIGRATION_14_15, - MIGRATION_15_16, - MIGRATION_16_17, - MIGRATION_17_18, - MIGRATION_18_19, - MIGRATION_19_20, - MIGRATION_20_21, - MIGRATION_21_22, - MIGRATION_22_23, - MigrationFrom23To24(di), - MigrationFrom24To25(di), - MigrationFrom25To26(di), - MigrationFrom26To27(di), - MigrationFrom27To28(di), - MigrationFrom28To29(di), - MigrationFrom29To30(di), - MigrationFrom30To31(di), -) +fun getAllMigrations(di: DI) = + arrayOf( + MIGRATION_5_7, + MIGRATION_6_7, + MIGRATION_7_8, + MIGRATION_8_9, + MIGRATION_9_10, + MIGRATION_10_11, + MIGRATION_11_12, + MIGRATION_12_13, + MIGRATION_13_14, + MIGRATION_14_15, + MIGRATION_15_16, + MIGRATION_16_17, + MIGRATION_17_18, + MIGRATION_18_19, + MIGRATION_19_20, + MIGRATION_20_21, + MIGRATION_21_22, + MIGRATION_22_23, + MigrationFrom23To24(di), + MigrationFrom24To25(di), + MigrationFrom25To26(di), + MigrationFrom26To27(di), + MigrationFrom27To28(di), + MigrationFrom28To29(di), + MigrationFrom29To30(di), + MigrationFrom30To31(di), + ) /* * 6 represents legacy database @@ -128,7 +136,7 @@ class MigrationFrom30To31(override val di: DI) : Migration(30, 31), DIAware { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ - alter table feed_items add column word_count_full integer not null default 0 + alter table feed_items add column word_count_full integer not null default 0 """.trimIndent(), ) } @@ -138,7 +146,7 @@ class MigrationFrom29To30(override val di: DI) : Migration(29, 30), DIAware { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ - alter table feed_items add column word_count integer not null default 0 + alter table feed_items add column word_count integer not null default 0 """.trimIndent(), ) } @@ -148,7 +156,7 @@ class MigrationFrom28To29(override val di: DI) : Migration(28, 29), DIAware { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ - alter table feed_items add column enclosure_type text + alter table feed_items add column enclosure_type text """.trimIndent(), ) } @@ -158,7 +166,7 @@ class MigrationFrom27To28(override val di: DI) : Migration(27, 28), DIAware { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( """ - alter table feeds add column site_fetched integer not null default 0 + alter table feeds add column site_fetched integer not null default 0 """.trimIndent(), ) } @@ -260,8 +268,9 @@ class MigrationFrom23To24(override val di: DI) : Migration(23, 24), DIAware { """.trimIndent(), ) - val blocks = sharedPrefs.getStringSet("pref_block_list_values", null) - ?: emptySet() + val blocks = + sharedPrefs.getStringSet("pref_block_list_values", null) + ?: emptySet() if (blocks.isNotEmpty()) { // ('*foo*'), ('*bar*'), ('*car*') @@ -331,30 +340,30 @@ object MIGRATION_20_21 : Migration(20, 21) { ) database.execSQL( """ - CREATE TABLE IF NOT EXISTS `remote_feed` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_remote` INTEGER NOT NULL, `url` TEXT NOT NULL, FOREIGN KEY(`sync_remote`) REFERENCES `sync_remote`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) + CREATE TABLE IF NOT EXISTS `remote_feed` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_remote` INTEGER NOT NULL, `url` TEXT NOT NULL, FOREIGN KEY(`sync_remote`) REFERENCES `sync_remote`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) """.trimIndent(), ) database.execSQL( """ - CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_feed_sync_remote_url` ON `remote_feed` (`sync_remote`, `url`) + CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_feed_sync_remote_url` ON `remote_feed` (`sync_remote`, `url`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_remote_feed_url` ON `remote_feed` (`url`) + CREATE INDEX IF NOT EXISTS `index_remote_feed_url` ON `remote_feed` (`url`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_remote_feed_sync_remote` ON `remote_feed` (`sync_remote`) + CREATE INDEX IF NOT EXISTS `index_remote_feed_sync_remote` ON `remote_feed` (`sync_remote`) """.trimIndent(), ) // And generate encryption key database.execSQL( """ - UPDATE sync_remote - SET secret_key = ? - WHERE id IS 1 + UPDATE sync_remote + SET secret_key = ? + WHERE id IS 1 """.trimIndent(), arrayOf(AesCbcWithIntegrity.generateKey().toString()), ) @@ -378,17 +387,17 @@ object MIGRATION_19_20 : Migration(19, 20) { ) database.execSQL( """ - CREATE TABLE IF NOT EXISTS `sync_device` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_remote` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `device_name` TEXT NOT NULL, FOREIGN KEY(`sync_remote`) REFERENCES `sync_remote`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) + CREATE TABLE IF NOT EXISTS `sync_device` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_remote` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `device_name` TEXT NOT NULL, FOREIGN KEY(`sync_remote`) REFERENCES `sync_remote`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) """.trimIndent(), ) database.execSQL( """ - CREATE UNIQUE INDEX IF NOT EXISTS `index_sync_device_sync_remote_device_id` ON `sync_device` (`sync_remote`, `device_id`) + CREATE UNIQUE INDEX IF NOT EXISTS `index_sync_device_sync_remote_device_id` ON `sync_device` (`sync_remote`, `device_id`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_sync_device_sync_remote` ON `sync_device` (`sync_remote`) + CREATE INDEX IF NOT EXISTS `index_sync_device_sync_remote` ON `sync_device` (`sync_remote`) """.trimIndent(), ) } @@ -404,22 +413,22 @@ object MIGRATION_18_19 : Migration(18, 19) { ) database.execSQL( """ - CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_read_mark_sync_remote_feed_url_guid` ON `remote_read_mark` (`sync_remote`, `feed_url`, `guid`) + CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_read_mark_sync_remote_feed_url_guid` ON `remote_read_mark` (`sync_remote`, `feed_url`, `guid`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_remote_read_mark_feed_url_guid` ON `remote_read_mark` (`feed_url`, `guid`) + CREATE INDEX IF NOT EXISTS `index_remote_read_mark_feed_url_guid` ON `remote_read_mark` (`feed_url`, `guid`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_remote_read_mark_sync_remote` ON `remote_read_mark` (`sync_remote`) + CREATE INDEX IF NOT EXISTS `index_remote_read_mark_sync_remote` ON `remote_read_mark` (`sync_remote`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_remote_read_mark_timestamp` ON `remote_read_mark` (`timestamp`) + CREATE INDEX IF NOT EXISTS `index_remote_read_mark_timestamp` ON `remote_read_mark` (`timestamp`) """.trimIndent(), ) } @@ -435,17 +444,17 @@ object MIGRATION_17_18 : Migration(17, 18) { ) database.execSQL( """ - CREATE UNIQUE INDEX IF NOT EXISTS `index_read_status_synced_feed_item_sync_remote` ON `read_status_synced` (`feed_item`, `sync_remote`) + CREATE UNIQUE INDEX IF NOT EXISTS `index_read_status_synced_feed_item_sync_remote` ON `read_status_synced` (`feed_item`, `sync_remote`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_read_status_synced_feed_item` ON `read_status_synced` (`feed_item`); + CREATE INDEX IF NOT EXISTS `index_read_status_synced_feed_item` ON `read_status_synced` (`feed_item`); """.trimIndent(), ) database.execSQL( """ - CREATE INDEX IF NOT EXISTS `index_read_status_synced_sync_remote` ON `read_status_synced` (`sync_remote`); + CREATE INDEX IF NOT EXISTS `index_read_status_synced_sync_remote` ON `read_status_synced` (`sync_remote`); """.trimIndent(), ) } @@ -629,73 +638,77 @@ object MIGRATION_5_7 : Migration(5, 7) { } } -private fun legacyMigration(database: SupportSQLiteDatabase, version: Int) { +private fun legacyMigration( + database: SupportSQLiteDatabase, + version: Int, +) { // Create new tables and indices // Feeds database.execSQL( """ - CREATE TABLE IF NOT EXISTS `feeds` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `custom_title` TEXT NOT NULL, `url` TEXT NOT NULL, `tag` TEXT NOT NULL, `notify` INTEGER NOT NULL, `image_url` TEXT) + CREATE TABLE IF NOT EXISTS `feeds` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `custom_title` TEXT NOT NULL, `url` TEXT NOT NULL, `tag` TEXT NOT NULL, `notify` INTEGER NOT NULL, `image_url` TEXT) """.trimIndent(), ) database.execSQL( """ - CREATE UNIQUE INDEX `index_Feed_url` ON `feeds` (`url`) + CREATE UNIQUE INDEX `index_Feed_url` ON `feeds` (`url`) """.trimIndent(), ) database.execSQL( """ - CREATE UNIQUE INDEX `index_Feed_id_url_title` ON `feeds` (`id`, `url`, `title`) + CREATE UNIQUE INDEX `index_Feed_id_url_title` ON `feeds` (`id`, `url`, `title`) """.trimIndent(), ) // Items database.execSQL( """ - CREATE TABLE IF NOT EXISTS `feed_items` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `guid` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `plain_title` TEXT NOT NULL, `plain_snippet` TEXT NOT NULL, `image_url` TEXT, `enclosure_link` TEXT, `author` TEXT, `pub_date` TEXT, `link` TEXT, `unread` INTEGER NOT NULL, `notified` INTEGER NOT NULL, `feed_id` INTEGER, FOREIGN KEY(`feed_id`) REFERENCES `feeds`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) + CREATE TABLE IF NOT EXISTS `feed_items` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `guid` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `plain_title` TEXT NOT NULL, `plain_snippet` TEXT NOT NULL, `image_url` TEXT, `enclosure_link` TEXT, `author` TEXT, `pub_date` TEXT, `link` TEXT, `unread` INTEGER NOT NULL, `notified` INTEGER NOT NULL, `feed_id` INTEGER, FOREIGN KEY(`feed_id`) REFERENCES `feeds`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) """.trimIndent(), ) database.execSQL( """ - CREATE UNIQUE INDEX `index_feed_item_guid_feed_id` ON `feed_items` (`guid`, `feed_id`) + CREATE UNIQUE INDEX `index_feed_item_guid_feed_id` ON `feed_items` (`guid`, `feed_id`) """.trimIndent(), ) database.execSQL( """ - CREATE INDEX `index_feed_item_feed_id` ON `feed_items` (`feed_id`) + CREATE INDEX `index_feed_item_feed_id` ON `feed_items` (`feed_id`) """.trimIndent(), ) // Migrate to new tables database.query( """ - SELECT _id, title, url, tag, customtitle, notify ${if (version == 6) ", imageUrl" else ""} - FROM Feed + SELECT _id, title, url, tag, customtitle, notify ${if (version == 6) ", imageUrl" else ""} + FROM Feed """.trimIndent(), ).use { cursor -> cursor.forEach { _ -> val oldFeedId = cursor.getLong(0) - val newFeedId = database.insert( - "feeds", - SQLiteDatabase.CONFLICT_FAIL, - contentValues { - setString("title" to cursor.getString(1)) - setString("custom_title" to cursor.getString(4)) - setString("url" to cursor.getString(2)) - setString("tag" to cursor.getString(3)) - setInt("notify" to cursor.getInt(5)) - if (version == 6) { - setString("image_url" to cursor.getString(6)) - } - }, - ) + val newFeedId = + database.insert( + "feeds", + SQLiteDatabase.CONFLICT_FAIL, + contentValues { + setString("title" to cursor.getString(1)) + setString("custom_title" to cursor.getString(4)) + setString("url" to cursor.getString(2)) + setString("tag" to cursor.getString(3)) + setInt("notify" to cursor.getInt(5)) + if (version == 6) { + setString("image_url" to cursor.getString(6)) + } + }, + ) database.query( """ - SELECT title, description, plainTitle, plainSnippet, imageUrl, link, author, - pubdate, unread, feed, enclosureLink, notified, guid - FROM FeedItem - WHERE feed = $oldFeedId + SELECT title, description, plainTitle, plainSnippet, imageUrl, link, author, + pubdate, unread, feed, enclosureLink, notified, guid + FROM FeedItem + WHERE feed = $oldFeedId """.trimIndent(), ).use { cursor -> database.inTransaction { diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/BlocklistEntry.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/BlocklistEntry.kt index 677aaa6040..b4c70c96e4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/BlocklistEntry.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/BlocklistEntry.kt @@ -14,11 +14,13 @@ import com.nononsenseapps.feeder.db.COL_ID Index(value = [COL_GLOB_PATTERN], unique = true), ], ) -data class BlocklistEntry @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = COL_GLOB_PATTERN) var globPattern: String = "", -) { - constructor() : this(id = ID_UNSET) -} +data class BlocklistEntry + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = COL_GLOB_PATTERN) var globPattern: String = "", + ) { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/Converters.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/Converters.kt index 7146e3f3d9..07da38fbae 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/Converters.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/Converters.kt @@ -7,7 +7,6 @@ import java.time.Instant import java.time.ZonedDateTime class Converters { - @TypeConverter fun dateTimeFromString(value: String?): ZonedDateTime? { var dt: ZonedDateTime? = null @@ -21,16 +20,13 @@ class Converters { } @TypeConverter - fun stringFromDateTime(value: ZonedDateTime?): String? = - value?.toString() + fun stringFromDateTime(value: ZonedDateTime?): String? = value?.toString() @TypeConverter - fun stringFromURL(value: URL?): String? = - value?.toString() + fun stringFromURL(value: URL?): String? = value?.toString() @TypeConverter - fun urlFromString(value: String?): URL? = - value?.let { sloppyLinkToStrictURLNoThrows(it) } + fun urlFromString(value: String?): URL? = value?.let { sloppyLinkToStrictURLNoThrows(it) } @TypeConverter fun instantFromLong(value: Long?): Instant? = @@ -41,6 +37,5 @@ class Converters { } @TypeConverter - fun longFromInstant(value: Instant?): Long? = - value?.toEpochMilli() + fun longFromInstant(value: Instant?): Long? = value?.toEpochMilli() } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/Feed.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/Feed.kt index 3ff76e3dfb..afc6fecf03 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/Feed.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/Feed.kt @@ -33,28 +33,30 @@ const val OPEN_ARTICLE_WITH_APPLICATION_DEFAULT = "" Index(value = [COL_ID, COL_URL, COL_TITLE], unique = true), ], ) -data class Feed @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = COL_TITLE) var title: String = "", - @ColumnInfo(name = COL_CUSTOM_TITLE) var customTitle: String = "", - @ColumnInfo(name = COL_URL) var url: URL = URL("http://"), - @ColumnInfo(name = COL_TAG) var tag: String = "", - @ColumnInfo(name = COL_NOTIFY) var notify: Boolean = false, - @ColumnInfo(name = COL_IMAGEURL) var imageUrl: URL? = null, - @ColumnInfo(name = COL_LASTSYNC, typeAffinity = ColumnInfo.INTEGER) var lastSync: Instant = Instant.EPOCH, - @ColumnInfo(name = COL_RESPONSEHASH) var responseHash: Int = 0, - @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, - @ColumnInfo(name = COL_OPEN_ARTICLES_WITH) var openArticlesWith: String = OPEN_ARTICLE_WITH_APPLICATION_DEFAULT, - @ColumnInfo(name = COL_ALTERNATE_ID) var alternateId: Boolean = false, - @ColumnInfo(name = COL_CURRENTLY_SYNCING) var currentlySyncing: Boolean = false, - // Only update this field when user modifies the feed - @ColumnInfo(name = COL_WHEN_MODIFIED) var whenModified: Instant = Instant.EPOCH, - @ColumnInfo(name = COL_SITE_FETCHED) var siteFetched: Instant = Instant.EPOCH, -) { - constructor() : this(id = ID_UNSET) +data class Feed + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = COL_TITLE) var title: String = "", + @ColumnInfo(name = COL_CUSTOM_TITLE) var customTitle: String = "", + @ColumnInfo(name = COL_URL) var url: URL = URL("http://"), + @ColumnInfo(name = COL_TAG) var tag: String = "", + @ColumnInfo(name = COL_NOTIFY) var notify: Boolean = false, + @ColumnInfo(name = COL_IMAGEURL) var imageUrl: URL? = null, + @ColumnInfo(name = COL_LASTSYNC, typeAffinity = ColumnInfo.INTEGER) var lastSync: Instant = Instant.EPOCH, + @ColumnInfo(name = COL_RESPONSEHASH) var responseHash: Int = 0, + @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, + @ColumnInfo(name = COL_OPEN_ARTICLES_WITH) var openArticlesWith: String = OPEN_ARTICLE_WITH_APPLICATION_DEFAULT, + @ColumnInfo(name = COL_ALTERNATE_ID) var alternateId: Boolean = false, + @ColumnInfo(name = COL_CURRENTLY_SYNCING) var currentlySyncing: Boolean = false, + // Only update this field when user modifies the feed + @ColumnInfo(name = COL_WHEN_MODIFIED) var whenModified: Instant = Instant.EPOCH, + @ColumnInfo(name = COL_SITE_FETCHED) var siteFetched: Instant = Instant.EPOCH, + ) { + constructor() : this(id = ID_UNSET) - val displayTitle: String - get() = (customTitle.ifBlank { title }) -} + val displayTitle: String + get() = (customTitle.ifBlank { title }) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedDao.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedDao.kt index c219bc1764..34e2ff7dc2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedDao.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedDao.kt @@ -13,13 +13,12 @@ import com.nononsenseapps.feeder.db.COL_ID import com.nononsenseapps.feeder.db.COL_TAG import com.nononsenseapps.feeder.db.COL_TITLE import com.nononsenseapps.feeder.model.FeedUnreadCount +import kotlinx.coroutines.flow.Flow import java.net.URL import java.time.Instant -import kotlinx.coroutines.flow.Flow @Dao interface FeedDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertFeed(feed: Feed): Long @@ -70,13 +69,19 @@ interface FeedDao { AND last_sync < :staleTime """, ) - suspend fun loadFeedIfStale(feedId: Long, staleTime: Long): Feed? + suspend fun loadFeedIfStale( + feedId: Long, + staleTime: Long, + ): Feed? @Query("SELECT * FROM feeds WHERE tag IS :tag") suspend fun loadFeeds(tag: String): List @Query("SELECT * FROM feeds WHERE tag IS :tag AND last_sync < :staleTime") - suspend fun loadFeedsIfStale(tag: String, staleTime: Long): List + suspend fun loadFeedsIfStale( + tag: String, + staleTime: Long, + ): List @Query("SELECT * FROM feeds") suspend fun loadFeeds(): List @@ -115,10 +120,16 @@ interface FeedDao { fun loadFlowOfFeedsWithUnreadCounts(): Flow> @Query("UPDATE feeds SET notify = :notify WHERE id IS :id") - suspend fun setNotify(id: Long, notify: Boolean) + suspend fun setNotify( + id: Long, + notify: Boolean, + ) @Query("UPDATE feeds SET notify = :notify WHERE tag IS :tag") - suspend fun setNotify(tag: String, notify: Boolean) + suspend fun setNotify( + tag: String, + notify: Boolean, + ) @Query("UPDATE feeds SET notify = :notify") suspend fun setAllNotify(notify: Boolean) @@ -161,7 +172,10 @@ interface FeedDao { WHERE id IS :feedId """, ) - suspend fun setCurrentlySyncingOn(feedId: Long, syncing: Boolean) + suspend fun setCurrentlySyncingOn( + feedId: Long, + syncing: Boolean, + ) @Query( """ @@ -170,7 +184,11 @@ interface FeedDao { WHERE id IS :feedId """, ) - suspend fun setCurrentlySyncingOn(feedId: Long, syncing: Boolean, lastSync: Instant) + suspend fun setCurrentlySyncingOn( + feedId: Long, + syncing: Boolean, + lastSync: Instant, + ) @Query( """ diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedForSettings.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedForSettings.kt index 44e3137f3d..e3d0d0aa00 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedForSettings.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedForSettings.kt @@ -6,10 +6,12 @@ import com.nononsenseapps.feeder.db.COL_ID import com.nononsenseapps.feeder.db.COL_NOTIFY import com.nononsenseapps.feeder.db.COL_TITLE -data class FeedForSettings @Ignore constructor( - @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, - @ColumnInfo(name = COL_TITLE) var title: String = "", - @ColumnInfo(name = COL_NOTIFY) var notify: Boolean = false, -) { - constructor() : this(id = ID_UNSET) -} +data class FeedForSettings + @Ignore + constructor( + @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, + @ColumnInfo(name = COL_TITLE) var title: String = "", + @ColumnInfo(name = COL_NOTIFY) var notify: Boolean = false, + ) { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt index d1bd8ade3b..73d13f4d15 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItem.kt @@ -62,140 +62,145 @@ private val patternWhitespace = "\\s+".toRegex() ), ], ) -data class FeedItem @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - override var id: Long = ID_UNSET, - @ColumnInfo(name = COL_GUID) var guid: String = "", - @Deprecated("This is never different from plainTitle", replaceWith = ReplaceWith("plainTitle")) - @ColumnInfo(name = COL_TITLE) - var title: String = "", - @ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "", - @ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "", - @ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null, - @ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null, - @ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null, - @ColumnInfo(name = COL_AUTHOR) var author: String? = null, - @ColumnInfo( - name = COL_PUBDATE, - typeAffinity = ColumnInfo.TEXT, - ) override var pubDate: ZonedDateTime? = null, - @ColumnInfo(name = COL_LINK) override var link: String? = null, - @Deprecated( - "This column has been 'removed' but sqlite doesn't support drop column.", - replaceWith = ReplaceWith("readTime"), - ) - @ColumnInfo(name = "unread") - var oldUnread: Boolean = true, - @ColumnInfo(name = COL_NOTIFIED) var notified: Boolean = false, - @ColumnInfo(name = COL_FEEDID) var feedId: Long? = null, - @ColumnInfo( - name = COL_FIRSTSYNCEDTIME, - typeAffinity = ColumnInfo.INTEGER, - ) var firstSyncedTime: Instant = Instant.EPOCH, - @ColumnInfo( - name = COL_PRIMARYSORTTIME, - typeAffinity = ColumnInfo.INTEGER, - ) override var primarySortTime: Instant = Instant.EPOCH, - @Deprecated("This column has been 'removed' but sqlite doesn't support drop column.") - @ColumnInfo(name = "pinned") - var oldPinned: Boolean = false, - @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, - @ColumnInfo(name = COL_FULLTEXT_DOWNLOADED) var fullTextDownloaded: Boolean = false, - @ColumnInfo( - name = COL_READ_TIME, - typeAffinity = ColumnInfo.INTEGER, - ) var readTime: Instant? = null, - @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, - @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, -) : FeedItemForFetching, FeedItemCursor { - - constructor() : this(id = ID_UNSET) - - val unread: Boolean - get() = readTime == null - - fun updateFromParsedEntry( - entry: Item, - entryGuid: String, - feed: com.nononsenseapps.jsonfeed.Feed, - ) { - val converter = HtmlToPlainTextConverter() - // Be careful about nulls. - val plainText = converter.convert( - entry.content_html - ?: entry.content_text - ?: "", +data class FeedItem + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + override var id: Long = ID_UNSET, + @ColumnInfo(name = COL_GUID) var guid: String = "", + @Deprecated("This is never different from plainTitle", replaceWith = ReplaceWith("plainTitle")) + @ColumnInfo(name = COL_TITLE) + var title: String = "", + @ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "", + @ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "", + @ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null, + @ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null, + @ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null, + @ColumnInfo(name = COL_AUTHOR) var author: String? = null, + @ColumnInfo( + name = COL_PUBDATE, + typeAffinity = ColumnInfo.TEXT, + ) override var pubDate: ZonedDateTime? = null, + @ColumnInfo(name = COL_LINK) override var link: String? = null, + @Deprecated( + "This column has been 'removed' but sqlite doesn't support drop column.", + replaceWith = ReplaceWith("readTime"), ) - this.wordCount = estimateWordCount(plainText) - - val summary: String = ( - entry.summary - ?: entry.content_text - ?: plainText - ).take(MAX_SNIPPET_LENGTH) - - // Make double sure no base64 images are used as thumbnails - val safeImage = when { - entry.image?.startsWith("data") == true -> null - else -> entry.image - } + @ColumnInfo(name = "unread") + var oldUnread: Boolean = true, + @ColumnInfo(name = COL_NOTIFIED) var notified: Boolean = false, + @ColumnInfo(name = COL_FEEDID) var feedId: Long? = null, + @ColumnInfo( + name = COL_FIRSTSYNCEDTIME, + typeAffinity = ColumnInfo.INTEGER, + ) var firstSyncedTime: Instant = Instant.EPOCH, + @ColumnInfo( + name = COL_PRIMARYSORTTIME, + typeAffinity = ColumnInfo.INTEGER, + ) override var primarySortTime: Instant = Instant.EPOCH, + @Deprecated("This column has been 'removed' but sqlite doesn't support drop column.") + @ColumnInfo(name = "pinned") + var oldPinned: Boolean = false, + @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, + @ColumnInfo(name = COL_FULLTEXT_DOWNLOADED) var fullTextDownloaded: Boolean = false, + @ColumnInfo( + name = COL_READ_TIME, + typeAffinity = ColumnInfo.INTEGER, + ) var readTime: Instant? = null, + @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, + @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, + ) : FeedItemForFetching, FeedItemCursor { + constructor() : this(id = ID_UNSET) + + val unread: Boolean + get() = readTime == null + + fun updateFromParsedEntry( + entry: Item, + entryGuid: String, + feed: com.nononsenseapps.jsonfeed.Feed, + ) { + val converter = HtmlToPlainTextConverter() + // Be careful about nulls. + val plainText = + converter.convert( + entry.content_html + ?: entry.content_text + ?: "", + ) + this.wordCount = estimateWordCount(plainText) + + val summary: String = + ( + entry.summary + ?: entry.content_text + ?: plainText + ).take(MAX_SNIPPET_LENGTH) + + // Make double sure no base64 images are used as thumbnails + val safeImage = + when { + entry.image?.startsWith("data") == true -> null + else -> entry.image + } - val absoluteImage = when { - feed.feed_url != null && safeImage != null -> { - relativeLinkIntoAbsolute(sloppyLinkToStrictURL(feed.feed_url), safeImage) - } + val absoluteImage = + when { + feed.feed_url != null && safeImage != null -> { + relativeLinkIntoAbsolute(sloppyLinkToStrictURL(feed.feed_url), safeImage) + } - else -> safeImage - } + else -> safeImage + } - this.guid = entryGuid - entry.title?.let { this.plainTitle = it.take(MAX_TITLE_LENGTH) } - @Suppress("DEPRECATION") - this.title = this.plainTitle - this.plainSnippet = summary - - this.imageUrl = absoluteImage - val firstEnclosure = entry.attachments?.firstOrNull() - this.enclosureLink = firstEnclosure?.url - this.enclosureType = firstEnclosure?.mime_type?.lowercase() - - this.author = entry.author?.name ?: feed.author?.name - this.link = entry.url - - this.pubDate = - try { - // Allow an actual pubdate to be updated - ZonedDateTime.parse(entry.date_published) - } catch (t: Throwable) { - // If a pubdate is missing, then don't update if one is already set - this.pubDate ?: ZonedDateTime.now(ZoneOffset.UTC) - } - primarySortTime = minOf(firstSyncedTime, pubDate?.toInstant() ?: firstSyncedTime) - } + this.guid = entryGuid + entry.title?.let { this.plainTitle = it.take(MAX_TITLE_LENGTH) } + @Suppress("DEPRECATION") + this.title = this.plainTitle + this.plainSnippet = summary + + this.imageUrl = absoluteImage + val firstEnclosure = entry.attachments?.firstOrNull() + this.enclosureLink = firstEnclosure?.url + this.enclosureType = firstEnclosure?.mime_type?.lowercase() - val enclosureFilename: String? - get() { - enclosureLink?.let { enclosureLink -> - var fname: String? = null + this.author = entry.author?.name ?: feed.author?.name + this.link = entry.url + + this.pubDate = try { - fname = URI(enclosureLink).path.split("/").last() - } catch (_: Exception) { + // Allow an actual pubdate to be updated + ZonedDateTime.parse(entry.date_published) + } catch (t: Throwable) { + // If a pubdate is missing, then don't update if one is already set + this.pubDate ?: ZonedDateTime.now(ZoneOffset.UTC) } - return if (fname.isNullOrEmpty()) { - null - } else { - fname + primarySortTime = minOf(firstSyncedTime, pubDate?.toInstant() ?: firstSyncedTime) + } + + val enclosureFilename: String? + get() { + enclosureLink?.let { enclosureLink -> + var fname: String? = null + try { + fname = URI(enclosureLink).path.split("/").last() + } catch (_: Exception) { + } + return if (fname.isNullOrEmpty()) { + null + } else { + fname + } } + return null } - return null - } - val domain: String? - get() { - return (enclosureLink ?: link)?.host() - } -} + val domain: String? + get() { + return (enclosureLink ?: link)?.host() + } + } interface FeedItemForFetching { val id: Long diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemDao.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemDao.kt index e09fde312a..4152224ead 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemDao.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemDao.kt @@ -17,9 +17,9 @@ import com.nononsenseapps.feeder.db.COL_URL import com.nononsenseapps.feeder.db.FEEDS_TABLE_NAME import com.nononsenseapps.feeder.model.PreviewItem import com.nononsenseapps.feeder.model.previewColumns +import kotlinx.coroutines.flow.Flow import java.net.URL import java.time.Instant -import kotlinx.coroutines.flow.Flow @Dao interface FeedItemDao { @@ -52,7 +52,10 @@ interface FeedItemDao { where id = :id """, ) - suspend fun updateWordCountFull(id: Long, wordCount: Int) + suspend fun updateWordCountFull( + id: Long, + wordCount: Int, + ) @Query( """ @@ -62,10 +65,16 @@ interface FeedItemDao { LIMIT -1 OFFSET :keepCount """, ) - suspend fun getItemsToBeCleanedFromFeed(feedId: Long, keepCount: Int): List + suspend fun getItemsToBeCleanedFromFeed( + feedId: Long, + keepCount: Int, + ): List @Query("SELECT * FROM feed_items WHERE guid IS :guid AND feed_id IS :feedId") - suspend fun loadFeedItem(guid: String, feedId: Long?): FeedItem? + suspend fun loadFeedItem( + guid: String, + feedId: Long?, + ): FeedItem? @Query("SELECT * FROM feed_items WHERE id IS :id") suspend fun loadFeedItem(id: Long): FeedItem? @@ -269,7 +278,10 @@ interface FeedItemDao { suspend fun markAllAsRead(readTime: Instant = Instant.now()) @Query("UPDATE feed_items SET read_time = coalesce(read_time, :readTime), notified = 1 WHERE feed_id IS :feedId") - suspend fun markAllAsRead(feedId: Long?, readTime: Instant = Instant.now()) + suspend fun markAllAsRead( + feedId: Long?, + readTime: Instant = Instant.now(), + ) @Query( """ @@ -282,25 +294,43 @@ interface FeedItemDao { WHERE tag IS :tag )""", ) - suspend fun markAllAsRead(tag: String, readTime: Instant = Instant.now()) + suspend fun markAllAsRead( + tag: String, + readTime: Instant = Instant.now(), + ) @Query("UPDATE feed_items SET read_time = coalesce(read_time, :readTime), notified = 1 WHERE id IS :id") - suspend fun markAsRead(id: Long, readTime: Instant = Instant.now()) + suspend fun markAsRead( + id: Long, + readTime: Instant = Instant.now(), + ) @Query("UPDATE feed_items SET read_time = null WHERE id IS :id") suspend fun markAsUnread(id: Long) @Query("UPDATE feed_items SET read_time = coalesce(read_time, :readTime), notified = 1 WHERE id IN (:ids)") - suspend fun markAsRead(ids: List, readTime: Instant = Instant.now()) + suspend fun markAsRead( + ids: List, + readTime: Instant = Instant.now(), + ) @Query("UPDATE feed_items SET bookmarked = :bookmarked WHERE id IS :id") - suspend fun setBookmarked(id: Long, bookmarked: Boolean) + suspend fun setBookmarked( + id: Long, + bookmarked: Boolean, + ) @Query("UPDATE feed_items SET notified = :notified WHERE id IN (:ids)") - suspend fun markAsNotified(ids: List, notified: Boolean = true) + suspend fun markAsNotified( + ids: List, + notified: Boolean = true, + ) @Query("UPDATE feed_items SET notified = :notified WHERE id IS :id") - suspend fun markAsNotified(id: Long, notified: Boolean = true) + suspend fun markAsNotified( + id: Long, + notified: Boolean = true, + ) @Query( """ @@ -313,16 +343,25 @@ interface FeedItemDao { WHERE tag IS :tag )""", ) - suspend fun markTagAsNotified(tag: String, notified: Boolean = true) + suspend fun markTagAsNotified( + tag: String, + notified: Boolean = true, + ) @Query("UPDATE feed_items SET notified = :notified") suspend fun markAllAsNotified(notified: Boolean = true) @Query("UPDATE feed_items SET read_time = coalesce(read_time, :readTime), notified = 1 WHERE id IS :id") - suspend fun markAsReadAndNotified(id: Long, readTime: Instant = Instant.now()) + suspend fun markAsReadAndNotified( + id: Long, + readTime: Instant = Instant.now(), + ) @Query("UPDATE feed_items SET read_time = :readTime, notified = 1 WHERE id IS :id") - suspend fun markAsReadAndNotifiedAndOverwriteReadTime(id: Long, readTime: Instant = Instant.now()) + suspend fun markAsReadAndNotifiedAndOverwriteReadTime( + id: Long, + readTime: Instant = Instant.now(), + ) @Query( """ @@ -384,7 +423,10 @@ interface FeedItemDao { and NOT EXISTS (SELECT 1 FROM blocklist WHERE lower(fi.plain_title) GLOB blocklist.glob_pattern) """, ) - fun getFeedItemCount(minReadTime: Instant, bookmarked: Boolean): Flow + fun getFeedItemCount( + minReadTime: Instant, + bookmarked: Boolean, + ): Flow @Query( """ @@ -397,7 +439,11 @@ interface FeedItemDao { AND NOT EXISTS (SELECT 1 FROM blocklist WHERE lower(fi.plain_title) GLOB blocklist.glob_pattern) """, ) - fun getFeedItemCount(tag: String, minReadTime: Instant, bookmarked: Boolean): Flow + fun getFeedItemCount( + tag: String, + minReadTime: Instant, + bookmarked: Boolean, + ): Flow @Query( """ @@ -409,7 +455,11 @@ interface FeedItemDao { AND NOT EXISTS (SELECT 1 FROM blocklist WHERE lower(fi.plain_title) GLOB blocklist.glob_pattern) """, ) - fun getFeedItemCount(feedId: Long, minReadTime: Instant, bookmarked: Boolean): Flow + fun getFeedItemCount( + feedId: Long, + minReadTime: Instant, + bookmarked: Boolean, + ): Flow @Query( """ @@ -430,7 +480,10 @@ interface FeedItemDao { where f.url IS :feedUrl AND fi.guid IS :articleGuid """, ) - suspend fun getItemWith(feedUrl: URL, articleGuid: String): Long? + suspend fun getItemWith( + feedUrl: URL, + articleGuid: String, + ): Long? companion object { // These are backed by a database index @@ -445,14 +498,16 @@ suspend fun FeedItemDao.upsertFeedItems( itemsWithText: List>, block: suspend (FeedItem, String) -> Unit, ) { - val updatedItems = itemsWithText.filter { (item, _) -> - item.id > ID_UNSET - } + val updatedItems = + itemsWithText.filter { (item, _) -> + item.id > ID_UNSET + } updateFeedItems(updatedItems.map { (item, _) -> item }) - val insertedItems = itemsWithText.filter { (item, _) -> - item.id <= ID_UNSET - } + val insertedItems = + itemsWithText.filter { (item, _) -> + item.id <= ID_UNSET + } val insertedIds = insertFeedItems(insertedItems.map { (item, _) -> item }) updatedItems.forEach { (item, text) -> diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemForReadMark.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemForReadMark.kt index 0c4c6cb29a..c0ca429e43 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemForReadMark.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemForReadMark.kt @@ -8,11 +8,13 @@ import com.nononsenseapps.feeder.db.COL_GUID import com.nononsenseapps.feeder.db.COL_ID import java.net.URL -data class FeedItemForReadMark @Ignore constructor( - @ColumnInfo(name = COL_ID) override var id: Long = ID_UNSET, - @ColumnInfo(name = COL_FEEDID) override var feedId: Long = ID_UNSET, - @ColumnInfo(name = COL_GUID) override var guid: String = "", - @ColumnInfo(name = COL_FEEDURL) override var feedUrl: URL = URL("http://"), -) : ReadStatusFeedItem { - constructor() : this(id = ID_UNSET) -} +data class FeedItemForReadMark + @Ignore + constructor( + @ColumnInfo(name = COL_ID) override var id: Long = ID_UNSET, + @ColumnInfo(name = COL_FEEDID) override var feedId: Long = ID_UNSET, + @ColumnInfo(name = COL_GUID) override var guid: String = "", + @ColumnInfo(name = COL_FEEDURL) override var feedUrl: URL = URL("http://"), + ) : ReadStatusFeedItem { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemIdWithLink.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemIdWithLink.kt index 9a9a56dd48..2108d12978 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemIdWithLink.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemIdWithLink.kt @@ -5,9 +5,11 @@ import androidx.room.Ignore import com.nononsenseapps.feeder.db.COL_ID import com.nononsenseapps.feeder.db.COL_LINK -data class FeedItemIdWithLink @Ignore constructor( - @ColumnInfo(name = COL_ID) override var id: Long = ID_UNSET, - @ColumnInfo(name = COL_LINK) override var link: String? = null, -) : FeedItemForFetching { - constructor() : this(id = ID_UNSET) -} +data class FeedItemIdWithLink + @Ignore + constructor( + @ColumnInfo(name = COL_ID) override var id: Long = ID_UNSET, + @ColumnInfo(name = COL_LINK) override var link: String? = null, + ) : FeedItemForFetching { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemWithFeed.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemWithFeed.kt index fe2ff822fb..b29ab4674e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemWithFeed.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedItemWithFeed.kt @@ -47,54 +47,56 @@ const val feedItemColumnsWithFeed = """ $COL_WORD_COUNT_FULL """ -data class FeedItemWithFeed @Ignore constructor( - override var id: Long = ID_UNSET, - var guid: String = "", - @Deprecated("This is never different from plainTitle", replaceWith = ReplaceWith("plainTitle")) - var title: String = "", - @ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "", - @ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "", - @ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null, - @ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null, - @ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null, - var author: String? = null, - @ColumnInfo(name = COL_PUBDATE) var pubDate: ZonedDateTime? = null, - override var link: String? = null, - var tag: String = "", - @ColumnInfo(name = COL_READ_TIME) var readTime: Instant? = null, - @ColumnInfo(name = COL_FEEDID) var feedId: Long? = null, - @ColumnInfo(name = COL_FEEDTITLE) var feedTitle: String = "", - @ColumnInfo(name = COL_FEEDCUSTOMTITLE) var feedCustomTitle: String = "", - @ColumnInfo(name = COL_FEEDURL) var feedUrl: URL = sloppyLinkToStrictURLNoThrows(""), - @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, - @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, - @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, - @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, -) : FeedItemForFetching { - constructor() : this(id = ID_UNSET) +data class FeedItemWithFeed + @Ignore + constructor( + override var id: Long = ID_UNSET, + var guid: String = "", + @Deprecated("This is never different from plainTitle", replaceWith = ReplaceWith("plainTitle")) + var title: String = "", + @ColumnInfo(name = COL_PLAINTITLE) var plainTitle: String = "", + @ColumnInfo(name = COL_PLAINSNIPPET) var plainSnippet: String = "", + @ColumnInfo(name = COL_IMAGEURL) var imageUrl: String? = null, + @ColumnInfo(name = COL_ENCLOSURELINK) var enclosureLink: String? = null, + @ColumnInfo(name = COL_ENCLOSURE_TYPE) var enclosureType: String? = null, + var author: String? = null, + @ColumnInfo(name = COL_PUBDATE) var pubDate: ZonedDateTime? = null, + override var link: String? = null, + var tag: String = "", + @ColumnInfo(name = COL_READ_TIME) var readTime: Instant? = null, + @ColumnInfo(name = COL_FEEDID) var feedId: Long? = null, + @ColumnInfo(name = COL_FEEDTITLE) var feedTitle: String = "", + @ColumnInfo(name = COL_FEEDCUSTOMTITLE) var feedCustomTitle: String = "", + @ColumnInfo(name = COL_FEEDURL) var feedUrl: URL = sloppyLinkToStrictURLNoThrows(""), + @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, + @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, + @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, + @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, + ) : FeedItemForFetching { + constructor() : this(id = ID_UNSET) - val feedDisplayTitle: String - get() = feedCustomTitle.ifBlank { feedTitle } + val feedDisplayTitle: String + get() = feedCustomTitle.ifBlank { feedTitle } - val enclosureFilename: String? - get() { - enclosureLink?.let { enclosureLink -> - var fname: String? = null - try { - fname = URI(enclosureLink).path.split("/").last() - } catch (_: Exception) { - } - return if (fname.isNullOrEmpty()) { - null - } else { - fname + val enclosureFilename: String? + get() { + enclosureLink?.let { enclosureLink -> + var fname: String? = null + try { + fname = URI(enclosureLink).path.split("/").last() + } catch (_: Exception) { + } + return if (fname.isNullOrEmpty()) { + null + } else { + fname + } } + return null } - return null - } - val domain: String? - get() { - return (enclosureLink ?: link)?.host() - } -} + val domain: String? + get() { + return (enclosureLink ?: link)?.host() + } + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedTitle.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedTitle.kt index 2ca6765ef1..163bacfd51 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedTitle.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/FeedTitle.kt @@ -6,13 +6,15 @@ import com.nononsenseapps.feeder.db.COL_CUSTOM_TITLE import com.nononsenseapps.feeder.db.COL_ID import com.nononsenseapps.feeder.db.COL_TITLE -data class FeedTitle @Ignore constructor( - @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, - @ColumnInfo(name = COL_TITLE) var title: String = "", - @ColumnInfo(name = COL_CUSTOM_TITLE) var customTitle: String = "", -) { - constructor() : this(id = ID_UNSET) +data class FeedTitle + @Ignore + constructor( + @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, + @ColumnInfo(name = COL_TITLE) var title: String = "", + @ColumnInfo(name = COL_CUSTOM_TITLE) var customTitle: String = "", + ) { + constructor() : this(id = ID_UNSET) - val displayTitle: String - get() = (customTitle.ifBlank { title }) -} + val displayTitle: String + get() = (customTitle.ifBlank { title }) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/ReadStatusSynced.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/ReadStatusSynced.kt index ad91b2dd6a..bda3ac1473 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/ReadStatusSynced.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/ReadStatusSynced.kt @@ -31,15 +31,17 @@ import java.net.URL ), ], ) -data class ReadStatusSynced @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = "sync_remote") var sync_remote: Long = ID_UNSET, - @ColumnInfo(name = "feed_item") var feed_item: Long = ID_UNSET, -) { - constructor() : this(id = ID_UNSET) -} +data class ReadStatusSynced + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = "sync_remote") var sync_remote: Long = ID_UNSET, + @ColumnInfo(name = "feed_item") var feed_item: Long = ID_UNSET, + ) { + constructor() : this(id = ID_UNSET) + } interface ReadStatusFeedItem { val id: Long diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteFeed.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteFeed.kt index 937f81f5be..ed39bd0cec 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteFeed.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteFeed.kt @@ -26,12 +26,14 @@ import java.net.URL ), ], ) -data class RemoteFeed @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = "sync_remote") var syncRemote: Long = ID_UNSET, - @ColumnInfo(name = COL_URL) var url: URL = URL("http://"), -) { - constructor() : this(id = ID_UNSET) -} +data class RemoteFeed + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = "sync_remote") var syncRemote: Long = ID_UNSET, + @ColumnInfo(name = COL_URL) var url: URL = URL("http://"), + ) { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMark.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMark.kt index be266f4497..1ff6c30a6a 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMark.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMark.kt @@ -29,18 +29,20 @@ import java.time.Instant ), ], ) -data class RemoteReadMark @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = "sync_remote") var sync_remote: Long = ID_UNSET, - @ColumnInfo(name = COL_FEEDURL) var feedUrl: URL = URL("http://"), - @ColumnInfo(name = COL_GUID) var guid: String = "", - @ColumnInfo( - name = "timestamp", - typeAffinity = ColumnInfo.INTEGER, - ) var timestamp: Instant = Instant.EPOCH, -) { - @Suppress("unused") - constructor() : this(id = ID_UNSET) -} +data class RemoteReadMark + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = "sync_remote") var sync_remote: Long = ID_UNSET, + @ColumnInfo(name = COL_FEEDURL) var feedUrl: URL = URL("http://"), + @ColumnInfo(name = COL_GUID) var guid: String = "", + @ColumnInfo( + name = "timestamp", + typeAffinity = ColumnInfo.INTEGER, + ) var timestamp: Instant = Instant.EPOCH, + ) { + @Suppress("unused") + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMarkDao.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMarkDao.kt index b7a5ed1446..93a1bb0e89 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMarkDao.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/RemoteReadMarkDao.kt @@ -48,9 +48,11 @@ interface RemoteReadMarkDao { suspend fun getGuidsWhichAreSyncedAsReadInFeed(feedUrl: URL): List } -data class RemoteReadMarkReadyToBeApplied @Ignore constructor( - @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, - @ColumnInfo(name = "feed_item_id") var feedItemId: Long = ID_UNSET, -) { - constructor() : this(id = ID_UNSET) -} +data class RemoteReadMarkReadyToBeApplied + @Ignore + constructor( + @ColumnInfo(name = COL_ID) var id: Long = ID_UNSET, + @ColumnInfo(name = "feed_item_id") var feedItemId: Long = ID_UNSET, + ) { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncDevice.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncDevice.kt index eed2a6e2a1..8f7ee9cd54 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncDevice.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncDevice.kt @@ -24,13 +24,15 @@ import com.nononsenseapps.feeder.db.SYNC_DEVICE_TABLE_NAME ), ], ) -data class SyncDevice @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = "sync_remote") var syncRemote: Long = ID_UNSET, - @ColumnInfo(name = "device_id") var deviceId: Long = ID_UNSET, - @ColumnInfo(name = "device_name") var deviceName: String = "", -) { - constructor() : this(id = ID_UNSET) -} +data class SyncDevice + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = "sync_remote") var syncRemote: Long = ID_UNSET, + @ColumnInfo(name = "device_id") var deviceId: Long = ID_UNSET, + @ColumnInfo(name = "device_name") var deviceName: String = "", + ) { + constructor() : this(id = ID_UNSET) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemote.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemote.kt index 36ea7bc757..e847990b19 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemote.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemote.kt @@ -22,35 +22,39 @@ import kotlin.random.Random @Entity( tableName = SYNC_REMOTE_TABLE_NAME, ) -data class SyncRemote @Ignore constructor( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = COL_ID) - var id: Long = ID_UNSET, - @ColumnInfo(name = COL_URL) var url: URL = URL(DEFAULT_SERVER_ADDRESS), - @ColumnInfo(name = COL_SYNC_CHAIN_ID) var syncChainId: String = "", - @ColumnInfo( - name = COL_LATEST_MESSAGE_TIMESTAMP, - typeAffinity = ColumnInfo.INTEGER, - ) var latestMessageTimestamp: Instant = Instant.EPOCH, - @ColumnInfo(name = COL_DEVICE_ID) var deviceId: Long = 0L, - @ColumnInfo(name = COL_DEVICE_NAME) var deviceName: String = generateDeviceName(), - @ColumnInfo(name = COL_SECRET_KEY) var secretKey: String = "", - @ColumnInfo(name = COL_LAST_FEEDS_REMOTE_HASH) var lastFeedsRemoteHash: Int = 0, -) { - constructor() : this(id = ID_UNSET) +data class SyncRemote + @Ignore + constructor( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + var id: Long = ID_UNSET, + @ColumnInfo(name = COL_URL) var url: URL = URL(DEFAULT_SERVER_ADDRESS), + @ColumnInfo(name = COL_SYNC_CHAIN_ID) var syncChainId: String = "", + @ColumnInfo( + name = COL_LATEST_MESSAGE_TIMESTAMP, + typeAffinity = ColumnInfo.INTEGER, + ) var latestMessageTimestamp: Instant = Instant.EPOCH, + @ColumnInfo(name = COL_DEVICE_ID) var deviceId: Long = 0L, + @ColumnInfo(name = COL_DEVICE_NAME) var deviceName: String = generateDeviceName(), + @ColumnInfo(name = COL_SECRET_KEY) var secretKey: String = "", + @ColumnInfo(name = COL_LAST_FEEDS_REMOTE_HASH) var lastFeedsRemoteHash: Int = 0, + ) { + constructor() : this(id = ID_UNSET) - fun hasSyncChain(): Boolean = syncChainId.length == 64 - fun notHasSyncChain() = !hasSyncChain() -} + fun hasSyncChain(): Boolean = syncChainId.length == 64 + + fun notHasSyncChain() = !hasSyncChain() + } private const val DEFAULT_SERVER_HOST = "feeder-sync.nononsenseapps.com" private const val DEFAULT_SERVER_PORT = 443 const val DEFAULT_SERVER_ADDRESS = "https://$DEFAULT_SERVER_HOST:$DEFAULT_SERVER_PORT" -val DEPRECATED_SYNC_HOSTS = listOf( - "feederapp.nononsenseapps.com", - "feeder-sync.nononsenseapps.workers.dev", -) +val DEPRECATED_SYNC_HOSTS = + listOf( + "feederapp.nononsenseapps.com", + "feeder-sync.nononsenseapps.workers.dev", + ) fun generateDeviceName(): String { val manufacturer = Build.MANUFACTURER ?: "" diff --git a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemoteDao.kt b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemoteDao.kt index 205dbb436a..3e4d092145 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemoteDao.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/db/room/SyncRemoteDao.kt @@ -9,8 +9,8 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import com.nononsenseapps.feeder.crypto.AesCbcWithIntegrity -import java.time.Instant import kotlinx.coroutines.flow.Flow +import java.time.Instant @Dao interface SyncRemoteDao { diff --git a/app/src/main/java/com/nononsenseapps/feeder/di/AndroidModule.kt b/app/src/main/java/com/nononsenseapps/feeder/di/AndroidModule.kt index 867cd3b968..1923b15f41 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/di/AndroidModule.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/di/AndroidModule.kt @@ -5,6 +5,7 @@ import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.singleton -val androidModule = DI.Module(name = "android module") { - bind() with singleton { AndroidSystemStore(di) } -} +val androidModule = + DI.Module(name = "android module") { + bind() with singleton { AndroidSystemStore(di) } + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/di/ArchModelModule.kt b/app/src/main/java/com/nononsenseapps/feeder/di/ArchModelModule.kt index d1cd907771..6df1d559b2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/di/ArchModelModule.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/di/ArchModelModule.kt @@ -23,23 +23,24 @@ import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.singleton -val archModelModule = DI.Module(name = "arch models") { - bind() with singleton { Repository(di) } - bind() with singleton { SessionStore() } - bind() with singleton { SettingsStore(di) } - bind() with singleton { FeedStore(di) } - bind() with singleton { FeedItemStore(di) } - bind() with singleton { SyncRemoteStore(di) } - bind() with singleton { OPMLImporter(di) } +val archModelModule = + DI.Module(name = "arch models") { + bind() with singleton { Repository(di) } + bind() with singleton { SessionStore() } + bind() with singleton { SettingsStore(di) } + bind() with singleton { FeedStore(di) } + bind() with singleton { FeedItemStore(di) } + bind() with singleton { SyncRemoteStore(di) } + bind() with singleton { OPMLImporter(di) } - bindWithActivityViewModelScope() - bindWithActivityViewModelScope() - bindWithActivityViewModelScope() + bindWithActivityViewModelScope() + bindWithActivityViewModelScope() + bindWithActivityViewModelScope() - bindWithComposableViewModelScope() - bindWithComposableViewModelScope() - bindWithComposableViewModelScope() - bindWithComposableViewModelScope() - bindWithComposableViewModelScope() - bindWithComposableViewModelScope() -} + bindWithComposableViewModelScope() + bindWithComposableViewModelScope() + bindWithComposableViewModelScope() + bindWithComposableViewModelScope() + bindWithComposableViewModelScope() + bindWithComposableViewModelScope() + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/di/NetworkModule.kt b/app/src/main/java/com/nononsenseapps/feeder/di/NetworkModule.kt index f849511516..f28e1154be 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/di/NetworkModule.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/di/NetworkModule.kt @@ -15,13 +15,14 @@ import org.kodein.di.instance import org.kodein.di.provider import org.kodein.di.singleton -val networkModule = DI.Module(name = "network") { - // Parsers can carry state so safer to use providers - bind>() with provider { feedAdapter() } - bind() with provider { JsonFeedParser(instance(), instance()) } - bind() with provider { FeedParser(di) } - // These don't have state issues - bind() with singleton { SyncRestClient(di) } - bind() with singleton { RssLocalSync(di) } - bind() with singleton { FullTextParser(di) } -} +val networkModule = + DI.Module(name = "network") { + // Parsers can carry state so safer to use providers + bind>() with provider { feedAdapter() } + bind() with provider { JsonFeedParser(instance(), instance()) } + bind() with provider { FeedParser(di) } + // These don't have state issues + bind() with singleton { SyncRestClient(di) } + bind() with singleton { RssLocalSync(di) } + bind() with singleton { FullTextParser(di) } + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt b/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt index 6e27dad490..5483c16628 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/FeedParser.kt @@ -13,12 +13,6 @@ import com.nononsenseapps.jsonfeed.Feed import com.nononsenseapps.jsonfeed.JsonFeedParser import com.rometools.rome.io.SyndFeedInput import com.rometools.rome.io.XmlReader -import java.io.IOException -import java.net.MalformedURLException -import java.net.URL -import java.net.URLDecoder -import java.util.Locale -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -34,6 +28,12 @@ import org.jsoup.nodes.Document import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.io.IOException +import java.net.MalformedURLException +import java.net.URL +import java.net.URLDecoder +import java.util.Locale +import java.util.concurrent.TimeUnit private const val YOUTUBE_CHANNEL_ID_ATTR = "data-channel-external-id" @@ -81,22 +81,24 @@ class FeedParser(override val di: DI) : DIAware { html: String, baseUrl: URL? = null, ): String? { - val doc = html.byteInputStream().use { - Jsoup.parse(it, "UTF-8", baseUrl?.toString() ?: "") - } + val doc = + html.byteInputStream().use { + Jsoup.parse(it, "UTF-8", baseUrl?.toString() ?: "") + } return ( doc.getElementsByAttributeValue("rel", "apple-touch-icon") + doc.getElementsByAttributeValue("rel", "icon") + doc.getElementsByAttributeValue("rel", "shortcut icon") - ) + ) .filter { it.hasAttr("href") } .firstNotNullOfOrNull { e -> when { - baseUrl != null -> relativeLinkIntoAbsolute( - base = baseUrl, - link = e.attr("href"), - ) + baseUrl != null -> + relativeLinkIntoAbsolute( + base = baseUrl, + link = e.attr("href"), + ) else -> sloppyLinkToStrictURLOrNull(e.attr("href"))?.toString() } @@ -110,83 +112,90 @@ class FeedParser(override val di: DI) : DIAware { html: String, baseUrl: URL? = null, ): List { - val doc = html.byteInputStream().use { - Jsoup.parse(it, "UTF-8", "") - } - - val feeds = doc.getElementsByAttributeValue("rel", "alternate") - ?.filter { it.hasAttr("href") && it.hasAttr("type") } - ?.filter { - val t = it.attr("type").lowercase(Locale.getDefault()) - when { - t.contains("application/atom") -> true - t.contains("application/rss") -> true - // Youtube for example has alternate links with application/json+oembed type. - t == "application/json" -> true - else -> false - } + val doc = + html.byteInputStream().use { + Jsoup.parse(it, "UTF-8", "") } - ?.filter { - val l = it.attr("href").lowercase(Locale.getDefault()) - try { - if (baseUrl != null) { - relativeLinkIntoAbsoluteOrThrow(base = baseUrl, link = l) - } else { - URL(l) + + val feeds = + doc.getElementsByAttributeValue("rel", "alternate") + ?.filter { it.hasAttr("href") && it.hasAttr("type") } + ?.filter { + val t = it.attr("type").lowercase(Locale.getDefault()) + when { + t.contains("application/atom") -> true + t.contains("application/rss") -> true + // Youtube for example has alternate links with application/json+oembed type. + t == "application/json" -> true + else -> false } - true - } catch (_: MalformedURLException) { - false } - } - ?.mapNotNull { e -> - when { - baseUrl != null -> { - try { - AlternateLink( - type = e.attr("type"), - link = relativeLinkIntoAbsoluteOrThrow( - base = baseUrl, - link = e.attr("href"), - ), - ) - } catch (e: Exception) { - null + ?.filter { + val l = it.attr("href").lowercase(Locale.getDefault()) + try { + if (baseUrl != null) { + relativeLinkIntoAbsoluteOrThrow(base = baseUrl, link = l) + } else { + URL(l) } + true + } catch (_: MalformedURLException) { + false } + } + ?.mapNotNull { e -> + when { + baseUrl != null -> { + try { + AlternateLink( + type = e.attr("type"), + link = + relativeLinkIntoAbsoluteOrThrow( + base = baseUrl, + link = e.attr("href"), + ), + ) + } catch (e: Exception) { + null + } + } - else -> sloppyLinkToStrictURLOrNull(e.attr("href"))?.let { l -> - AlternateLink( - type = e.attr("type"), - link = l, - ) + else -> + sloppyLinkToStrictURLOrNull(e.attr("href"))?.let { l -> + AlternateLink( + type = e.attr("type"), + link = l, + ) + } } - } - } ?: emptyList() + } ?: emptyList() return when { feeds.isNotEmpty() -> feeds - baseUrl?.host == "www.youtube.com" || baseUrl?.host == "youtube.com" -> findFeedLinksForYoutube( - doc, - ) + baseUrl?.host == "www.youtube.com" || baseUrl?.host == "youtube.com" -> + findFeedLinksForYoutube( + doc, + ) else -> emptyList() } } private fun findFeedLinksForYoutube(doc: Document): List { - val channelId: String? = doc.body()?.getElementsByAttribute(YOUTUBE_CHANNEL_ID_ATTR) - ?.firstOrNull() - ?.attr(YOUTUBE_CHANNEL_ID_ATTR) + val channelId: String? = + doc.body()?.getElementsByAttribute(YOUTUBE_CHANNEL_ID_ATTR) + ?.firstOrNull() + ?.attr(YOUTUBE_CHANNEL_ID_ATTR) return when (channelId) { null -> emptyList() - else -> listOf( - AlternateLink( - type = "atom", - link = URL("https://www.youtube.com/feeds/videos.xml?channel_id=$channelId"), - ), - ) + else -> + listOf( + AlternateLink( + type = "atom", + link = URL("https://www.youtube.com/feeds/videos.xml?channel_id=$channelId"), + ), + ) } } @@ -223,13 +232,14 @@ class FeedParser(override val di: DI) : DIAware { responseBody: ResponseBody, ): Either { return when (responseBody.contentType()?.subtype?.contains("json")) { - true -> Either.catching( - onCatch = { t -> - JsonFeedParseError(url = url.toString(), throwable = t) - }, - ) { - jsonFeedParser.parseJson(responseBody) - } + true -> + Either.catching( + onCatch = { t -> + JsonFeedParseError(url = url.toString(), throwable = t) + }, + ) { + jsonFeedParser.parseJson(responseBody) + } else -> parseRssAtom(url, responseBody) } @@ -252,13 +262,14 @@ class FeedParser(override val di: DI) : DIAware { contentType: MediaType?, ): Either { return when (contentType?.subtype?.contains("json")) { - true -> Either.catching( - onCatch = { t -> - JsonFeedParseError(url = url.toString(), throwable = t) - }, - ) { - jsonFeedParser.parseJson(body) - } + true -> + Either.catching( + onCatch = { t -> + JsonFeedParseError(url = url.toString(), throwable = t) + }, + ) { + jsonFeedParser.parseJson(body) + } else -> parseRssAtom(url, body) }.map { feed -> @@ -277,22 +288,23 @@ class FeedParser(override val di: DI) : DIAware { responseBody: ResponseBody, ): Either { val contentType = responseBody.contentType() - val validMimeType = when (contentType?.type) { - "application" -> { - when { - contentType.subtype.contains("xml") -> true - else -> false + val validMimeType = + when (contentType?.type) { + "application" -> { + when { + contentType.subtype.contains("xml") -> true + else -> false + } } - } - "text" -> { - // So many sites on the internet return mimetype text/html for rss feeds... - // So try to parse it despite it being wrong - true - } + "text" -> { + // So many sites on the internet return mimetype text/html for rss feeds... + // So try to parse it despite it being wrong + true + } - else -> false - } + else -> false + } if (!validMimeType) { return Either.Left( UnsupportedContentType(url = url.toString(), mimeType = contentType.toString()), @@ -305,33 +317,38 @@ class FeedParser(override val di: DI) : DIAware { }, ) { responseBody.byteStream().use { bs -> - val feed = XmlReader(bs, true, responseBody.contentType()?.charset()?.name()).use { - SyndFeedInput() - .apply { - isPreserveWireFeed = true - } - .build(it) - } + val feed = + XmlReader(bs, true, responseBody.contentType()?.charset()?.name()).use { + SyndFeedInput() + .apply { + isPreserveWireFeed = true + } + .build(it) + } feed.asFeed(baseUrl = url) } } } @Throws(FeedParsingError::class) - internal fun parseRssAtom(baseUrl: URL, body: String): Either { + internal fun parseRssAtom( + baseUrl: URL, + body: String, + ): Either { return Either.catching( onCatch = { t -> RSSParseError(url = baseUrl.toString(), throwable = t) }, ) { body.byteInputStream().use { bs -> - val feed = XmlReader(bs, true).use { - SyndFeedInput() - .apply { - isPreserveWireFeed = true - } - .build(it) - } + val feed = + XmlReader(bs, true).use { + SyndFeedInput() + .apply { + isPreserveWireFeed = true + } + .build(it) + } feed.asFeed(baseUrl = baseUrl) } } @@ -348,67 +365,70 @@ suspend fun OkHttpClient.getResponse( url: URL, forceNetwork: Boolean = false, ): Response { - val request = Request.Builder() - .url(url) - .cacheControl( - CacheControl.Builder() - // The time between cache re-validations - .maxAge( - if (forceNetwork) { - 0 - } else { - // Matches fastest sync schedule - 15 - }, - TimeUnit.MINUTES, - ) - .build(), - ) - .build() + val request = + Request.Builder() + .url(url) + .cacheControl( + CacheControl.Builder() + // The time between cache re-validations + .maxAge( + if (forceNetwork) { + 0 + } else { + // Matches fastest sync schedule + 15 + }, + TimeUnit.MINUTES, + ) + .build(), + ) + .build() @Suppress("BlockingMethodInNonBlockingContext") - val clientToUse = if (url.userInfo?.isNotBlank() == true) { - val parts = url.userInfo.split(':') - val user = parts.first() - val pass = if (parts.size > 1) { - parts[1] - } else { - "" - } - val decodedUser = URLDecoder.decode(user, "UTF-8") - val decodedPass = URLDecoder.decode(pass, "UTF-8") - val credentials = Credentials.basic(decodedUser, decodedPass) - newBuilder() - .authenticator { _, response -> - when { - response.request.header("Authorization") != null -> { - null - } + val clientToUse = + if (url.userInfo?.isNotBlank() == true) { + val parts = url.userInfo.split(':') + val user = parts.first() + val pass = + if (parts.size > 1) { + parts[1] + } else { + "" + } + val decodedUser = URLDecoder.decode(user, "UTF-8") + val decodedPass = URLDecoder.decode(pass, "UTF-8") + val credentials = Credentials.basic(decodedUser, decodedPass) + newBuilder() + .authenticator { _, response -> + when { + response.request.header("Authorization") != null -> { + null + } - else -> { - response.request.newBuilder() - .header("Authorization", credentials) - .build() + else -> { + response.request.newBuilder() + .header("Authorization", credentials) + .build() + } } } - } - .proxyAuthenticator { _, response -> - when { - response.request.header("Proxy-Authorization") != null -> { - null - } + .proxyAuthenticator { _, response -> + when { + response.request.header("Proxy-Authorization") != null -> { + null + } - else -> { - response.request.newBuilder() - .header("Proxy-Authorization", credentials) - .build() + else -> { + response.request.newBuilder() + .header("Proxy-Authorization", credentials) + .build() + } } } - } - .build() - } else { - this - } + .build() + } else { + this + } return withContext(IO) { clientToUse.newCall(request).execute() @@ -440,21 +460,23 @@ suspend fun OkHttpClient.curl(url: URL): Either { ) } - else -> Either.Left( - UnsupportedContentType( - url = url.toString(), - mimeType = contentType.toString(), - ), - ) + else -> + Either.Left( + UnsupportedContentType( + url = url.toString(), + mimeType = contentType.toString(), + ), + ) } } - else -> Either.Left( - UnsupportedContentType( - url = url.toString(), - mimeType = contentType.toString(), - ), - ) + else -> + Either.Left( + UnsupportedContentType( + url = url.toString(), + mimeType = contentType.toString(), + ), + ) } } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/FeedUnreadCount.kt b/app/src/main/java/com/nononsenseapps/feeder/model/FeedUnreadCount.kt index 3613836744..ab96e5005a 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/FeedUnreadCount.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/FeedUnreadCount.kt @@ -8,71 +8,72 @@ import com.nononsenseapps.feeder.db.room.ID_UNSET import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLNoThrows import java.net.URL -data class FeedUnreadCount @Ignore constructor( - var id: Long = ID_UNSET, - var title: String = "", - var url: URL = sloppyLinkToStrictURLNoThrows(""), - var tag: String = "", - @ColumnInfo(name = "custom_title") - var customTitle: String = "", - var notify: Boolean = false, - @ColumnInfo(name = COL_CURRENTLY_SYNCING) var currentlySyncing: Boolean = false, - @ColumnInfo(name = "image_url") var imageUrl: URL? = null, - @ColumnInfo(name = "unread_count") var unreadCount: Int = 0, +data class FeedUnreadCount + @Ignore + constructor( + var id: Long = ID_UNSET, + var title: String = "", + var url: URL = sloppyLinkToStrictURLNoThrows(""), + var tag: String = "", + @ColumnInfo(name = "custom_title") + var customTitle: String = "", + var notify: Boolean = false, + @ColumnInfo(name = COL_CURRENTLY_SYNCING) var currentlySyncing: Boolean = false, + @ColumnInfo(name = "image_url") var imageUrl: URL? = null, + @ColumnInfo(name = "unread_count") var unreadCount: Int = 0, + ) : Comparable { + constructor() : this(id = ID_UNSET) -) : Comparable { - constructor() : this(id = ID_UNSET) + val displayTitle: String + get() = customTitle.ifBlank { title } - val displayTitle: String - get() = customTitle.ifBlank { title } + val isTop: Boolean + get() = id == ID_ALL_FEEDS - val isTop: Boolean - get() = id == ID_ALL_FEEDS + val isTag: Boolean + get() = id < 1 && tag.isNotEmpty() - val isTag: Boolean - get() = id < 1 && tag.isNotEmpty() - - override operator fun compareTo(other: FeedUnreadCount): Int { - return when { - // Top tag is always at the top (implies empty tags) - isTop -> -1 - other.isTop -> 1 - // Feeds with no tags are always last - isTag && !other.isTag && other.tag.isEmpty() -> -1 - !isTag && other.isTag && tag.isEmpty() -> 1 - !isTag && !other.isTag && tag.isNotEmpty() && other.tag.isEmpty() -> -1 - !isTag && !other.isTag && tag.isEmpty() && other.tag.isNotEmpty() -> 1 - // Feeds with identical tags compare by title - tag == other.tag -> displayTitle.compareTo(other.displayTitle, ignoreCase = true) - // In other cases it's just a matter of comparing tags - else -> tag.compareTo(other.tag, ignoreCase = true) + override operator fun compareTo(other: FeedUnreadCount): Int { + return when { + // Top tag is always at the top (implies empty tags) + isTop -> -1 + other.isTop -> 1 + // Feeds with no tags are always last + isTag && !other.isTag && other.tag.isEmpty() -> -1 + !isTag && other.isTag && tag.isEmpty() -> 1 + !isTag && !other.isTag && tag.isNotEmpty() && other.tag.isEmpty() -> -1 + !isTag && !other.isTag && tag.isEmpty() && other.tag.isNotEmpty() -> 1 + // Feeds with identical tags compare by title + tag == other.tag -> displayTitle.compareTo(other.displayTitle, ignoreCase = true) + // In other cases it's just a matter of comparing tags + else -> tag.compareTo(other.tag, ignoreCase = true) + } } - } - override fun equals(other: Any?): Boolean { - return when (other) { - null -> false - is FeedUnreadCount -> { - // val f = other as FeedWrapper? - if (isTag && other.isTag) { - // Compare tags - tag == other.tag - } else { - // Compare items - !isTag && !other.isTag && id == other.id + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is FeedUnreadCount -> { + // val f = other as FeedWrapper? + if (isTag && other.isTag) { + // Compare tags + tag == other.tag + } else { + // Compare items + !isTag && !other.isTag && id == other.id + } } + else -> false } - else -> false } - } - override fun hashCode(): Int { - return if (isTag) { - // Tag - tag.hashCode() - } else { - // Item - id.hashCode() + override fun hashCode(): Int { + return if (isTag) { + // Tag + tag.hashCode() + } else { + // Item + id.hashCode() + } } } -} diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/FullTextParser.kt b/app/src/main/java/com/nononsenseapps/feeder/model/FullTextParser.kt index baa987a6a2..24ab474360 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/FullTextParser.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/FullTextParser.kt @@ -18,9 +18,6 @@ import com.nononsenseapps.feeder.util.FilePathProvider import com.nononsenseapps.feeder.util.flatten import com.nononsenseapps.feeder.util.left import com.nononsenseapps.feeder.util.logDebug -import java.net.URL -import java.nio.charset.Charset -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext @@ -30,14 +27,16 @@ import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance +import java.net.URL +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit -fun scheduleFullTextParse( - di: DI, -) { +fun scheduleFullTextParse(di: DI) { Log.i(LOG_TAG, "Scheduling a full text parse work") - val workRequest = OneTimeWorkRequestBuilder() - .addTag("feeder") - .keepResultsForAtLeast(1, TimeUnit.MINUTES) + val workRequest = + OneTimeWorkRequestBuilder() + .addTag("feeder") + .keepResultsForAtLeast(1, TimeUnit.MINUTES) val workManager by di.instance() workManager.enqueue(workRequest.build()) @@ -116,15 +115,17 @@ class FullTextParser(override val di: DI) : DIAware { ) { val body = response.body ?: return@catching NoBody(url = url).left() - val bytes = body.use { - it.bytes() - } + val bytes = + body.use { + it.bytes() + } - val contentType = body.contentType() - ?: return@catching UnsupportedContentType( - url = url, - mimeType = "null", - ).left() + val contentType = + body.contentType() + ?: return@catching UnsupportedContentType( + url = url, + mimeType = "null", + ).left() if (contentType.type != "text" || contentType.subtype != "html") { return@catching UnsupportedContentType( @@ -197,38 +198,47 @@ class FullTextParser(override val di: DI) : DIAware { return null } - private fun nextCharsetState(byte: Byte, state: CharsetState): CharsetState = + private fun nextCharsetState( + byte: Byte, + state: CharsetState, + ): CharsetState = when (state) { CharsetState.END -> CharsetState.END - CharsetState.INIT -> when (byte.toInt().toChar()) { - '<' -> CharsetState.TAG_START - else -> CharsetState.INIT - } + CharsetState.INIT -> + when (byte.toInt().toChar()) { + '<' -> CharsetState.TAG_START + else -> CharsetState.INIT + } - CharsetState.TAG_START -> when (byte.toInt().toChar()) { - 'm' -> CharsetState.META_M - else -> CharsetState.TAG_END - } + CharsetState.TAG_START -> + when (byte.toInt().toChar()) { + 'm' -> CharsetState.META_M + else -> CharsetState.TAG_END + } - CharsetState.TAG_END -> when (byte.toInt().toChar()) { - '>' -> CharsetState.INIT - else -> CharsetState.TAG_END - } + CharsetState.TAG_END -> + when (byte.toInt().toChar()) { + '>' -> CharsetState.INIT + else -> CharsetState.TAG_END + } - CharsetState.META_M -> when (byte.toInt().toChar()) { - 'e' -> CharsetState.META_E - else -> CharsetState.TAG_END - } + CharsetState.META_M -> + when (byte.toInt().toChar()) { + 'e' -> CharsetState.META_E + else -> CharsetState.TAG_END + } - CharsetState.META_E -> when (byte.toInt().toChar()) { - 't' -> CharsetState.META_T - else -> CharsetState.TAG_END - } + CharsetState.META_E -> + when (byte.toInt().toChar()) { + 't' -> CharsetState.META_T + else -> CharsetState.TAG_END + } - CharsetState.META_T -> when (byte.toInt().toChar()) { - 'a' -> CharsetState.META_A - else -> CharsetState.TAG_END - } + CharsetState.META_T -> + when (byte.toInt().toChar()) { + 'a' -> CharsetState.META_A + else -> CharsetState.TAG_END + } CharsetState.META_A -> { val c = byte.toInt().toChar() @@ -247,55 +257,65 @@ class FullTextParser(override val di: DI) : DIAware { } } - CharsetState.CHARSET_C -> when (byte.toInt().toChar()) { - 'h' -> CharsetState.CHARSET_H - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_C -> + when (byte.toInt().toChar()) { + 'h' -> CharsetState.CHARSET_H + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_H -> when (byte.toInt().toChar()) { - 'a' -> CharsetState.CHARSET_A - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_H -> + when (byte.toInt().toChar()) { + 'a' -> CharsetState.CHARSET_A + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_A -> when (byte.toInt().toChar()) { - 'r' -> CharsetState.CHARSET_R - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_A -> + when (byte.toInt().toChar()) { + 'r' -> CharsetState.CHARSET_R + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_R -> when (byte.toInt().toChar()) { - 's' -> CharsetState.CHARSET_S - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_R -> + when (byte.toInt().toChar()) { + 's' -> CharsetState.CHARSET_S + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_S -> when (byte.toInt().toChar()) { - 'e' -> CharsetState.CHARSET_E - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_S -> + when (byte.toInt().toChar()) { + 'e' -> CharsetState.CHARSET_E + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_E -> when (byte.toInt().toChar()) { - 't' -> CharsetState.CHARSET_T - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_E -> + when (byte.toInt().toChar()) { + 't' -> CharsetState.CHARSET_T + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_T -> when (byte.toInt().toChar()) { - '=' -> CharsetState.CHARSET_EQUALS - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_T -> + when (byte.toInt().toChar()) { + '=' -> CharsetState.CHARSET_EQUALS + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_EQUALS -> when (byte.toInt().toChar()) { - '"' -> CharsetState.CHARSET_QUOTE - else -> CharsetState.TAG_END - } + CharsetState.CHARSET_EQUALS -> + when (byte.toInt().toChar()) { + '"' -> CharsetState.CHARSET_QUOTE + else -> CharsetState.TAG_END + } - CharsetState.CHARSET_QUOTE -> when (byte.toInt().toChar()) { - '"' -> CharsetState.END - else -> CharsetState.CHARSET - } + CharsetState.CHARSET_QUOTE -> + when (byte.toInt().toChar()) { + '"' -> CharsetState.END + else -> CharsetState.CHARSET + } - CharsetState.CHARSET -> when (byte.toInt().toChar()) { - '"' -> CharsetState.END - else -> CharsetState.CHARSET - } + CharsetState.CHARSET -> + when (byte.toInt().toChar()) { + '"' -> CharsetState.END + else -> CharsetState.CHARSET + } } companion object { diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/OPMLParserHandler.kt b/app/src/main/java/com/nononsenseapps/feeder/model/OPMLParserHandler.kt index 7cb892e9e6..ff684795fb 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/OPMLParserHandler.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/OPMLParserHandler.kt @@ -3,9 +3,12 @@ package com.nononsenseapps.feeder.model import com.nononsenseapps.feeder.db.room.Feed interface OPMLParserHandler { - suspend fun saveFeed(feed: Feed) - suspend fun saveSetting(key: String, value: String) + suspend fun saveSetting( + key: String, + value: String, + ) + suspend fun saveBlocklistPatterns(patterns: Iterable) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/PreviewItem.kt b/app/src/main/java/com/nononsenseapps/feeder/model/PreviewItem.kt index f862210ebb..f45457e00d 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/PreviewItem.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/PreviewItem.kt @@ -24,46 +24,49 @@ const val previewColumns = """ feeds.fulltext_by_default as fulltext_by_default """ -data class PreviewItem @Ignore constructor( - var id: Long = ID_UNSET, - var guid: String = "", - @ColumnInfo(name = "plain_title") var plainTitle: String = "", - @ColumnInfo(name = "plain_snippet") var plainSnippet: String = "", - @ColumnInfo(name = "image_url") var imageUrl: String? = null, - @ColumnInfo(name = "enclosure_link") var enclosureLink: String? = null, - var author: String? = null, - @ColumnInfo(name = "pub_date") var pubDate: ZonedDateTime? = null, - var link: String? = null, - var tag: String = "", - @ColumnInfo(name = COL_READ_TIME) var readTime: Instant? = null, - @ColumnInfo(name = "feed_id") var feedId: Long? = null, - @ColumnInfo(name = "feed_title") var feedTitle: String = "", - @ColumnInfo(name = "feed_customtitle") var feedCustomTitle: String = "", - @ColumnInfo(name = "feed_url") var feedUrl: URL = sloppyLinkToStrictURLNoThrows(""), - @ColumnInfo(name = "feed_open_articles_with") var feedOpenArticlesWith: String = "", - @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, - @ColumnInfo(name = "feed_image_url") var feedImageUrl: URL? = null, - @ColumnInfo(name = COL_PRIMARYSORTTIME) var primarySortTime: Instant = Instant.EPOCH, - @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, - @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, - @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, -) { - constructor() : this(id = ID_UNSET) +data class PreviewItem + @Ignore + constructor( + var id: Long = ID_UNSET, + var guid: String = "", + @ColumnInfo(name = "plain_title") var plainTitle: String = "", + @ColumnInfo(name = "plain_snippet") var plainSnippet: String = "", + @ColumnInfo(name = "image_url") var imageUrl: String? = null, + @ColumnInfo(name = "enclosure_link") var enclosureLink: String? = null, + var author: String? = null, + @ColumnInfo(name = "pub_date") var pubDate: ZonedDateTime? = null, + var link: String? = null, + var tag: String = "", + @ColumnInfo(name = COL_READ_TIME) var readTime: Instant? = null, + @ColumnInfo(name = "feed_id") var feedId: Long? = null, + @ColumnInfo(name = "feed_title") var feedTitle: String = "", + @ColumnInfo(name = "feed_customtitle") var feedCustomTitle: String = "", + @ColumnInfo(name = "feed_url") var feedUrl: URL = sloppyLinkToStrictURLNoThrows(""), + @ColumnInfo(name = "feed_open_articles_with") var feedOpenArticlesWith: String = "", + @ColumnInfo(name = COL_BOOKMARKED) var bookmarked: Boolean = false, + @ColumnInfo(name = "feed_image_url") var feedImageUrl: URL? = null, + @ColumnInfo(name = COL_PRIMARYSORTTIME) var primarySortTime: Instant = Instant.EPOCH, + @ColumnInfo(name = COL_WORD_COUNT) var wordCount: Int = 0, + @ColumnInfo(name = COL_WORD_COUNT_FULL) var wordCountFull: Int = 0, + @ColumnInfo(name = COL_FULLTEXT_BY_DEFAULT) var fullTextByDefault: Boolean = false, + ) { + constructor() : this(id = ID_UNSET) - val feedDisplayTitle: String - get() = feedCustomTitle.ifBlank { feedTitle } + val feedDisplayTitle: String + get() = feedCustomTitle.ifBlank { feedTitle } - val domain: String? - get() { - return (enclosureLink ?: link)?.host() - } + val domain: String? + get() { + return (enclosureLink ?: link)?.host() + } - val bestWordCount: Int - get() = when (fullTextByDefault) { - true -> wordCountFull - false -> wordCount - } -} + val bestWordCount: Int + get() = + when (fullTextByDefault) { + true -> wordCountFull + false -> wordCount + } + } fun String?.host(): String? { val l: String? = this diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt b/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt index 3e923c47be..2b325b1611 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/ReadAloudStateHolder.kt @@ -13,7 +13,6 @@ import androidx.annotation.RequiresApi import androidx.compose.runtime.Immutable import androidx.compose.ui.text.AnnotatedString import com.nononsenseapps.feeder.R -import java.util.Locale import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,6 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import java.util.Locale /** * Any callers must call #shutdown when shutting down @@ -56,7 +56,10 @@ class TTSStateHolder( override fun onStart(utteranceId: String) { } - override fun onError(utteranceId: String?, errorCode: Int) { + override fun onError( + utteranceId: String?, + errorCode: Int, + ) { Log.e(LOG_TAG, "onError utteranceId $utteranceId, errorCode $errorCode") if (utteranceId != null) { @@ -142,7 +145,10 @@ class TTSStateHolder( } } - fun tts(textArray: List, useDetectLanguage: Boolean) { + fun tts( + textArray: List, + useDetectLanguage: Boolean, + ) { this.useDetectLanguage = useDetectLanguage // val textArray = fullText.split(*PUNCTUATION) for (text in textArray) { @@ -156,46 +162,48 @@ class TTSStateHolder( fun play() { startJob?.cancel() - startJob = coroutineScope.launch { - if (mutex.isLocked) { - // Oops, I was double clicked - return@launch - } - mutex.withLock { - if (textToSpeech == null) { - initializedState = null - textToSpeech = TextToSpeech( - context, - this@TTSStateHolder, - ) - } - while (initializedState == null) { - delay(100) - } - if (initializedState != TextToSpeech.SUCCESS) { - withContext(Dispatchers.Main) { - Toast.makeText( - context, - R.string.failed_to_load_text_to_speech, - Toast.LENGTH_SHORT, - ) - .show() - } + startJob = + coroutineScope.launch { + if (mutex.isLocked) { + // Oops, I was double clicked return@launch } - _ttsState.value = PlaybackStatus.PLAYING - - // Can only set this once engine has been initialized - textToSpeech?.setOnUtteranceProgressListener(speechListener) - try { - updateAvailableLanguages() - speakNext() - } catch (e: ConcurrentModificationException) { - Log.e(LOG_TAG, "User probably double clicked play", e) - // State will be weird. But mutex should prevent it happening + mutex.withLock { + if (textToSpeech == null) { + initializedState = null + textToSpeech = + TextToSpeech( + context, + this@TTSStateHolder, + ) + } + while (initializedState == null) { + delay(100) + } + if (initializedState != TextToSpeech.SUCCESS) { + withContext(Dispatchers.Main) { + Toast.makeText( + context, + R.string.failed_to_load_text_to_speech, + Toast.LENGTH_SHORT, + ) + .show() + } + return@launch + } + _ttsState.value = PlaybackStatus.PLAYING + + // Can only set this once engine has been initialized + textToSpeech?.setOnUtteranceProgressListener(speechListener) + try { + updateAvailableLanguages() + speakNext() + } catch (e: ConcurrentModificationException) { + Log.e(LOG_TAG, "User probably double clicked play", e) + // State will be weird. But mutex should prevent it happening + } } } - } } fun pause() { @@ -246,31 +254,32 @@ class TTSStateHolder( fun updateAvailableLanguages() { allAvailableLanguages = textToSpeech?.availableLanguages ?: emptySet() - val sortedLanguages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - context.detectLocaleFromText( - textToSpeechQueue.joinToString("\n\n"), - minConfidence = 0f, - ) - .sortedByDescending { it.confidence } - .map { it.locale } - .plus( - context.getLocales() - .sortedBy { it.getDisplayName(it).lowercase(it) }, + val sortedLanguages = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + context.detectLocaleFromText( + textToSpeechQueue.joinToString("\n\n"), + minConfidence = 0f, ) - .plus( - allAvailableLanguages.asSequence() - .sortedBy { it.getDisplayName(it).lowercase(it) }, - ) - } else { - context.getLocales() - .sortedBy { it.displayName } - .plus( - allAvailableLanguages.asSequence() - .sortedBy { it.getDisplayName(it).lowercase(it) }, - ) - } - .distinctBy { it.toLanguageTag() } - .toList() + .sortedByDescending { it.confidence } + .map { it.locale } + .plus( + context.getLocales() + .sortedBy { it.getDisplayName(it).lowercase(it) }, + ) + .plus( + allAvailableLanguages.asSequence() + .sortedBy { it.getDisplayName(it).lowercase(it) }, + ) + } else { + context.getLocales() + .sortedBy { it.displayName } + .plus( + allAvailableLanguages.asSequence() + .sortedBy { it.getDisplayName(it).lowercase(it) }, + ) + } + .distinctBy { it.toLanguageTag() } + .toList() _availableLanguages.update { sortedLanguages @@ -311,60 +320,61 @@ class TTSStateHolder( companion object { private const val LOG_TAG = "FeederTextToSpeech" - private val PUNCTUATION = arrayOf( - // New-lines - "\n", - // Very useful: https://unicodelookup.com/ - // Full stop - ".", - "։", - "۔", - "܁", - "܂", - "。", - "︒", - "﹒", - ".", - "。", - // Question mark - "?", - ";", - "՞", - "؟", - "⁇", - "⁈", - "⁉", - "︖", - "﹖", - "?", - // Exclamation mark - "!", - "՜", - "‼", - "︕", - "﹗", - "!", - // Colon and semi-colon - ":", - ";", - "؛", - "︓", - "︔", - "﹔", - "﹕", - ":", - ";", - // Ellipsis - "...", - "…", - "⋯", - "⋮", - "︙", - // Dash - "—", - "〜", - "〰", - ) + private val PUNCTUATION = + arrayOf( + // New-lines + "\n", + // Very useful: https://unicodelookup.com/ + // Full stop + ".", + "։", + "۔", + "܁", + "܂", + "。", + "︒", + "﹒", + ".", + "。", + // Question mark + "?", + ";", + "՞", + "؟", + "⁇", + "⁈", + "⁉", + "︖", + "﹖", + "?", + // Exclamation mark + "!", + "՜", + "‼", + "︕", + "﹗", + "!", + // Colon and semi-colon + ":", + ";", + "؛", + "︓", + "︔", + "﹔", + "﹕", + ":", + ";", + // Ellipsis + "...", + "…", + "⋯", + "⋮", + "︙", + // Dash + "—", + "〜", + "〰", + ) } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/RssLocalSync.kt b/app/src/main/java/com/nononsenseapps/feeder/model/RssLocalSync.kt index 8c4d741067..d45ee77daa 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/RssLocalSync.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/RssLocalSync.kt @@ -15,13 +15,6 @@ import com.nononsenseapps.feeder.util.left import com.nononsenseapps.feeder.util.logDebug import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLNoThrows import com.nononsenseapps.jsonfeed.Item -import java.io.IOException -import java.net.URL -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.concurrent.Executors -import kotlin.math.max -import kotlin.system.measureTimeMillis import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher @@ -35,6 +28,13 @@ import okhttp3.OkHttpClient import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance +import java.io.IOException +import java.net.URL +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.concurrent.Executors +import kotlin.math.max +import kotlin.system.measureTimeMillis val singleThreadedSync = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val syncMutex = Mutex() @@ -77,88 +77,92 @@ class RssLocalSync(override val di: DI) : DIAware { var needFullTextSync = false // Let all new items share download time val downloadTime = Instant.now() - val time = measureTimeMillis { - try { - supervisorScope { - val staleTime: Long = if (forceNetwork) { - Instant.now().toEpochMilli() - } else { - Instant.now().minus(minFeedAgeMinutes.toLong(), ChronoUnit.MINUTES) - .toEpochMilli() - } - // Fetch sync stuff first - this is fast - try { - syncClient.getFeeds() - syncClient.getRead() - syncClient.getDevices() - syncClient.sendUpdatedFeeds() - syncClient.markAsRead() - } catch (e: Exception) { - Log.e(LOG_TAG, "error with syncClient: ${e.message}", e) - } + val time = + measureTimeMillis { + try { + supervisorScope { + val staleTime: Long = + if (forceNetwork) { + Instant.now().toEpochMilli() + } else { + Instant.now().minus(minFeedAgeMinutes.toLong(), ChronoUnit.MINUTES) + .toEpochMilli() + } + // Fetch sync stuff first - this is fast + try { + syncClient.getFeeds() + syncClient.getRead() + syncClient.getDevices() + syncClient.sendUpdatedFeeds() + syncClient.markAsRead() + } catch (e: Exception) { + Log.e(LOG_TAG, "error with syncClient: ${e.message}", e) + } - val feedsToFetch = - feedsToSync(feedId, feedTag, staleTime = staleTime) + val feedsToFetch = + feedsToSync(feedId, feedTag, staleTime = staleTime) - logDebug(LOG_TAG, "Syncing ${feedsToFetch.size} feeds") + logDebug(LOG_TAG, "Syncing ${feedsToFetch.size} feeds") - val coroutineContext = - this.coroutineContext + CoroutineExceptionHandler { _, throwable -> - Log.e(LOG_TAG, "Error during sync", throwable) - } + val coroutineContext = + this.coroutineContext + + CoroutineExceptionHandler { _, throwable -> + Log.e(LOG_TAG, "Error during sync", throwable) + } - val jobs = feedsToFetch.map { - needFullTextSync = needFullTextSync || it.fullTextByDefault - launch(coroutineContext) { - try { - // Want unique sync times so UI gets updated state - repository.setCurrentlySyncingOn( - feedId = it.id, - syncing = true, - lastSync = Instant.now(), - ) - syncFeed( - feedSql = it, - maxFeedItemCount = maxFeedItemCount, - forceNetwork = forceNetwork, - downloadTime = downloadTime, - ).onLeft { feedParserError -> - Log.e( - LOG_TAG, - "Failed to sync ${it.displayTitle}: ${it.url} because:\n${feedParserError.description}", - ) + val jobs = + feedsToFetch.map { + needFullTextSync = needFullTextSync || it.fullTextByDefault + launch(coroutineContext) { + try { + // Want unique sync times so UI gets updated state + repository.setCurrentlySyncingOn( + feedId = it.id, + syncing = true, + lastSync = Instant.now(), + ) + syncFeed( + feedSql = it, + maxFeedItemCount = maxFeedItemCount, + forceNetwork = forceNetwork, + downloadTime = downloadTime, + ).onLeft { feedParserError -> + Log.e( + LOG_TAG, + "Failed to sync ${it.displayTitle}: ${it.url} because:\n${feedParserError.description}", + ) + } + } catch (e: Throwable) { + Log.e( + LOG_TAG, + "Failed to sync ${it.displayTitle}: ${it.url}", + e, + ) + } finally { + repository.setCurrentlySyncingOn(feedId = it.id, syncing = false) + } } - } catch (e: Throwable) { - Log.e( - LOG_TAG, - "Failed to sync ${it.displayTitle}: ${it.url}", - e, - ) - } finally { - repository.setCurrentlySyncingOn(feedId = it.id, syncing = false) } + + jobs.joinAll() + try { + repository.applyRemoteReadMarks() + } catch (e: Exception) { + Log.e(LOG_TAG, "Error on final apply", e) } - } - jobs.joinAll() - try { - repository.applyRemoteReadMarks() - } catch (e: Exception) { - Log.e(LOG_TAG, "Error on final apply", e) + result = true + } + } catch (e: Throwable) { + Log.e(LOG_TAG, "Outer error", e) + } finally { + if (needFullTextSync) { + scheduleFullTextParse( + di = di, + ) } - - result = true - } - } catch (e: Throwable) { - Log.e(LOG_TAG, "Outer error", e) - } finally { - if (needFullTextSync) { - scheduleFullTextParse( - di = di, - ) } } - } logDebug(LOG_TAG, "Completed in $time ms") return result } @@ -222,10 +226,11 @@ class RssLocalSync(override val di: DI) : DIAware { val feedItemSqls = items ?.map { - val guid = when (isNotUniqueIds || feedSql.alternateId) { - true -> it.alternateId - else -> it.id ?: it.alternateId - } + val guid = + when (isNotUniqueIds || feedSql.alternateId) { + true -> it.alternateId + else -> it.id ?: it.alternateId + } it to guid } @@ -265,12 +270,13 @@ class RssLocalSync(override val di: DI) : DIAware { } // Try to look for image if not done before if (feedSql.imageUrl == null && feedSql.siteFetched == Instant.EPOCH) { - val siteUrl = try { - URL(feed.home_page_url) - } catch (e: Throwable) { - logDebug(LOG_TAG, "Bad site url: ${feed.home_page_url}", e) - null - } + val siteUrl = + try { + URL(feed.home_page_url) + } catch (e: Throwable) { + logDebug(LOG_TAG, "Bad site url: ${feed.home_page_url}", e) + null + } if (siteUrl != null) { feedSql.siteFetched = Instant.now() feedParser.getSiteMetaData(siteUrl) @@ -301,10 +307,11 @@ class RssLocalSync(override val di: DI) : DIAware { repository.upsertFeed(feedSql) // Finally, prune database of old items - val ids = repository.getItemsToBeCleanedFromFeed( - feedId = feedSql.id, - keepCount = max(maxFeedItemCount, items?.size ?: 0), - ) + val ids = + repository.getItemsToBeCleanedFromFeed( + feedId = feedSql.id, + keepCount = max(maxFeedItemCount, items?.size ?: 0), + ) for (id in ids) { blobFile(itemId = id, filesDir = filePathProvider.articleDir).let { file -> @@ -345,14 +352,15 @@ class RssLocalSync(override val di: DI) : DIAware { ): List { return when { feedId > 0 -> { - val feed = if (staleTime > 0) { - repository.loadFeedIfStale( - feedId, - staleTime = staleTime, - ) - } else { - repository.loadFeed(feedId) - } + val feed = + if (staleTime > 0) { + repository.loadFeedIfStale( + feedId, + staleTime = staleTime, + ) + } else { + repository.loadFeed(feedId) + } if (feed != null) { listOf(feed) } else { @@ -360,14 +368,15 @@ class RssLocalSync(override val di: DI) : DIAware { } } - tag.isNotEmpty() -> if (staleTime > 0) { - repository.loadFeedsIfStale( - tag = tag, - staleTime = staleTime, - ) - } else { - repository.loadFeeds(tag) - } + tag.isNotEmpty() -> + if (staleTime > 0) { + repository.loadFeedsIfStale( + tag = tag, + staleTime = staleTime, + ) + } else { + repository.loadFeeds(tag) + } else -> if (staleTime > 0) repository.loadFeedsIfStale(staleTime) else repository.loadFeeds() } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotificationBroadcastReceiver.kt b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotificationBroadcastReceiver.kt index 1f612bbd34..77184e0ae1 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotificationBroadcastReceiver.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotificationBroadcastReceiver.kt @@ -19,24 +19,32 @@ const val ACTION_MARK_AS_READ: String = "mark_as_read" const val EXTRA_FEEDITEM_ID_ARRAY: String = "extra_feeditem_id_array" class RssNotificationBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { + override fun onReceive( + context: Context, + intent: Intent, + ) { val ids = intent.getLongArrayExtra(EXTRA_FEEDITEM_ID_ARRAY) Log.d("RssNotificationReceiver", "onReceive: ${intent.action}; ${ids?.joinToString(", ")}") val di by closestDI(context) val dao: FeedItemDao by di.instance() when (intent.action) { ACTION_MARK_AS_NOTIFIED -> markAsNotified(context.applicationContext, dao, ids) - ACTION_MARK_AS_READ -> markAsReadAndNotified( - context.applicationContext, - dao, - intent.data?.lastPathSegment?.toLongOrNull() ?: ID_UNSET, - ) + ACTION_MARK_AS_READ -> + markAsReadAndNotified( + context.applicationContext, + dao, + intent.data?.lastPathSegment?.toLongOrNull() ?: ID_UNSET, + ) } } } @OptIn(DelicateCoroutinesApi::class) -private fun markAsReadAndNotified(context: Context, feedItemDao: FeedItemDao, itemId: Long) { +private fun markAsReadAndNotified( + context: Context, + feedItemDao: FeedItemDao, + itemId: Long, +) { GlobalScope.launch(Dispatchers.Default) { feedItemDao.markAsReadAndNotified(itemId) cancelNotification(context, itemId) @@ -44,7 +52,11 @@ private fun markAsReadAndNotified(context: Context, feedItemDao: FeedItemDao, it } @OptIn(DelicateCoroutinesApi::class) -private fun markAsNotified(context: Context, feedItemDao: FeedItemDao, itemIds: LongArray?) { +private fun markAsNotified( + context: Context, + feedItemDao: FeedItemDao, + itemIds: LongArray?, +) { if (itemIds != null) { GlobalScope.launch(Dispatchers.Default) { val idList = itemIds.toList() diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt index b9243e9107..596e5ac1fd 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/RssNotifications.kt @@ -93,29 +93,33 @@ suspend fun notify( } } -suspend fun cancelNotification(context: Context, feedItemId: Long) = - cancelNotifications(context, listOf(feedItemId)) +suspend fun cancelNotification( + context: Context, + feedItemId: Long, +) = cancelNotifications(context, listOf(feedItemId)) -suspend fun cancelNotifications(context: Context, feedItemIds: List) = - withContext(Dispatchers.Default) { - if (ContextCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS, - ) != PackageManager.PERMISSION_GRANTED - ) { - return@withContext - } +suspend fun cancelNotifications( + context: Context, + feedItemIds: List, +) = withContext(Dispatchers.Default) { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + return@withContext + } - val nm = context.notificationManager + val nm = context.notificationManager - for (feedItemId in feedItemIds) { - nm.cancel(feedItemId.toInt()) - } + for (feedItemId in feedItemIds) { + nm.cancel(feedItemId.toInt()) + } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - notify(context) - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + notify(context) } +} /** * This is an update operation if channel already exists so it's safe to call multiple times @@ -135,7 +139,10 @@ private fun createNotificationChannel(context: Context) { notificationManager.createNotificationChannel(channel) } -private suspend fun singleNotification(context: Context, item: FeedItemWithFeed): Notification { +private suspend fun singleNotification( + context: Context, + item: FeedItemWithFeed, +): Notification { val di by closestDI(context) val repository: Repository by di.instance() @@ -146,21 +153,23 @@ private suspend fun singleNotification(context: Context, item: FeedItemWithFeed) style.bigText(text) style.setBigContentTitle(title) - val contentIntent = Intent( - Intent.ACTION_VIEW, - "$DEEP_LINK_BASE_URI/article/${item.id}".toUri(), - context, - MainActivity::class.java, - ).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val contentIntent = + Intent( + Intent.ACTION_VIEW, + "$DEEP_LINK_BASE_URI/article/${item.id}".toUri(), + context, + MainActivity::class.java, + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } - val pendingIntent = PendingIntent.getActivity( - context, - item.id.toInt(), - contentIntent, - PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE, - ) + val pendingIntent = + PendingIntent.getActivity( + context, + item.id.toInt(), + contentIntent, + PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE, + ) val builder = notificationBuilder(context) @@ -263,7 +272,10 @@ internal fun getMarkAsReadIntent( /** * Use this on platforms older than 24 to bundle notifications together */ -private fun inboxNotification(context: Context, feedItems: List): Notification { +private fun inboxNotification( + context: Context, + feedItems: List, +): Notification { val style = NotificationCompat.InboxStyle() val title = context.getString(R.string.updated_feeds) val text = feedItems.map { it.feedDisplayTitle }.toSet().joinToString(separator = ", ") @@ -277,46 +289,50 @@ private fun inboxNotification(context: Context, feedItems: List feedItems[i].id }, - ) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val contentIntent = + Intent( + Intent.ACTION_VIEW, + deepLinkUri.toUri(), + context, + OpenLinkInDefaultActivity::class.java, // Proxy activity to mark as read + ).apply { + putExtra( + EXTRA_FEEDITEMS_TO_MARK_AS_NOTIFIED, + LongArray(feedItems.size) { i -> feedItems[i].id }, + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } - val pendingIntent = PendingIntent.getActivity( - context, - summaryNotificationId, - contentIntent, - PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE, - ) + val pendingIntent = + PendingIntent.getActivity( + context, + summaryNotificationId, + contentIntent, + PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE, + ) val builder = notificationBuilder(context) @@ -333,7 +349,10 @@ private fun inboxNotification(context: Context, feedItems: List): PendingIntent { +private fun getDeleteIntent( + context: Context, + feedItems: List, +): PendingIntent { val intent = Intent(context, RssNotificationBroadcastReceiver::class.java) intent.action = ACTION_MARK_AS_NOTIFIED @@ -348,7 +367,10 @@ private fun getDeleteIntent(context: Context, feedItems: List) ) } -internal fun getDeleteIntent(context: Context, feedItem: FeedItemWithFeed): Intent { +internal fun getDeleteIntent( + context: Context, + feedItem: FeedItemWithFeed, +): Intent { val intent = Intent(context, RssNotificationBroadcastReceiver::class.java) intent.action = ACTION_MARK_AS_NOTIFIED intent.data = Uri.withAppendedPath(URI_FEEDITEMS, "${feedItem.id}") @@ -358,7 +380,10 @@ internal fun getDeleteIntent(context: Context, feedItem: FeedItemWithFeed): Inte return intent } -private fun getPendingDeleteIntent(context: Context, feedItem: FeedItemWithFeed): PendingIntent = +private fun getPendingDeleteIntent( + context: Context, + feedItem: FeedItemWithFeed, +): PendingIntent = PendingIntent.getBroadcast( context, 0, diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OPMLImporter.kt b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OPMLImporter.kt index 11a019677f..6fb8da8260 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OPMLImporter.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OPMLImporter.kt @@ -34,41 +34,52 @@ open class OPMLImporter(override val di: DI) : OPMLParserHandler, DIAware { } } - override suspend fun saveSetting(key: String, value: String) { + override suspend fun saveSetting( + key: String, + value: String, + ) { when (UserSettings.fromKey(key)) { null -> Log.w(LOG_TAG, "Unrecognized setting during import: $key") UserSettings.SETTING_ADDED_FEEDER_NEWS -> settingsStore.setAddedFeederNews(value.toBoolean()) - UserSettings.SETTING_THEME -> settingsStore.setCurrentTheme( - themeOptionsFromString(value), - ) - UserSettings.SETTING_DARK_THEME -> settingsStore.setDarkThemePreference( - darkThemePreferenceFromString(value), - ) + UserSettings.SETTING_THEME -> + settingsStore.setCurrentTheme( + themeOptionsFromString(value), + ) + UserSettings.SETTING_DARK_THEME -> + settingsStore.setDarkThemePreference( + darkThemePreferenceFromString(value), + ) UserSettings.SETTING_DYNAMIC_THEME -> settingsStore.setUseDynamicTheme(value.toBoolean()) - UserSettings.SETTING_SORT -> settingsStore.setCurrentSorting( - sortingOptionsFromString(value), - ) + UserSettings.SETTING_SORT -> + settingsStore.setCurrentSorting( + sortingOptionsFromString(value), + ) UserSettings.SETTING_SHOW_FAB -> settingsStore.setShowFab(value.toBoolean()) - UserSettings.SETTING_FEED_ITEM_STYLE -> settingsStore.setFeedItemStyle( - feedItemStyleFromString(value), - ) - UserSettings.SETTING_SWIPE_AS_READ -> settingsStore.setSwipeAsRead( - swipeAsReadFromString(value), - ) + UserSettings.SETTING_FEED_ITEM_STYLE -> + settingsStore.setFeedItemStyle( + feedItemStyleFromString(value), + ) + UserSettings.SETTING_SWIPE_AS_READ -> + settingsStore.setSwipeAsRead( + swipeAsReadFromString(value), + ) UserSettings.SETTING_SYNC_ONLY_CHARGING -> settingsStore.setSyncOnlyWhenCharging(value.toBoolean()) UserSettings.SETTING_SYNC_ONLY_WIFI -> settingsStore.setSyncOnlyOnWifi(value.toBoolean()) - UserSettings.SETTING_SYNC_FREQ -> settingsStore.setSyncFrequency( - syncFrequencyFromString(value), - ) + UserSettings.SETTING_SYNC_FREQ -> + settingsStore.setSyncFrequency( + syncFrequencyFromString(value), + ) UserSettings.SETTING_SYNC_ON_RESUME -> settingsStore.setSyncOnResume(value.toBoolean()) UserSettings.SETTING_IMG_ONLY_WIFI -> settingsStore.setLoadImageOnlyOnWifi(value.toBoolean()) UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> settingsStore.setShowThumbnails(value.toBoolean()) - UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> settingsStore.setItemOpener( - itemOpenerFromString(value), - ) - UserSettings.SETTING_OPEN_LINKS_WITH -> settingsStore.setLinkOpener( - linkOpenerFromString(value), - ) + UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> + settingsStore.setItemOpener( + itemOpenerFromString(value), + ) + UserSettings.SETTING_OPEN_LINKS_WITH -> + settingsStore.setLinkOpener( + linkOpenerFromString(value), + ) UserSettings.SETTING_TEXT_SCALE -> settingsStore.setTextScale(value.toFloatOrNull() ?: 1.0f) UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> settingsStore.setIsMarkAsReadOnScroll(value.toBoolean()) UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> settingsStore.setUseDetectLanguage(value.toBoolean()) diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlActions.kt b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlActions.kt index a4144cd6c9..75b7720592 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlActions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlActions.kt @@ -11,20 +11,23 @@ import com.nononsenseapps.feeder.model.workmanager.requestFeedSync import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.ToastMaker import com.nononsenseapps.feeder.util.logDebug -import kotlin.system.measureTimeMillis import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import org.kodein.di.DI import org.kodein.di.direct import org.kodein.di.instance +import kotlin.system.measureTimeMillis private const val LOG_TAG = "FEEDER_OPMLACTIONS" /** * Exports OPML on a background thread */ -suspend fun exportOpml(di: DI, uri: Uri): Either = +suspend fun exportOpml( + di: DI, + uri: Uri, +): Either = Either.catching( onCatch = { Log.e(LOG_TAG, "Failed to export OPML", it) @@ -38,21 +41,22 @@ suspend fun exportOpml(di: DI, uri: Uri): Either = }, ) { withContext(Dispatchers.IO) { - val time = measureTimeMillis { - val contentResolver: ContentResolver by di.instance() - val feedDao: FeedDao by di.instance() - val settingsStore: SettingsStore by di.instance() - contentResolver.openOutputStream(uri)?.let { - writeOutputStream( - os = it, - settings = settingsStore.getAllSettings(), - blockedPatterns = settingsStore.blockListPreference.first(), - tags = feedDao.loadTags(), - ) { tag -> - feedDao.loadFeeds(tag = tag) + val time = + measureTimeMillis { + val contentResolver: ContentResolver by di.instance() + val feedDao: FeedDao by di.instance() + val settingsStore: SettingsStore by di.instance() + contentResolver.openOutputStream(uri)?.let { + writeOutputStream( + os = it, + settings = settingsStore.getAllSettings(), + blockedPatterns = settingsStore.blockListPreference.first(), + tags = feedDao.loadTags(), + ) { tag -> + feedDao.loadFeeds(tag = tag) + } } } - } logDebug(LOG_TAG, "Exported OPML in $time ms on ${Thread.currentThread().name}") } } @@ -60,24 +64,29 @@ suspend fun exportOpml(di: DI, uri: Uri): Either = /** * Imports OPML on a background thread */ -suspend fun importOpml(di: DI, uri: Uri) = withContext(Dispatchers.IO) { +suspend fun importOpml( + di: DI, + uri: Uri, +) = withContext(Dispatchers.IO) { val opmlToRoom: OPMLParserHandler by di.instance() try { - val time = measureTimeMillis { - val parser = OpmlPullParser(opmlToRoom) - val contentResolver: ContentResolver by di.instance() - val result = contentResolver.openInputStream(uri).use { - it?.let { stream -> - parser.parseInputStreamWithFallback(stream) - } - } - requestFeedSync(di = di) + val time = + measureTimeMillis { + val parser = OpmlPullParser(opmlToRoom) + val contentResolver: ContentResolver by di.instance() + val result = + contentResolver.openInputStream(uri).use { + it?.let { stream -> + parser.parseInputStreamWithFallback(stream) + } + } + requestFeedSync(di = di) - if (result?.isLeft() == true) { - val toastMaker = di.direct.instance() - toastMaker.makeToast(R.string.failed_to_import_OPML) + if (result?.isLeft() == true) { + val toastMaker = di.direct.instance() + toastMaker.makeToast(R.string.failed_to_import_OPML) + } } - } logDebug(LOG_TAG, "Imported OPML in $time ms on ${Thread.currentThread().name}") } catch (e: Throwable) { Log.e(LOG_TAG, "Failed to import OPML", e) diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlPullParser.kt b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlPullParser.kt index 4b771c6bf4..79194e4357 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlPullParser.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlPullParser.kt @@ -6,17 +6,17 @@ import com.nononsenseapps.feeder.db.room.Feed import com.nononsenseapps.feeder.model.OPMLParserHandler import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.flatMap +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.toByteString +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException import java.io.IOException import java.io.InputStream import java.net.MalformedURLException import java.net.URL import java.util.Arrays import kotlin.reflect.KProperty -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.withContext -import okio.ByteString.Companion.toByteString -import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException private const val TAG_SETTING = "setting" @@ -197,7 +197,10 @@ class OpmlPullParser(private val opmlToDb: OPMLParserHandler) { } @Throws(XmlPullParserException::class, IOException::class) - private fun readOutline(parser: XmlPullParser, parentOutlineTag: String) { + private fun readOutline( + parser: XmlPullParser, + parentOutlineTag: String, + ) { parser.require(XmlPullParser.START_TAG, null, TAG_OUTLINE) val xmlUrl by this @@ -211,11 +214,12 @@ class OpmlPullParser(private val opmlToDb: OPMLParserHandler) { private fun readOutlineAsTag() { parser.require(XmlPullParser.START_TAG, null, TAG_OUTLINE) - val tag = unescape( - parser.getAttributeValue(null, ATTR_TITLE) - ?: parser.getAttributeValue(null, ATTR_TEXT) - ?: "", - ) + val tag = + unescape( + parser.getAttributeValue(null, ATTR_TITLE) + ?: parser.getAttributeValue(null, ATTR_TEXT) + ?: "", + ) while (parser.next() != XmlPullParser.END_TAG) { if (parser.eventType != XmlPullParser.START_TAG) { @@ -230,60 +234,70 @@ class OpmlPullParser(private val opmlToDb: OPMLParserHandler) { } @Throws(XmlPullParserException::class, IOException::class) - private fun readOutlineAsRss(parser: XmlPullParser, tag: String) { + private fun readOutlineAsRss( + parser: XmlPullParser, + tag: String, + ) { parser.require(XmlPullParser.START_TAG, null, TAG_OUTLINE) - val feedTitle = unescape( - parser.getAttributeValue(null, ATTR_TITLE) - ?: parser.getAttributeValue(null, ATTR_TEXT) - ?: "", - ) + val feedTitle = + unescape( + parser.getAttributeValue(null, ATTR_TITLE) + ?: parser.getAttributeValue(null, ATTR_TEXT) + ?: "", + ) try { val feedUrl = URL(parser.getAttributeValue(null, ATTR_XMLURL)) - val feed = Feed( - // Ensure not both are empty string: title will get replaced on sync - title = feedTitle.ifBlank { feedUrl.toString() }, - customTitle = feedTitle, - tag = tag, - url = feedUrl, - ).let { feed -> - // Copy so default values can be referenced - feed.copy( - notify = parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_NOTIFY) - ?.toBoolean() - ?: feed.notify, - fullTextByDefault = ( - parser.getAttributeValue( - OPML_FEEDER_NAMESPACE, - ATTR_FULL_TEXT_BY_DEFAULT, - ) - ?.toBoolean() - // Support Flym's value for this - ?: parser.getAttributeValue(null, ATTR_FLYM_RETRIEVE_FULL_TEXT) + val feed = + Feed( + // Ensure not both are empty string: title will get replaced on sync + title = feedTitle.ifBlank { feedUrl.toString() }, + customTitle = feedTitle, + tag = tag, + url = feedUrl, + ).let { feed -> + // Copy so default values can be referenced + feed.copy( + notify = + parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_NOTIFY) ?.toBoolean() - ) ?: feed.fullTextByDefault, - alternateId = parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_ALTERNATE_ID) - ?.toBoolean() - ?: feed.alternateId, - openArticlesWith = parser.getAttributeValue( - OPML_FEEDER_NAMESPACE, - ATTR_OPEN_ARTICLES_WITH, - ) ?: feed.openArticlesWith, - imageUrl = parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_IMAGE_URL) - ?.let { imageUrl -> - try { - URL(imageUrl) - } catch (e: MalformedURLException) { - Log.e( - LOG_TAG, - "Invalid imageUrl [$imageUrl] on feed [$feedTitle] in OPML", - e, + ?: feed.notify, + fullTextByDefault = + ( + parser.getAttributeValue( + OPML_FEEDER_NAMESPACE, + ATTR_FULL_TEXT_BY_DEFAULT, ) - null - } - } ?: feed.imageUrl, - ) - } + ?.toBoolean() + // Support Flym's value for this + ?: parser.getAttributeValue(null, ATTR_FLYM_RETRIEVE_FULL_TEXT) + ?.toBoolean() + ) ?: feed.fullTextByDefault, + alternateId = + parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_ALTERNATE_ID) + ?.toBoolean() + ?: feed.alternateId, + openArticlesWith = + parser.getAttributeValue( + OPML_FEEDER_NAMESPACE, + ATTR_OPEN_ARTICLES_WITH, + ) ?: feed.openArticlesWith, + imageUrl = + parser.getAttributeValue(OPML_FEEDER_NAMESPACE, ATTR_IMAGE_URL) + ?.let { imageUrl -> + try { + URL(imageUrl) + } catch (e: MalformedURLException) { + Log.e( + LOG_TAG, + "Invalid imageUrl [$imageUrl] on feed [$feedTitle] in OPML", + e, + ) + null + } + } ?: feed.imageUrl, + ) + } feeds.add(feed) } catch (e: MalformedURLException) { @@ -330,6 +344,7 @@ sealed class OpmlError { } data class OpmlUnknownError(override val throwable: Throwable?) : OpmlError() + data class OpmlParsingError(override val throwable: Throwable) : OpmlError() fun InputStream.readTheBytes(): ByteArray { diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlWriter.kt b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlWriter.kt index c95062c095..c1de5944b8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlWriter.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/opml/OpmlWriter.kt @@ -2,11 +2,11 @@ package com.nononsenseapps.feeder.model.opml import android.util.Log import com.nononsenseapps.feeder.db.room.Feed +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.FileOutputStream import java.io.IOException import java.io.OutputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext suspend fun writeFile( path: String, @@ -116,11 +116,17 @@ suspend fun opml(init: suspend Opml.() -> Unit): Opml { } interface Element { - fun render(builder: StringBuilder, indent: String) + fun render( + builder: StringBuilder, + indent: String, + ) } class TextElement(val text: String) : Element { - override fun render(builder: StringBuilder, indent: String) { + override fun render( + builder: StringBuilder, + indent: String, + ) { builder.append("$indent$text\n") } } @@ -129,13 +135,19 @@ abstract class Tag(val name: String) : Element { val children = arrayListOf() val attributes = linkedMapOf() - protected suspend fun initTag(tag: T, init: suspend T.() -> Unit): T { + protected suspend fun initTag( + tag: T, + init: suspend T.() -> Unit, + ): T { tag.init() children.add(tag) return tag } - override fun render(builder: StringBuilder, indent: String) { + override fun render( + builder: StringBuilder, + indent: String, + ) { builder.append("$indent<$name${renderAttributes()}") if (children.isEmpty()) { builder.append("/>\n") @@ -176,6 +188,7 @@ class Opml : TagWithText("opml") { } suspend fun head(init: suspend Head.() -> Unit) = initTag(Head(), init) + suspend fun body(init: suspend Body.() -> Unit) = initTag(Body(), init) } @@ -186,9 +199,7 @@ class Head : TagWithText("head") { class Title : TagWithText("title") abstract class BodyTag(name: String) : TagWithText(name) { - suspend fun feederSettings( - init: suspend FeederSettings.() -> Unit, - ) { + suspend fun feederSettings(init: suspend FeederSettings.() -> Unit) { initTag(FeederSettings(), init) } @@ -210,32 +221,29 @@ abstract class BodyTag(name: String) : TagWithText(name) { } } - suspend fun Feed.toOutline() = outline( - title = escape(displayTitle), - type = "rss", - xmlUrl = escape(url.toString()), - ) { - val feed = this@toOutline - notify = feed.notify - feed.imageUrl?.let { imageUrl = escape(it.toString()) } - fullTextByDefault = feed.fullTextByDefault - openArticlesWith = feed.openArticlesWith - alternateId = feed.alternateId - } + suspend fun Feed.toOutline() = + outline( + title = escape(displayTitle), + type = "rss", + xmlUrl = escape(url.toString()), + ) { + val feed = this@toOutline + notify = feed.notify + feed.imageUrl?.let { imageUrl = escape(it.toString()) } + fullTextByDefault = feed.fullTextByDefault + openArticlesWith = feed.openArticlesWith + alternateId = feed.alternateId + } } class Body : BodyTag("body") class FeederSettings : BodyTag("feeder:settings") { - suspend fun feederSetting( - init: suspend FeederSetting.() -> Unit, - ) { + suspend fun feederSetting(init: suspend FeederSetting.() -> Unit) { initTag(FeederSetting(), init) } - suspend fun feederBlocked( - init: suspend FeederBlocked.() -> Unit, - ) { + suspend fun feederBlocked(init: suspend FeederBlocked.() -> Unit) { initTag(FeederBlocked(), init) } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt index e647d1de7d..28f95247a4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/FeedSyncer.kt @@ -16,21 +16,22 @@ import com.nononsenseapps.feeder.model.RssLocalSync import com.nononsenseapps.feeder.model.notify import com.nononsenseapps.feeder.ui.ARG_FEED_ID import com.nononsenseapps.feeder.ui.ARG_FEED_TAG -import java.util.concurrent.TimeUnit import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance +import java.util.concurrent.TimeUnit const val ARG_FORCE_NETWORK = "force_network" const val UNIQUE_PERIODIC_NAME = "feeder_periodic_3" // Clear this for scheduler -val oldPeriodics = listOf( - "feeder_periodic", - "feeder_periodic_2", -) +val oldPeriodics = + listOf( + "feeder_periodic", + "feeder_periodic_2", + ) private const val UNIQUE_FEEDSYNC_NAME = "feeder_sync_onetime" private const val MIN_FEED_AGE_MINUTES = "min_feed_age_minutes" @@ -54,12 +55,13 @@ class FeedSyncer(val context: Context, workerParams: WorkerParameters) : val forceNetwork = inputData.getBoolean(ARG_FORCE_NETWORK, false) val minFeedAgeMinutes = inputData.getInt(MIN_FEED_AGE_MINUTES, 5) - success = rssLocalSync.syncFeeds( - feedId = feedId, - feedTag = feedTag, - forceNetwork = forceNetwork, - minFeedAgeMinutes = minFeedAgeMinutes, - ) + success = + rssLocalSync.syncFeeds( + feedId = feedId, + feedTag = feedTag, + forceNetwork = forceNetwork, + minFeedAgeMinutes = minFeedAgeMinutes, + ) } catch (e: Exception) { success = false Log.e("FeederFeedSyncer", "Failure during sync", e) @@ -81,16 +83,18 @@ fun requestFeedSync( feedTag: String = "", forceNetwork: Boolean = false, ) { - val workRequest = OneTimeWorkRequestBuilder() - .addTag("feeder") - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .keepResultsForAtLeast(5, TimeUnit.MINUTES) + val workRequest = + OneTimeWorkRequestBuilder() + .addTag("feeder") + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .keepResultsForAtLeast(5, TimeUnit.MINUTES) - val data = workDataOf( - ARG_FEED_ID to feedId, - ARG_FEED_TAG to feedTag, - ARG_FORCE_NETWORK to forceNetwork, - ) + val data = + workDataOf( + ARG_FEED_ID to feedId, + ARG_FEED_TAG to feedTag, + ARG_FORCE_NETWORK to forceNetwork, + ) workRequest.setInputData(data) val workManager by di.instance() diff --git a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/SyncServiceGetUpdatesWorker.kt b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/SyncServiceGetUpdatesWorker.kt index 1193e42df2..9cc4dda7ad 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/SyncServiceGetUpdatesWorker.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/model/workmanager/SyncServiceGetUpdatesWorker.kt @@ -14,11 +14,11 @@ import androidx.work.WorkerParameters import com.nononsenseapps.feeder.archmodel.Repository import com.nononsenseapps.feeder.model.workmanager.SyncServiceGetUpdatesWorker.Companion.UNIQUE_GETUPDATES_NAME import com.nononsenseapps.feeder.sync.SyncRestClient -import java.util.concurrent.TimeUnit import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance +import java.util.concurrent.TimeUnit class SyncServiceGetUpdatesWorker(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams), DIAware { @@ -55,9 +55,10 @@ fun scheduleGetUpdates(di: DI) { Log.d(SyncServiceGetUpdatesWorker.LOG_TAG, "Scheduling work") val repository by di.instance() - val constraints = Constraints.Builder() - // This prevents expedited if true - .setRequiresCharging(repository.syncOnlyWhenCharging.value) + val constraints = + Constraints.Builder() + // This prevents expedited if true + .setRequiresCharging(repository.syncOnlyWhenCharging.value) if (repository.syncOnlyOnWifi.value) { constraints.setRequiredNetworkType(NetworkType.UNMETERED) @@ -65,10 +66,11 @@ fun scheduleGetUpdates(di: DI) { constraints.setRequiredNetworkType(NetworkType.CONNECTED) } - val workRequest = OneTimeWorkRequestBuilder() - .addTag("feeder") - .keepResultsForAtLeast(5, TimeUnit.MINUTES) - .setConstraints(constraints.build()) + val workRequest = + OneTimeWorkRequestBuilder() + .addTag("feeder") + .keepResultsForAtLeast(5, TimeUnit.MINUTES) + .setConstraints(constraints.build()) val workManager by di.instance() workManager.enqueueUniqueWork( diff --git a/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt b/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt index 04c9b82c82..a96946e44d 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/notifications/NotificationsWorker.kt @@ -28,33 +28,37 @@ class NotificationsWorker(override val di: DI) : DIAware { fun runForever() { job?.cancel("runForever") - job = applicationCoroutineScope.launch { - repository.getFeedItemsNeedingNotifying() - .runningReduce { prev, current -> - try { - unNotifyForMissingItems(prev, current) - } catch (e: Exception) { - Log.e(LOG_TAG, "Error in notifications worker", e) + job = + applicationCoroutineScope.launch { + repository.getFeedItemsNeedingNotifying() + .runningReduce { prev, current -> + try { + unNotifyForMissingItems(prev, current) + } catch (e: Exception) { + Log.e(LOG_TAG, "Error in notifications worker", e) + } + // Always pass current on + current } - // Always pass current on - current - } - .collectLatest { items -> - delay(100) - // Individual notifications are triggered during sync, not here - // but the summary notification still needs updating - if (items.isNotEmpty()) { - notify(context, updateSummaryOnly = true) + .collectLatest { items -> + delay(100) + // Individual notifications are triggered during sync, not here + // but the summary notification still needs updating + if (items.isNotEmpty()) { + notify(context, updateSummaryOnly = true) + } } - } - } + } } fun stopForever() { job?.cancel("stopForever") } - internal suspend fun unNotifyForMissingItems(prev: List, current: List) { + internal suspend fun unNotifyForMissingItems( + prev: List, + current: List, + ) { if (current.isEmpty()) { cancelNotification(summaryNotificationId.toLong()) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/sync/Moshi.kt b/app/src/main/java/com/nononsenseapps/feeder/sync/Moshi.kt index 668fdf0397..b22b00b059 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/sync/Moshi.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/sync/Moshi.kt @@ -8,31 +8,27 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import java.net.URL import java.time.Instant -fun getMoshi(): Moshi = Moshi.Builder() - .add(InstantAdapter()) - .add(URLAdapter()) - .addLast(KotlinJsonAdapterFactory()) - .build() +fun getMoshi(): Moshi = + Moshi.Builder() + .add(InstantAdapter()) + .add(URLAdapter()) + .addLast(KotlinJsonAdapterFactory()) + .build() class InstantAdapter { @ToJson - fun toJSon(value: Instant): Long = - value.toEpochMilli() + fun toJSon(value: Instant): Long = value.toEpochMilli() @FromJson - fun fromJson(value: Long): Instant = - Instant.ofEpochMilli(value) + fun fromJson(value: Long): Instant = Instant.ofEpochMilli(value) } class URLAdapter { @ToJson - fun toJSon(value: URL): String = - value.toString() + fun toJSon(value: URL): String = value.toString() @FromJson - fun fromJson(value: String): URL = - URL(value) + fun fromJson(value: String): URL = URL(value) } -inline fun Moshi.adapter(): JsonAdapter = - adapter(T::class.java) +inline fun Moshi.adapter(): JsonAdapter = adapter(T::class.java) diff --git a/app/src/main/java/com/nononsenseapps/feeder/sync/Retrofit.kt b/app/src/main/java/com/nononsenseapps/feeder/sync/Retrofit.kt index 81e21d6b78..bfa138b983 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/sync/Retrofit.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/sync/Retrofit.kt @@ -2,12 +2,12 @@ package com.nononsenseapps.feeder.sync import android.util.Log import com.nononsenseapps.feeder.db.room.SyncRemote -import java.net.URL import okhttp3.Credentials import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.create +import java.net.URL fun getFeederSyncClient( syncRemote: SyncRemote, @@ -15,35 +15,36 @@ fun getFeederSyncClient( ): FeederSync { val moshi = getMoshi() - val retrofit = Retrofit.Builder() - .client( - okHttpClient.newBuilder() - // Auth only used to prevent automatic scanning of the API - .addInterceptor { chain -> - chain.proceed( - chain.request().newBuilder() - .header( - "Authorization", - Credentials.basic(HARDCODED_USER, HARDCODED_PASSWORD), - ) - .build(), - ) - } - .addInterceptor { chain -> - val response = chain.proceed(chain.request()) - val isCachedResponse = - response.cacheResponse != null && (response.networkResponse == null || response.networkResponse?.code == 304) - Log.v( - "FEEDER_SYNC_CLIENT", - "Response cached: $isCachedResponse, ${response.networkResponse?.code}, cache-Control: ${response.cacheControl}", - ) - response - } - .build(), - ) - .baseUrl(URL(syncRemote.url, "/api/v1/")) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() + val retrofit = + Retrofit.Builder() + .client( + okHttpClient.newBuilder() + // Auth only used to prevent automatic scanning of the API + .addInterceptor { chain -> + chain.proceed( + chain.request().newBuilder() + .header( + "Authorization", + Credentials.basic(HARDCODED_USER, HARDCODED_PASSWORD), + ) + .build(), + ) + } + .addInterceptor { chain -> + val response = chain.proceed(chain.request()) + val isCachedResponse = + response.cacheResponse != null && (response.networkResponse == null || response.networkResponse?.code == 304) + Log.v( + "FEEDER_SYNC_CLIENT", + "Response cached: $isCachedResponse, ${response.networkResponse?.code}, cache-Control: ${response.cacheControl}", + ) + response + } + .build(), + ) + .baseUrl(URL(syncRemote.url, "/api/v1/")) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() return retrofit.create() } diff --git a/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt b/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt index 16a50246e2..e4d03ad7ca 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/sync/SyncRestClient.kt @@ -16,15 +16,15 @@ import com.nononsenseapps.feeder.db.room.SyncRemote import com.nononsenseapps.feeder.db.room.generateDeviceName import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.logDebug -import java.net.URL -import java.time.Instant -import kotlin.contracts.ExperimentalContracts import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient import org.kodein.di.DI import org.kodein.di.DIAware import org.kodein.di.instance import retrofit2.Response +import java.net.URL +import java.time.Instant +import kotlin.contracts.ExperimentalContracts class SyncRestClient(override val di: DI) : DIAware { private val repository: Repository by instance() @@ -55,17 +55,19 @@ class SyncRestClient(override val di: DI) : DIAware { LOG_TAG, "Updating to latest sync host: $DEFAULT_SERVER_ADDRESS", ) - syncRemote = syncRemote.copy( - url = URL(DEFAULT_SERVER_ADDRESS), - ) + syncRemote = + syncRemote.copy( + url = URL(DEFAULT_SERVER_ADDRESS), + ) repository.updateSyncRemote(syncRemote) } if (syncRemote.hasSyncChain()) { secretKey = AesCbcWithIntegrity.decodeKey(syncRemote.secretKey) - feederSync = getFeederSyncClient( - syncRemote = syncRemote, - okHttpClient = okHttpClient, - ) + feederSync = + getFeederSyncClient( + syncRemote = syncRemote, + okHttpClient = okHttpClient, + ) } } catch (e: Exception) { Log.e(LOG_TAG, "Failed to initialize", e) @@ -103,10 +105,11 @@ class SyncRestClient(override val di: DI) : DIAware { val secretKey = AesCbcWithIntegrity.decodeKey(syncRemote.secretKey) this.secretKey = secretKey - val feederSync = getFeederSyncClient( - syncRemote = syncRemote, - okHttpClient = okHttpClient, - ) + val feederSync = + getFeederSyncClient( + syncRemote = syncRemote, + okHttpClient = okHttpClient, + ) this.feederSync = feederSync val deviceName = generateDeviceName() @@ -131,7 +134,10 @@ class SyncRestClient(override val di: DI) : DIAware { } } - suspend fun join(syncCode: String, remoteSecretKey: String): Either { + suspend fun join( + syncCode: String, + remoteSecretKey: String, + ): Either { logDebug(LOG_TAG, "join") try { logDebug(LOG_TAG, "Really joining") @@ -144,22 +150,25 @@ class SyncRestClient(override val di: DI) : DIAware { val secretKey = AesCbcWithIntegrity.decodeKey(syncRemote.secretKey) this.secretKey = secretKey - val feederSync = getFeederSyncClient( - syncRemote = syncRemote, - okHttpClient = okHttpClient, - ) + val feederSync = + getFeederSyncClient( + syncRemote = syncRemote, + okHttpClient = okHttpClient, + ) this.feederSync = feederSync logDebug(LOG_TAG, "Updated objects") return feederSync.join( syncChainId = syncCode, - request = JoinRequest( - deviceName = AesCbcWithIntegrity.encryptString( - syncRemote.deviceName, - secretKey, + request = + JoinRequest( + deviceName = + AesCbcWithIntegrity.encryptString( + syncRemote.deviceName, + secretKey, + ), ), - ), ).toEither() .onRight { response -> logDebug(LOG_TAG, "Join response: $response") @@ -182,7 +191,7 @@ class SyncRestClient(override val di: DI) : DIAware { Log.e( LOG_TAG, "Error during leave: msg: code: ${e.code()}, error: ${ - e.response()?.errorBody()?.string() + e.response()?.errorBody()?.string() }", e, ) @@ -238,10 +247,11 @@ class SyncRestClient(override val di: DI) : DIAware { deviceListResponse.devices.map { SyncDevice( deviceId = it.deviceId, - deviceName = AesCbcWithIntegrity.decryptString( - it.deviceName, - secretKey, - ), + deviceName = + AesCbcWithIntegrity.decryptString( + it.deviceName, + secretKey, + ), syncRemote = syncRemote.id, ) }, @@ -266,21 +276,25 @@ class SyncRestClient(override val di: DI) : DIAware { feederSync.sendEncryptedReadMarks( currentDeviceId = syncRemote.deviceId, syncChainId = syncRemote.syncChainId, - request = SendEncryptedReadMarkBulkRequest( - items = feedItems.map { feedItem -> - SendEncryptedReadMarkRequest( - encrypted = AesCbcWithIntegrity.encryptString( - secretKeys = secretKey, - plaintext = readMarkAdapter.toJson( - ReadMarkContent( - feedUrl = feedItem.feedUrl, - articleGuid = feedItem.guid, - ), - ), - ), - ) - }, - ), + request = + SendEncryptedReadMarkBulkRequest( + items = + feedItems.map { feedItem -> + SendEncryptedReadMarkRequest( + encrypted = + AesCbcWithIntegrity.encryptString( + secretKeys = secretKey, + plaintext = + readMarkAdapter.toJson( + ReadMarkContent( + feedUrl = feedItem.feedUrl, + articleGuid = feedItem.guid, + ), + ), + ), + ) + }, + ), ).toEither() .onRight { for (feedItem in feedItems) { @@ -342,10 +356,11 @@ class SyncRestClient(override val di: DI) : DIAware { logDebug(LOG_TAG, "device: $it") SyncDevice( deviceId = it.deviceId, - deviceName = AesCbcWithIntegrity.decryptString( - it.deviceName, - secretKey, - ), + deviceName = + AesCbcWithIntegrity.decryptString( + it.deviceName, + secretKey, + ), syncRemote = syncRemote.id, ) }, @@ -376,9 +391,10 @@ class SyncRestClient(override val di: DI) : DIAware { .onRight { response -> logDebug(LOG_TAG, "getRead: ${response.readMarks.size} read marks") for (readMark in response.readMarks) { - val readMarkContent = readMarkAdapter.fromJson( - AesCbcWithIntegrity.decryptString(readMark.encrypted, secretKey), - ) + val readMarkContent = + readMarkAdapter.fromJson( + AesCbcWithIntegrity.decryptString(readMark.encrypted, secretKey), + ) if (readMarkContent == null) { Log.e(LOG_TAG, "Failed to decrypt readMark content") @@ -418,12 +434,13 @@ class SyncRestClient(override val di: DI) : DIAware { return@onRight } - val encryptedFeeds = feedsAdapter.fromJson( - AesCbcWithIntegrity.decryptString( - response.encrypted, - secretKeys = secretKey, - ), - ) + val encryptedFeeds = + feedsAdapter.fromJson( + AesCbcWithIntegrity.decryptString( + response.encrypted, + secretKeys = secretKey, + ), + ) if (encryptedFeeds == null) { Log.e(LOG_TAG, "Failed to decrypt encrypted feeds") @@ -444,15 +461,14 @@ class SyncRestClient(override val di: DI) : DIAware { } } - private suspend fun feedDiffing( - remoteFeeds: List, - ) { + private suspend fun feedDiffing(remoteFeeds: List) { try { logDebug(LOG_TAG, "feedDiffing: ${remoteFeeds.size}") val remotelySeenFeedUrls = repository.getRemotelySeenFeeds() - val feedUrlsWhichWereDeletedOnRemote = remotelySeenFeedUrls - .filterNot { url -> remoteFeeds.asSequence().map { it.url }.contains(url) } + val feedUrlsWhichWereDeletedOnRemote = + remotelySeenFeedUrls + .filterNot { url -> remoteFeeds.asSequence().map { it.url }.contains(url) } logDebug(LOG_TAG, "RemotelyDeleted: ${feedUrlsWhichWereDeletedOnRemote.size}") @@ -521,8 +537,9 @@ class SyncRestClient(override val di: DI) : DIAware { // Only send if hash does not match // Important to keep iteration order stable - across devices. So sort on URL, not ID or date - val feeds = repository.getFeedsOrderedByUrl() - .map { it.toEncryptedFeed() } + val feeds = + repository.getFeedsOrderedByUrl() + .map { it.toEncryptedFeed() } // Yes, List hashCodes are based on elements. Just remember to hash what you send // - and not raw database objects @@ -534,14 +551,15 @@ class SyncRestClient(override val di: DI) : DIAware { return@safeBlock Either.Right(false) } - val encrypted = AesCbcWithIntegrity.encryptString( - feedsAdapter.toJson( - EncryptedFeeds( - feeds = feeds, + val encrypted = + AesCbcWithIntegrity.encryptString( + feedsAdapter.toJson( + EncryptedFeeds( + feeds = feeds, + ), ), - ), - secretKeys = secretKey, - ) + secretKeys = secretKey, + ) logDebug( LOG_TAG, @@ -552,10 +570,11 @@ class SyncRestClient(override val di: DI) : DIAware { syncChainId = syncRemote.syncChainId, currentDeviceId = syncRemote.deviceId, etagValue = syncRemote.lastFeedsRemoteHash.asWeakETagValue(), - request = UpdateFeedsRequest( - contentHash = currentContentHash, - encrypted = encrypted, - ), + request = + UpdateFeedsRequest( + contentHash = currentContentHash, + encrypted = encrypted, + ), ).toEither() .onLeft { if (it.code == 412) { @@ -599,8 +618,7 @@ class SyncRestClient(override val di: DI) : DIAware { } } -fun Any.asWeakETagValue() = - "W/\"$this\"" +fun Any.asWeakETagValue() = "W/\"$this\"" fun Response.toEither(): Either { return try { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/ImportOMPLFileActivity.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/ImportOMPLFileActivity.kt index 77b0852c95..e7eb326363 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/ImportOMPLFileActivity.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/ImportOMPLFileActivity.kt @@ -56,14 +56,15 @@ class ImportOMPLFileActivity : DIAwareComponentActivity() { val deepLinkUri = "$DEEP_LINK_BASE_URI/feed?id=$ID_ALL_FEEDS" - val intent = Intent( - Intent.ACTION_VIEW, - deepLinkUri.toUri(), - this@ImportOMPLFileActivity, - MainActivity::class.java, - ).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val intent = + Intent( + Intent.ACTION_VIEW, + deepLinkUri.toUri(), + this@ImportOMPLFileActivity, + MainActivity::class.java, + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } startActivity(intent) finish() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivity.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivity.kt index 2108195efb..816b00db10 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivity.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivity.kt @@ -49,16 +49,17 @@ class MainActivity : DIAwareComponentActivity() { maybeRequestSync() } - private fun maybeRequestSync() = lifecycleScope.launch { - if (mainActivityViewModel.shouldSyncOnResume) { - if (mainActivityViewModel.isOkToSyncAutomatically()) { - requestFeedSync( - di = di, - forceNetwork = false, - ) + private fun maybeRequestSync() = + lifecycleScope.launch { + if (mainActivityViewModel.shouldSyncOnResume) { + if (mainActivityViewModel.isOkToSyncAutomatically()) { + requestFeedSync( + di = di, + forceNetwork = false, + ) + } } } - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -96,11 +97,12 @@ class MainActivity : DIAwareComponentActivity() { } DisposableEffect(navController) { - val listener = Consumer { intent -> - if (!navController.handleDeepLink(intent)) { - Log.e(LOG_TAG, "NavController rejected intent: $intent") + val listener = + Consumer { intent -> + if (!navController.handleDeepLink(intent)) { + Log.e(LOG_TAG, "NavController rejected intent: $intent") + } } - } addOnNewIntentListener(listener) onDispose { removeOnNewIntentListener(listener) } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivityViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivityViewModel.kt index 82e25596f6..37e8442801 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivityViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/MainActivityViewModel.kt @@ -7,10 +7,10 @@ import com.nononsenseapps.feeder.base.DIAwareViewModel import com.nononsenseapps.feeder.util.currentlyCharging import com.nononsenseapps.feeder.util.currentlyConnected import com.nononsenseapps.feeder.util.currentlyUnmetered -import java.time.Instant import kotlinx.coroutines.launch import org.kodein.di.DI import org.kodein.di.instance +import java.time.Instant class MainActivityViewModel(di: DI) : DIAwareViewModel(di) { private val repository: Repository by instance() @@ -23,9 +23,10 @@ class MainActivityViewModel(di: DI) : DIAwareViewModel(di) { val shouldSyncOnResume: Boolean = repository.syncOnResume.value - fun ensurePeriodicSyncConfigured() = viewModelScope.launch { - repository.ensurePeriodicSyncConfigured() - } + fun ensurePeriodicSyncConfigured() = + viewModelScope.launch { + repository.ensurePeriodicSyncConfigured() + } fun isOkToSyncAutomatically(): Boolean = currentlyConnected(context) && diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/NavigationDeepLinkViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/NavigationDeepLinkViewModel.kt index 5606cf6638..fc688de8ba 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/NavigationDeepLinkViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/NavigationDeepLinkViewModel.kt @@ -10,7 +10,10 @@ import org.kodein.di.instance class NavigationDeepLinkViewModel(di: DI) : DIAwareViewModel(di) { private val repository: Repository by instance() - fun setCurrentFeedAndTag(feedId: Long, tag: String) { + fun setCurrentFeedAndTag( + feedId: Long, + tag: String, + ) { repository.setCurrentFeedAndTag(feedId, tag) // Should open feed in portrait repository.setIsArticleOpen(false) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivity.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivity.kt index 12d1981e8e..aee0620ece 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivity.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/OpenLinkInDefaultActivity.kt @@ -34,19 +34,21 @@ class OpenLinkInDefaultActivity : DIAwareComponentActivity() { val uri = intent.data if (uri?.host == DEEP_LINK_HOST && uri.lastPathSegment == "feed") { - val feedItemIds = intent.getLongArrayExtra(EXTRA_FEEDITEMS_TO_MARK_AS_NOTIFIED) - ?: longArrayOf() + val feedItemIds = + intent.getLongArrayExtra(EXTRA_FEEDITEMS_TO_MARK_AS_NOTIFIED) + ?: longArrayOf() viewModel.markAsNotifiedInBackground(feedItemIds.toList()) activityLauncher.startActivity( openAdjacentIfSuitable = false, - intent = Intent( - Intent.ACTION_VIEW, - uri, - this, - MainActivity::class.java, - ), + intent = + Intent( + Intent.ACTION_VIEW, + uri, + this, + MainActivity::class.java, + ), ) } else { handleNotificationActions(intent) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/coil/TooLargeImageInterceptor.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/coil/TooLargeImageInterceptor.kt index 0aff1b474f..fdb2c1f9d3 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/coil/TooLargeImageInterceptor.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/coil/TooLargeImageInterceptor.kt @@ -20,7 +20,9 @@ class TooLargeImageInterceptor : Interceptor { return ErrorResult( chain.request.error, chain.request, - RuntimeException("Image was (probably) too large to render within memory constraints: ${result.drawable.intrinsicWidth} x ${result.drawable.intrinsicHeight} > 2500 x 2500"), + RuntimeException( + "Image was (probably) too large to render within memory constraints: ${result.drawable.intrinsicWidth} x ${result.drawable.intrinsicHeight} > 2500 x 2500", + ), ) } else { result diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt index a13969d6e9..6636043465 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/AutoCompleteText.kt @@ -42,21 +42,23 @@ fun AutoCompleteResults( AnimatedVisibility(visible = displaySuggestions) { LazyColumn( - modifier = Modifier - .heightIn(max = maxHeight) - .fillMaxWidth(0.9f) - .border( - border = BorderStroke(2.dp, MaterialTheme.colorScheme.onBackground), - shape = RoundedCornerShape(8.dp), - ), + modifier = + Modifier + .heightIn(max = maxHeight) + .fillMaxWidth(0.9f) + .border( + border = BorderStroke(2.dp, MaterialTheme.colorScheme.onBackground), + shape = RoundedCornerShape(8.dp), + ), ) { items( suggestions.item, key = { item -> item }, ) { item -> Box( - modifier = Modifier - .clickable { onSuggestionClicked(item) }, + modifier = + Modifier + .clickable { onSuggestionClicked(item) }, ) { suggestionContent(item) } @@ -68,7 +70,7 @@ fun AutoCompleteResults( @Preview @Composable -fun PreviewAutoCompleteOutlinedText() { +private fun PreviewAutoCompleteOutlinedText() { AutoCompleteResults( displaySuggestions = true, suggestions = immutableListHolderOf("One", "Two", "Three"), diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/BottomAppBar.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/BottomAppBar.kt index bff08c7be6..3b9063fa3e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/BottomAppBar.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/BottomAppBar.kt @@ -80,7 +80,7 @@ fun PaddedBottomAppBar( @Preview @Composable -fun PreviewPaddedBottomBar() { +private fun PreviewPaddedBottomBar() { FeederTheme { PaddedBottomAppBar( actions = { @@ -135,16 +135,18 @@ private fun BottomAppBar( modifier = modifier, ) { Box( - modifier = Modifier.windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Bottom), - ), + modifier = + Modifier.windowInsetsPadding( + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom), + ), ) { Row( - modifier = Modifier - .fillMaxWidth() - .height(bottomBarHeight) - .padding(contentPadding) - .focusGroup(), + modifier = + Modifier + .fillMaxWidth() + .height(bottomBarHeight) + .padding(contentPadding) + .focusGroup(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, content = content, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/ConfirmDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/ConfirmDialog.kt index 4749dfc4ab..890b6a102e 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/ConfirmDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/ConfirmDialog.kt @@ -39,8 +39,9 @@ fun ConfirmDialog( text = stringResource(id = title), style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), ) }, text = { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/OkCancel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/OkCancel.kt index 70894d5d1c..cc316fb9c8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/OkCancel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/components/OkCancel.kt @@ -38,9 +38,10 @@ fun OkCancelWithContent( Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .verticalScroll(scrollState), + modifier = + modifier + .fillMaxWidth() + .verticalScroll(scrollState), ) { Spacer(modifier = Modifier.height(16.dp)) content() @@ -66,8 +67,9 @@ fun OkCancelWithNonScrollableContent( Column( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth(), + modifier = + modifier + .fillMaxWidth(), ) { content() OkCancelButtons( @@ -82,7 +84,7 @@ fun OkCancelWithNonScrollableContent( @Composable @Preview(showBackground = true) -fun OkCancelButtons( +private fun OkCancelButtons( modifier: Modifier = Modifier, onOk: () -> Unit = {}, onCancel: () -> Unit = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/deletefeed/DeleteFeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/deletefeed/DeleteFeedScreen.kt index 530cbf9bca..980ef65825 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/deletefeed/DeleteFeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/deletefeed/DeleteFeedScreen.kt @@ -90,8 +90,9 @@ fun DeleteFeedDialog( text = stringResource(id = R.string.delete_feed), style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), ) }, text = { @@ -102,23 +103,25 @@ fun DeleteFeedDialog( feeds.item, key = { feed -> feed.id }, ) { feed -> - val stateLabel = if (isChecked(feed.id)) { - stringResource(androidx.compose.ui.R.string.selected) - } else { - stringResource(androidx.compose.ui.R.string.not_selected) - } + val stateLabel = + if (isChecked(feed.id)) { + stringResource(androidx.compose.ui.R.string.selected) + } else { + stringResource(androidx.compose.ui.R.string.not_selected) + } Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .requiredHeightIn(min = minimumTouchSize) - .clickable { - onToggleFeed(feed.id, !isChecked(feed.id)) - } - .safeSemantics(mergeDescendants = true) { - stateDescription = stateLabel - }, + modifier = + Modifier + .fillMaxWidth() + .requiredHeightIn(min = minimumTouchSize) + .clickable { + onToggleFeed(feed.id, !isChecked(feed.id)) + } + .safeSemantics(mergeDescendants = true) { + stateDescription = stateLabel + }, ) { Checkbox( checked = isChecked(feed.id), @@ -149,10 +152,11 @@ data class DeletableFeed( @Preview private fun Preview() = DeleteFeedDialog( - feeds = immutableListHolderOf( - DeletableFeed(1, "A Feed"), - DeletableFeed(2, "Another Feed"), - ), + feeds = + immutableListHolderOf( + DeletableFeed(1, "A Feed"), + DeletableFeed(2, "Another Feed"), + ), onDismiss = {}, onDelete = {}, ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/EditableListDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/EditableListDialog.kt index 2abd9c791f..20f91a5d43 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/EditableListDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/EditableListDialog.kt @@ -74,27 +74,30 @@ fun EditableListDialog( label = { Text(stringResource(id = R.string.add_item)) }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - keyboardActions = KeyboardActions( - onNext = { - onAddItem(newValue) - newValue = "" - }, - ), - modifier = Modifier - .fillMaxWidth() - .interceptKey(Key.Enter) { - onAddItem(newValue) - newValue = "" - } - .interceptKey(Key.Escape) { - onDismiss() - }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next, + ), + keyboardActions = + KeyboardActions( + onNext = { + onAddItem(newValue) + newValue = "" + }, + ), + modifier = + Modifier + .fillMaxWidth() + .interceptKey(Key.Enter) { + onAddItem(newValue) + newValue = "" + } + .interceptKey(Key.Escape) { + onDismiss() + }, ) } }, @@ -102,9 +105,10 @@ fun EditableListDialog( text = { LazyColumn( state = lazyListState, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TextFieldDefaults.MinHeight * 3.3f), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = TextFieldDefaults.MinHeight * 3.3f), ) { items( items.item, @@ -113,9 +117,10 @@ fun EditableListDialog( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minimumTouchSize), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = minimumTouchSize), ) { Text( text = item, @@ -141,7 +146,7 @@ fun EditableListDialog( @Preview @Composable -fun PreviewDialog() { +private fun PreviewDialog() { FeederTheme { EditableListDialog( title = { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/FeedNotificationsDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/FeedNotificationsDialog.kt index 9e42aaaaf6..37f4203432 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/FeedNotificationsDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/dialog/FeedNotificationsDialog.kt @@ -68,9 +68,10 @@ fun FeedNotificationsDialog( text = { LazyColumn( state = lazyListState, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TextFieldDefaults.MinHeight * 3.3f), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = TextFieldDefaults.MinHeight * 3.3f), ) { items( items.item, @@ -79,33 +80,36 @@ fun FeedNotificationsDialog( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minimumTouchSize), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = minimumTouchSize), ) { - val stateLabel = if (item.notify) { - stringResource(androidx.compose.ui.R.string.on) - } else { - stringResource(androidx.compose.ui.R.string.off) - } + val stateLabel = + if (item.notify) { + stringResource(androidx.compose.ui.R.string.on) + } else { + stringResource(androidx.compose.ui.R.string.off) + } val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .heightIn(min = 64.dp) - .clickable( - enabled = true, - onClick = { - onToggleItem( - item.feedId, - !item.notify, - ) + modifier = + modifier + .width(dimens.maxContentWidth) + .heightIn(min = 64.dp) + .clickable( + enabled = true, + onClick = { + onToggleItem( + item.feedId, + !item.notify, + ) + }, + ) + .safeSemantics(mergeDescendants = true) { + stateDescription = stateLabel + role = Role.Switch }, - ) - .safeSemantics(mergeDescendants = true) { - stateDescription = stateLabel - role = Role.Switch - }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -139,7 +143,7 @@ fun FeedNotificationsDialog( @Preview @Composable -fun PreviewNotificationsDialog() { +private fun PreviewNotificationsDialog() { FeederTheme { FeedNotificationsDialog( title = { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/CreateFeedScreenViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/CreateFeedScreenViewModel.kt index fa885db265..d46cf205d8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/CreateFeedScreenViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/CreateFeedScreenViewModel.kt @@ -15,11 +15,11 @@ import com.nononsenseapps.feeder.db.room.Feed import com.nononsenseapps.feeder.model.workmanager.requestFeedSync import com.nononsenseapps.feeder.ui.compose.utils.mutableSavedStateOf import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLOrNull -import java.net.URL -import java.time.Instant import kotlinx.coroutines.launch import org.kodein.di.DI import org.kodein.di.instance +import java.net.URL +import java.time.Instant class CreateFeedScreenViewModel( di: DI, @@ -55,15 +55,16 @@ class CreateFeedScreenViewModel( get() = articleOpener == PREF_VAL_OPEN_WITH_READER override val isOpenItemWithAppDefault: Boolean - get() = when (articleOpener) { - PREF_VAL_OPEN_WITH_READER, - PREF_VAL_OPEN_WITH_WEBVIEW, - PREF_VAL_OPEN_WITH_BROWSER, - PREF_VAL_OPEN_WITH_CUSTOM_TAB, - -> false + get() = + when (articleOpener) { + PREF_VAL_OPEN_WITH_READER, + PREF_VAL_OPEN_WITH_WEBVIEW, + PREF_VAL_OPEN_WITH_BROWSER, + PREF_VAL_OPEN_WITH_CUSTOM_TAB, + -> false - else -> true - } + else -> true + } init { viewModelScope.launch { @@ -74,24 +75,26 @@ class CreateFeedScreenViewModel( } } - fun saveAndRequestSync(action: (Long) -> Unit) = viewModelScope.launch { - val feedId = repository.saveFeed( - Feed( - url = URL(feedUrl), - title = feedTitle, - customTitle = feedTitle, - tag = feedTag, - fullTextByDefault = fullTextByDefault, - notify = notify, - openArticlesWith = articleOpener, - alternateId = alternateId, - whenModified = Instant.now(), - imageUrl = sloppyLinkToStrictURLOrNull(feedImage), - ), - ) + fun saveAndRequestSync(action: (Long) -> Unit) = + viewModelScope.launch { + val feedId = + repository.saveFeed( + Feed( + url = URL(feedUrl), + title = feedTitle, + customTitle = feedTitle, + tag = feedTag, + fullTextByDefault = fullTextByDefault, + notify = notify, + openArticlesWith = articleOpener, + alternateId = alternateId, + whenModified = Instant.now(), + imageUrl = sloppyLinkToStrictURLOrNull(feedImage), + ), + ) - requestFeedSync(di, feedId = feedId) + requestFeedSync(di, feedId = feedId) - action(feedId) - } + action(feedId) + } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt index 663f31600a..c4e9ba11ef 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreen.kt @@ -155,12 +155,13 @@ fun EditFeedScreen( onCancel: () -> Unit, modifier: Modifier = Modifier, ) { - val notificationsPermissionState = rememberApiPermissionState( - permission = "android.permission.POST_NOTIFICATIONS", - minimumApiLevel = 33, - ) { value -> - viewState.notify = value - } + val notificationsPermissionState = + rememberApiPermissionState( + permission = "android.permission.POST_NOTIFICATIONS", + minimumApiLevel = 33, + ) { value -> + viewState.notify = value + } val shouldShowExplanationForPermission by remember { derivedStateOf { @@ -172,39 +173,41 @@ fun EditFeedScreen( mutableStateOf(true) } - val screenState = remember { - object : EditFeedScreenState by viewState { - override var notify: Boolean - get() = viewState.notify - set(value) { - if (!value) { - viewState.notify = false - } else { - when (notificationsPermissionState.status) { - is PermissionStatus.Denied -> { - if (notificationsPermissionState.status.shouldShowRationale) { - // Dialog is shown inside EditFeedScreen with a button - permissionDismissed = false - } else { - notificationsPermissionState.launchPermissionRequest() + val screenState = + remember { + object : EditFeedScreenState by viewState { + override var notify: Boolean + get() = viewState.notify + set(value) { + if (!value) { + viewState.notify = false + } else { + when (notificationsPermissionState.status) { + is PermissionStatus.Denied -> { + if (notificationsPermissionState.status.shouldShowRationale) { + // Dialog is shown inside EditFeedScreen with a button + permissionDismissed = false + } else { + notificationsPermissionState.launchPermissionRequest() + } } - } - PermissionStatus.Granted -> viewState.notify = true + PermissionStatus.Granted -> viewState.notify = true + } } } - } + } } - } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior) Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( @@ -262,8 +265,9 @@ fun EditFeedView( }, onCancel = onCancel, okEnabled = viewState.isOkToSave, - modifier = modifier - .padding(horizontal = LocalDimens.current.margin), + modifier = + modifier + .padding(horizontal = LocalDimens.current.margin), ) { if (screenType == ScreenType.DUAL) { Row( @@ -271,9 +275,10 @@ fun EditFeedView( ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .weight(1f, fill = true) - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + Modifier + .weight(1f, fill = true) + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { LeftContent( viewState = viewState, @@ -283,9 +288,10 @@ fun EditFeedView( Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .weight(1f, fill = true) - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + Modifier + .weight(1f, fill = true) + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { RightContent( viewState = viewState, @@ -350,24 +356,27 @@ fun ColumnScope.LeftContent( Text(stringResource(id = R.string.url)) }, isError = viewState.isNotValidUrl, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrect = false, - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Next, - ), - keyboardActions = KeyboardActions( - onNext = { - focusManager.moveFocus(focusDirection = FocusDirection.Down) - }, - ), + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Next, + ), + keyboardActions = + KeyboardActions( + onNext = { + focusManager.moveFocus(focusDirection = FocusDirection.Down) + }, + ), singleLine = true, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) - .interceptKey(Key.Enter) { - focusManager.moveFocus(FocusDirection.Down) - }, + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + .interceptKey(Key.Enter) { + focusManager.moveFocus(FocusDirection.Down) + }, ) AnimatedVisibility(visible = viewState.isNotValidUrl) { Text( @@ -386,32 +395,36 @@ fun ColumnScope.LeftContent( Text(stringResource(id = R.string.title)) }, singleLine = true, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - keyboardActions = KeyboardActions( - onNext = { - focusManager.moveFocus(focusDirection = FocusDirection.Down) - }, - ), - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) - .interceptKey(Key.Enter) { - focusManager.moveFocus(FocusDirection.Down) - }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next, + ), + keyboardActions = + KeyboardActions( + onNext = { + focusManager.moveFocus(focusDirection = FocusDirection.Down) + }, + ), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + .interceptKey(Key.Enter) { + focusManager.moveFocus(FocusDirection.Down) + }, ) AutoCompleteResults( - modifier = Modifier - .focusGroup() - .onFocusChanged { - // Someone in hierarchy has focus - different from isFocused - showTagSuggestions = it.hasFocus - }, + modifier = + Modifier + .focusGroup() + .onFocusChanged { + // Someone in hierarchy has focus - different from isFocused + showTagSuggestions = it.hasFocus + }, displaySuggestions = showTagSuggestions, suggestions = filteredTags, onSuggestionClicked = { tag -> @@ -422,10 +435,11 @@ fun ColumnScope.LeftContent( suggestionContent = { Box( contentAlignment = Alignment.CenterStart, - modifier = Modifier - .padding(horizontal = 16.dp) - .height(48.dp) - .fillMaxWidth(), + modifier = + Modifier + .padding(horizontal = 16.dp) + .height(48.dp) + .fillMaxWidth(), ) { Text( text = it, @@ -441,25 +455,28 @@ fun ColumnScope.LeftContent( Text(stringResource(id = R.string.tag)) }, singleLine = true, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Words, - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { - showTagSuggestions = false - keyboardController?.hide() - rightFocusRequester.requestFocus() - }, - ), - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) - .interceptKey(Key.Enter) { - rightFocusRequester.requestFocus() - }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + ), + keyboardActions = + KeyboardActions( + onDone = { + showTagSuggestions = false + keyboardController?.hide() + rightFocusRequester.requestFocus() + }, + ), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 64.dp) + .interceptKey(Key.Enter) { + rightFocusRequester.requestFocus() + }, ) } } @@ -475,11 +492,12 @@ fun ColumnScope.RightContent( title = stringResource(id = R.string.fetch_full_articles_by_default), checked = viewState.fullTextByDefault, icon = null, - modifier = Modifier - .focusRequester(rightFocusRequester) - .focusProperties { - previous = leftFocusRequester - }, + modifier = + Modifier + .focusRequester(rightFocusRequester) + .focusProperties { + previous = leftFocusRequester + }, ) { viewState.fullTextByDefault = it } SwitchSetting( title = stringResource(id = R.string.notify_for_new_items), @@ -583,7 +601,7 @@ private class ScreenState( @Preview("Edit Feed Phone") @Composable -fun PreviewEditFeedScreenPhone() { +private fun PreviewEditFeedScreenPhone() { FeederTheme { EditFeedScreen( screenType = ScreenType.SINGLE, @@ -598,7 +616,7 @@ fun PreviewEditFeedScreenPhone() { @Preview("Edit Feed Foldable", device = Devices.FOLDABLE) @Preview("Edit Feed Tablet", device = Devices.PIXEL_C) @Composable -fun PreviewEditFeedScreenLarge() { +private fun PreviewEditFeedScreenLarge() { FeederTheme { EditFeedScreen( screenType = ScreenType.DUAL, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreenViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreenViewModel.kt index 0b731dd163..7f6c491965 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreenViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/editfeed/EditFeedScreenViewModel.kt @@ -14,11 +14,11 @@ import com.nononsenseapps.feeder.base.DIAwareViewModel import com.nononsenseapps.feeder.db.room.Feed import com.nononsenseapps.feeder.model.workmanager.requestFeedSync import com.nononsenseapps.feeder.ui.compose.utils.mutableSavedStateOf -import java.net.URL -import java.time.Instant import kotlinx.coroutines.launch import org.kodein.di.DI import org.kodein.di.instance +import java.net.URL +import java.time.Instant class EditFeedScreenViewModel( di: DI, @@ -26,8 +26,9 @@ class EditFeedScreenViewModel( ) : DIAwareViewModel(di), EditFeedScreenState { private val repository: Repository by instance() - val feedId: Long = state["feedId"] - ?: throw IllegalArgumentException("Missing feedId in savedState") + val feedId: Long = + state["feedId"] + ?: throw IllegalArgumentException("Missing feedId in savedState") // These two are updated as a result of url updating override var isNotValidUrl by mutableStateOf(false) @@ -60,21 +61,23 @@ class EditFeedScreenViewModel( get() = articleOpener == PREF_VAL_OPEN_WITH_READER override val isOpenItemWithAppDefault: Boolean - get() = when (articleOpener) { - PREF_VAL_OPEN_WITH_READER, - PREF_VAL_OPEN_WITH_WEBVIEW, - PREF_VAL_OPEN_WITH_BROWSER, - PREF_VAL_OPEN_WITH_CUSTOM_TAB, - -> false - - else -> true - } + get() = + when (articleOpener) { + PREF_VAL_OPEN_WITH_READER, + PREF_VAL_OPEN_WITH_WEBVIEW, + PREF_VAL_OPEN_WITH_BROWSER, + PREF_VAL_OPEN_WITH_CUSTOM_TAB, + -> false + + else -> true + } init { viewModelScope.launch { // Set initial state in case state is empty - val feed = repository.getFeed(feedId) - ?: throw IllegalArgumentException("No feed with id $feedId!") + val feed = + repository.getFeed(feedId) + ?: throw IllegalArgumentException("No feed with id $feedId!") defaultTitle = feed.title feedImage = feed.imageUrl?.toString() ?: "" @@ -108,37 +111,41 @@ class EditFeedScreenViewModel( } } - fun saveAndRequestSync(action: (Long) -> Unit) = viewModelScope.launch { - val feed = repository.getFeed(feedId) - ?: Feed() // Feed was deleted while being edited? - - val updatedFeed = feed.copy( - url = URL(feedUrl), - title = feedTitle, - customTitle = feedTitle, - tag = feedTag, - fullTextByDefault = fullTextByDefault, - notify = notify, - openArticlesWith = articleOpener, - alternateId = alternateId, - ) - - // No point in doing anything unless they actually differ - if (feed != updatedFeed) { - // In case clocks between different devices differ don't allow this date to go backwards - updatedFeed.whenModified = maxOf(Instant.now(), feed.whenModified.plusMillis(1)) - val savedId = repository.saveFeed( - updatedFeed, - ) - requestFeedSync( - di, - feedId = savedId, - forceNetwork = false, - ) - } + fun saveAndRequestSync(action: (Long) -> Unit) = + viewModelScope.launch { + val feed = + repository.getFeed(feedId) + ?: Feed() // Feed was deleted while being edited? + + val updatedFeed = + feed.copy( + url = URL(feedUrl), + title = feedTitle, + customTitle = feedTitle, + tag = feedTag, + fullTextByDefault = fullTextByDefault, + notify = notify, + openArticlesWith = articleOpener, + alternateId = alternateId, + ) + + // No point in doing anything unless they actually differ + if (feed != updatedFeed) { + // In case clocks between different devices differ don't allow this date to go backwards + updatedFeed.whenModified = maxOf(Instant.now(), feed.whenModified.plusMillis(1)) + val savedId = + repository.saveFeed( + updatedFeed, + ) + requestFeedSync( + di, + feedId = savedId, + forceNetwork = false, + ) + } - action(feed.id) - } + action(feed.id) + } } internal fun isValidUrl(value: String): Boolean { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/empty/NothingToRead.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/empty/NothingToRead.kt index d506922d38..d0e58644e5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/empty/NothingToRead.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/empty/NothingToRead.kt @@ -37,10 +37,11 @@ fun NothingToRead( ) { Box( contentAlignment = Alignment.Center, - modifier = modifier - .padding(horizontal = LocalDimens.current.margin) - .fillMaxHeight() - .fillMaxWidth(), + modifier = + modifier + .padding(horizontal = LocalDimens.current.margin) + .fillMaxHeight() + .fillMaxWidth(), ) { Column( verticalArrangement = Arrangement.Center, @@ -48,44 +49,49 @@ fun NothingToRead( ) { Text( text = stringResource(id = R.string.empty_feed_top), - style = MaterialTheme.typography.headlineMedium.merge( - TextStyle(fontWeight = FontWeight.Light), - ), + style = + MaterialTheme.typography.headlineMedium.merge( + TextStyle(fontWeight = FontWeight.Light), + ), textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(16.dp)) Box( contentAlignment = Alignment.Center, - modifier = Modifier - .heightIn(min = TextFieldDefaults.MinHeight) - .fillMaxWidth() - .clickable { - onOpenOtherFeed() - }, + modifier = + Modifier + .heightIn(min = TextFieldDefaults.MinHeight) + .fillMaxWidth() + .clickable { + onOpenOtherFeed() + }, ) { Text( text = annotatedStringResource(id = R.string.empty_feed_open), - style = MaterialTheme.typography.headlineMedium.merge( - TextStyle(fontWeight = FontWeight.Light), - ), + style = + MaterialTheme.typography.headlineMedium.merge( + TextStyle(fontWeight = FontWeight.Light), + ), textAlign = TextAlign.Center, ) } Spacer(modifier = Modifier.height(16.dp)) Box( contentAlignment = Alignment.Center, - modifier = Modifier - .heightIn(min = TextFieldDefaults.MinHeight) - .fillMaxWidth() - .clickable { - onAddFeed() - }, + modifier = + Modifier + .heightIn(min = TextFieldDefaults.MinHeight) + .fillMaxWidth() + .clickable { + onAddFeed() + }, ) { Text( text = annotatedStringResource(id = R.string.empty_feed_add), - style = MaterialTheme.typography.headlineMedium.merge( - TextStyle(fontWeight = FontWeight.Light), - ), + style = + MaterialTheme.typography.headlineMedium.merge( + TextStyle(fontWeight = FontWeight.Light), + ), textAlign = TextAlign.Center, ) } @@ -102,7 +108,7 @@ fun NothingToRead( uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable -fun PreviewNothingToRead() { +private fun PreviewNothingToRead() { FeederTheme { Surface { NothingToRead() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt index 87bd572f16..942c50677c 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/EditFeedDialog.kt @@ -56,14 +56,16 @@ fun EditFeedDialog( text = stringResource(id = R.string.edit_feed), style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), ) }, text = { LazyColumn( - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) { items( feeds.item, @@ -72,14 +74,15 @@ fun EditFeedDialog( Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .requiredHeightIn(min = minimumTouchSize) - .clickable { - onEdit(feed.id) - onDismiss() - } - .semantics(mergeDescendants = true) {}, + modifier = + Modifier + .fillMaxWidth() + .requiredHeightIn(min = minimumTouchSize) + .clickable { + onEdit(feed.id) + onDismiss() + } + .semantics(mergeDescendants = true) {}, ) { RadioButton( selected = false, @@ -106,10 +109,11 @@ fun EditFeedDialog( private fun Preview() { FeederTheme { EditFeedDialog( - feeds = immutableListHolderOf( - DeletableFeed(1, "A Feed"), - DeletableFeed(2, "Another Feed"), - ), + feeds = + immutableListHolderOf( + DeletableFeed(1, "A Feed"), + DeletableFeed(2, "Another Feed"), + ), onDismiss = {}, ) {} } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/ExplainPermissionDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/ExplainPermissionDialog.kt index 677ccf33db..e938692b5f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/ExplainPermissionDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/ExplainPermissionDialog.kt @@ -32,8 +32,9 @@ fun ExplainPermissionDialog( Text( text = stringResource(id = explanation), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), ) }, ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt index 661144216a..e37566aba5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCard.kt @@ -57,9 +57,9 @@ import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.theme.titleFontWeight import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews import com.nononsenseapps.feeder.ui.compose.utils.onKeyEventLikeEscape +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL import java.time.Instant -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @Composable fun FeedItemCard( @@ -82,43 +82,48 @@ fun FeedItemCard( ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .requiredHeightIn(min = minimumTouchSize), + modifier = + Modifier + .requiredHeightIn(min = minimumTouchSize), ) { if (showThumbnail) { item.imageUrl?.let { imageUrl -> BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val pixels = with(LocalDensity.current) { - val width = maxWidth.roundToPx() - Size(width, (width * 9) / 16) - } - val alpha = if (item.unread) { - 1f - } else { - 0.74f - } + val pixels = + with(LocalDensity.current) { + val width = maxWidth.roundToPx() + Size(width, (width * 9) / 16) + } + val alpha = + if (item.unread) { + 1f + } else { + 0.74f + } AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .listener( - onError = { a, b -> - Log.e("FEEDER_CARD", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FILL) - .size(pixels) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .listener( + onError = { a, b -> + Log.e("FEEDER_CARD", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FILL) + .size(pixels) + .precision(Precision.INEXACT) + .build(), placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = stringResource(id = R.string.article_image), contentScale = ContentScale.Crop, alignment = Alignment.Center, - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .fillMaxWidth() - .aspectRatio(16.0f / 9.0f) - .alpha(alpha), + modifier = + Modifier + .clip(MaterialTheme.shapes.medium) + .fillMaxWidth() + .aspectRatio(16.0f / 9.0f) + .alpha(alpha), ) } } @@ -126,8 +131,9 @@ fun FeedItemCard( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp, horizontal = 8.dp), ) { FeedItemEitherIndicator( bookmarked = item.bookmarked && bookmarkIndicator, @@ -167,26 +173,28 @@ fun RowScope.FeedItemText( modifier: Modifier = Modifier, ) { val snippetStyle = FeedListItemSnippetTextStyle() - val joinedText = remember(item, showOnlyTitle) { - buildAnnotatedString { - if (item.title.isNotBlank()) { - append(item.title) - if (!showOnlyTitle && item.snippet.isNotBlank()) { - withStyle(snippetStyle.toSpanStyle()) { - append('\n') - append(item.snippet) + val joinedText = + remember(item, showOnlyTitle) { + buildAnnotatedString { + if (item.title.isNotBlank()) { + append(item.title) + if (!showOnlyTitle && item.snippet.isNotBlank()) { + withStyle(snippetStyle.toSpanStyle()) { + append('\n') + append(item.snippet) + } } + } else { + // Heard of one feed which did not have titles. If so always include snippet + append(item.snippet) } - } else { - // Heard of one feed which did not have titles. If so always include snippet - append(item.snippet) } } - } Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .weight(1f), + modifier = + modifier + .weight(1f), ) { WithBidiDeterminedLayoutDirection(paragraph = joinedText.text) { Text( @@ -195,16 +203,18 @@ fun RowScope.FeedItemText( fontWeight = titleFontWeight(item.unread), overflow = TextOverflow.Ellipsis, maxLines = maxLines, - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) } // Want the dropdown to center on the middle text row Box { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { WithBidiDeterminedLayoutDirection(paragraph = item.feedTitle) { @@ -213,8 +223,9 @@ fun RowScope.FeedItemText( style = FeedListItemFeedTitleStyle(), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(1f), + modifier = + Modifier + .weight(1f), ) } WithBidiDeterminedLayoutDirection(paragraph = item.pubDate) { @@ -240,12 +251,13 @@ fun RowScope.FeedItemText( }, text = { Text( - text = stringResource( - when (item.bookmarked) { - true -> R.string.unsave_article - false -> R.string.save_article - }, - ), + text = + stringResource( + when (item.bookmarked) { + true -> R.string.unsave_article + false -> R.string.save_article + }, + ), ) }, ) @@ -285,9 +297,10 @@ fun RowScope.FeedItemText( } } if (showReadingTime) { - val readTimeSecs = remember(item.wordCount) { - wordsToReadTimeSecs(item.wordCount) - } + val readTimeSecs = + remember(item.wordCount) { + wordsToReadTimeSecs(item.wordCount) + } if (readTimeSecs > 0) { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { val readTimeText = @@ -300,8 +313,9 @@ fun RowScope.FeedItemText( .format(item.wordCount) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) { WithBidiDeterminedLayoutDirection(paragraph = readTimeText) { Text( @@ -333,21 +347,22 @@ fun RowScope.FeedItemText( private fun Preview() { FeederTheme { FeedItemCard( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf dsaf asd fsa dfasdf", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = null, - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 588, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf dsaf asd fsa dfasdf", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = null, + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 588, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, @@ -371,21 +386,22 @@ private fun PreviewWithImageUnread() { modifier = Modifier.width((300 - 2 * 16).dp), ) { FeedItemCard( - item = FeedListItem( - title = "title can be one line", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Feed", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = "blabla", - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = URL("https://foo/bar.png"), - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 939, - ), + item = + FeedListItem( + title = "title can be one line", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Feed", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = "blabla", + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = URL("https://foo/bar.png"), + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 939, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, @@ -410,21 +426,22 @@ private fun PreviewWithImageRead() { modifier = Modifier.width((300 - 2 * 16).dp), ) { FeedItemCard( - item = FeedListItem( - title = "title can be one line", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed", - pubDate = "Jun 9, 2021", - unread = false, - imageUrl = "blabla", - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 950, - ), + item = + FeedListItem( + title = "title can be one line", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed", + pubDate = "Jun 9, 2021", + unread = false, + imageUrl = "blabla", + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 950, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt index e3fe9460fc..01912a9b5f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemCompact.kt @@ -62,9 +62,10 @@ fun FeedItemCompact( ) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .height(IntrinsicSize.Min) - .padding(start = LocalDimens.current.margin), + modifier = + modifier + .height(IntrinsicSize.Min) + .padding(start = LocalDimens.current.margin), ) { FeedItemText( item = item, @@ -77,9 +78,10 @@ fun FeedItemCompact( maxLines = maxLines, showOnlyTitle = showOnlyTitle, showReadingTime = showReadingTime, - modifier = Modifier - .requiredHeightIn(min = minimumTouchSize) - .padding(vertical = 8.dp), + modifier = + Modifier + .requiredHeightIn(min = minimumTouchSize) + .padding(vertical = 8.dp), ) if ((item.bookmarked && bookmarkIndicator) || showThumbnail && (item.imageUrl != null || item.feedImageUrl != null)) { @@ -93,39 +95,44 @@ fun FeedItemCompact( itemImage = null, feedImageUrl = null, size = 24.dp, - modifier = Modifier - .fillMaxHeight() - .width(64.dp), + modifier = + Modifier + .fillMaxHeight() + .width(64.dp), ) } else { (item.imageUrl ?: item.feedImageUrl?.toString())?.let { imageUrl -> - val scale = if (item.imageUrl != null) { - ContentScale.Crop - } else { - ContentScale.Fit - } - val pixels = with(LocalDensity.current) { - Size(64.dp.roundToPx(), 96.dp.roundToPx()) - } + val scale = + if (item.imageUrl != null) { + ContentScale.Crop + } else { + ContentScale.Fit + } + val pixels = + with(LocalDensity.current) { + Size(64.dp.roundToPx(), 96.dp.roundToPx()) + } AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .listener( - onError = { a, b -> - Log.e("FEEDER_COMPACT", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FILL) - .size(pixels) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .listener( + onError = { a, b -> + Log.e("FEEDER_COMPACT", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FILL) + .size(pixels) + .precision(Precision.INEXACT) + .build(), placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = stringResource(id = R.string.article_image), contentScale = scale, - modifier = Modifier - .width(imageWidth) - .fillMaxHeight(), + modifier = + Modifier + .width(imageWidth) + .fillMaxHeight(), ) } } @@ -154,21 +161,23 @@ data class FeedListItem( val wordCount: Int, ) { val cursor: FeedItemCursor - get() = object : FeedItemCursor { - override val primarySortTime: Instant = this@FeedListItem.primarySortTime - override val pubDate: ZonedDateTime? = this@FeedListItem.rawPubDate - override val id: Long = this@FeedListItem.id - } + get() = + object : FeedItemCursor { + override val primarySortTime: Instant = this@FeedListItem.primarySortTime + override val pubDate: ZonedDateTime? = this@FeedListItem.rawPubDate + override val id: Long = this@FeedListItem.id + } /** * Used so lazylist/grid can re-use items. * * Type will depend on having images as that will influence visible items */ - fun contentType(feedItemStyle: FeedItemStyle): String = when { - imageUrl?.isNotBlank() == true -> "$feedItemStyle/image" - else -> "$feedItemStyle/other" - } + fun contentType(feedItemStyle: FeedItemStyle): String = + when { + imageUrl?.isNotBlank() == true -> "$feedItemStyle/image" + else -> "$feedItemStyle/other" + } } @Composable @@ -177,21 +186,22 @@ private fun PreviewRead() { FeederTheme { Surface { FeedItemCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = false, - imageUrl = null, - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = false, + imageUrl = null, + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, @@ -215,21 +225,22 @@ private fun PreviewUnread() { FeederTheme { Surface { FeedItemCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = null, - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = null, + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, @@ -256,21 +267,22 @@ private fun PreviewWithImage() { modifier = Modifier.width(400.dp), ) { FeedItemCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = "blabla", - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = "blabla", + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), showThumbnail = true, onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemIndicator.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemIndicator.kt index 407debe906..6bce3283ab 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemIndicator.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemIndicator.kt @@ -46,21 +46,24 @@ fun FeedItemEitherIndicator( when { bookmarked -> FeedItemSavedIndicator(size = size, modifier = modifier) // unread -> FeedItemNewIndicator(size = size, modifier = modifier) - itemImage != null -> FeedItemImageIndicator( - imageUrl = itemImage, - size = size, - modifier = modifier, - ) + itemImage != null -> + FeedItemImageIndicator( + imageUrl = itemImage, + size = size, + modifier = modifier, + ) - feedImageUrl != null -> FeedItemFeedIconIndicator( - feedImageUrl = feedImageUrl, - size = size, - modifier = modifier, - ) + feedImageUrl != null -> + FeedItemFeedIconIndicator( + feedImageUrl = feedImageUrl, + size = size, + modifier = modifier, + ) - else -> Box(modifier = modifier) { - Spacer(modifier = Modifier.size(size)) - } + else -> + Box(modifier = modifier) { + Spacer(modifier = Modifier.size(size)) + } } } @@ -70,32 +73,35 @@ fun FeedItemImageIndicator( size: Dp, modifier: Modifier = Modifier, ) { - val pixels = with(LocalDensity.current) { - size.roundToPx() - } + val pixels = + with(LocalDensity.current) { + size.roundToPx() + } Box( contentAlignment = Alignment.Center, modifier = modifier, ) { AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .listener( - onError = { a, b -> - Log.e("FEEDER_INDICATOR", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FIT) - .size(pixels) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .listener( + onError = { a, b -> + Log.e("FEEDER_INDICATOR", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FIT) + .size(pixels) + .precision(Precision.INEXACT) + .build(), placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = stringResource(id = R.string.feed_icon), contentScale = ContentScale.Fit, - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .size(size), + modifier = + Modifier + .clip(MaterialTheme.shapes.small) + .size(size), ) } } @@ -106,31 +112,34 @@ fun FeedItemFeedIconIndicator( size: Dp, modifier: Modifier = Modifier, ) { - val pixels = with(LocalDensity.current) { - size.roundToPx() - } + val pixels = + with(LocalDensity.current) { + size.roundToPx() + } Box( contentAlignment = Alignment.Center, modifier = modifier, ) { AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(feedImageUrl) - .listener( - onError = { a, b -> - Log.e("FEEDER_INDICATOR", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FIT) - .size(pixels) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(feedImageUrl) + .listener( + onError = { a, b -> + Log.e("FEEDER_INDICATOR", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FIT) + .size(pixels) + .precision(Precision.INEXACT) + .build(), placeholder = rememberTintedVectorPainter(Icons.Outlined.Terrain), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = stringResource(id = R.string.feed_icon), contentScale = ContentScale.Fit, - modifier = Modifier - .size(size), + modifier = + Modifier + .size(size), ) } } @@ -144,8 +153,9 @@ fun FeedItemNewIndicator( Icon( Icons.Outlined.Circle, contentDescription = stringResource(id = R.string.unread_adjective), - modifier = Modifier - .size(size), + modifier = + Modifier + .size(size), tint = MaterialTheme.colorScheme.primary, ) } @@ -163,8 +173,9 @@ fun FeedItemSavedIndicator( Icon( Icons.Default.Star, contentDescription = stringResource(id = R.string.saved_article), - modifier = Modifier - .size(size), + modifier = + Modifier + .size(size), tint = MaterialTheme.colorScheme.primary, ) } @@ -172,13 +183,14 @@ fun FeedItemSavedIndicator( @Preview("Light") @Composable -fun PreviewLightFeedItemIndicatorRow() { +private fun PreviewLightFeedItemIndicatorRow() { FeederTheme(currentTheme = ThemeOptions.DAY) { Surface { Box( contentAlignment = Alignment.Center, - modifier = Modifier - .padding(32.dp), + modifier = + Modifier + .padding(32.dp), ) { Row { FeedItemNewIndicator(size = 8.dp) @@ -191,13 +203,14 @@ fun PreviewLightFeedItemIndicatorRow() { @Preview("Dark") @Composable -fun PreviewDarkFeedItemIndicatorRow() { +private fun PreviewDarkFeedItemIndicatorRow() { FeederTheme(currentTheme = ThemeOptions.NIGHT) { Surface { Box( contentAlignment = Alignment.Center, - modifier = Modifier - .padding(32.dp), + modifier = + Modifier + .padding(32.dp), ) { Row { FeedItemNewIndicator(size = 8.dp) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt index 9741c6abc2..3a4f1750d9 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedItemSuperCompact.kt @@ -14,9 +14,9 @@ import com.nononsenseapps.feeder.db.room.ID_UNSET import com.nononsenseapps.feeder.ui.compose.minimumTouchSize import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.theme.LocalDimens +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.net.URL import java.time.Instant -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @Composable fun FeedItemSuperCompact( @@ -36,9 +36,10 @@ fun FeedItemSuperCompact( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - .requiredHeightIn(min = minimumTouchSize) - .padding(vertical = 8.dp, horizontal = LocalDimens.current.margin), + modifier = + modifier + .requiredHeightIn(min = minimumTouchSize) + .padding(vertical = 8.dp, horizontal = LocalDimens.current.margin), ) { FeedItemEitherIndicator( bookmarked = item.bookmarked && bookmarkIndicator, @@ -67,21 +68,22 @@ private fun PreviewRead() { FeederTheme { Surface { FeedItemSuperCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = false, - imageUrl = null, - link = null, - id = ID_UNSET, - bookmarked = true, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = false, + imageUrl = null, + link = null, + id = ID_UNSET, + bookmarked = true, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, @@ -103,21 +105,22 @@ private fun PreviewUnread() { FeederTheme { Surface { FeedItemSuperCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = null, - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = null, - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = null, + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = null, + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, @@ -139,21 +142,22 @@ private fun PreviewWithImage() { FeederTheme { Surface { FeedItemSuperCompact( - item = FeedListItem( - title = "title", - snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", - feedTitle = "Super Duper Feed One two three hup di too dasf", - pubDate = "Jun 9, 2021", - unread = true, - imageUrl = "blabla", - link = null, - id = ID_UNSET, - bookmarked = false, - feedImageUrl = URL("https://example.com/image.png"), - primarySortTime = Instant.EPOCH, - rawPubDate = null, - wordCount = 900, - ), + item = + FeedListItem( + title = "title", + snippet = "snippet which is quite long as you might expect from a snipper of a story. It keeps going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and going and snowing", + feedTitle = "Super Duper Feed One two three hup di too dasf", + pubDate = "Jun 9, 2021", + unread = true, + imageUrl = "blabla", + link = null, + id = ID_UNSET, + bookmarked = false, + feedImageUrl = URL("https://example.com/image.png"), + primarySortTime = Instant.EPOCH, + rawPubDate = null, + wordCount = 900, + ), onMarkAboveAsRead = {}, onMarkBelowAsRead = {}, onShareItem = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt index 20a32f41e7..21d46acb3f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/FeedScreen.kt @@ -141,8 +141,6 @@ import com.nononsenseapps.feeder.util.ActivityLauncher import com.nononsenseapps.feeder.util.ToastMaker import com.nononsenseapps.feeder.util.emailBugReportIntent import com.nononsenseapps.feeder.util.logDebug -import java.time.Instant -import java.time.LocalDateTime import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -150,6 +148,8 @@ import kotlinx.coroutines.launch import org.kodein.di.compose.LocalDI import org.kodein.di.compose.instance import org.kodein.di.instance +import java.time.Instant +import java.time.LocalDateTime private const val LOG_TAG = "FEEDER_FEEDSCREEN" @@ -168,38 +168,42 @@ fun FeedScreen( val pagedFeedItems = viewModel.currentFeedListItems.collectAsLazyPagingItems() val di = LocalDI.current - val opmlExporter = rememberLauncherForActivityResult( - ActivityResultContracts.CreateDocument("text/x-opml"), - ) { uri -> - if (uri != null) { - val applicationCoroutineScope: ApplicationCoroutineScope by di.instance() - applicationCoroutineScope.launch { - exportOpml(di, uri) + val opmlExporter = + rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("text/x-opml"), + ) { uri -> + if (uri != null) { + val applicationCoroutineScope: ApplicationCoroutineScope by di.instance() + applicationCoroutineScope.launch { + exportOpml(di, uri) + } } } - } - val opmlImporter = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument(), - ) { uri -> - if (uri != null) { - val applicationCoroutineScope: ApplicationCoroutineScope by di.instance() - applicationCoroutineScope.launch { - importOpml(di, uri) + val opmlImporter = + rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocument(), + ) { uri -> + if (uri != null) { + val applicationCoroutineScope: ApplicationCoroutineScope by di.instance() + applicationCoroutineScope.launch { + importOpml(di, uri) + } } } - } val activityLauncher: ActivityLauncher by LocalDI.current.instance() // Each feed gets its own scroll state. Persists across device rotations, but is cleared when // switching feeds - val feedListState = key(viewState.currentFeedOrTag) { - pagedFeedItems.rememberLazyListState() - } + val feedListState = + key(viewState.currentFeedOrTag) { + pagedFeedItems.rememberLazyListState() + } - val feedGridState = key(viewState.currentFeedOrTag) { - rememberLazyStaggeredGridState() - } + val feedGridState = + key(viewState.currentFeedOrTag) { + rememberLazyStaggeredGridState() + } val toolbarColor = MaterialTheme.colorScheme.surface.toArgb() @@ -237,10 +241,11 @@ fun FeedScreen( }, focusRequester = focusNavDrawer, drawerState = drawerState, - navDrawerListState = when (viewState.drawerItemsWithUnreadCounts.size) { - 0 -> workaroundNavDrawerListState - else -> navDrawerListState - }, + navDrawerListState = + when (viewState.drawerItemsWithUnreadCounts.size) { + 0 -> workaroundNavDrawerListState + else -> navDrawerListState + }, ) { FeedScreen( viewState = viewState, @@ -469,9 +474,10 @@ fun FeedScreen( DropdownMenu( expanded = viewState.showFilterMenu, onDismissRequest = { onShowFilterMenu(false) }, - modifier = Modifier.onKeyEventLikeEscape { - onShowFilterMenu(false) - }, + modifier = + Modifier.onKeyEventLikeEscape { + onShowFilterMenu(false) + }, ) { DropdownMenuItem( enabled = false, @@ -488,14 +494,16 @@ fun FeedScreen( text = { Text(stringResource(id = R.string.unread_adjective)) }, - modifier = Modifier - .safeSemantics { - stateDescription = when (viewState.filter.unread) { - true -> context.getString(androidx.compose.ui.R.string.selected) - else -> context.getString(androidx.compose.ui.R.string.not_selected) - } - role = Role.Checkbox - }, + modifier = + Modifier + .safeSemantics { + stateDescription = + when (viewState.filter.unread) { + true -> context.getString(androidx.compose.ui.R.string.selected) + else -> context.getString(androidx.compose.ui.R.string.not_selected) + } + role = Role.Checkbox + }, ) DropdownMenuItem( onClick = { @@ -513,14 +521,16 @@ fun FeedScreen( text = { Text(stringResource(id = R.string.saved_adjective)) }, - modifier = Modifier - .safeSemantics { - stateDescription = when (viewState.filter.saved) { - true -> context.getString(androidx.compose.ui.R.string.selected) - else -> context.getString(androidx.compose.ui.R.string.not_selected) - } - role = Role.Checkbox - }, + modifier = + Modifier + .safeSemantics { + stateDescription = + when (viewState.filter.saved) { + true -> context.getString(androidx.compose.ui.R.string.selected) + else -> context.getString(androidx.compose.ui.R.string.not_selected) + } + role = Role.Checkbox + }, ) DropdownMenuItem( onClick = { @@ -538,14 +548,16 @@ fun FeedScreen( text = { Text(stringResource(id = R.string.recently_read_adjective)) }, - modifier = Modifier - .safeSemantics { - stateDescription = when (viewState.filter.recentlyRead) { - true -> context.getString(androidx.compose.ui.R.string.selected) - else -> context.getString(androidx.compose.ui.R.string.not_selected) - } - role = Role.Checkbox - }, + modifier = + Modifier + .safeSemantics { + stateDescription = + when (viewState.filter.recentlyRead) { + true -> context.getString(androidx.compose.ui.R.string.selected) + else -> context.getString(androidx.compose.ui.R.string.not_selected) + } + role = Role.Checkbox + }, ) DropdownMenuItem( onClick = { @@ -563,14 +575,16 @@ fun FeedScreen( text = { Text(stringResource(id = R.string.read_adjective)) }, - modifier = Modifier - .safeSemantics { - stateDescription = when (viewState.filter.read) { - true -> context.getString(androidx.compose.ui.R.string.selected) - else -> context.getString(androidx.compose.ui.R.string.not_selected) - } - role = Role.Checkbox - }, + modifier = + Modifier + .safeSemantics { + stateDescription = + when (viewState.filter.read) { + true -> context.getString(androidx.compose.ui.R.string.selected) + else -> context.getString(androidx.compose.ui.R.string.not_selected) + } + role = Role.Checkbox + }, ) } } @@ -591,9 +605,10 @@ fun FeedScreen( DropdownMenu( expanded = viewState.showToolbarMenu, onDismissRequest = { onShowToolbarMenu(false) }, - modifier = Modifier.onKeyEventLikeEscape { - onShowToolbarMenu(false) - }, + modifier = + Modifier.onKeyEventLikeEscape { + onShowToolbarMenu(false) + }, ) { DropdownMenuItem( onClick = { @@ -744,57 +759,60 @@ fun FeedScreen( } }, ) { innerModifier -> - val screenType = when (isCompactDevice()) { - true -> FeedScreenType.FeedList - false -> FeedScreenType.FeedGrid - } + val screenType = + when (isCompactDevice()) { + true -> FeedScreenType.FeedList + false -> FeedScreenType.FeedGrid + } when (screenType) { - FeedScreenType.FeedGrid -> FeedGridContent( - viewState = viewState, - onOpenNavDrawer = { - coroutineScope.launch { - if (drawerState.isOpen) { - drawerState.close() - } else { - drawerState.open() + FeedScreenType.FeedGrid -> + FeedGridContent( + viewState = viewState, + onOpenNavDrawer = { + coroutineScope.launch { + if (drawerState.isOpen) { + drawerState.close() + } else { + drawerState.open() + } } - } - }, - onAddFeed = onAddFeed, - markAsUnread = markAsUnread, - markAsReadOnSwipe = markAsReadOnSwipe, - markBeforeAsRead = markBeforeAsRead, - markAfterAsRead = markAfterAsRead, - onItemClick = onOpenFeedItem, - onSetBookmarked = onSetBookmarked, - gridState = feedGridState, - pagedFeedItems = pagedFeedItems, - modifier = innerModifier, - ).also { logDebug(LOG_TAG, "Showing GRID") } + }, + onAddFeed = onAddFeed, + markAsUnread = markAsUnread, + markAsReadOnSwipe = markAsReadOnSwipe, + markBeforeAsRead = markBeforeAsRead, + markAfterAsRead = markAfterAsRead, + onItemClick = onOpenFeedItem, + onSetBookmarked = onSetBookmarked, + gridState = feedGridState, + pagedFeedItems = pagedFeedItems, + modifier = innerModifier, + ).also { logDebug(LOG_TAG, "Showing GRID") } - FeedScreenType.FeedList -> FeedListContent( - viewState = viewState, - onOpenNavDrawer = { - coroutineScope.launch { - if (drawerState.isOpen) { - drawerState.close() - } else { - drawerState.open() + FeedScreenType.FeedList -> + FeedListContent( + viewState = viewState, + onOpenNavDrawer = { + coroutineScope.launch { + if (drawerState.isOpen) { + drawerState.close() + } else { + drawerState.open() + } } - } - }, - onAddFeed = onAddFeed, - markAsUnread = markAsUnread, - markAsReadOnSwipe = markAsReadOnSwipe, - markBeforeAsRead = markBeforeAsRead, - markAfterAsRead = markAfterAsRead, - onItemClick = onOpenFeedItem, - onSetBookmarked = onSetBookmarked, - listState = feedListState, - pagedFeedItems = pagedFeedItems, - modifier = innerModifier, - ).also { logDebug(LOG_TAG, "Showing LIST") } + }, + onAddFeed = onAddFeed, + markAsUnread = markAsUnread, + markAsReadOnSwipe = markAsReadOnSwipe, + markBeforeAsRead = markBeforeAsRead, + markAfterAsRead = markAfterAsRead, + onItemClick = onOpenFeedItem, + onSetBookmarked = onSetBookmarked, + listState = feedListState, + pagedFeedItems = pagedFeedItems, + modifier = innerModifier, + ).also { logDebug(LOG_TAG, "Showing LIST") } } } } @@ -867,9 +885,10 @@ fun FeedScreen( PlainTooltipBox(tooltip = { Text(stringResource(R.string.mark_all_as_read)) }) { FloatingActionButton( onClick = onMarkAllAsRead, - modifier = Modifier - .navigationBarsPadding() - .tooltipAnchor(), + modifier = + Modifier + .navigationBarsPadding() + .tooltipAnchor(), ) { Icon( Icons.Default.DoneAll, @@ -883,32 +902,35 @@ fun FeedScreen( bottomBarVisibleState.targetState = viewState.isBottomBarVisible } - val topAppBarState = key(viewState.currentFeedOrTag) { - rememberTopAppBarState() - } + val topAppBarState = + key(viewState.currentFeedOrTag) { + rememberTopAppBarState() + } val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior) - val pullRefreshState = rememberPullRefreshState( - refreshing = isRefreshing, - onRefresh = { - manuallyTriggeredRefresh = true - syncIndicatorMax = Instant.now().plusSeconds(10) - onRefreshVisible() - }, - ) + val pullRefreshState = + rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + manuallyTriggeredRefresh = true + syncIndicatorMax = Instant.now().plusSeconds(10) + onRefreshVisible() + }, + ) Scaffold( topBar = { SensibleTopAppBar( scrollBehavior = scrollBehavior, - title = when (viewState.feedScreenTitle.type) { - FeedType.FEED -> viewState.feedScreenTitle.title - FeedType.TAG -> viewState.feedScreenTitle.title - FeedType.SAVED_ARTICLES -> stringResource(id = R.string.saved_articles) - FeedType.ALL_FEEDS -> stringResource(id = R.string.all_feeds) - } ?: "", + title = + when (viewState.feedScreenTitle.type) { + FeedType.FEED -> viewState.feedScreenTitle.title + FeedType.TAG -> viewState.feedScreenTitle.title + FeedType.SAVED_ARTICLES -> stringResource(id = R.string.saved_articles) + FeedType.ALL_FEEDS -> stringResource(id = R.string.all_feeds) + } ?: "", navigationIcon = { IconButton( onClick = onOpenNavDrawer, @@ -931,10 +953,11 @@ fun FeedScreen( onStop = ttsOnStop, onSkipNext = ttsOnSkipNext, languages = ImmutableHolder(viewState.ttsLanguages), - floatingActionButton = when (viewState.showFab) { - true -> floatingActionButton - false -> null - }, + floatingActionButton = + when (viewState.showFab) { + true -> floatingActionButton + false -> null + }, onSelectLanguage = ttsOnSelectLanguage, ) }, @@ -949,16 +972,18 @@ fun FeedScreen( } } }, - modifier = modifier - // The order is important! PullToRefresh MUST come first - .pullRefresh(state = pullRefreshState) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + // The order is important! PullToRefresh MUST come first + .pullRefresh(state = pullRefreshState) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, ) { padding -> Box( - modifier = Modifier - .padding(padding), + modifier = + Modifier + .padding(padding), ) { content( Modifier, @@ -967,18 +992,20 @@ fun FeedScreen( PullRefreshIndicator( isRefreshing, pullRefreshState, - modifier = Modifier - .align(Alignment.TopCenter), + modifier = + Modifier + .align(Alignment.TopCenter), ) } if (viewState.showDeleteDialog) { DeleteFeedDialog( - feeds = ImmutableHolder( - viewState.visibleFeeds.map { - DeletableFeed(it.id, it.displayTitle) - }, - ), + feeds = + ImmutableHolder( + viewState.visibleFeeds.map { + DeletableFeed(it.id, it.displayTitle) + }, + ), onDismiss = onDismissDeleteDialog, onDelete = onDelete, ) @@ -986,14 +1013,15 @@ fun FeedScreen( if (viewState.showEditDialog) { EditFeedDialog( - feeds = ImmutableHolder( - viewState.visibleFeeds.map { - DeletableFeed( - it.id, - it.displayTitle, - ) - }, - ), + feeds = + ImmutableHolder( + viewState.visibleFeeds.map { + DeletableFeed( + it.id, + it.displayTitle, + ) + }, + ), onDismiss = onDismissEditDialog, onEdit = onEditFeed, ) @@ -1028,9 +1056,10 @@ fun FeedListContent( // Keeping the Box behind so the scrollability doesn't override clickable // Separate box because scrollable will ignore max size. Box( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) NothingToRead( modifier = Modifier, @@ -1039,38 +1068,41 @@ fun FeedListContent( ) } - val arrangement = when (viewState.feedItemStyle) { - FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.margin) - FeedItemStyle.COMPACT -> Arrangement.spacedBy(0.dp) - FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(0.dp) - } + val arrangement = + when (viewState.feedItemStyle) { + FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.margin) + FeedItemStyle.COMPACT -> Arrangement.spacedBy(0.dp) + FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(0.dp) + } AnimatedVisibility( enter = fadeIn(), exit = fadeOut(), visible = viewState.haveVisibleFeedItems, ) { - val screenHeightPx = with(LocalDensity.current) { - LocalConfiguration.current.screenHeightDp.dp.toPx().toInt() - } + val screenHeightPx = + with(LocalDensity.current) { + LocalConfiguration.current.screenHeightDp.dp.toPx().toInt() + } LazyColumn( state = listState, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = arrangement, - contentPadding = if (viewState.isBottomBarVisible) { - PaddingValues(0.dp) - } else { - WindowInsets.navigationBars.only( - WindowInsetsSides.Bottom, - ).run { - when (viewState.feedItemStyle) { - FeedItemStyle.CARD -> addMargin(horizontal = LocalDimens.current.margin) - // No margin since dividers - FeedItemStyle.COMPACT, FeedItemStyle.SUPER_COMPACT -> this + contentPadding = + if (viewState.isBottomBarVisible) { + PaddingValues(0.dp) + } else { + WindowInsets.navigationBars.only( + WindowInsetsSides.Bottom, + ).run { + when (viewState.feedItemStyle) { + FeedItemStyle.CARD -> addMargin(horizontal = LocalDimens.current.margin) + // No margin since dividers + FeedItemStyle.COMPACT, FeedItemStyle.SUPER_COMPACT -> this + } } - } - .asPaddingValues() - }, + .asPaddingValues() + }, modifier = Modifier.fillMaxSize(), ) { /* @@ -1089,8 +1121,9 @@ fun FeedListContent( pagedFeedItems.itemSnapshotList.items[itemIndex].contentType(viewState.feedItemStyle) }, ) { itemIndex -> - val previewItem = pagedFeedItems[itemIndex] - ?: return@items + val previewItem = + pagedFeedItems[itemIndex] + ?: return@items if (viewState.markAsReadOnScroll && previewItem.unread) { val visible: Boolean by listState.rememberIsItemVisible( @@ -1142,16 +1175,17 @@ fun FeedListContent( onSetBookmarked(previewItem.id, !previewItem.bookmarked) }, onShareItem = { - val intent = Intent.createChooser( - Intent(Intent.ACTION_SEND).apply { - if (previewItem.link != null) { - putExtra(Intent.EXTRA_TEXT, previewItem.link) - } - putExtra(Intent.EXTRA_TITLE, previewItem.title) - type = "text/plain" - }, - null, - ) + val intent = + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + if (previewItem.link != null) { + putExtra(Intent.EXTRA_TEXT, previewItem.link) + } + putExtra(Intent.EXTRA_TITLE, previewItem.title) + type = "text/plain" + }, + null, + ) activityLauncher.startActivity( openAdjacentIfSuitable = false, intent = intent, @@ -1164,9 +1198,10 @@ fun FeedListContent( if (viewState.feedItemStyle != FeedItemStyle.CARD) { if (itemIndex < pagedFeedItems.itemCount - 1) { Divider( - modifier = Modifier - .height(1.dp) - .fillMaxWidth(), + modifier = + Modifier + .height(1.dp) + .fillMaxWidth(), ) } } @@ -1177,9 +1212,10 @@ fun FeedListContent( if (viewState.showFab && !viewState.isBottomBarVisible) { item { Spacer( - modifier = Modifier - .fillMaxWidth() - .height((56 + 16).dp), + modifier = + Modifier + .fillMaxWidth() + .height((56 + 16).dp), ) } } @@ -1207,9 +1243,10 @@ fun FeedGridContent( val coroutineScope = rememberCoroutineScope() val activityLauncher: ActivityLauncher by LocalDI.current.instance() - val screenHeightPx = with(LocalDensity.current) { - LocalConfiguration.current.screenHeightDp.dp.toPx().toInt() - } + val screenHeightPx = + with(LocalDensity.current) { + LocalConfiguration.current.screenHeightDp.dp.toPx().toInt() + } Box(modifier = modifier) { AnimatedVisibility( @@ -1220,9 +1257,10 @@ fun FeedGridContent( // Keeping the Box behind so the scrollability doesn't override clickable // Separate box because scrollable will ignore max size. Box( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) NothingToRead( modifier = Modifier, @@ -1232,15 +1270,17 @@ fun FeedGridContent( } // Grid has hard-coded card - val feedItemStyle = remember { - FeedItemStyle.CARD - } + val feedItemStyle = + remember { + FeedItemStyle.CARD + } - val arrangement = when (feedItemStyle) { - FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.gutter) - FeedItemStyle.COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter) - FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter) - } + val arrangement = + when (feedItemStyle) { + FeedItemStyle.CARD -> Arrangement.spacedBy(LocalDimens.current.gutter) + FeedItemStyle.COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter) + FeedItemStyle.SUPER_COMPACT -> Arrangement.spacedBy(LocalDimens.current.gutter) + } AnimatedVisibility( enter = fadeIn(), @@ -1250,14 +1290,15 @@ fun FeedGridContent( LazyVerticalStaggeredGrid( state = gridState, columns = StaggeredGridCells.Fixed(LocalDimens.current.feedScreenColumns), - contentPadding = if (viewState.isBottomBarVisible) { - PaddingValues(0.dp) - } else { - WindowInsets.navigationBars.only( - WindowInsetsSides.Bottom, - ).addMargin(LocalDimens.current.margin) - .asPaddingValues() - }, + contentPadding = + if (viewState.isBottomBarVisible) { + PaddingValues(0.dp) + } else { + WindowInsets.navigationBars.only( + WindowInsetsSides.Bottom, + ).addMargin(LocalDimens.current.margin) + .asPaddingValues() + }, verticalItemSpacing = LocalDimens.current.gutter, horizontalArrangement = arrangement, modifier = Modifier.fillMaxSize(), @@ -1271,8 +1312,9 @@ fun FeedGridContent( pagedFeedItems.itemSnapshotList.items[itemIndex].contentType(feedItemStyle) }, ) { itemIndex -> - val previewItem = pagedFeedItems[itemIndex] - ?: return@items + val previewItem = + pagedFeedItems[itemIndex] + ?: return@items // Very important that items don't change size or disappear when scrolling if (viewState.markAsReadOnScroll && previewItem.unread) { @@ -1325,16 +1367,17 @@ fun FeedGridContent( onSetBookmarked(previewItem.id, !previewItem.bookmarked) }, onShareItem = { - val intent = Intent.createChooser( - Intent(Intent.ACTION_SEND).apply { - if (previewItem.link != null) { - putExtra(Intent.EXTRA_TEXT, previewItem.link) - } - putExtra(Intent.EXTRA_TITLE, previewItem.title) - type = "text/plain" - }, - null, - ) + val intent = + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + if (previewItem.link != null) { + putExtra(Intent.EXTRA_TEXT, previewItem.link) + } + putExtra(Intent.EXTRA_TITLE, previewItem.title) + type = "text/plain" + }, + null, + ) activityLauncher.startActivity( openAdjacentIfSuitable = false, intent = intent, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt index 5210c3ff2e..ae5cb335d7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feed/SwipeableFeedItemPreview.kt @@ -56,9 +56,9 @@ import com.nononsenseapps.feeder.ui.compose.theme.SwipingItemToReadColor import com.nononsenseapps.feeder.ui.compose.theme.SwipingItemToUnreadColor import com.nononsenseapps.feeder.ui.compose.utils.isCompactLandscape import com.nononsenseapps.feeder.util.logDebug +import kotlinx.coroutines.launch import kotlin.math.absoluteValue import kotlin.math.roundToInt -import kotlinx.coroutines.launch private const val LOG_TAG = "FEEDER_SWIPEITEM" @@ -97,11 +97,12 @@ fun SwipeableFeedItemPreview( } val color by animateColorAsState( - targetValue = when { - swipeableState.targetValue == FeedItemSwipeState.NONE -> Color.Transparent - item.unread || filter.onlyUnread -> SwipingItemToReadColor - else -> SwipingItemToUnreadColor - }, + targetValue = + when { + swipeableState.targetValue == FeedItemSwipeState.NONE -> Color.Transparent + item.unread || filter.onlyUnread -> SwipingItemToReadColor + else -> SwipingItemToUnreadColor + }, label = "swipeBackground", ) @@ -155,56 +156,60 @@ fun SwipeableFeedItemPreview( val dimens = LocalDimens.current BoxWithConstraints( - modifier = modifier - .width(dimens.maxContentWidth) - .combinedClickable( - onLongClick = { - dropDownMenuExpanded = true - }, - onClick = onItemClick, - ) - .safeSemantics { - stateDescription = readStatusLabel - customActions = listOf( - CustomAccessibilityAction(toggleReadStatusLabel) { - coroutineScope.launch { - onSwipe(item.unread) - } - true - }, - CustomAccessibilityAction( - when (item.bookmarked) { - true -> unSaveArticleLabel - false -> saveArticleLabel - }, - ) { - onToggleBookmarked() - true - }, - CustomAccessibilityAction(markAboveAsReadLabel) { - onMarkAboveAsRead() - true - }, - CustomAccessibilityAction(markBelowAsReadLabel) { - onMarkBelowAsRead() - true - }, - CustomAccessibilityAction(shareLabel) { - onShareItem() - true + modifier = + modifier + .width(dimens.maxContentWidth) + .combinedClickable( + onLongClick = { + dropDownMenuExpanded = true }, + onClick = onItemClick, ) - }, + .safeSemantics { + stateDescription = readStatusLabel + customActions = + listOf( + CustomAccessibilityAction(toggleReadStatusLabel) { + coroutineScope.launch { + onSwipe(item.unread) + } + true + }, + CustomAccessibilityAction( + when (item.bookmarked) { + true -> unSaveArticleLabel + false -> saveArticleLabel + }, + ) { + onToggleBookmarked() + true + }, + CustomAccessibilityAction(markAboveAsReadLabel) { + onMarkAboveAsRead() + true + }, + CustomAccessibilityAction(markBelowAsReadLabel) { + onMarkBelowAsRead() + true + }, + CustomAccessibilityAction(shareLabel) { + onShareItem() + true + }, + ) + }, ) { - val maxWidthPx = with(LocalDensity.current) { - maxWidth.toPx() - } + val maxWidthPx = + with(LocalDensity.current) { + maxWidth.toPx() + } Box( contentAlignment = swipeIconAlignment, - modifier = Modifier - .matchParentSize() - .background(color) - .padding(horizontal = 24.dp), + modifier = + Modifier + .matchParentSize() + .background(color) + .padding(horizontal = 24.dp), ) { AnimatedVisibility( visible = swipeableState.targetValue != FeedItemSwipeState.NONE, @@ -250,9 +255,10 @@ fun SwipeableFeedItemPreview( maxLines = maxLines, showOnlyTitle = showOnlyTitle, showReadingTime = showReadingTime, - modifier = Modifier - .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } - .graphicsLayer(alpha = itemAlpha), + modifier = + Modifier + .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } + .graphicsLayer(alpha = itemAlpha), ) } @@ -270,13 +276,15 @@ fun SwipeableFeedItemPreview( maxLines = maxLines, showOnlyTitle = showOnlyTitle, showReadingTime = showReadingTime, - modifier = Modifier - .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } - .graphicsLayer(alpha = itemAlpha), - imageWidth = when (compactLandscape) { - true -> 196.dp - false -> 64.dp - }, + modifier = + Modifier + .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } + .graphicsLayer(alpha = itemAlpha), + imageWidth = + when (compactLandscape) { + true -> 196.dp + false -> 64.dp + }, ) } @@ -293,9 +301,10 @@ fun SwipeableFeedItemPreview( maxLines = maxLines, showOnlyTitle = showOnlyTitle, showReadingTime = showReadingTime, - modifier = Modifier - .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } - .graphicsLayer(alpha = itemAlpha), + modifier = + Modifier + .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } + .graphicsLayer(alpha = itemAlpha), ) } } @@ -305,48 +314,50 @@ fun SwipeableFeedItemPreview( // Wrapped in an outer box to get the height set properly if (swipeAsRead != SwipeAsRead.DISABLED) { Box( - modifier = Modifier - .matchParentSize(), + modifier = + Modifier + .matchParentSize(), ) { val anchors = mutableMapOf(0f to FeedItemSwipeState.NONE) Box( - modifier = Modifier - .run { - @Suppress("KotlinConstantConditions") - when (swipeAsRead) { - // This never actually gets called due to outer if - SwipeAsRead.DISABLED -> - this - .height(0.dp) - .width(0.dp) + modifier = + Modifier + .run { + @Suppress("KotlinConstantConditions") + when (swipeAsRead) { + // This never actually gets called due to outer if + SwipeAsRead.DISABLED -> + this + .height(0.dp) + .width(0.dp) - SwipeAsRead.ONLY_FROM_END -> { - anchors[-maxWidthPx] = FeedItemSwipeState.LEFT - this - .fillMaxHeight() - .width(this@BoxWithConstraints.maxWidth / 4) - .align(Alignment.CenterEnd) - } + SwipeAsRead.ONLY_FROM_END -> { + anchors[-maxWidthPx] = FeedItemSwipeState.LEFT + this + .fillMaxHeight() + .width(this@BoxWithConstraints.maxWidth / 4) + .align(Alignment.CenterEnd) + } - SwipeAsRead.FROM_ANYWHERE -> { - anchors[-maxWidthPx] = FeedItemSwipeState.LEFT - anchors[maxWidthPx] = FeedItemSwipeState.RIGHT - this - .padding(start = 48.dp) - .matchParentSize() + SwipeAsRead.FROM_ANYWHERE -> { + anchors[-maxWidthPx] = FeedItemSwipeState.LEFT + anchors[maxWidthPx] = FeedItemSwipeState.RIGHT + this + .padding(start = 48.dp) + .matchParentSize() + } } } - } - .swipeable( - state = swipeableState, - anchors = anchors, - orientation = Orientation.Horizontal, - reverseDirection = isRtl, - velocityThreshold = 1000.dp, - thresholds = { _, _ -> - FractionalThreshold(0.50f) - }, - ), + .swipeable( + state = swipeableState, + anchors = anchors, + orientation = Orientation.Horizontal, + reverseDirection = isRtl, + velocityThreshold = 1000.dp, + thresholds = { _, _ -> + FractionalThreshold(0.50f) + }, + ), ) } } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt index c33bce5ddd..d2044be427 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ArticleScreen.kt @@ -71,9 +71,9 @@ import com.nononsenseapps.feeder.ui.compose.utils.onKeyEventLikeEscape import com.nononsenseapps.feeder.util.ActivityLauncher import com.nononsenseapps.feeder.util.FilePathProvider import com.nononsenseapps.feeder.util.unicodeWrap -import java.time.ZonedDateTime import org.kodein.di.compose.LocalDI import org.kodein.di.instance +import java.time.ZonedDateTime @Composable fun ArticleScreen( @@ -88,9 +88,10 @@ fun ArticleScreen( // Each article gets its own scroll state. Persists across device rotations, but is cleared // when switching articles. - val articleListState = key(viewState.articleId) { - rememberLazyListState() - } + val articleListState = + key(viewState.articleId) { + rememberLazyListState() + } val toolbarColor = MaterialTheme.colorScheme.surface.toArgb() @@ -113,16 +114,17 @@ fun ArticleScreen( }, onShare = { if (viewState.articleId > ID_UNSET) { - val intent = Intent.createChooser( - Intent(Intent.ACTION_SEND).apply { - if (viewState.articleLink != null) { - putExtra(Intent.EXTRA_TEXT, viewState.articleLink) - } - putExtra(Intent.EXTRA_TITLE, viewState.articleTitle) - type = "text/plain" - }, - null, - ) + val intent = + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + if (viewState.articleLink != null) { + putExtra(Intent.EXTRA_TEXT, viewState.articleLink) + } + putExtra(Intent.EXTRA_TITLE, viewState.articleTitle) + type = "text/plain" + }, + null, + ) activityLauncher.startActivity( openAdjacentIfSuitable = false, intent = intent, @@ -192,18 +194,20 @@ fun ArticleScreen( val focusTopBar = remember { FocusRequester() } Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( - modifier = Modifier - .focusGroup() - .focusRequester(focusTopBar) - .focusProperties { - down = focusArticle - }, + modifier = + Modifier + .focusGroup() + .focusRequester(focusTopBar) + .focusProperties { + down = focusArticle + }, scrollBehavior = scrollBehavior, title = viewState.feedDisplayTitle, navigationIcon = { @@ -243,8 +247,9 @@ fun ArticleScreen( Box { IconButton( onClick = { onShowToolbarMenu(true) }, - modifier = Modifier - .tooltipAnchor(), + modifier = + Modifier + .tooltipAnchor(), ) { Icon( Icons.Default.MoreVert, @@ -254,10 +259,11 @@ fun ArticleScreen( DropdownMenu( expanded = viewState.showToolbarMenu, onDismissRequest = { onShowToolbarMenu(false) }, - modifier = Modifier - .onKeyEventLikeEscape { - onShowToolbarMenu(false) - }, + modifier = + Modifier + .onKeyEventLikeEscape { + onShowToolbarMenu(false) + }, ) { DropdownMenuItem( onClick = { @@ -353,13 +359,14 @@ fun ArticleScreen( articleListState = articleListState, onFeedTitleClick = onFeedTitleClick, displayFullText = displayFullText, - modifier = Modifier - .padding(padding) - .focusGroup() - .focusRequester(focusArticle) - .focusProperties { - up = focusTopBar - }, + modifier = + Modifier + .padding(padding) + .focusGroup() + .focusRequester(focusArticle) + .focusProperties { + up = focusTopBar + }, ) } } @@ -404,24 +411,25 @@ fun ArticleContent( enclosure = viewState.enclosure, articleTitle = viewState.articleTitle, feedTitle = viewState.feedDisplayTitle, - authorDate = when { - viewState.author == null && viewState.pubDate != null -> - stringResource( - R.string.on_date, - (viewState.pubDate ?: ZonedDateTime.now()).format(dateTimeFormat), - ) + authorDate = + when { + viewState.author == null && viewState.pubDate != null -> + stringResource( + R.string.on_date, + (viewState.pubDate ?: ZonedDateTime.now()).format(dateTimeFormat), + ) - viewState.author != null && viewState.pubDate != null -> - stringResource( - R.string.by_author_on_date, - // Must wrap author in unicode marks to ensure it formats - // correctly in RTL - context.unicodeWrap(viewState.author ?: ""), - (viewState.pubDate ?: ZonedDateTime.now()).format(dateTimeFormat), - ) + viewState.author != null && viewState.pubDate != null -> + stringResource( + R.string.by_author_on_date, + // Must wrap author in unicode marks to ensure it formats + // correctly in RTL + context.unicodeWrap(viewState.author ?: ""), + (viewState.pubDate ?: ZonedDateTime.now()).format(dateTimeFormat), + ) - else -> null - }, + else -> null + }, ) { // Can take a composition or two before viewstate is set to its actual values if (viewState.articleId > ID_UNSET) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt index a8f37c96b7..8ab913e9c9 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/FeedArticleViewModel.kt @@ -40,10 +40,6 @@ import com.nononsenseapps.feeder.ui.compose.navdrawer.DrawerItemWithUnreadCount import com.nononsenseapps.feeder.ui.compose.text.htmlToAnnotatedString import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.FilePathProvider -import java.io.FileNotFoundException -import java.time.Instant -import java.time.ZonedDateTime -import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -58,6 +54,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.kodein.di.DI import org.kodein.di.instance +import java.io.FileNotFoundException +import java.time.Instant +import java.time.ZonedDateTime +import java.util.Locale class FeedArticleViewModel( di: DI, @@ -99,20 +99,26 @@ class FeedArticleViewModel( emptyList(), ) - fun deleteFeeds(feedIds: List) = applicationCoroutineScope.launch { - repository.deleteFeeds(feedIds) - } + fun deleteFeeds(feedIds: List) = + applicationCoroutineScope.launch { + repository.deleteFeeds(feedIds) + } - fun markAllAsRead() = applicationCoroutineScope.launch { - val (feedId, feedTag) = repository.currentFeedAndTag.value - repository.markAllAsReadInFeedOrTag(feedId, feedTag) - } + fun markAllAsRead() = + applicationCoroutineScope.launch { + val (feedId, feedTag) = repository.currentFeedAndTag.value + repository.markAllAsReadInFeedOrTag(feedId, feedTag) + } - fun markAsUnread(itemId: Long) = applicationCoroutineScope.launch { - repository.markAsUnread(itemId) - } + fun markAsUnread(itemId: Long) = + applicationCoroutineScope.launch { + repository.markAsUnread(itemId) + } - fun markAsRead(itemId: Long, feedOrTag: FeedOrTag?) = applicationCoroutineScope.launch { + fun markAsRead( + itemId: Long, + feedOrTag: FeedOrTag?, + ) = applicationCoroutineScope.launch { val (feedId, tag) = repository.currentFeedAndTag.value // Ensure mark as read on scroll doesn't fire when navigating between feeds if (feedOrTag == null || feedId == feedOrTag.id && tag == feedOrTag.tag) { @@ -120,21 +126,27 @@ class FeedArticleViewModel( } } - fun markAsReadOnSwipe(itemId: Long) = applicationCoroutineScope.launch { - repository.markAsReadAndNotified(itemId = itemId, readTimeBeforeMinReadTime = true) - } + fun markAsReadOnSwipe(itemId: Long) = + applicationCoroutineScope.launch { + repository.markAsReadAndNotified(itemId = itemId, readTimeBeforeMinReadTime = true) + } - fun markBeforeAsRead(cursor: FeedItemCursor) = applicationCoroutineScope.launch { - val (feedId, feedTag) = repository.currentFeedAndTag.value - repository.markBeforeAsRead(cursor, feedId, feedTag) - } + fun markBeforeAsRead(cursor: FeedItemCursor) = + applicationCoroutineScope.launch { + val (feedId, feedTag) = repository.currentFeedAndTag.value + repository.markBeforeAsRead(cursor, feedId, feedTag) + } - fun markAfterAsRead(cursor: FeedItemCursor) = applicationCoroutineScope.launch { - val (feedId, feedTag) = repository.currentFeedAndTag.value - repository.markAfterAsRead(cursor, feedId, feedTag) - } + fun markAfterAsRead(cursor: FeedItemCursor) = + applicationCoroutineScope.launch { + val (feedId, feedTag) = repository.currentFeedAndTag.value + repository.markAfterAsRead(cursor, feedId, feedTag) + } - fun setBookmarked(itemId: Long, bookmarked: Boolean) = applicationCoroutineScope.launch { + fun setBookmarked( + itemId: Long, + bookmarked: Boolean, + ) = applicationCoroutineScope.launch { repository.setBookmarked(itemId, bookmarked) } @@ -176,11 +188,13 @@ class FeedArticleViewModel( fun toggleTagExpansion(tag: String) = repository.toggleTagExpansion(tag) private val editDialogVisible = MutableStateFlow(false) + fun setShowEditDialog(visible: Boolean) { editDialogVisible.update { visible } } private val deleteDialogVisible = MutableStateFlow(false) + fun setShowDeleteDialog(visible: Boolean) { deleteDialogVisible.update { visible } } @@ -237,12 +251,16 @@ class FeedArticleViewModel( // Used to trigger state update private val textToDisplayTrigger: MutableStateFlow = MutableStateFlow(0) + private suspend fun getTextToDisplayFor(itemId: Long): TextToDisplay = state["textToDisplayFor$itemId"] ?: repository.getTextToDisplayForItem(itemId) // Only affect the state by design, settings is done in EditFeed - private fun setTextToDisplayFor(itemId: Long, value: TextToDisplay) { + private fun setTextToDisplayFor( + itemId: Long, + value: TextToDisplay, + ) { state["textToDisplayFor$itemId"] = value textToDisplayTrigger.update { textToDisplayTrigger.value + 1 @@ -332,19 +350,20 @@ class FeedArticleViewModel( filter = params[26] as FeedListFilter, showOnlyTitle = params[27] as Boolean, showReadingTime = params[28] as Boolean, - wordCount = when (textToDisplay) { - TextToDisplay.DEFAULT -> article.wordCount - - TextToDisplay.FULLTEXT, - TextToDisplay.LOADING_FULLTEXT, - -> article.wordCountFull - - TextToDisplay.FAILED_TO_LOAD_FULLTEXT, - TextToDisplay.FAILED_MISSING_BODY, - TextToDisplay.FAILED_MISSING_LINK, - TextToDisplay.FAILED_NOT_HTML, - -> 0 - }, + wordCount = + when (textToDisplay) { + TextToDisplay.DEFAULT -> article.wordCount + + TextToDisplay.FULLTEXT, + TextToDisplay.LOADING_FULLTEXT, + -> article.wordCountFull + + TextToDisplay.FAILED_TO_LOAD_FULLTEXT, + TextToDisplay.FAILED_MISSING_BODY, + TextToDisplay.FAILED_MISSING_LINK, + TextToDisplay.FAILED_NOT_HTML, + -> 0 + }, ) } .stateIn( @@ -372,12 +391,13 @@ class FeedArticleViewModel( setTextToDisplayFor(itemId, TextToDisplay.LOADING_FULLTEXT) val link = viewState.value.articleLink - val result = fullTextParser.parseFullArticleIfMissing( - object : FeedItemForFetching { - override val id = viewState.value.articleId - override val link = link - }, - ) + val result = + fullTextParser.parseFullArticleIfMissing( + object : FeedItemForFetching { + override val id = viewState.value.articleId + override val link = link + }, + ) setTextToDisplayFor( itemId, @@ -414,49 +434,52 @@ class FeedArticleViewModel( fun ttsPlay() { viewModelScope.launch(Dispatchers.IO) { - val fullText = when (viewState.value.textToDisplay) { - TextToDisplay.DEFAULT -> Either.catching( - onCatch = { - when (it) { - is FileNotFoundException -> TTSFileNotFound - else -> TTSUnknownError + val fullText = + when (viewState.value.textToDisplay) { + TextToDisplay.DEFAULT -> + Either.catching( + onCatch = { + when (it) { + is FileNotFoundException -> TTSFileNotFound + else -> TTSUnknownError + } + }, + ) { + blobInputStream(viewState.value.articleId, filePathProvider.articleDir).use { + htmlToAnnotatedString( + inputStream = it, + baseUrl = viewState.value.articleFeedUrl ?: "", + ) + } } - }, - ) { - blobInputStream(viewState.value.articleId, filePathProvider.articleDir).use { - htmlToAnnotatedString( - inputStream = it, - baseUrl = viewState.value.articleFeedUrl ?: "", - ) - } - } - TextToDisplay.FULLTEXT -> Either.catching( - onCatch = { - when (it) { - is FileNotFoundException -> TTSFileNotFound - else -> TTSUnknownError + TextToDisplay.FULLTEXT -> + Either.catching( + onCatch = { + when (it) { + is FileNotFoundException -> TTSFileNotFound + else -> TTSUnknownError + } + }, + ) { + blobFullInputStream( + viewState.value.articleId, + filePathProvider.fullArticleDir, + ).use { + htmlToAnnotatedString( + inputStream = it, + baseUrl = viewState.value.articleFeedUrl ?: "", + ) + } } - }, - ) { - blobFullInputStream( - viewState.value.articleId, - filePathProvider.fullArticleDir, - ).use { - htmlToAnnotatedString( - inputStream = it, - baseUrl = viewState.value.articleFeedUrl ?: "", - ) - } - } - TextToDisplay.LOADING_FULLTEXT, - TextToDisplay.FAILED_TO_LOAD_FULLTEXT, - TextToDisplay.FAILED_MISSING_BODY, - TextToDisplay.FAILED_MISSING_LINK, - TextToDisplay.FAILED_NOT_HTML, - -> Either.Left(TTSUnknownError) - } + TextToDisplay.LOADING_FULLTEXT, + TextToDisplay.FAILED_TO_LOAD_FULLTEXT, + TextToDisplay.FAILED_MISSING_BODY, + TextToDisplay.FAILED_MISSING_LINK, + TextToDisplay.FAILED_NOT_HTML, + -> Either.Left(TTSUnknownError) + } // TODO show error some message fullText.onRight { @@ -547,16 +570,19 @@ interface FeedListFilter { val read: Boolean } -val emptyFeedListFilter = object : FeedListFilter { - override val unread: Boolean = true - override val saved: Boolean = false - override val recentlyRead: Boolean = false - override val read: Boolean = false -} +val emptyFeedListFilter = + object : FeedListFilter { + override val unread: Boolean = true + override val saved: Boolean = false + override val recentlyRead: Boolean = false + override val read: Boolean = false + } interface FeedListFilterCallback { fun setSaved(value: Boolean) + fun setRecentlyRead(value: Boolean) + fun setRead(value: Boolean) } @@ -568,6 +594,7 @@ val FeedListFilter.onlyUnreadAndSaved: Boolean object RotatingArticleItemKeyHolder : ArticleItemKeyHolder { private var key: Long = 0L + override fun getAndIncrementKey(): Long { return key++ } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ReaderView.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ReaderView.kt index aec68d6f81..a3860284cd 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ReaderView.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/feedarticle/ReaderView.kt @@ -93,76 +93,85 @@ fun ReaderView( ) { val dimens = LocalDimens.current - val readTimeSecs = remember(wordCount) { - wordsToReadTimeSecs(wordCount) - } + val readTimeSecs = + remember(wordCount) { + wordsToReadTimeSecs(wordCount) + } SelectionContainer { LazyColumn( state = articleListState, - contentPadding = PaddingValues( - bottom = 92.dp, - start = when (screenType) { - ScreenType.DUAL -> 0.dp // List items have enough padding - ScreenType.SINGLE -> dimens.margin - }, - end = dimens.margin, - ), + contentPadding = + PaddingValues( + bottom = 92.dp, + start = + when (screenType) { + ScreenType.DUAL -> 0.dp // List items have enough padding + ScreenType.SINGLE -> dimens.margin + }, + end = dimens.margin, + ), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = modifier - .fillMaxWidth() - .focusGroup(), + modifier = + modifier + .fillMaxWidth() + .focusGroup(), ) { item { val goToFeedLabel = stringResource(R.string.go_to_feed, feedTitle) Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .width(dimens.maxReaderWidth) - .semantics(mergeDescendants = true) { - try { - customActions = listOf( - // TODO enclosure? - CustomAccessibilityAction(goToFeedLabel) { - onFeedTitleClick() - true - }, - ) - } catch (e: Exception) { - // Observed nullpointer exception when setting customActions - // No clue why it could be null - Log.e("FeederReaderScreen", "Exception in semantics", e) - } - }, + modifier = + Modifier + .width(dimens.maxReaderWidth) + .semantics(mergeDescendants = true) { + try { + customActions = + listOf( + // TODO enclosure? + CustomAccessibilityAction(goToFeedLabel) { + onFeedTitleClick() + true + }, + ) + } catch (e: Exception) { + // Observed nullpointer exception when setting customActions + // No clue why it could be null + Log.e("FeederReaderScreen", "Exception in semantics", e) + } + }, ) { WithBidiDeterminedLayoutDirection(paragraph = articleTitle) { val interactionSource = remember { MutableInteractionSource() } Text( text = articleTitle, style = MaterialTheme.typography.headlineLarge, - modifier = Modifier - .indication(interactionSource, LocalIndication.current) - .focusableInNonTouchMode(interactionSource = interactionSource) - .width(dimens.maxReaderWidth), + modifier = + Modifier + .indication(interactionSource, LocalIndication.current) + .focusableInNonTouchMode(interactionSource = interactionSource) + .width(dimens.maxReaderWidth), ) } ProvideScaledText( - style = MaterialTheme.typography.titleMedium.merge( - LinkTextStyle(), - ), + style = + MaterialTheme.typography.titleMedium.merge( + LinkTextStyle(), + ), ) { WithBidiDeterminedLayoutDirection(paragraph = feedTitle) { Text( text = feedTitle, - modifier = Modifier - .width(dimens.maxReaderWidth) - .clearAndSetSemantics { - contentDescription = feedTitle - } - .clickable { - onFeedTitleClick() - }, + modifier = + Modifier + .width(dimens.maxReaderWidth) + .clearAndSetSemantics { + contentDescription = feedTitle + } + .clickable { + onFeedTitleClick() + }, ) } } @@ -173,10 +182,11 @@ fun ReaderView( val interactionSource = remember { MutableInteractionSource() } Text( text = authorDate, - modifier = Modifier - .width(dimens.maxReaderWidth) - .indication(interactionSource, LocalIndication.current) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + Modifier + .width(dimens.maxReaderWidth) + .indication(interactionSource, LocalIndication.current) + .focusableInNonTouchMode(interactionSource = interactionSource), ) } } @@ -204,13 +214,14 @@ fun ReaderView( remember { MutableInteractionSource() } Text( text = readTimeText, - modifier = Modifier - .weight(1f) - .indication( - interactionSource, - LocalIndication.current, - ) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + Modifier + .weight(1f) + .indication( + interactionSource, + LocalIndication.current, + ) + .focusableInNonTouchMode(interactionSource = interactionSource), ) } } @@ -224,75 +235,84 @@ fun ReaderView( item { if (enclosure.isImage) { BoxWithConstraints( - modifier = Modifier - .clip(RectangleShape) - .fillMaxWidth(), + modifier = + Modifier + .clip(RectangleShape) + .fillMaxWidth(), ) { WithTooltipIfNotBlank(tooltip = enclosure.name) { innerModifier -> val imageWidth by rememberMaxImageWidth() AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(enclosure.link) - .scale(Scale.FIT) - .size(imageWidth) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(enclosure.link) + .scale(Scale.FIT) + .size(imageWidth) + .precision(Precision.INEXACT) + .build(), contentDescription = enclosure.name, - placeholder = rememberTintedVectorPainter( - Icons.Outlined.Terrain, - ), + placeholder = + rememberTintedVectorPainter( + Icons.Outlined.Terrain, + ), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), - contentScale = if (dimens.hasImageAspectRatioInReader) { - ContentScale.Fit - } else { - ContentScale.FillWidth - }, - modifier = innerModifier - .fillMaxWidth() - .run { - dimens.imageAspectRatioInReader?.let { ratio -> - aspectRatio(ratio) - } ?: this + contentScale = + if (dimens.hasImageAspectRatioInReader) { + ContentScale.Fit + } else { + ContentScale.FillWidth }, + modifier = + innerModifier + .fillMaxWidth() + .run { + dimens.imageAspectRatioInReader?.let { ratio -> + aspectRatio(ratio) + } ?: this + }, ) } } } else { - val openLabel = if (enclosure.name.isBlank()) { - stringResource(R.string.open_enclosed_media) - } else { - stringResource(R.string.open_enclosed_media_file, enclosure.name) - } + val openLabel = + if (enclosure.name.isBlank()) { + stringResource(R.string.open_enclosed_media) + } else { + stringResource(R.string.open_enclosed_media_file, enclosure.name) + } ProvideScaledText( - style = MaterialTheme.typography.bodyLarge.merge( - LinkTextStyle(), - ), + style = + MaterialTheme.typography.bodyLarge.merge( + LinkTextStyle(), + ), ) { Text( text = openLabel, - modifier = Modifier - .width(dimens.maxReaderWidth) - .clickable { - onEnclosureClick() - } - .clearAndSetSemantics { - try { - customActions = listOf( - CustomAccessibilityAction(openLabel) { - onEnclosureClick() - true - }, - ) - } catch (e: Exception) { - // Observed nullpointer exception when setting customActions - // No clue why it could be null - Log.e( - LOG_TAG, - "Exception in semantics", - e, - ) + modifier = + Modifier + .width(dimens.maxReaderWidth) + .clickable { + onEnclosureClick() } - }, + .clearAndSetSemantics { + try { + customActions = + listOf( + CustomAccessibilityAction(openLabel) { + onEnclosureClick() + true + }, + ) + } catch (e: Exception) { + // Observed nullpointer exception when setting customActions + // No clue why it could be null + Log.e( + LOG_TAG, + "Exception in semantics", + e, + ) + } + }, ) } } @@ -306,7 +326,7 @@ fun ReaderView( @Preview @Composable -fun ReaderPreview() { +private fun ReaderPreview() { FeederTheme { Surface { ReaderView( diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/Lerp.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/Lerp.kt index ecd6c1f224..59c59a5fe7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/Lerp.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/Lerp.kt @@ -1,6 +1,10 @@ package com.nononsenseapps.feeder.ui.compose.material3 /** Linear interpolation between `startValue` and `endValue` by `fraction`. */ -fun lerp(startValue: Float, endValue: Float, fraction: Float): Float { +fun lerp( + startValue: Float, + endValue: Float, + fraction: Float, +): Float { return startValue + fraction * (endValue - startValue) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt index fb69202bf7..e08d3b904f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/NavigationDrawer.kt @@ -85,9 +85,9 @@ import androidx.compose.ui.unit.dp import com.nononsenseapps.feeder.ui.compose.material3.tokens.NavigationDrawerTokens import com.nononsenseapps.feeder.ui.compose.material3.tokens.NavigationDrawerTokens.getContainerWidth import com.nononsenseapps.feeder.ui.compose.material3.tokens.ScrimTokens -import kotlin.math.roundToInt import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch +import kotlin.math.roundToInt /** * Possible values of [DrawerState]. @@ -118,12 +118,12 @@ class DrawerState( initialValue: DrawerValue, confirmStateChange: (DrawerValue) -> Boolean = { true }, ) { - - internal val swipeableState = SwipeableState( - initialValue = initialValue, - animationSpec = AnimationSpec, - confirmStateChange = confirmStateChange, - ) + internal val swipeableState = + SwipeableState( + initialValue = initialValue, + animationSpec = AnimationSpec, + confirmStateChange = confirmStateChange, + ) /** * Whether the drawer is open. @@ -182,7 +182,10 @@ class DrawerState( * @param anim The animation that will be used to animate to the new value. */ @ExperimentalMaterial3Api - suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec) { + suspend fun animateTo( + targetValue: DrawerValue, + anim: AnimationSpec, + ) { swipeableState.animateTo(targetValue, anim) } @@ -327,7 +330,8 @@ fun ModalNavigationDrawer( .confirmStateChange(DrawerValue.Closed) ) { scope.launch { drawerState.close() } - }; true + } + true } } }, @@ -399,7 +403,8 @@ fun DismissibleNavigationDrawer( .confirmStateChange(DrawerValue.Closed) ) { scope.launch { drawerState.close() } - }; true + } + true } } }, @@ -409,7 +414,7 @@ fun DismissibleNavigationDrawer( Box { content() } - },) { measurables, constraints -> + }) { measurables, constraints -> val sheetPlaceable = measurables[0].measure(constraints) val contentPlaceable = measurables[1].measure(constraints) layout(contentPlaceable.width, contentPlaceable.height) { @@ -584,12 +589,13 @@ private fun DrawerSheet( content: @Composable ColumnScope.() -> Unit, ) { Surface( - modifier = modifier - .sizeIn( - minWidth = MinimumDrawerWidth, - maxWidth = DrawerDefaults.getMaximumDrawerWidth(), - ) - .fillMaxHeight(), + modifier = + modifier + .sizeIn( + minWidth = MinimumDrawerWidth, + maxWidth = DrawerDefaults.getMaximumDrawerWidth(), + ) + .fillMaxHeight(), shape = drawerShape, color = drawerContainerColor, contentColor = drawerContentColor, @@ -642,16 +648,16 @@ object DrawerDefaults { /** Default and maximum width of a navigation drawer **/ @Composable - fun getMaximumDrawerWidth() = - getContainerWidth() + fun getMaximumDrawerWidth() = getContainerWidth() /** * Default window insets for drawer sheets */ val windowInsets: WindowInsets @Composable - get() = WindowInsets.systemBarsForVisualComponents - .only(WindowInsetsSides.Vertical + WindowInsetsSides.Start) + get() = + WindowInsets.systemBarsForVisualComponents + .only(WindowInsetsSides.Vertical + WindowInsetsSides.Start) } /** @@ -690,9 +696,10 @@ fun NavigationDrawerItem( Surface( selected = selected, onClick = onClick, - modifier = modifier - .height(NavigationDrawerTokens.ActiveIndicatorHeight) - .fillMaxWidth(), + modifier = + modifier + .height(NavigationDrawerTokens.ActiveIndicatorHeight) + .fillMaxWidth(), shape = shape, color = colors.containerColor(selected).value, interactionSource = interactionSource, @@ -785,16 +792,17 @@ object NavigationDrawerItemDefaults { unselectedTextColor: Color = NavigationDrawerTokens.InactiveLabelTextColor.toColor(), selectedBadgeColor: Color = selectedTextColor, unselectedBadgeColor: Color = unselectedTextColor, - ): NavigationDrawerItemColors = DefaultDrawerItemsColor( - selectedIconColor, - unselectedIconColor, - selectedTextColor, - unselectedTextColor, - selectedContainerColor, - unselectedContainerColor, - selectedBadgeColor, - unselectedBadgeColor, - ) + ): NavigationDrawerItemColors = + DefaultDrawerItemsColor( + selectedIconColor, + unselectedIconColor, + selectedTextColor, + unselectedTextColor, + selectedContainerColor, + unselectedContainerColor, + selectedBadgeColor, + unselectedBadgeColor, + ) /** * Default external padding for a [NavigationDrawerItem] according to the Material @@ -867,8 +875,11 @@ private class DefaultDrawerItemsColor( } } -private fun calculateFraction(a: Float, b: Float, pos: Float) = - ((pos - a) / (b - a)).coerceIn(0f, 1f) +private fun calculateFraction( + a: Float, + b: Float, + pos: Float, +) = ((pos - a) / (b - a)).coerceIn(0f, 1f) @Composable private fun Scrim( @@ -878,16 +889,20 @@ private fun Scrim( color: Color, ) { val closeDrawer = getString(Strings.CloseDrawer) - val dismissDrawer = if (open) { - Modifier - .pointerInput(onClose) { detectTapGestures { onClose() } } - .semantics(mergeDescendants = true) { - contentDescription = closeDrawer - onClick { onClose(); true } - } - } else { - Modifier - } + val dismissDrawer = + if (open) { + Modifier + .pointerInput(onClose) { detectTapGestures { onClose() } } + .semantics(mergeDescendants = true) { + contentDescription = closeDrawer + onClick { + onClose() + true + } + } + } else { + Modifier + } Canvas( Modifier diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt index 93db588564..1a4a05e6f7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/material3/SwipeableState.kt @@ -52,15 +52,15 @@ import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.Animatio import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.StandardResistanceFactor import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.VelocityThreshold import com.nononsenseapps.feeder.ui.compose.material3.SwipeableDefaults.resistanceConfig -import kotlin.math.PI -import kotlin.math.abs -import kotlin.math.sign -import kotlin.math.sin import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.sign +import kotlin.math.sin /** * State of the [swipeable] modifier. @@ -165,19 +165,20 @@ internal open class SwipeableState( maxBound = Float.POSITIVE_INFINITY val animationTargetValue = animationTarget.value // if we're in the animation already, let's find it a new home - val targetOffset = if (animationTargetValue != null) { - // first, try to map old state to the new state - val oldState = oldAnchors[animationTargetValue] - val newState = newAnchors.getOffset(oldState) - // return new state if exists, or find the closes one among new anchors - newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!! - } else { - // we're not animating, proceed by finding the new anchors for an old value - val actualOldValue = oldAnchors[offset.value] - val value = if (actualOldValue == currentValue) currentValue else actualOldValue - newAnchors.getOffset(value) ?: newAnchors - .keys.minByOrNull { abs(it - offset.value) }!! - } + val targetOffset = + if (animationTargetValue != null) { + // first, try to map old state to the new state + val oldState = oldAnchors[animationTargetValue] + val newState = newAnchors.getOffset(oldState) + // return new state if exists, or find the closes one among new anchors + newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!! + } else { + // we're not animating, proceed by finding the new anchors for an old value + val actualOldValue = oldAnchors[offset.value] + val value = if (actualOldValue == currentValue) currentValue else actualOldValue + newAnchors.getOffset(value) ?: newAnchors + .keys.minByOrNull { abs(it - offset.value) }!! + } try { animateInternalToOffset(targetOffset, animationSpec) } catch (c: CancellationException) { @@ -197,15 +198,16 @@ internal open class SwipeableState( internal var resistance: ResistanceConfig? by mutableStateOf(null) - internal val draggableState = DraggableState { - val newAbsolute = absoluteOffset.value + it - val clamped = newAbsolute.coerceIn(minBound, maxBound) - val overflow = newAbsolute - clamped - val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f - offsetState.value = clamped + resistanceDelta - overflowState.value = overflow - absoluteOffset.value = newAbsolute - } + internal val draggableState = + DraggableState { + val newAbsolute = absoluteOffset.value + it + val clamped = newAbsolute.coerceIn(minBound, maxBound) + val overflow = newAbsolute - clamped + val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f + offsetState.value = clamped + resistanceDelta + overflowState.value = overflow + absoluteOffset.value = newAbsolute + } private suspend fun snapInternalToOffset(target: Float) { draggableState.drag { @@ -213,7 +215,10 @@ internal open class SwipeableState( } } - private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec) { + private suspend fun animateInternalToOffset( + target: Float, + spec: AnimationSpec, + ) { draggableState.drag { var prevValue = absoluteOffset.value animationTarget.value = target @@ -241,14 +246,15 @@ internal open class SwipeableState( internal val targetValue: T get() { // TODO(calintat): Track current velocity (b/149549482) and use that here. - val target = animationTarget.value ?: computeTarget( - offset = offset.value, - lastValue = anchors.getOffset(currentValue) ?: offset.value, - anchors = anchors.keys, - thresholds = thresholds, - velocity = 0f, - velocityThreshold = Float.POSITIVE_INFINITY, - ) + val target = + animationTarget.value ?: computeTarget( + offset = offset.value, + lastValue = anchors.getOffset(currentValue) ?: offset.value, + anchors = anchors.keys, + thresholds = thresholds, + velocity = 0f, + velocityThreshold = Float.POSITIVE_INFINITY, + ) return anchors[target] ?: currentValue } @@ -324,7 +330,10 @@ internal open class SwipeableState( * @param anim The animation that will be used to animate to the new value. */ @ExperimentalMaterial3Api - internal suspend fun animateTo(targetValue: T, anim: AnimationSpec = animationSpec) { + internal suspend fun animateTo( + targetValue: T, + anim: AnimationSpec = animationSpec, + ) { latestNonEmptyAnchorsFlow.collect { anchors -> try { val targetOffset = anchors.getOffset(targetValue) @@ -334,10 +343,11 @@ internal open class SwipeableState( animateInternalToOffset(targetOffset, anim) } finally { val endOffset = absoluteOffset.value - val endValue = anchors - // fighting rounding error once again, anchor should be as close as 0.5 pixels - .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f } - .values.firstOrNull() ?: currentValue + val endValue = + anchors + // fighting rounding error once again, anchor should be as close as 0.5 pixels + .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f } + .values.firstOrNull() ?: currentValue currentValue = endValue } } @@ -359,14 +369,15 @@ internal open class SwipeableState( internal suspend fun performFling(velocity: Float) { latestNonEmptyAnchorsFlow.collect { anchors -> val lastAnchor = anchors.getOffset(currentValue)!! - val targetValue = computeTarget( - offset = offset.value, - lastValue = lastAnchor, - anchors = anchors.keys, - thresholds = thresholds, - velocity = velocity, - velocityThreshold = velocityThreshold, - ) + val targetValue = + computeTarget( + offset = offset.value, + lastValue = lastAnchor, + anchors = anchors.keys, + thresholds = thresholds, + velocity = velocity, + velocityThreshold = velocityThreshold, + ) val targetState = anchors[targetValue] if (targetState != null && confirmStateChange(targetState)) { animateTo(targetState) @@ -432,7 +443,7 @@ internal open class SwipeableState( internal class SwipeProgress( val from: T, val to: T, - /*@FloatRange(from = 0.0, to = 1.0)*/ + // @FloatRange(from = 0.0, to = 1.0) val fraction: Float, ) { override fun equals(other: Any?): Boolean { @@ -473,10 +484,11 @@ internal fun rememberSwipeableState( confirmStateChange: (newValue: T) -> Boolean = { true }, ): SwipeableState { return rememberSaveable( - saver = SwipeableState.Saver( - animationSpec = animationSpec, - confirmStateChange = confirmStateChange, - ), + saver = + SwipeableState.Saver( + animationSpec = animationSpec, + confirmStateChange = confirmStateChange, + ), ) { SwipeableState( initialValue = initialValue, @@ -501,13 +513,14 @@ internal fun rememberSwipeableStateFor( onValueChange: (T) -> Unit, animationSpec: AnimationSpec = AnimationSpec, ): SwipeableState { - val swipeableState = remember { - SwipeableState( - initialValue = value, - animationSpec = animationSpec, - confirmStateChange = { true }, - ) - } + val swipeableState = + remember { + SwipeableState( + initialValue = value, + animationSpec = animationSpec, + confirmStateChange = { true }, + ) + } val forceAnimationCheck = remember { mutableStateOf(false) } LaunchedEffect(value, forceAnimationCheck.value) { if (value != swipeableState.currentValue) { @@ -569,18 +582,19 @@ internal fun Modifier.swipeable( resistance: ResistanceConfig? = resistanceConfig(anchors.keys), velocityThreshold: Dp = VelocityThreshold, ) = composed( - inspectorInfo = debugInspectorInfo { - name = "swipeable" - properties["state"] = state - properties["anchors"] = anchors - properties["orientation"] = orientation - properties["enabled"] = enabled - properties["reverseDirection"] = reverseDirection - properties["interactionSource"] = interactionSource - properties["thresholds"] = thresholds - properties["resistance"] = resistance - properties["velocityThreshold"] = velocityThreshold - }, + inspectorInfo = + debugInspectorInfo { + name = "swipeable" + properties["state"] = state + properties["anchors"] = anchors + properties["orientation"] = orientation + properties["enabled"] = enabled + properties["reverseDirection"] = reverseDirection + properties["interactionSource"] = interactionSource + properties["thresholds"] = thresholds + properties["resistance"] = resistance + properties["velocityThreshold"] = velocityThreshold + }, ) { require(anchors.isNotEmpty()) { "You must have at least one anchor." @@ -627,7 +641,10 @@ internal interface ThresholdConfig { /** * Compute the value of the threshold (in pixels), once the values of the anchors are known. */ - fun Density.computeThreshold(fromValue: Float, toValue: Float): Float + fun Density.computeThreshold( + fromValue: Float, + toValue: Float, + ): Float } /** @@ -638,7 +655,10 @@ internal interface ThresholdConfig { @Immutable @ExperimentalMaterial3Api internal data class FixedThreshold(private val offset: Dp) : ThresholdConfig { - override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { + override fun Density.computeThreshold( + fromValue: Float, + toValue: Float, + ): Float { return fromValue + offset.toPx() * sign(toValue - fromValue) } } @@ -651,10 +671,13 @@ internal data class FixedThreshold(private val offset: Dp) : ThresholdConfig { @Immutable @ExperimentalMaterial3Api internal data class FractionalThreshold( - /*@FloatRange(from = 0.0, to = 1.0)*/ + // @FloatRange(from = 0.0, to = 1.0) private val fraction: Float, ) : ThresholdConfig { - override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { + override fun Density.computeThreshold( + fromValue: Float, + toValue: Float, + ): Float { return lerp(fromValue, toValue, fraction) } } @@ -683,11 +706,11 @@ internal data class FractionalThreshold( */ @Immutable internal class ResistanceConfig( - /*@FloatRange(from = 0.0, fromInclusive = false)*/ + // @FloatRange(from = 0.0, fromInclusive = false) val basis: Float, - /*@FloatRange(from = 0.0)*/ + // @FloatRange(from = 0.0) val factorAtMin: Float = StandardResistanceFactor, - /*@FloatRange(from = 0.0)*/ + // @FloatRange(from = 0.0) val factorAtMax: Float = StandardResistanceFactor, ) { fun computeResistance(overflow: Float): Float { @@ -843,45 +866,52 @@ internal object SwipeableDefaults { // revisit in b/174756744 as all types will have their own specific connection probably @ExperimentalMaterial3Api internal val SwipeableState.PreUpPostDownNestedScrollConnection: NestedScrollConnection - get() = object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - performDrag(delta).toOffset() - } else { - Offset.Zero + get() = + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + performDrag(delta).toOffset() + } else { + Offset.Zero + } } - } - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - return if (source == NestedScrollSource.Drag) { - performDrag(available.toFloat()).toOffset() - } else { - Offset.Zero + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (source == NestedScrollSource.Drag) { + performDrag(available.toFloat()).toOffset() + } else { + Offset.Zero + } } - } - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = Offset(available.x, available.y).toFloat() - return if (toFling < 0 && offset.value > minBound) { - performFling(velocity = toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = Offset(available.x, available.y).toFloat() + return if (toFling < 0 && offset.value > minBound) { + performFling(velocity = toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } } - } - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - performFling(velocity = Offset(available.x, available.y).toFloat()) - return available - } + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + performFling(velocity = Offset(available.x, available.y).toFloat()) + return available + } - private fun Float.toOffset(): Offset = Offset(0f, this) + private fun Float.toOffset(): Offset = Offset(0f, this) - private fun Offset.toFloat(): Float = this.y - } + private fun Offset.toFloat(): Float = this.y + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/modifiers/KeyEvents.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/modifiers/KeyEvents.kt index c9f931094c..6a978095d5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/modifiers/KeyEvents.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/modifiers/KeyEvents.kt @@ -26,7 +26,10 @@ import androidx.compose.ui.input.key.type /** * Intercepts a key event rather than passing it on to children */ -fun Modifier.interceptKey(key: Key, onKeyEvent: () -> Unit): Modifier { +fun Modifier.interceptKey( + key: Key, + onKeyEvent: () -> Unit, +): Modifier { return this.onPreviewKeyEvent { if (it.key == key && it.type == KeyUp) { // fire onKeyEvent on KeyUp to prevent duplicates onKeyEvent() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/DrawerItemWithUnreadCount.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/DrawerItemWithUnreadCount.kt index e720ebf3a2..6b62c37861 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/DrawerItemWithUnreadCount.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/DrawerItemWithUnreadCount.kt @@ -16,65 +16,70 @@ sealed class DrawerItemWithUnreadCount( ) : Comparable, FeedIdTag { abstract val uiId: Long - override fun compareTo(other: DrawerItemWithUnreadCount): Int = when (this) { - is DrawerFeed -> { - when (other) { - is DrawerFeed -> when { - tag.equals(other.tag, ignoreCase = true) -> displayTitle.compareTo( - other.displayTitle, - ignoreCase = true, - ) - - tag.isEmpty() -> 1 - other.tag.isEmpty() -> -1 - else -> tag.compareTo(other.tag, ignoreCase = true) - } - - is DrawerTag -> when { - tag.isEmpty() -> 1 - tag.equals(other.tag, ignoreCase = true) -> 1 - else -> tag.compareTo(other.tag, ignoreCase = true) - } - - is DrawerTop, - is DrawerSavedArticles, - -> { - 1 + override fun compareTo(other: DrawerItemWithUnreadCount): Int = + when (this) { + is DrawerFeed -> { + when (other) { + is DrawerFeed -> + when { + tag.equals(other.tag, ignoreCase = true) -> + displayTitle.compareTo( + other.displayTitle, + ignoreCase = true, + ) + + tag.isEmpty() -> 1 + other.tag.isEmpty() -> -1 + else -> tag.compareTo(other.tag, ignoreCase = true) + } + + is DrawerTag -> + when { + tag.isEmpty() -> 1 + tag.equals(other.tag, ignoreCase = true) -> 1 + else -> tag.compareTo(other.tag, ignoreCase = true) + } + + is DrawerTop, + is DrawerSavedArticles, + -> { + 1 + } } } - } - - is DrawerTag -> { - when (other) { - is DrawerFeed -> when { - other.tag.isEmpty() -> -1 - tag.equals(other.tag, ignoreCase = true) -> -1 - else -> tag.compareTo(other.tag, ignoreCase = true) - } - is DrawerTag -> { - tag.compareTo(other.tag, ignoreCase = true) - } - - is DrawerTop, - is DrawerSavedArticles, - -> { - 1 + is DrawerTag -> { + when (other) { + is DrawerFeed -> + when { + other.tag.isEmpty() -> -1 + tag.equals(other.tag, ignoreCase = true) -> -1 + else -> tag.compareTo(other.tag, ignoreCase = true) + } + + is DrawerTag -> { + tag.compareTo(other.tag, ignoreCase = true) + } + + is DrawerTop, + is DrawerSavedArticles, + -> { + 1 + } } } - } - is DrawerTop -> { - -1 - } + is DrawerTop -> { + -1 + } - is DrawerSavedArticles -> { - when (other) { - is DrawerTop -> 1 - else -> -1 + is DrawerSavedArticles -> { + when (other) { + is DrawerTop -> 1 + else -> -1 + } } } - } } @Immutable diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt index 4f9c53ce7a..3307073b86 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navdrawer/NavDrawer.kt @@ -75,8 +75,8 @@ import com.nononsenseapps.feeder.ui.compose.theme.FeederTheme import com.nononsenseapps.feeder.ui.compose.utils.ImmutableHolder import com.nononsenseapps.feeder.ui.compose.utils.immutableListHolderOf import com.nononsenseapps.feeder.ui.compose.utils.onKeyEventLikeEscape -import java.net.URL import kotlinx.coroutines.launch +import java.net.URL @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable @@ -101,19 +101,21 @@ fun ScreenWithNavDrawer( } DismissibleNavigationDrawer( - modifier = modifier - .onKeyEventLikeEscape { - coroutineScope.launch { - drawerState.close() - } - }, + modifier = + modifier + .onKeyEventLikeEscape { + coroutineScope.launch { + drawerState.close() + } + }, drawerState = drawerState, drawerContent = { DismissibleDrawerSheet { ListOfFeedsAndTags( state = navDrawerListState, - modifier = Modifier - .focusRequester(focusRequester), + modifier = + Modifier + .focusRequester(focusRequester), feedsAndTags = feedsAndTags, expandedTags = expandedTags, unreadBookmarksCount = unreadBookmarksCount, @@ -138,60 +140,62 @@ private fun ListOfFeedsAndTagsPreview() { FeederTheme { Surface { ListOfFeedsAndTags( - feedsAndTags = immutableListHolderOf( - DrawerTop(unreadCount = 100, totalChildren = 4), - DrawerSavedArticles(unreadCount = 5), - DrawerTag( - tag = "News tag", - unreadCount = 0, - -1111, - totalChildren = 2, - ), - DrawerFeed( - id = 1, - displayTitle = "Times", - tag = "News tag", - unreadCount = 0, - ), - DrawerFeed( - id = 2, - displayTitle = "Post", - imageUrl = URL("https://cowboyprogrammer.org/apple-touch-icon.png"), - tag = "News tag", - unreadCount = 2, + feedsAndTags = + immutableListHolderOf( + DrawerTop(unreadCount = 100, totalChildren = 4), + DrawerSavedArticles(unreadCount = 5), + DrawerTag( + tag = "News tag", + unreadCount = 0, + -1111, + totalChildren = 2, + ), + DrawerFeed( + id = 1, + displayTitle = "Times", + tag = "News tag", + unreadCount = 0, + ), + DrawerFeed( + id = 2, + displayTitle = "Post", + imageUrl = URL("https://cowboyprogrammer.org/apple-touch-icon.png"), + tag = "News tag", + unreadCount = 2, + ), + DrawerTag( + tag = "Funny tag", + unreadCount = 6, + -2222, + totalChildren = 1, + ), + DrawerFeed( + id = 3, + displayTitle = "Hidden", + tag = "Funny tag", + unreadCount = 6, + ), + DrawerFeed( + id = 4, + displayTitle = "Top Dog", + unreadCount = 99, + tag = "", + ), + DrawerFeed( + id = 5, + imageUrl = URL("https://cowboyprogrammer.org/apple-touch-icon.png"), + displayTitle = "Cowboy Programmer", + unreadCount = 7, + tag = "", + ), ), - DrawerTag( - tag = "Funny tag", - unreadCount = 6, - -2222, - totalChildren = 1, + expandedTags = + ImmutableHolder( + setOf( + "News tag", + "Funny tag", + ), ), - DrawerFeed( - id = 3, - displayTitle = "Hidden", - tag = "Funny tag", - unreadCount = 6, - ), - DrawerFeed( - id = 4, - displayTitle = "Top Dog", - unreadCount = 99, - tag = "", - ), - DrawerFeed( - id = 5, - imageUrl = URL("https://cowboyprogrammer.org/apple-touch-icon.png"), - displayTitle = "Cowboy Programmer", - unreadCount = 7, - tag = "", - ), - ), - expandedTags = ImmutableHolder( - setOf( - "News tag", - "Funny tag", - ), - ), unreadBookmarksCount = 1, onToggleTagExpansion = {}, state = rememberLazyListState(), @@ -222,22 +226,23 @@ fun ListOfFeedsAndTags( LazyColumn( state = state, contentPadding = WindowInsets.systemBars.asPaddingValues(), - modifier = modifier - .fillMaxSize() - .semantics { - testTag = "feedsAndTags" - }, + modifier = + modifier + .fillMaxSize() + .semantics { + testTag = "feedsAndTags" + }, ) { item( key = ID_ALL_FEEDS, contentType = ID_ALL_FEEDS, ) { - val item = feedsAndTags.item.firstOrNull() ?: DrawerTop( - { stringResource(id = R.string.all_feeds) }, - - 0, - 0, - ) + val item = + feedsAndTags.item.firstOrNull() ?: DrawerTop( + { stringResource(id = R.string.all_feeds) }, + 0, + 0, + ) AllFeeds( unreadCount = item.unreadCount, title = stringResource(id = R.string.all_feeds), @@ -367,23 +372,26 @@ private fun ExpandableTag( Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(top = 2.dp, bottom = 2.dp, end = 16.dp) - .fillMaxWidth() - .height(48.dp) - .safeSemantics(mergeDescendants = true) { - stateDescription = if (expanded) { - expandedLabel - } else { - contractedLabel - } - customActions = listOf( - CustomAccessibilityAction(toggleExpandLabel) { - onToggleExpansion(title) - true - }, - ) - }, + modifier = + Modifier + .padding(top = 2.dp, bottom = 2.dp, end = 16.dp) + .fillMaxWidth() + .height(48.dp) + .safeSemantics(mergeDescendants = true) { + stateDescription = + if (expanded) { + expandedLabel + } else { + contractedLabel + } + customActions = + listOf( + CustomAccessibilityAction(toggleExpandLabel) { + onToggleExpansion(title) + true + }, + ) + }, ) { ExpandArrow( degrees = angle, @@ -392,32 +400,36 @@ private fun ExpandableTag( }, ) Box( - modifier = Modifier - .clickable(onClick = onItemClick) - .fillMaxHeight() - .weight(1.0f, fill = true), + modifier = + Modifier + .clickable(onClick = onItemClick) + .fillMaxHeight() + .weight(1.0f, fill = true), ) { Text( text = title, maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart), + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), ) } if (unreadCount > 0) { - val unreadLabel = LocalContext.current.resources.getQuantityString( - R.plurals.n_unread_articles, - unreadCount, - unreadCount, - ) + val unreadLabel = + LocalContext.current.resources.getQuantityString( + R.plurals.n_unread_articles, + unreadCount, + unreadCount, + ) Text( text = unreadCount.toString(), maxLines = 1, - modifier = Modifier - .semantics { - contentDescription = unreadLabel - }, + modifier = + Modifier + .semantics { + contentDescription = unreadLabel + }, ) } } @@ -505,42 +517,47 @@ private fun AllFeeds( Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = onItemClick) - .padding( - start = 16.dp, - end = 16.dp, - top = 2.dp, - bottom = 2.dp, - ) - .fillMaxWidth() - .height(48.dp), + modifier = + Modifier + .clickable(onClick = onItemClick) + .padding( + start = 16.dp, + end = 16.dp, + top = 2.dp, + bottom = 2.dp, + ) + .fillMaxWidth() + .height(48.dp), ) { Box( - modifier = Modifier - .fillMaxHeight() - .weight(1.0f, fill = true), + modifier = + Modifier + .fillMaxHeight() + .weight(1.0f, fill = true), ) { Text( text = title, maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart), + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), ) } if (unreadCount > 0) { - val unreadLabel = LocalContext.current.resources.getQuantityString( - R.plurals.n_unread_articles, - unreadCount, - unreadCount, - ) + val unreadLabel = + LocalContext.current.resources.getQuantityString( + R.plurals.n_unread_articles, + unreadCount, + unreadCount, + ) Text( text = unreadCount.toString(), maxLines = 1, - modifier = Modifier.semantics { - contentDescription = unreadLabel - }, + modifier = + Modifier.semantics { + contentDescription = unreadLabel + }, ) } } @@ -556,52 +573,58 @@ private fun Feed( Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = onItemClick) - .padding( - start = 0.dp, - end = 16.dp, - top = 2.dp, - bottom = 2.dp, - ) - .fillMaxWidth() - .height(48.dp), + modifier = + Modifier + .clickable(onClick = onItemClick) + .padding( + start = 0.dp, + end = 16.dp, + top = 2.dp, + bottom = 2.dp, + ) + .fillMaxWidth() + .height(48.dp), ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier - .minimumInteractiveComponentSize(), -// .height(48.dp) + modifier = + Modifier + .minimumInteractiveComponentSize(), + // .height(48.dp) // // Taking 4dp spacing into account // .width(44.dp), ) { image() } Box( - modifier = Modifier - .fillMaxHeight() - .weight(1.0f, fill = true), + modifier = + Modifier + .fillMaxHeight() + .weight(1.0f, fill = true), ) { Text( text = title, maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart), + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterStart), ) } if (unreadCount > 0) { - val unreadLabel = LocalContext.current.resources.getQuantityString( - R.plurals.n_unread_articles, - unreadCount, - unreadCount, - ) + val unreadLabel = + LocalContext.current.resources.getQuantityString( + R.plurals.n_unread_articles, + unreadCount, + unreadCount, + ) Text( text = unreadCount.toString(), maxLines = 1, - modifier = Modifier.semantics { - contentDescription = unreadLabel - }, + modifier = + Modifier.semantics { + contentDescription = unreadLabel + }, ) } } @@ -618,31 +641,34 @@ private fun Feed( title = title, unreadCount = unreadCount, onItemClick = onItemClick, - image = if (imageUrl != null) { - { - val pixels = with(LocalDensity.current) { - 24.dp.roundToPx() + image = + if (imageUrl != null) { + { + val pixels = + with(LocalDensity.current) { + 24.dp.roundToPx() + } + AsyncImage( + model = + ImageRequest.Builder(LocalContext.current) + .data(imageUrl.toString()).listener( + onError = { a, b -> + Log.e("FEEDER_DRAWER", "error ${a.data}", b.throwable) + }, + ) + .scale(Scale.FIT) + .size(pixels) + .precision(Precision.INEXACT) + .build(), + contentDescription = stringResource(id = R.string.feed_icon), + contentScale = ContentScale.Crop, + modifier = Modifier.size(24.dp), + ) } - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl.toString()).listener( - onError = { a, b -> - Log.e("FEEDER_DRAWER", "error ${a.data}", b.throwable) - }, - ) - .scale(Scale.FIT) - .size(pixels) - .precision(Precision.INEXACT) - .build(), - contentDescription = stringResource(id = R.string.feed_icon), - contentScale = ContentScale.Crop, - modifier = Modifier.size(24.dp), - ) - } - } else { - { - Box(modifier = Modifier.size(24.dp)) {} - } - }, + } else { + { + Box(modifier = Modifier.size(24.dp)) {} + } + }, ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navigation/NavigationDestinations.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navigation/NavigationDestinations.kt index e416a61af4..5005893603 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navigation/NavigationDestinations.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/navigation/NavigationDestinations.kt @@ -62,23 +62,27 @@ sealed class NavigationDestination( val route: String init { - val completePath = ( - listOf(path) + navArguments.asSequence() - .filterIsInstance() - .map { "{${it.name}}" } - .toList() + val completePath = + ( + listOf(path) + + navArguments.asSequence() + .filterIsInstance() + .map { "{${it.name}}" } + .toList() ).joinToString(separator = "/") - val queryParams = navArguments.asSequence() - .filterIsInstance() - .map { "${it.name}={${it.name}}" } - .joinToString(prefix = "?", separator = "&") - - route = if (queryParams == "?") { - completePath - } else { - completePath + queryParams - } + val queryParams = + navArguments.asSequence() + .filterIsInstance() + .map { "${it.name}={${it.name}}" } + .joinToString(prefix = "?", separator = "&") + + route = + if (queryParams == "?") { + completePath + } else { + completePath + queryParams + } } @OptIn(ExperimentalAnimationApi::class) @@ -132,20 +136,24 @@ class PathParamArgument( @OptIn(ExperimentalAnimationApi::class) object SearchFeedDestination : NavigationDestination( path = "search/feed", - navArguments = listOf( - QueryParamArgument("feedUrl") { - type = NavType.StringType - defaultValue = null - nullable = true - }, - ), + navArguments = + listOf( + QueryParamArgument("feedUrl") { + type = NavType.StringType + defaultValue = null + nullable = true + }, + ), deepLinks = emptyList(), ) { - - fun navigate(navController: NavController, feedUrl: String? = null) { - val params = queryParams { - +("feedUrl" to feedUrl) - } + fun navigate( + navController: NavController, + feedUrl: String? = null, + ) { + val params = + queryParams { + +("feedUrl" to feedUrl) + } navController.navigate(path + params) { launchSingleTop = true @@ -178,32 +186,33 @@ object SearchFeedDestination : NavigationDestination( @OptIn(ExperimentalAnimationApi::class) object AddFeedDestination : NavigationDestination( path = "add/feed", - navArguments = listOf( - PathParamArgument("feedUrl") { - type = NavType.StringType - }, - QueryParamArgument("feedTitle") { - type = NavType.StringType - defaultValue = "" - }, - QueryParamArgument("feedImage") { - type = NavType.StringType - defaultValue = "" - }, - ), + navArguments = + listOf( + PathParamArgument("feedUrl") { + type = NavType.StringType + }, + QueryParamArgument("feedTitle") { + type = NavType.StringType + defaultValue = "" + }, + QueryParamArgument("feedImage") { + type = NavType.StringType + defaultValue = "" + }, + ), deepLinks = emptyList(), ) { - fun navigate( navController: NavController, feedUrl: String, feedTitle: String = "", feedImage: String = "", ) { - val params = queryParams { - +("feedTitle" to feedTitle) - +("feedImage" to feedImage) - } + val params = + queryParams { + +("feedTitle" to feedTitle) + +("feedImage" to feedImage) + } navController.navigate("$path/${feedUrl.urlEncode()}$params") { launchSingleTop = true @@ -232,15 +241,18 @@ object AddFeedDestination : NavigationDestination( @OptIn(ExperimentalAnimationApi::class) object EditFeedDestination : NavigationDestination( path = "edit/feed", - navArguments = listOf( - PathParamArgument("feedId") { - type = NavType.LongType - }, - ), + navArguments = + listOf( + PathParamArgument("feedId") { + type = NavType.LongType + }, + ), deepLinks = emptyList(), ) { - - fun navigate(navController: NavController, feedId: Long) { + fun navigate( + navController: NavController, + feedId: Long, + ) { navController.navigate("$path/$feedId") { launchSingleTop = true } @@ -303,29 +315,36 @@ object SettingsDestination : NavigationDestination( @OptIn(ExperimentalAnimationApi::class) object FeedDestination : NavigationDestination( path = "feed", - navArguments = listOf( - QueryParamArgument("id") { - type = NavType.LongType - defaultValue = ID_UNSET - }, - QueryParamArgument("tag") { - type = NavType.StringType - defaultValue = "" - }, - ), - deepLinks = listOf( - navDeepLink { - uriPattern = "$DEEP_LINK_BASE_URI/feed?id={id}&tag={tag}" - }, - ), + navArguments = + listOf( + QueryParamArgument("id") { + type = NavType.LongType + defaultValue = ID_UNSET + }, + QueryParamArgument("tag") { + type = NavType.StringType + defaultValue = "" + }, + ), + deepLinks = + listOf( + navDeepLink { + uriPattern = "$DEEP_LINK_BASE_URI/feed?id={id}&tag={tag}" + }, + ), ) { - fun navigate(navController: NavController, feedId: Long = ID_UNSET, tag: String = "") { - val params = queryParams { - if (feedId != ID_UNSET) { - +("id" to "$feedId") + fun navigate( + navController: NavController, + feedId: Long = ID_UNSET, + tag: String = "", + ) { + val params = + queryParams { + if (feedId != ID_UNSET) { + +("id" to "$feedId") + } + +("tag" to tag) } - +("tag" to tag) - } logDebug(LOG_TAG, "Navigate to $path$params. Current: ${navController.currentDestination?.route}") @@ -348,14 +367,16 @@ object FeedDestination : NavigationDestination( backStackEntry: NavBackStackEntry, navDrawerListState: LazyListState, ) { - val feedId = remember { - backStackEntry.arguments?.getLong("id") - ?: ID_UNSET - } - val tag = remember { - backStackEntry.arguments?.getString("tag") - ?: "" - } + val feedId = + remember { + backStackEntry.arguments?.getLong("id") + ?: ID_UNSET + } + val tag = + remember { + backStackEntry.arguments?.getString("tag") + ?: "" + } val navigationDeepLinkViewModel: NavigationDeepLinkViewModel = backStackEntry.diAwareViewModel() @@ -377,18 +398,23 @@ object FeedDestination : NavigationDestination( @OptIn(ExperimentalAnimationApi::class) object ArticleDestination : NavigationDestination( path = "reader", - navArguments = listOf( - PathParamArgument("itemId") { - type = NavType.LongType - }, - ), - deepLinks = listOf( - navDeepLink { - uriPattern = "$DEEP_LINK_BASE_URI/article/{itemId}" - }, - ), + navArguments = + listOf( + PathParamArgument("itemId") { + type = NavType.LongType + }, + ), + deepLinks = + listOf( + navDeepLink { + uriPattern = "$DEEP_LINK_BASE_URI/article/{itemId}" + }, + ), ) { - fun navigate(navController: NavController, itemId: Long) { + fun navigate( + navController: NavController, + itemId: Long, + ) { navController.navigate("$path/$itemId") { launchSingleTop = true } @@ -400,10 +426,11 @@ object ArticleDestination : NavigationDestination( backStackEntry: NavBackStackEntry, navDrawerListState: LazyListState, ) { - val itemId = remember { - backStackEntry.arguments?.getLong("itemId") - ?: error("Missing mandatory argument: itemId") - } + val itemId = + remember { + backStackEntry.arguments?.getLong("itemId") + ?: error("Missing mandatory argument: itemId") + } val navigationDeepLinkViewModel: NavigationDeepLinkViewModel = backStackEntry.diAwareViewModel() @@ -429,31 +456,38 @@ object ArticleDestination : NavigationDestination( @OptIn(ExperimentalAnimationApi::class) object SyncScreenDestination : NavigationDestination( path = "sync", - navArguments = listOf( - QueryParamArgument("syncCode") { - type = NavType.StringType - defaultValue = "" - }, - QueryParamArgument("secretKey") { - type = NavType.StringType - defaultValue = "" - }, - ), - deepLinks = listOf( - navDeepLink { - uriPattern = "$DEEP_LINK_BASE_URI/sync/join?sync_code={syncCode}&key={secretKey}" - }, - ), + navArguments = + listOf( + QueryParamArgument("syncCode") { + type = NavType.StringType + defaultValue = "" + }, + QueryParamArgument("secretKey") { + type = NavType.StringType + defaultValue = "" + }, + ), + deepLinks = + listOf( + navDeepLink { + uriPattern = "$DEEP_LINK_BASE_URI/sync/join?sync_code={syncCode}&key={secretKey}" + }, + ), ) { - fun navigate(navController: NavController, syncCode: String, secretKey: String) { - val params = queryParams { - if (syncCode.isNotBlank()) { - +("syncCode" to syncCode) - } - if (secretKey.isNotBlank()) { - +("secretKey" to secretKey) + fun navigate( + navController: NavController, + syncCode: String, + secretKey: String, + ) { + val params = + queryParams { + if (syncCode.isNotBlank()) { + +("syncCode" to syncCode) + } + if (secretKey.isNotBlank()) { + +("secretKey" to secretKey) + } } - } navController.navigate("$path$params") { launchSingleTop = true @@ -490,7 +524,10 @@ class QueryParamsBuilder { appendIfNotEmpty(first, second) } - private fun appendIfNotEmpty(name: String, value: String?) { + private fun appendIfNotEmpty( + name: String, + value: String?, + ) { if (value?.isNotEmpty() != true) { return } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt index d477167ba4..b6aa740dc4 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/ompl/OpmlImportScreen.kt @@ -56,10 +56,10 @@ import com.nononsenseapps.feeder.ui.compose.utils.ProvideScaledText import com.nononsenseapps.feeder.ui.compose.utils.ScreenType import com.nononsenseapps.feeder.ui.compose.utils.ThemePreviews import com.nononsenseapps.feeder.ui.compose.utils.getScreenType -import java.net.URL import kotlinx.coroutines.launch import org.kodein.di.compose.LocalDI import org.kodein.di.instance +import java.net.URL private const val LOG_TAG = "FEEDER_OPMLIMPORT" @@ -81,22 +81,27 @@ fun OpmlImportScreen( viewState = ViewState(error = true, initial = false) } else { try { - val parser = OpmlPullParser( - object : OPMLParserHandler { - override suspend fun saveFeed(feed: Feed) { - viewState = viewState.copy( - initial = false, - feeds = viewState.feeds + (feed.url to feed), - ) - } + val parser = + OpmlPullParser( + object : OPMLParserHandler { + override suspend fun saveFeed(feed: Feed) { + viewState = + viewState.copy( + initial = false, + feeds = viewState.feeds + (feed.url to feed), + ) + } - override suspend fun saveSetting(key: String, value: String) { - } + override suspend fun saveSetting( + key: String, + value: String, + ) { + } - override suspend fun saveBlocklistPatterns(patterns: Iterable) { - } - }, - ) + override suspend fun saveBlocklistPatterns(patterns: Iterable) { + } + }, + ) val contentResolver: ContentResolver by di.instance() contentResolver.openInputStream(uri).use { it?.let { stream -> @@ -148,10 +153,11 @@ fun OpmlImportScreen( // } // ScreenType.SINGLE -> { Scaffold( - modifier = modifier - .fillMaxHeight(screenHeight) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .fillMaxHeight(screenHeight) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( @@ -199,28 +205,31 @@ fun OpmlImportView( onOk = onOk, onCancel = onDismiss, okEnabled = viewState.okEnabled, - modifier = modifier - .padding(horizontal = LocalDimens.current.margin) - .fillMaxHeight(), + modifier = + modifier + .padding(horizontal = LocalDimens.current.margin) + .fillMaxHeight(), ) { ProvideScaledText( style = MaterialTheme.typography.titleMedium, ) { - val text = when { - viewState.initial -> "" - viewState.error -> stringResource(id = R.string.failed_to_import_OPML) - viewState.feeds.isEmpty() -> stringResource(id = R.string.no_feeds) - else -> stringResource( - id = R.string.import_x_feeds_with_y_tags, - pluralStringResource( - id = R.plurals.n_feeds, - count = viewState.feedCount, - ).format(viewState.feedCount), - pluralStringResource(id = R.plurals.n_tags, count = viewState.tagCount).format( - viewState.tagCount, - ), - ) - } + val text = + when { + viewState.initial -> "" + viewState.error -> stringResource(id = R.string.failed_to_import_OPML) + viewState.feeds.isEmpty() -> stringResource(id = R.string.no_feeds) + else -> + stringResource( + id = R.string.import_x_feeds_with_y_tags, + pluralStringResource( + id = R.plurals.n_feeds, + count = viewState.feedCount, + ).format(viewState.feedCount), + pluralStringResource(id = R.plurals.n_tags, count = viewState.tagCount).format( + viewState.tagCount, + ), + ) + } WithBidiDeterminedLayoutDirection(paragraph = text) { Text( text = text, @@ -238,10 +247,11 @@ fun OpmlImportView( LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(vertical = 16.dp) - .fillMaxWidth() - .weight(1f), + modifier = + Modifier + .padding(vertical = 16.dp) + .fillMaxWidth() + .weight(1f), ) { items( count = feedValues.size, @@ -292,14 +302,17 @@ private fun PreviewOpmlImportScreenSingle() { FeederTheme { Surface { OpmlImportView( - viewState = ViewState( - feeds = mapOf( - URL("https://example.com/foo") to Feed( - title = "Foo Feed", - url = URL("https://example.com/foo"), - ), + viewState = + ViewState( + feeds = + mapOf( + URL("https://example.com/foo") to + Feed( + title = "Foo Feed", + url = URL("https://example.com/foo"), + ), + ), ), - ), onDismiss = {}, onOk = {}, ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullRefresh.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullRefresh.kt index 5ce1f5328e..3e18247555 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullRefresh.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullRefresh.kt @@ -24,11 +24,12 @@ fun Modifier.pullRefresh( state: PullRefreshState, enabled: Boolean = true, ) = inspectable( - inspectorInfo = debugInspectorInfo { - name = "pullRefresh" - properties["state"] = state - properties["enabled"] = enabled - }, + inspectorInfo = + debugInspectorInfo { + name = "pullRefresh" + properties["state"] = state + properties["enabled"] = enabled + }, ) { Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled) } @@ -53,12 +54,13 @@ fun Modifier.pullRefresh( onRelease: suspend (flingVelocity: Float) -> Unit, enabled: Boolean = true, ) = inspectable( - inspectorInfo = debugInspectorInfo { - name = "pullRefresh" - properties["onPull"] = onPull - properties["onRelease"] = onRelease - properties["enabled"] = enabled - }, + inspectorInfo = + debugInspectorInfo { + name = "pullRefresh" + properties["onPull"] = onPull + properties["onRelease"] = onRelease + properties["enabled"] = enabled + }, ) { Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled)) } @@ -68,25 +70,26 @@ private class PullRefreshNestedScrollConnection( private val onRelease: suspend (flingVelocity: Float) -> Unit, private val enabled: Boolean, ) : NestedScrollConnection { - override fun onPreScroll( available: Offset, source: NestedScrollSource, - ): Offset = when { - !enabled -> Offset.Zero - source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up - else -> Offset.Zero - } + ): Offset = + when { + !enabled -> Offset.Zero + source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up + else -> Offset.Zero + } override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource, - ): Offset = when { - !enabled -> Offset.Zero - source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down - else -> Offset.Zero - } + ): Offset = + when { + !enabled -> Offset.Zero + source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down + else -> Offset.Zero + } override suspend fun onPreFling(available: Velocity): Velocity { onRelease(available.y) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt index 8d8217700d..706b89efe3 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshIndicator.kt @@ -60,9 +60,10 @@ fun PullRefreshIndicator( } Surface( - modifier = modifier - .size(IndicatorSize) - .pullRefreshIndicatorTransform(state, scale), + modifier = + modifier + .size(IndicatorSize) + .pullRefreshIndicatorTransform(state, scale), shape = SpinnerShape, color = backgroundColor, tonalElevation = if (showElevation) Elevation else 0.dp, @@ -104,12 +105,13 @@ private fun CircularArrowIndicator( rotate(degrees = values.rotation) { val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f - val arcBounds = Rect( - size.center.x - arcRadius, - size.center.y - arcRadius, - size.center.x + arcRadius, - size.center.y + arcRadius, - ) + val arcBounds = + Rect( + size.center.x - arcRadius, + size.center.y - arcRadius, + size.center.x + arcRadius, + size.center.y + arcRadius, + ) drawArc( color = color, alpha = values.alpha, @@ -118,10 +120,11 @@ private fun CircularArrowIndicator( useCenter = false, topLeft = arcBounds.topLeft, size = arcBounds.size, - style = Stroke( - width = StrokeWidth.toPx(), - cap = StrokeCap.Square, - ), + style = + Stroke( + width = StrokeWidth.toPx(), + cap = StrokeCap.Square, + ), ) drawArrow(path, arcBounds, color, values) } @@ -158,7 +161,12 @@ private fun ArrowValues(progress: Float): ArrowValues { return ArrowValues(alpha, rotation, startAngle, endAngle, scale) } -private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) { +private fun DrawScope.drawArrow( + arrow: Path, + bounds: Rect, + color: Color, + values: ArrowValues, +) { arrow.reset() arrow.moveTo(0f, 0f) // Move to left corner arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner @@ -198,11 +206,12 @@ fun Modifier.pullRefreshIndicatorTransform( state: PullRefreshState, scale: Boolean = false, ) = composed( - inspectorInfo = debugInspectorInfo { - name = "pullRefreshIndicatorTransform" - properties["state"] = state - properties["scale"] = scale - }, + inspectorInfo = + debugInspectorInfo { + name = "pullRefreshIndicatorTransform" + properties["state"] = state + properties["scale"] = scale + }, ) { var height by remember { mutableStateOf(0) } @@ -212,9 +221,10 @@ fun Modifier.pullRefreshIndicatorTransform( translationY = state.position - height if (scale && !state.refreshing) { - val scaleFraction = LinearOutSlowInEasing - .transform(state.position / state.threshold) - .coerceIn(0f, 1f) + val scaleFraction = + LinearOutSlowInEasing + .transform(state.position / state.threshold) + .coerceIn(0f, 1f) scaleX = scaleFraction scaleY = scaleFraction } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt index 49c4c41165..6f995a246b 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/pullrefresh/PullToRefreshState.kt @@ -15,10 +15,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import kotlin.math.abs -import kotlin.math.pow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.pow /** * Creates a [PullRefreshState] that is remembered across compositions. @@ -56,9 +56,10 @@ fun rememberPullRefreshState( // refreshThreshold and refreshingOffset should not be changed after instantiation, so any // changes to these values are ignored. - val state = remember(scope) { - PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) - } + val state = + remember(scope) { + PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) + } SideEffect { state.setRefreshing(refreshing) @@ -134,27 +135,29 @@ class PullRefreshState internal constructor( } } - private fun animateIndicatorTo(offset: Float) = animationScope.launch { - animate(initialValue = _position, targetValue = offset) { value, _ -> - _position = value + private fun animateIndicatorTo(offset: Float) = + animationScope.launch { + animate(initialValue = _position, targetValue = offset) { value, _ -> + _position = value + } } - } - private fun calculateIndicatorPosition(): Float = when { - // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. - adjustedDistancePulled <= threshold -> adjustedDistancePulled - else -> { - // How far beyond the threshold pull has gone, as a percentage of the threshold. - val overshootPercent = abs(progress) - 1.0f - // Limit the overshoot to 200%. Linear between 0 and 200. - val linearTension = overshootPercent.coerceIn(0f, 2f) - // Non-linear tension. Increases with linearTension, but at a decreasing rate. - val tensionPercent = linearTension - linearTension.pow(2) / 4 - // The additional offset beyond the threshold. - val extraOffset = threshold * tensionPercent - threshold + extraOffset + private fun calculateIndicatorPosition(): Float = + when { + // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. + adjustedDistancePulled <= threshold -> adjustedDistancePulled + else -> { + // How far beyond the threshold pull has gone, as a percentage of the threshold. + val overshootPercent = abs(progress) - 1.0f + // Limit the overshoot to 200%. Linear between 0 and 200. + val linearTension = overshootPercent.coerceIn(0f, 2f) + // Non-linear tension. Increases with linearTension, but at a decreasing rate. + val tensionPercent = linearTension - linearTension.pow(2) / 4 + // The additional offset beyond the threshold. + val extraOffset = threshold * tensionPercent + threshold + extraOffset + } } - } } /** diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/readaloud/ReadAloudPlayer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/readaloud/ReadAloudPlayer.kt index 050f4183f0..4d2d3838b6 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/readaloud/ReadAloudPlayer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/readaloud/ReadAloudPlayer.kt @@ -162,9 +162,10 @@ fun TTSPlayer( DropdownMenu( expanded = showMenu, onDismissRequest = { showMenu = false }, - modifier = Modifier.onKeyEventLikeEscape { - showMenu = false - }, + modifier = + Modifier.onKeyEventLikeEscape { + showMenu = false + }, ) { DropdownMenuItem( onClick = { @@ -206,7 +207,7 @@ fun TTSPlayer( @Preview @Composable -fun PlayerPreview() { +private fun PlayerPreview() { FeederTheme { TTSPlayer( currentlyPlaying = true, @@ -221,7 +222,7 @@ fun PlayerPreview() { @Preview @Composable -fun PlayerPreviewWithFab() { +private fun PlayerPreviewWithFab() { FeederTheme { TTSPlayer( currentlyPlaying = true, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt index 53ef4838a8..40e5aca07b 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedScreen.kt @@ -90,11 +90,11 @@ import com.nononsenseapps.feeder.ui.compose.utils.StableHolder import com.nononsenseapps.feeder.ui.compose.utils.getScreenType import com.nononsenseapps.feeder.ui.compose.utils.stableListHolderOf import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLNoThrows -import java.net.MalformedURLException -import java.net.URL import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import java.net.MalformedURLException +import java.net.URL private const val LOG_TAG = "FEEDER_SEARCH" @@ -119,9 +119,10 @@ fun SearchFeedScreen( SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior) Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( @@ -224,7 +225,8 @@ fun SearchFeedView( // If screen is opened from intent with pre-filled URL, trigger search directly LaunchedEffect(Unit) { - if (results.item.isEmpty() && errors.item.isEmpty() && feedUrl.isNotBlank() && isValidUrl( + if (results.item.isEmpty() && errors.item.isEmpty() && feedUrl.isNotBlank() && + isValidUrl( feedUrl, ) ) { @@ -236,9 +238,10 @@ fun SearchFeedView( Box( contentAlignment = Alignment.TopCenter, - modifier = modifier - .fillMaxWidth() - .verticalScroll(scrollState), + modifier = + modifier + .fillMaxWidth() + .verticalScroll(scrollState), ) { if (screenType == ScreenType.DUAL) { Row( @@ -247,9 +250,10 @@ fun SearchFeedView( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .weight(1f, fill = true) - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + Modifier + .weight(1f, fill = true) + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { leftContent( feedUrl = feedUrl, @@ -264,9 +268,10 @@ fun SearchFeedView( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .weight(1f, fill = true) - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + Modifier + .weight(1f, fill = true) + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { rightContent( results = results, @@ -280,9 +285,10 @@ fun SearchFeedView( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .padding(horizontal = dimens.margin, vertical = 8.dp) - .width(dimens.maxContentWidth), + modifier = + Modifier + .padding(horizontal = dimens.margin, vertical = 8.dp) + .width(dimens.maxContentWidth), ) { leftContent( feedUrl = feedUrl, @@ -332,35 +338,38 @@ fun ColumnScope.leftContent( Text(stringResource(id = R.string.add_feed_search_hint)) }, isError = isNotValidUrl, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrect = false, - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Search, - ), - keyboardActions = KeyboardActions( - onSearch = { - if (isValidUrl) { - onSearch(sloppyLinkToStrictURLNoThrows(feedUrl)) - keyboardController?.hide() - } - }, - ), + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Search, + ), + keyboardActions = + KeyboardActions( + onSearch = { + if (isValidUrl) { + onSearch(sloppyLinkToStrictURLNoThrows(feedUrl)) + keyboardController?.hide() + } + }, + ), singleLine = true, - modifier = modifier - .width(dimens.maxContentWidth) - .interceptKey(Key.Enter) { - if (isValidUrl(feedUrl)) { - onSearch(sloppyLinkToStrictURLNoThrows(feedUrl)) - keyboardController?.hide() + modifier = + modifier + .width(dimens.maxContentWidth) + .interceptKey(Key.Enter) { + if (isValidUrl(feedUrl)) { + onSearch(sloppyLinkToStrictURLNoThrows(feedUrl)) + keyboardController?.hide() + } } - } - .interceptKey(Key.Escape) { - focusManager.clearFocus() - } - .safeSemantics { - testTag = "urlField" - }, + .interceptKey(Key.Escape) { + focusManager.clearFocus() + } + .safeSemantics { + testTag = "urlField" + }, ) OutlinedButton( @@ -391,20 +400,21 @@ fun ColumnScope.rightContent( ) { if (results.item.isEmpty()) { for (error in errors.item) { - val title = when (error) { - is FetchError -> stringResource(R.string.failed_to_download) - is MetaDataParseError -> stringResource(R.string.failed_to_parse_the_page) - is NoAlternateFeeds -> stringResource(R.string.no_feeds_in_the_page) - is NotHTML -> stringResource(R.string.content_is_not_html) - is NotInitializedYet -> "Not initialized yet" // Should never happen - is RSSParseError -> stringResource(R.string.failed_to_parse_rss_feed) - is HttpError -> stringResource(R.string.http_error) - is JsonFeedParseError -> stringResource(R.string.failed_to_parse_json_feed) - is NoBody -> stringResource(R.string.no_body_in_response) - is UnsupportedContentType -> stringResource(R.string.unsupported_content_type) - is FullTextDecodingFailure -> stringResource(R.string.failed_to_parse_full_article) - is NoUrl -> stringResource(R.string.no_url) - } + val title = + when (error) { + is FetchError -> stringResource(R.string.failed_to_download) + is MetaDataParseError -> stringResource(R.string.failed_to_parse_the_page) + is NoAlternateFeeds -> stringResource(R.string.no_feeds_in_the_page) + is NotHTML -> stringResource(R.string.content_is_not_html) + is NotInitializedYet -> "Not initialized yet" // Should never happen + is RSSParseError -> stringResource(R.string.failed_to_parse_rss_feed) + is HttpError -> stringResource(R.string.http_error) + is JsonFeedParseError -> stringResource(R.string.failed_to_parse_json_feed) + is NoBody -> stringResource(R.string.no_body_in_response) + is UnsupportedContentType -> stringResource(R.string.unsupported_content_type) + is FullTextDecodingFailure -> stringResource(R.string.failed_to_parse_full_article) + is NoUrl -> stringResource(R.string.no_url) + } ErrorResultView( title = title, @@ -437,16 +447,15 @@ fun ColumnScope.rightContent( } @Composable -fun SearchingIndicator( - modifier: Modifier = Modifier, -) { +fun SearchingIndicator(modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, - modifier = modifier - .fillMaxWidth() - .safeSemantics { - testTag = "searchingIndicator" - }, + modifier = + modifier + .fillMaxWidth() + .safeSemantics { + testTag = "searchingIndicator" + }, ) { CircularProgressIndicator() } @@ -464,17 +473,19 @@ fun SearchResultView( val dimens = LocalDimens.current Card( onClick = onClick, - modifier = modifier - .width(dimens.maxContentWidth) - .safeSemantics { - testTag = "searchResult" - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .safeSemantics { + testTag = "searchResult" + }, ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .width(dimens.maxContentWidth) - .padding(8.dp), + modifier = + Modifier + .width(dimens.maxContentWidth) + .padding(8.dp), ) { Text( title, @@ -503,22 +514,25 @@ fun ErrorResultView( val dimens = LocalDimens.current Card( - modifier = modifier - .width(dimens.maxContentWidth) - .safeSemantics { - testTag = "errorResult" - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .safeSemantics { + testTag = "errorResult" + }, ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .width(dimens.maxContentWidth) - .padding(8.dp), + modifier = + Modifier + .width(dimens.maxContentWidth) + .padding(8.dp), ) { Text( title, - style = MaterialTheme.typography.titleSmall - .copy(color = MaterialTheme.colorScheme.error), + style = + MaterialTheme.typography.titleSmall + .copy(color = MaterialTheme.colorScheme.error), ) Text( url, @@ -542,27 +556,28 @@ fun ErrorResultView( uiMode = UI_MODE_NIGHT_NO, ) @Composable -fun SearchPreview() { +private fun SearchPreview() { FeederTheme { Surface { SearchFeedView( screenType = ScreenType.SINGLE, onUrlChanged = {}, onSearch = {}, - results = stableListHolderOf( - SearchResult( - title = "Atom feed", - url = "https://cowboyprogrammer.org/atom", - description = "An atom feed", - feedImage = "", + results = + stableListHolderOf( + SearchResult( + title = "Atom feed", + url = "https://cowboyprogrammer.org/atom", + description = "An atom feed", + feedImage = "", + ), + SearchResult( + title = "RSS feed", + url = "https://cowboyprogrammer.org/rss", + description = "An RSS feed", + feedImage = "", + ), ), - SearchResult( - title = "RSS feed", - url = "https://cowboyprogrammer.org/rss", - description = "An RSS feed", - feedImage = "", - ), - ), errors = stableListHolderOf(), currentlySearching = false, modifier = Modifier, @@ -579,7 +594,7 @@ fun SearchPreview() { uiMode = UI_MODE_NIGHT_NO, ) @Composable -fun ErrorPreview() { +private fun ErrorPreview() { FeederTheme { Surface { SearchFeedView( @@ -587,12 +602,13 @@ fun ErrorPreview() { onUrlChanged = {}, onSearch = {}, results = stableListHolderOf(), - errors = stableListHolderOf( - RSSParseError( - url = "https://example.com/bad", - throwable = NullPointerException("Missing header or something"), + errors = + stableListHolderOf( + RSSParseError( + url = "https://example.com/bad", + throwable = NullPointerException("Missing header or something"), + ), ), - ), currentlySearching = false, modifier = Modifier, feedUrl = "https://cowboyprogrammer.org", @@ -614,27 +630,28 @@ fun ErrorPreview() { uiMode = UI_MODE_NIGHT_NO, ) @Composable -fun SearchPreviewLarge() { +private fun SearchPreviewLarge() { FeederTheme { Surface { SearchFeedView( screenType = ScreenType.DUAL, onUrlChanged = {}, onSearch = {}, - results = stableListHolderOf( - SearchResult( - title = "Atom feed", - url = "https://cowboyprogrammer.org/atom", - description = "An atom feed", - feedImage = "", - ), - SearchResult( - title = "RSS feed", - url = "https://cowboyprogrammer.org/rss", - description = "An RSS feed", - feedImage = "", + results = + stableListHolderOf( + SearchResult( + title = "Atom feed", + url = "https://cowboyprogrammer.org/atom", + description = "An atom feed", + feedImage = "", + ), + SearchResult( + title = "RSS feed", + url = "https://cowboyprogrammer.org/rss", + description = "An RSS feed", + feedImage = "", + ), ), - ), errors = stableListHolderOf(), currentlySearching = false, modifier = Modifier, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedViewModel.kt index 1be80e9733..b571aff16f 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/searchfeed/SearchFeedViewModel.kt @@ -12,7 +12,6 @@ import com.nononsenseapps.feeder.model.SiteMetaData import com.nononsenseapps.feeder.util.Either import com.nononsenseapps.feeder.util.flatMap import com.nononsenseapps.feeder.util.sloppyLinkToStrictURLOrNull -import java.net.URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -20,6 +19,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import org.kodein.di.DI import org.kodein.di.instance +import java.net.URL class SearchFeedViewModel(di: DI) : DIAwareViewModel(di) { private val feedParser: FeedParser by instance() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt index dfc286e54f..91bfbac851 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/Settings.kt @@ -118,9 +118,10 @@ fun SettingsScreen( SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior) Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( @@ -205,7 +206,7 @@ fun SettingsScreen( @Composable @Preview(showBackground = true, device = PIXEL_C) @Preview(showBackground = true, device = NEXUS_5) -fun SettingsScreenPreview() { +private fun SettingsScreenPreview() { FeederTheme(ThemeOptions.DAY) { Surface { SettingsList( @@ -333,19 +334,21 @@ fun SettingsList( Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .padding(horizontal = dimens.margin) - .fillMaxWidth() - .verticalScroll(scrollState), + modifier = + modifier + .padding(horizontal = dimens.margin) + .fillMaxWidth() + .verticalScroll(scrollState), ) { MenuSetting( currentValue = currentThemeValue, - values = immutableListHolderOf( - ThemeOptions.SYSTEM.asThemeOption(), - ThemeOptions.DAY.asThemeOption(), - ThemeOptions.NIGHT.asThemeOption(), - ThemeOptions.E_INK.asThemeOption(), - ), + values = + immutableListHolderOf( + ThemeOptions.SYSTEM.asThemeOption(), + ThemeOptions.DAY.asThemeOption(), + ThemeOptions.NIGHT.asThemeOption(), + ThemeOptions.E_INK.asThemeOption(), + ), title = stringResource(id = R.string.theme), onSelection = onThemeChanged, ) @@ -353,18 +356,19 @@ fun SettingsList( SwitchSetting( title = stringResource(id = R.string.dynamic_theme_use), checked = useDynamicTheme, - description = when { - isAndroidSAndAbove -> { - null - } + description = + when { + isAndroidSAndAbove -> { + null + } - else -> { - stringResource( - id = R.string.only_available_on_android_n, - "12", - ) - } - }, + else -> { + stringResource( + id = R.string.only_available_on_android_n, + "12", + ) + } + }, enabled = isAndroidSAndAbove, onCheckedChanged = onUseDynamicTheme, ) @@ -372,10 +376,11 @@ fun SettingsList( MenuSetting( title = stringResource(id = R.string.dark_theme_preference), currentValue = currentDarkThemePreference, - values = immutableListHolderOf( - DarkThemePreferences.BLACK.asDarkThemeOption(), - DarkThemePreferences.DARK.asDarkThemeOption(), - ), + values = + immutableListHolderOf( + DarkThemePreferences.BLACK.asDarkThemeOption(), + DarkThemePreferences.DARK.asDarkThemeOption(), + ), onSelection = onDarkThemePreferenceChanged, ) @@ -436,11 +441,12 @@ fun SettingsList( MenuSetting( currentValue = currentSyncFrequencyValue.asSyncFreqOption(), - values = ImmutableHolder( - SyncFrequency.values().map { - it.asSyncFreqOption() - }, - ), + values = + ImmutableHolder( + SyncFrequency.values().map { + it.asSyncFreqOption() + }, + ), title = stringResource(id = R.string.check_for_updates), onSelection = { onSyncFrequencyChanged(it.syncFrequency) @@ -467,22 +473,24 @@ fun SettingsList( MenuSetting( currentValue = maxItemsPerFeedValue, - values = immutableListHolderOf( - 50, - 100, - 200, - 500, - 1000, - ), + values = + immutableListHolderOf( + 50, + 100, + 200, + 500, + 1000, + ), title = stringResource(id = R.string.max_feed_items), onSelection = onMaxItemsPerFeedChanged, ) ExternalSetting( - currentValue = when (batteryOptimizationIgnoredValue) { - true -> stringResource(id = R.string.battery_optimization_disabled) - false -> stringResource(id = R.string.battery_optimization_enabled) - }, + currentValue = + when (batteryOptimizationIgnoredValue) { + true -> stringResource(id = R.string.battery_optimization_disabled) + false -> stringResource(id = R.string.battery_optimization_enabled) + }, title = stringResource(id = R.string.battery_optimization), ) { activityLauncher.startActivity( @@ -509,10 +517,11 @@ fun SettingsList( MenuSetting( currentValue = currentSortingValue, - values = immutableListHolderOf( - SortingOptions.NEWEST_FIRST.asSortOption(), - SortingOptions.OLDEST_FIRST.asSortOption(), - ), + values = + immutableListHolderOf( + SortingOptions.NEWEST_FIRST.asSortOption(), + SortingOptions.OLDEST_FIRST.asSortOption(), + ), title = stringResource(id = R.string.sort), onSelection = onSortingChanged, ) @@ -585,11 +594,12 @@ fun SettingsList( MenuSetting( currentValue = currentItemOpenerValue.asItemOpenerOption(), - values = immutableListHolderOf( - ItemOpener.READER.asItemOpenerOption(), - ItemOpener.CUSTOM_TAB.asItemOpenerOption(), - ItemOpener.DEFAULT_BROWSER.asItemOpenerOption(), - ), + values = + immutableListHolderOf( + ItemOpener.READER.asItemOpenerOption(), + ItemOpener.CUSTOM_TAB.asItemOpenerOption(), + ItemOpener.DEFAULT_BROWSER.asItemOpenerOption(), + ), title = stringResource(id = R.string.open_item_by_default_with), onSelection = { onItemOpenerChanged(it.itemOpener) @@ -598,10 +608,11 @@ fun SettingsList( MenuSetting( currentValue = currentLinkOpenerValue.asLinkOpenerOption(), - values = immutableListHolderOf( - LinkOpener.CUSTOM_TAB.asLinkOpenerOption(), - LinkOpener.DEFAULT_BROWSER.asLinkOpenerOption(), - ), + values = + immutableListHolderOf( + LinkOpener.CUSTOM_TAB.asLinkOpenerOption(), + LinkOpener.DEFAULT_BROWSER.asLinkOpenerOption(), + ), title = stringResource(id = R.string.open_links_with), onSelection = { onLinkOpenerChanged(it.linkOpener) @@ -645,13 +656,15 @@ fun SettingsList( SwitchSetting( title = stringResource(id = R.string.use_detect_language), checked = useDetectLanguage, - description = when { - isAndroidQAndAbove -> stringResource(id = R.string.description_for_read_aloud) - else -> stringResource( - id = R.string.only_available_on_android_n, - "10", - ) - }, + description = + when { + isAndroidQAndAbove -> stringResource(id = R.string.description_for_read_aloud) + else -> + stringResource( + id = R.string.only_available_on_android_n, + "10", + ) + }, enabled = isAndroidQAndAbove, onCheckedChanged = onUseDetectLanguageChanged, ) @@ -669,26 +682,30 @@ fun GroupTitle( ) { val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth), + modifier = + modifier + .width(dimens.maxContentWidth), verticalAlignment = Alignment.CenterVertically, ) { if (startingSpace) { Box( - modifier = Modifier - .width(64.dp) - .height(height), + modifier = + Modifier + .width(64.dp) + .height(height), ) } Box( - modifier = Modifier - .height(height), + modifier = + Modifier + .height(height), contentAlignment = Alignment.CenterStart, ) { ProvideTextStyle( - value = MaterialTheme.typography.labelMedium.merge( - TextStyle(color = MaterialTheme.colorScheme.primary), - ), + value = + MaterialTheme.typography.labelMedium.merge( + TextStyle(color = MaterialTheme.colorScheme.primary), + ), ) { title(Modifier.semantics { heading() }) } @@ -706,12 +723,13 @@ fun ExternalSetting( ) { val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .clickable { onClick() } - .semantics { - role = Role.Button - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .clickable { onClick() } + .semantics { + role = Role.Button + }, verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -746,12 +764,13 @@ fun MenuSetting( var expanded by rememberSaveable { mutableStateOf(false) } val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .clickable { expanded = !expanded } - .semantics { - role = Role.Button - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .clickable { expanded = !expanded } + .semantics { + role = Role.Button + }, verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -775,9 +794,10 @@ fun MenuSetting( DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, - modifier = Modifier.onKeyEventLikeEscape { - expanded = false - }, + modifier = + Modifier.onKeyEventLikeEscape { + expanded = false + }, ) { for (value in values.item) { DropdownMenuItem( @@ -786,16 +806,17 @@ fun MenuSetting( onSelection(value) }, text = { - val style = if (value == currentValue) { - MaterialTheme.typography.bodyLarge.merge( - TextStyle( - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.secondary, - ), - ) - } else { - MaterialTheme.typography.bodyLarge - } + val style = + if (value == currentValue) { + MaterialTheme.typography.bodyLarge.merge( + TextStyle( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.secondary, + ), + ) + } else { + MaterialTheme.typography.bodyLarge + } Text( value.toString(), style = style, @@ -820,12 +841,13 @@ fun ListDialogSetting( var expanded by rememberSaveable { mutableStateOf(false) } val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .clickable { expanded = !expanded } - .semantics { - role = Role.Button - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .clickable { expanded = !expanded } + .semantics { + role = Role.Button + }, verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -874,12 +896,13 @@ fun NotificationsSetting( ) { var expanded by rememberSaveable { mutableStateOf(false) } - val notificationsPermissionState = rememberApiPermissionState( - permission = "android.permission.POST_NOTIFICATIONS", - minimumApiLevel = 33, - ) { value -> - expanded = value - } + val notificationsPermissionState = + rememberApiPermissionState( + permission = "android.permission.POST_NOTIFICATIONS", + minimumApiLevel = 33, + ) { value -> + expanded = value + } val shouldShowExplanationForPermission by remember { derivedStateOf { @@ -899,24 +922,25 @@ fun NotificationsSetting( val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .clickable { - when (notificationsPermissionState.status) { - is PermissionStatus.Denied -> { - if (notificationsPermissionState.status.shouldShowRationale) { - permissionDismissed = false - } else { - notificationsPermissionState.launchPermissionRequest() + modifier = + modifier + .width(dimens.maxContentWidth) + .clickable { + when (notificationsPermissionState.status) { + is PermissionStatus.Denied -> { + if (notificationsPermissionState.status.shouldShowRationale) { + permissionDismissed = false + } else { + notificationsPermissionState.launchPermissionRequest() + } } - } - PermissionStatus.Granted -> expanded = true + PermissionStatus.Granted -> expanded = true + } } - } - .semantics { - role = Role.Button - }, + .semantics { + role = Role.Button + }, verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -934,16 +958,17 @@ fun NotificationsSetting( }, subtitle = { Text( - text = when (permissionDenied) { - true -> stringResource(id = R.string.explanation_permission_notifications) - false -> { - items.item.asSequence() - .filter { it.notify } - .map { it.title } - .take(4) - .joinToString(", ", limit = 3) - } - }, + text = + when (permissionDenied) { + true -> stringResource(id = R.string.explanation_permission_notifications) + false -> { + items.item.asSequence() + .filter { it.notify } + .map { it.title } + .take(4) + .joinToString(", ", limit = 3) + } + }, overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -986,21 +1011,23 @@ fun RadioButtonSetting( minHeight: Dp = 64.dp, onClick: () -> Unit, ) { - val stateLabel = if (selected) { - stringResource(androidx.compose.ui.R.string.selected) - } else { - stringResource(androidx.compose.ui.R.string.not_selected) - } + val stateLabel = + if (selected) { + stringResource(androidx.compose.ui.R.string.selected) + } else { + stringResource(androidx.compose.ui.R.string.not_selected) + } val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .heightIn(min = minHeight) - .clickable { onClick() } - .safeSemantics(mergeDescendants = true) { - role = Role.RadioButton - stateDescription = stateLabel - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .heightIn(min = minHeight) + .clickable { onClick() } + .safeSemantics(mergeDescendants = true) { + role = Role.RadioButton + stateDescription = stateLabel + }, verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { @@ -1043,20 +1070,22 @@ fun SwitchSetting( val context = LocalContext.current val dimens = LocalDimens.current Row( - modifier = modifier - .width(dimens.maxContentWidth) - .heightIn(min = 64.dp) - .clickable( - enabled = enabled, - onClick = { onCheckedChanged(!checked) }, - ) - .safeSemantics(mergeDescendants = true) { - stateDescription = when (checked) { - true -> context.getString(androidx.compose.ui.R.string.on) - else -> context.getString(androidx.compose.ui.R.string.off) - } - role = Role.Switch - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .heightIn(min = 64.dp) + .clickable( + enabled = enabled, + onClick = { onCheckedChanged(!checked) }, + ) + .safeSemantics(mergeDescendants = true) { + stateDescription = + when (checked) { + true -> context.getString(androidx.compose.ui.R.string.on) + else -> context.getString(androidx.compose.ui.R.string.off) + } + role = Role.Switch + }, verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { @@ -1074,9 +1103,10 @@ fun SwitchSetting( title = { Text(title) }, - subtitle = description?.let { - { Text(it) } - }, + subtitle = + description?.let { + { Text(it) } + }, ) Spacer(modifier = Modifier.width(8.dp)) @@ -1104,29 +1134,32 @@ fun ScaleSetting( // so no point in adding screen reader action? Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - .width(dimens.maxContentWidth) - .heightIn(min = 64.dp) - .padding(start = 64.dp) - .safeSemantics(mergeDescendants = true) { - stateDescription = "%.1fx".format(safeCurrentValue) - }, + modifier = + modifier + .width(dimens.maxContentWidth) + .heightIn(min = 64.dp) + .padding(start = 64.dp) + .safeSemantics(mergeDescendants = true) { + stateDescription = "%.1fx".format(safeCurrentValue) + }, ) { Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), tonalElevation = 3.dp, ) { Text( "Lorem ipsum dolor sit amet.", - style = MaterialTheme.typography.bodyLarge - .merge( - TextStyle( - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.bodyLarge.fontSize * currentValue, + style = + MaterialTheme.typography.bodyLarge + .merge( + TextStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.bodyLarge.fontSize * currentValue, + ), ), - ), modifier = Modifier.padding(4.dp), ) } @@ -1135,24 +1168,26 @@ fun ScaleSetting( startLabel = { Text( "A", - style = MaterialTheme.typography.bodyLarge - .merge( - TextStyle( - fontSize = MaterialTheme.typography.bodyLarge.fontSize * valueRange.start, + style = + MaterialTheme.typography.bodyLarge + .merge( + TextStyle( + fontSize = MaterialTheme.typography.bodyLarge.fontSize * valueRange.start, + ), ), - ), modifier = Modifier.alignByBaseline(), ) }, endLabel = { Text( "A", - style = MaterialTheme.typography.bodyLarge - .merge( - TextStyle( - fontSize = MaterialTheme.typography.bodyLarge.fontSize * valueRange.endInclusive, + style = + MaterialTheme.typography.bodyLarge + .merge( + TextStyle( + fontSize = MaterialTheme.typography.bodyLarge.fontSize * valueRange.endInclusive, + ), ), - ), modifier = Modifier.alignByBaseline(), ) }, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt index a69b99125a..8d6b3b8066 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SettingsViewModel.kt @@ -54,13 +54,15 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { repository.setSyncOnResume(value) } - fun setSyncOnlyOnWifi(value: Boolean) = applicationCoroutineScope.launch { - repository.setSyncOnlyOnWifi(value) - } + fun setSyncOnlyOnWifi(value: Boolean) = + applicationCoroutineScope.launch { + repository.setSyncOnlyOnWifi(value) + } - fun setSyncOnlyWhenCharging(value: Boolean) = applicationCoroutineScope.launch { - repository.setSyncOnlyWhenCharging(value) - } + fun setSyncOnlyWhenCharging(value: Boolean) = + applicationCoroutineScope.launch { + repository.setSyncOnlyWhenCharging(value) + } fun setLoadImageOnlyOnWifi(value: Boolean) { repository.setLoadImageOnlyOnWifi(value) @@ -90,23 +92,29 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { repository.setLinkOpener(value) } - fun setSyncFrequency(value: SyncFrequency) = applicationCoroutineScope.launch { - repository.setSyncFrequency(value) - } + fun setSyncFrequency(value: SyncFrequency) = + applicationCoroutineScope.launch { + repository.setSyncFrequency(value) + } fun setFeedItemStyle(value: FeedItemStyle) { repository.setFeedItemStyle(value) } - fun addToBlockList(value: String) = applicationCoroutineScope.launch { - repository.addBlocklistPattern(value) - } + fun addToBlockList(value: String) = + applicationCoroutineScope.launch { + repository.addBlocklistPattern(value) + } - fun removeFromBlockList(value: String) = applicationCoroutineScope.launch { - repository.removeBlocklistPattern(value) - } + fun removeFromBlockList(value: String) = + applicationCoroutineScope.launch { + repository.removeBlocklistPattern(value) + } - fun toggleNotifications(feedId: Long, value: Boolean) = applicationCoroutineScope.launch { + fun toggleNotifications( + feedId: Long, + value: Boolean, + ) = applicationCoroutineScope.launch { repository.toggleNotifications(feedId, value) } @@ -152,10 +160,11 @@ class SettingsViewModel(di: DI) : DIAwareViewModel(di) { } } - private val batteryOptimizationIgnoredFlow: Flow = repository.resumeTime.map { - val powerManager: PowerManager? = context.getSystemService() - powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true - }.buffer(1) + private val batteryOptimizationIgnoredFlow: Flow = + repository.resumeTime.map { + val powerManager: PowerManager? = context.getSystemService() + powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true + }.buffer(1) private val _viewState = MutableStateFlow(SettingsViewState()) val viewState: StateFlow diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt index 3fe84f533a..73f2d82cb8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/settings/SliderWithLabel.kt @@ -43,20 +43,23 @@ fun SliderWithLabel( ) { Column { BoxWithConstraints( - modifier = modifier - .fillMaxWidth(), + modifier = + modifier + .fillMaxWidth(), ) { - val offset = getSliderOffset( - value = value, - valueRange = valueRange, - boxWidth = maxWidth, - labelWidth = labelMinWidth + 8.dp, // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation - ) + val offset = + getSliderOffset( + value = value, + valueRange = valueRange, + boxWidth = maxWidth, + labelWidth = labelMinWidth + 8.dp, // Since we use a padding of 4.dp on either sides of the SliderLabel, we need to account for this in our calculation + ) SliderLabel( label = valueToLabel(value), - modifier = Modifier - .padding(start = offset), + modifier = + Modifier + .padding(start = offset), minWidth = labelMinWidth, ) } @@ -111,13 +114,14 @@ fun SliderLabel( ) { Box( contentAlignment = Alignment.Center, - modifier = modifier - .background( - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(10.dp), - ) - .padding(4.dp) - .size(minWidth), + modifier = + modifier + .background( + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(10.dp), + ) + .padding(4.dp) + .size(minWidth), ) { Text( label, @@ -143,12 +147,15 @@ private fun getSliderOffset( } // Calculate the 0..1 fraction that `pos` value represents between `a` and `b` -private fun calcFraction(a: Float, b: Float, pos: Float) = - (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) +private fun calcFraction( + a: Float, + b: Float, + pos: Float, +) = (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f) @Preview @Composable -fun PreviewSliderWithLabel() { +private fun PreviewSliderWithLabel() { FeederTheme { Surface { var value by remember { @@ -167,7 +174,7 @@ fun PreviewSliderWithLabel() { @Preview @Composable -fun PreviewSliderWithEndLabels() { +private fun PreviewSliderWithEndLabels() { FeederTheme { Surface { var value by remember { @@ -178,26 +185,28 @@ fun PreviewSliderWithEndLabels() { startLabel = { Text( "A", - style = MaterialTheme.typography.bodyLarge - .merge( - TextStyle( - color = MaterialTheme.colorScheme.onBackground, - fontSize = MaterialTheme.typography.bodyLarge.fontSize, + style = + MaterialTheme.typography.bodyLarge + .merge( + TextStyle( + color = MaterialTheme.colorScheme.onBackground, + fontSize = MaterialTheme.typography.bodyLarge.fontSize, + ), ), - ), modifier = Modifier.alignByBaseline(), ) }, endLabel = { Text( "A", - style = MaterialTheme.typography.bodyLarge - .merge( - TextStyle( - color = MaterialTheme.colorScheme.onBackground, - fontSize = MaterialTheme.typography.bodyLarge.fontSize * 2, + style = + MaterialTheme.typography.bodyLarge + .merge( + TextStyle( + color = MaterialTheme.colorScheme.onBackground, + fontSize = MaterialTheme.typography.bodyLarge.fontSize * 2, + ), ), - ), modifier = Modifier.alignByBaseline(), ) }, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/LeaveSyncChainDialog.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/LeaveSyncChainDialog.kt index 19922836c2..81e631125d 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/LeaveSyncChainDialog.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/LeaveSyncChainDialog.kt @@ -18,7 +18,7 @@ fun LeaveSyncChainDialog( @Preview @Composable -fun PreviewLeaveSyncChainDialog() { +private fun PreviewLeaveSyncChainDialog() { LeaveSyncChainDialog( onDismiss = {}, onOk = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreen.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreen.kt index 2940d70fc1..472596c651 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreen.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreen.kt @@ -91,12 +91,12 @@ import com.nononsenseapps.feeder.util.ActivityLauncher import com.nononsenseapps.feeder.util.DEEP_LINK_BASE_URI import com.nononsenseapps.feeder.util.KOFI_URL import com.nononsenseapps.feeder.util.openKoFiIntent -import java.net.URL -import java.net.URLDecoder import net.glxn.qrgen.android.QRCode import net.glxn.qrgen.core.scheme.Url import org.kodein.di.compose.LocalDI import org.kodein.di.instance +import java.net.URL +import java.net.URLDecoder private const val LOG_TAG = "FEEDER_SYNCSCREEN" @@ -118,9 +118,10 @@ private fun SyncScaffold( SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior) Scaffold( - modifier = modifier - .nestedScroll(scrollBehavior.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), + modifier = + modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), contentWindowInsets = WindowInsets.statusBars, topBar = { SensibleTopAppBar( @@ -150,9 +151,10 @@ private fun SyncScaffold( DropdownMenu( expanded = showToolbar, onDismissRequest = { showToolbar = false }, - modifier = Modifier.onKeyEventLikeEscape { - showToolbar = false - }, + modifier = + Modifier.onKeyEventLikeEscape { + showToolbar = false + }, ) { DropdownMenuItem( onClick = { @@ -188,10 +190,11 @@ fun SyncScreen( val viewState: SyncScreenViewState by viewModel.viewState.collectAsStateWithLifecycle() val windowSize = LocalWindowSize() - val syncScreenType = getSyncScreenType( - windowSize = windowSize, - viewState = viewState, - ) + val syncScreenType = + getSyncScreenType( + windowSize = windowSize, + viewState = viewState, + ) var previousScreen: SyncScreenType? by remember { mutableStateOf(null) @@ -331,20 +334,22 @@ fun SyncScreen( AnimatedVisibility( visible = targetScreen == SyncScreenType.SINGLE_SETUP, - enter = when (previousScreen) { - null -> fadeIn(initialAlpha = 1.0f) - else -> fadeIn() - }, - /* + enter = + when (previousScreen) { + null -> fadeIn(initialAlpha = 1.0f) + else -> fadeIn() + }, + /* This may seem weird - but it's a special case. This exit animation actually runs when the first screen is device list. So to prevent a flicker effect it's important to block sideways movement. The setup screen will be momentarily on screen because it takes a few millis to fetch the sync remote. - */ - exit = when (previousScreen) { - null -> fadeOut(targetAlpha = 1.0f) - else -> fadeOut() - }, + */ + exit = + when (previousScreen) { + null -> fadeOut(targetAlpha = 1.0f) + else -> fadeOut() + }, ) { SyncSetupScreen( onNavigateUp = onLeaveSyncSettings, @@ -372,11 +377,12 @@ fun SyncScreen( AnimatedVisibility( visible = targetScreen == SyncScreenType.SINGLE_DEVICELIST, - enter = when (previousScreen) { - SyncScreenType.SINGLE_ADD_DEVICE -> fadeIn() - null -> fadeIn(initialAlpha = 1.0f) - else -> fadeIn() - }, + enter = + when (previousScreen) { + SyncScreenType.SINGLE_ADD_DEVICE -> fadeIn() + null -> fadeIn(initialAlpha = 1.0f) + else -> fadeIn() + }, exit = fadeOut(), ) { SyncDeviceListScreen( @@ -413,18 +419,19 @@ enum class SyncScreenType { fun getSyncScreenType( windowSize: WindowSizeClass, viewState: SyncScreenViewState, -): SyncScreenType = when (getScreenType(windowSize)) { - ScreenType.SINGLE -> { - when (viewState.singleScreenToShow) { - SyncScreenToShow.SETUP -> SyncScreenType.SINGLE_SETUP - SyncScreenToShow.DEVICELIST -> SyncScreenType.SINGLE_DEVICELIST - SyncScreenToShow.ADD_DEVICE -> SyncScreenType.SINGLE_ADD_DEVICE - SyncScreenToShow.JOIN -> SyncScreenType.SINGLE_JOIN +): SyncScreenType = + when (getScreenType(windowSize)) { + ScreenType.SINGLE -> { + when (viewState.singleScreenToShow) { + SyncScreenToShow.SETUP -> SyncScreenType.SINGLE_SETUP + SyncScreenToShow.DEVICELIST -> SyncScreenType.SINGLE_DEVICELIST + SyncScreenToShow.ADD_DEVICE -> SyncScreenType.SINGLE_ADD_DEVICE + SyncScreenToShow.JOIN -> SyncScreenType.SINGLE_JOIN + } } - } - ScreenType.DUAL -> SyncScreenType.DUAL -} + ScreenType.DUAL -> SyncScreenType.DUAL + } @Composable fun DualSyncScreen( @@ -462,15 +469,17 @@ fun DualSyncScreen( modifier = modifier, ) { innerModifier -> Row( - modifier = innerModifier - .verticalScroll(scrollState), + modifier = + innerModifier + .verticalScroll(scrollState), ) { when (leftScreenToShow) { LeftScreenToShow.SETUP -> { SyncSetupContent( onScanSyncCode = onScanSyncCode, - modifier = Modifier - .weight(1f, fill = true), + modifier = + Modifier + .weight(1f, fill = true), onStartNewSyncChain = onStartNewSyncChain, ) } @@ -482,8 +491,9 @@ fun DualSyncScreen( onAddNewDevice = onAddNewDevice, onDeleteDevice = onDeleteDevice, showAddDeviceButton = false, - modifier = Modifier - .weight(1f, fill = true), + modifier = + Modifier + .weight(1f, fill = true), ) } } @@ -492,8 +502,9 @@ fun DualSyncScreen( RightScreenToShow.ADD_DEVICE -> { SyncAddNewDeviceContent( syncUrl = addDeviceUrl, - modifier = Modifier - .weight(1f, fill = true), + modifier = + Modifier + .weight(1f, fill = true), ) } @@ -503,8 +514,9 @@ fun DualSyncScreen( syncCode = syncCode, onSetSyncCode = onSetSyncCode, secretKey = secretKey, - modifier = Modifier - .weight(1f, fill = true), + modifier = + Modifier + .weight(1f, fill = true), onSetSecretKey = onSetSecretKey, ) } @@ -551,9 +563,10 @@ fun SyncSetupContent( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + modifier + .fillMaxSize() + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { Text( text = stringResource(R.string.device_sync_description_1), @@ -575,14 +588,15 @@ fun SyncSetupContent( Text( text = KOFI_URL, style = MaterialTheme.typography.bodyLarge.merge(LinkTextStyle()), - modifier = Modifier - .fillMaxWidth() - .clickable { - activityLauncher.startActivity( - openAdjacentIfSuitable = true, - intent = openKoFiIntent(), - ) - }, + modifier = + Modifier + .fillMaxWidth() + .clickable { + activityLauncher.startActivity( + openAdjacentIfSuitable = true, + intent = openKoFiIntent(), + ) + }, ) } // Let this be hard-coded. It should not be localized. @@ -617,21 +631,22 @@ internal val String.syncCodeQueryParam get() = substringAfter("sync_code=").take(64) internal val String.secretKeyQueryParam - get() = substringAfter("key=") - .substringBefore("&") - .let { - // Deeplinks are already decoded - but not if you scan a QR code - if ("%3A" in it) { - try { - URLDecoder.decode(it, "UTF-8") - } catch (e: Exception) { - Log.e(LOG_TAG, "Failed to decode secret key", e) + get() = + substringAfter("key=") + .substringBefore("&") + .let { + // Deeplinks are already decoded - but not if you scan a QR code + if ("%3A" in it) { + try { + URLDecoder.decode(it, "UTF-8") + } catch (e: Exception) { + Log.e(LOG_TAG, "Failed to decode secret key", e) + it + } + } else { it } - } else { - it } - } @Composable fun SyncJoinScreen( @@ -679,9 +694,10 @@ fun SyncJoinContent( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + modifier + .fillMaxSize() + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { TextField( value = syncCode, @@ -691,9 +707,10 @@ fun SyncJoinContent( onValueChange = onSetSyncCode, isError = syncCode.syncCodeQueryParam.length != 64, singleLine = true, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 64.dp), ) TextField( value = secretKey, @@ -702,9 +719,10 @@ fun SyncJoinContent( }, onValueChange = onSetSecretKey, isError = !AesCbcWithIntegrity.isKeyDecodable(secretKey), - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp), + modifier = + Modifier + .fillMaxWidth() + .heightIn(min = 64.dp), ) Button( enabled = syncCode.syncCodeQueryParam.length == 64, @@ -769,16 +787,18 @@ fun SyncDeviceListContent( Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + modifier + .fillMaxSize() + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { Text( text = stringResource(R.string.devices_on_sync_chain), style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(top = 8.dp, bottom = 8.dp) - .fillMaxWidth(), + modifier = + Modifier + .padding(top = 8.dp, bottom = 8.dp) + .fillMaxWidth(), ) for (device in devices.item) { DeviceEntry( @@ -792,23 +812,25 @@ fun SyncDeviceListContent( Text( text = stringResource(R.string.device_sync_financed_by_community), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), ) // Google Play does not allow direct donation links if (!BuildConfig.BUILD_TYPE.contains("play", ignoreCase = true)) { Text( text = KOFI_URL, style = MaterialTheme.typography.bodyLarge.merge(LinkTextStyle()), - modifier = Modifier - .fillMaxWidth() - .clickable { - activityLauncher.startActivity( - openAdjacentIfSuitable = true, - intent = openKoFiIntent(), - ) - }, + modifier = + Modifier + .fillMaxWidth() + .clickable { + activityLauncher.startActivity( + openAdjacentIfSuitable = true, + intent = openKoFiIntent(), + ) + }, ) } if (showAddDeviceButton) { @@ -847,15 +869,17 @@ fun DeviceEntry( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier - .fillMaxWidth() - .heightIn(min = minimumTouchSize), + modifier = + modifier + .fillMaxWidth() + .heightIn(min = minimumTouchSize), ) { - val text = if (device.deviceId == currentDeviceId) { - stringResource(id = R.string.this_device, device.deviceName) - } else { - device.deviceName - } + val text = + if (device.deviceId == currentDeviceId) { + stringResource(id = R.string.this_device, device.deviceName) + } else { + device.deviceName + } Text( text = text, style = MaterialTheme.typography.titleMedium, @@ -872,7 +896,7 @@ fun DeviceEntry( @Preview @Composable -fun PreviewDeviceEntry() { +private fun PreviewDeviceEntry() { FeederTheme { Surface { DeviceEntry( @@ -907,8 +931,9 @@ fun DeleteDeviceDialog( text = stringResource(R.string.remove_device), style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - modifier = Modifier - .padding(vertical = 8.dp), + modifier = + Modifier + .padding(vertical = 8.dp), ) }, text = { @@ -968,27 +993,31 @@ fun SyncAddNewDeviceContent( Column( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxSize() - .padding(horizontal = dimens.margin, vertical = 8.dp), + modifier = + modifier + .fillMaxSize() + .padding(horizontal = dimens.margin, vertical = 8.dp), ) { Text( text = stringResource(R.string.press_scan_sync), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) Text( text = stringResource(R.string.or_open_device_sync_link), style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) Text( text = stringResource(R.string.treat_like_password), style = MaterialTheme.typography.bodyLarge.copy(color = Color.Red), - modifier = Modifier - .fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth(), ) Image( bitmap = qrCode, @@ -1000,29 +1029,31 @@ fun SyncAddNewDeviceContent( Text( text = "$syncUrl", style = LinkTextStyle(), - modifier = Modifier - .fillMaxWidth() - .clickable { - val intent = Intent.createChooser( - Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_TEXT, "$syncUrl") - putExtra(Intent.EXTRA_TITLE, intentTitle) - type = "text/plain" - }, - null, - ) - activityLauncher.startActivity( - openAdjacentIfSuitable = false, - intent = intent, - ) - }, + modifier = + Modifier + .fillMaxWidth() + .clickable { + val intent = + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, "$syncUrl") + putExtra(Intent.EXTRA_TITLE, intentTitle) + type = "text/plain" + }, + null, + ) + activityLauncher.startActivity( + openAdjacentIfSuitable = false, + intent = intent, + ) + }, ) } } @Preview("Device List Tablet", device = Devices.PIXEL_C) @Composable -fun PreviewDualSyncScreenDeviceList() { +private fun PreviewDualSyncScreenDeviceList() { FeederTheme { DualSyncScreen( onNavigateUp = { }, @@ -1034,13 +1065,14 @@ fun PreviewDualSyncScreenDeviceList() { onDeleteDevice = {}, onLeaveSyncChain = {}, currentDeviceId = 5L, - devices = ImmutableHolder( - listOf( - SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), - SyncDevice(deviceId = 2L, deviceName = "SM-T970"), - SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + devices = + ImmutableHolder( + listOf( + SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), + SyncDevice(deviceId = 2L, deviceName = "SM-T970"), + SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + ), ), - ), addDeviceUrl = ImmutableHolder(URL("$DEEP_LINK_BASE_URI/sync/join?sync_code=123foo")), onJoinSyncChain = { _, _ -> }, syncCode = "", @@ -1054,7 +1086,7 @@ fun PreviewDualSyncScreenDeviceList() { @Preview("Setup Tablet", device = Devices.PIXEL_C) @Preview("Setup Foldable", device = Devices.FOLDABLE, widthDp = 720, heightDp = 360) @Composable -fun PreviewDualSyncScreenSetup() { +private fun PreviewDualSyncScreenSetup() { FeederTheme { DualSyncScreen( onNavigateUp = { }, @@ -1066,13 +1098,14 @@ fun PreviewDualSyncScreenSetup() { onDeleteDevice = {}, onLeaveSyncChain = {}, currentDeviceId = 5L, - devices = ImmutableHolder( - listOf( - SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), - SyncDevice(deviceId = 2L, deviceName = "SM-T970"), - SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + devices = + ImmutableHolder( + listOf( + SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), + SyncDevice(deviceId = 2L, deviceName = "SM-T970"), + SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + ), ), - ), addDeviceUrl = ImmutableHolder(URL("$DEEP_LINK_BASE_URI/sync/join?sync_code=123foo&key=123ABF")), onJoinSyncChain = { _, _ -> }, syncCode = "", @@ -1086,7 +1119,7 @@ fun PreviewDualSyncScreenSetup() { @Preview("Scan or Enter Phone") @Preview("Scan or Enter Small Tablet", device = Devices.NEXUS_7_2013) @Composable -fun PreviewJoin() { +private fun PreviewJoin() { FeederTheme { SyncJoinScreen( onNavigateUp = {}, @@ -1103,7 +1136,7 @@ fun PreviewJoin() { @Preview("Empty Phone") @Preview("Empty Small Tablet", device = Devices.NEXUS_7_2013) @Composable -fun PreviewEmpty() { +private fun PreviewEmpty() { FeederTheme { SyncSetupScreen( onNavigateUp = {}, @@ -1117,18 +1150,19 @@ fun PreviewEmpty() { @Preview("Device List Phone") @Preview("Device List Small Tablet", device = Devices.NEXUS_7_2013) @Composable -fun PreviewDeviceList() { +private fun PreviewDeviceList() { FeederTheme { SyncDeviceListScreen( onNavigateUp = {}, currentDeviceId = 5L, - devices = ImmutableHolder( - listOf( - SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), - SyncDevice(deviceId = 2L, deviceName = "SM-T970"), - SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + devices = + ImmutableHolder( + listOf( + SyncDevice(deviceId = 1L, deviceName = "ONEPLUS A6003"), + SyncDevice(deviceId = 2L, deviceName = "SM-T970"), + SyncDevice(deviceId = 3L, deviceName = "Nexus 6"), + ), ), - ), onAddNewDevice = {}, onDeleteDevice = {}, onLeaveSyncChain = {}, @@ -1139,7 +1173,7 @@ fun PreviewDeviceList() { @Preview("Add New Device Phone") @Preview("Add New Device Small Tablet", device = Devices.NEXUS_7_2013) @Composable -fun PreviewAddNewDeviceContent() { +private fun PreviewAddNewDeviceContent() { FeederTheme { SyncAddNewDeviceScreen( onNavigateUp = {}, diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt index f791973c33..cd4092a1e7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/sync/SyncScreenViewModel.kt @@ -16,7 +16,6 @@ import com.nononsenseapps.feeder.model.workmanager.requestFeedSync import com.nononsenseapps.feeder.util.DEEP_LINK_BASE_URI import com.nononsenseapps.feeder.util.logDebug import com.nononsenseapps.feeder.util.urlEncode -import java.net.URL import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.kodein.di.DI import org.kodein.di.instance +import java.net.URL class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAwareViewModel(di) { private val context: Application by instance() @@ -33,17 +33,20 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware private val applicationCoroutineScope: ApplicationCoroutineScope by instance() - private val _syncCode: MutableStateFlow = MutableStateFlow( - state["syncCode"] ?: "", - ) + private val _syncCode: MutableStateFlow = + MutableStateFlow( + state["syncCode"] ?: "", + ) - private val _secretKey: MutableStateFlow = MutableStateFlow( - state["secretKey"] ?: "", - ) + private val _secretKey: MutableStateFlow = + MutableStateFlow( + state["secretKey"] ?: "", + ) - private val _screenToShow: MutableStateFlow = MutableStateFlow( - state["syncScreen"] ?: SyncScreenToShow.SETUP, - ) + private val _screenToShow: MutableStateFlow = + MutableStateFlow( + state["syncScreen"] ?: SyncScreenToShow.SETUP, + ) init { if (_syncCode.value.isNotBlank() || _secretKey.value.isNotBlank()) { @@ -56,11 +59,12 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware fun setSyncCode(value: String) { val possibleUrlCode = value.syncCodeQueryParam - val syncCode = if (possibleUrlCode.length == 64) { - possibleUrlCode - } else { - value - } + val syncCode = + if (possibleUrlCode.length == 64) { + possibleUrlCode + } else { + value + } state["syncCode"] = syncCode _syncCode.update { syncCode } @@ -88,7 +92,10 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware } } - fun joinSyncChain(syncCode: String, secretKey: String) { + fun joinSyncChain( + syncCode: String, + secretKey: String, + ) { logDebug(tag = LOG_TAG, "Joining sync chain") viewModelScope.launch { try { @@ -127,7 +134,10 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware } } - private fun joinedWithSyncCode(syncCode: String, secretKey: String) { + private fun joinedWithSyncCode( + syncCode: String, + secretKey: String, + ) { setSyncCode(syncCode) setSecretKey(secretKey) setScreen(SyncScreenToShow.ADD_DEVICE) @@ -180,28 +190,29 @@ class SyncScreenViewModel(di: DI, private val state: SavedStateHandle) : DIAware @Suppress("UNCHECKED_CAST") val deviceList = params[3] as List - val actualScreen = if (syncRemote?.syncChainId?.length == 64) { - when (screen) { - // Setup and join only possible if nothing setup already - SyncScreenToShow.SETUP, - SyncScreenToShow.JOIN, - -> SyncScreenToShow.DEVICELIST - - SyncScreenToShow.DEVICELIST, - SyncScreenToShow.ADD_DEVICE, - -> screen - } - } else { - when (screen) { - SyncScreenToShow.SETUP, - SyncScreenToShow.JOIN, - -> screen - - SyncScreenToShow.DEVICELIST, - SyncScreenToShow.ADD_DEVICE, - -> SyncScreenToShow.SETUP + val actualScreen = + if (syncRemote?.syncChainId?.length == 64) { + when (screen) { + // Setup and join only possible if nothing setup already + SyncScreenToShow.SETUP, + SyncScreenToShow.JOIN, + -> SyncScreenToShow.DEVICELIST + + SyncScreenToShow.DEVICELIST, + SyncScreenToShow.ADD_DEVICE, + -> screen + } + } else { + when (screen) { + SyncScreenToShow.SETUP, + SyncScreenToShow.JOIN, + -> screen + + SyncScreenToShow.DEVICELIST, + SyncScreenToShow.ADD_DEVICE, + -> SyncScreenToShow.SETUP + } } - } Log.v( LOG_TAG, @@ -239,16 +250,18 @@ data class SyncScreenViewState( val deviceList: List = emptyList(), ) { val leftScreenToShow: LeftScreenToShow - get() = when (singleScreenToShow) { - SyncScreenToShow.SETUP, SyncScreenToShow.JOIN -> LeftScreenToShow.SETUP - SyncScreenToShow.DEVICELIST, SyncScreenToShow.ADD_DEVICE -> LeftScreenToShow.DEVICELIST - } + get() = + when (singleScreenToShow) { + SyncScreenToShow.SETUP, SyncScreenToShow.JOIN -> LeftScreenToShow.SETUP + SyncScreenToShow.DEVICELIST, SyncScreenToShow.ADD_DEVICE -> LeftScreenToShow.DEVICELIST + } val rightScreenToShow: RightScreenToShow - get() = when (singleScreenToShow) { - SyncScreenToShow.SETUP, SyncScreenToShow.JOIN -> RightScreenToShow.JOIN - SyncScreenToShow.DEVICELIST, SyncScreenToShow.ADD_DEVICE -> RightScreenToShow.ADD_DEVICE - } + get() = + when (singleScreenToShow) { + SyncScreenToShow.SETUP, SyncScreenToShow.JOIN -> RightScreenToShow.JOIN + SyncScreenToShow.DEVICELIST, SyncScreenToShow.ADD_DEVICE -> RightScreenToShow.ADD_DEVICE + } } enum class SyncScreenToShow { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedString.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedString.kt index d9246bb017..1ed3340bb8 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedString.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedString.kt @@ -49,21 +49,18 @@ class AnnotatedParagraphStringBuilder { return false } - fun pushVerbatimTtsAnnotation(verbatim: String) = - builder.pushTtsAnnotation(VerbatimTtsAnnotation(verbatim)) + fun pushVerbatimTtsAnnotation(verbatim: String) = builder.pushTtsAnnotation(VerbatimTtsAnnotation(verbatim)) - fun pushStyle(style: SpanStyle): Int = - builder.pushStyle(style = style) + fun pushStyle(style: SpanStyle): Int = builder.pushStyle(style = style) - fun pop(index: Int) = - builder.pop(index) + fun pop(index: Int) = builder.pop(index) - fun pushStringAnnotation(tag: String, annotation: String): Int = - builder.pushStringAnnotation(tag = tag, annotation = annotation) + fun pushStringAnnotation( + tag: String, + annotation: String, + ): Int = builder.pushStringAnnotation(tag = tag, annotation = annotation) - fun pushComposableStyle( - style: @Composable () -> SpanStyle, - ): Int { + fun pushComposableStyle(style: @Composable () -> SpanStyle): Int { composableStyles.add( ComposableStyleWithStartEnd( style = style, @@ -73,9 +70,7 @@ class AnnotatedParagraphStringBuilder { return composableStyles.lastIndex } - fun popComposableStyle( - index: Int, - ) { + fun popComposableStyle(index: Int) { poppedComposableStyles.add( composableStyles.removeAt(index).copy(end = builder.length), ) @@ -123,6 +118,7 @@ class AnnotatedParagraphStringBuilder { } fun AnnotatedParagraphStringBuilder.isEmpty() = lastTwoChars.isEmpty() + fun AnnotatedParagraphStringBuilder.isNotEmpty() = lastTwoChars.isNotEmpty() private fun CharSequence.secondToLast(): Char { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedStringComposer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedStringComposer.kt index aee843ec68..9b317da006 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedStringComposer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/AnnotatedStringComposer.kt @@ -33,10 +33,11 @@ class AnnotatedStringComposer : HtmlParser() { for (span in spanStack) { when (span) { is SpanWithStyle -> builder.pushStyle(span.spanStyle) - is SpanWithAnnotation -> builder.pushStringAnnotation( - tag = span.tag, - annotation = span.annotation, - ) + is SpanWithAnnotation -> + builder.pushStringAnnotation( + tag = span.tag, + annotation = span.annotation, + ) is SpanWithComposableStyle -> builder.pushComposableStyle(span.spanStyle) is SpanWithVerbatim -> builder.pushVerbatimTtsAnnotation(span.verbatim) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/EagerComposer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/EagerComposer.kt index 9602831504..3d2d064f97 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/EagerComposer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/EagerComposer.kt @@ -26,16 +26,17 @@ class EagerComposer( emitParagraph() val url = link ?: findClosestLink() - val onClick: (() -> Unit) = when { - url?.isNotBlank() == true -> { - { - onLinkClick(url) + val onClick: (() -> Unit) = + when { + url?.isNotBlank() == true -> { + { + onLinkClick(url) + } + } + else -> { + {} } } - else -> { - {} - } - } paragraphs.add { block(onClick) @@ -67,10 +68,11 @@ class EagerComposer( for (span in spanStack) { when (span) { is SpanWithStyle -> builder.pushStyle(span.spanStyle) - is SpanWithAnnotation -> builder.pushStringAnnotation( - tag = span.tag, - annotation = span.annotation, - ) + is SpanWithAnnotation -> + builder.pushStringAnnotation( + tag = span.tag, + annotation = span.annotation, + ) is SpanWithComposableStyle -> builder.pushComposableStyle(span.spanStyle) is SpanWithVerbatim -> builder.pushVerbatimTtsAnnotation(span.verbatim) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/Extensions.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/Extensions.kt index b9439e141b..21ffd326ac 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/Extensions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/Extensions.kt @@ -22,7 +22,9 @@ fun resources(): Resources { } @Composable -fun annotatedStringResource(@StringRes id: Int): AnnotatedString { +fun annotatedStringResource( + @StringRes id: Int, +): AnnotatedString { val resources = resources() val text = resources.getText(id) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlComposer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlComposer.kt index 5ecd62432a..d095afab94 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlComposer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlComposer.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle abstract class HtmlComposer : HtmlParser() { - abstract fun appendImage( link: String? = null, onLinkClick: (String) -> Unit, @@ -28,38 +27,30 @@ abstract class HtmlParser { val endsWithWhitespace: Boolean get() = builder.endsWithWhitespace - fun append(text: String) = - builder.append(text) + fun append(text: String) = builder.append(text) - fun append(char: Char) = - builder.append(char) + fun append(char: Char) = builder.append(char) - fun pop(index: Int) = - builder.pop(index) + fun pop(index: Int) = builder.pop(index) - fun pushStyle(style: SpanStyle): Int = - builder.pushStyle(style) + fun pushStyle(style: SpanStyle): Int = builder.pushStyle(style) - fun pushSpan(span: Span) = - spanStack.add(span) + fun pushSpan(span: Span) = spanStack.add(span) - fun pushStringAnnotation(tag: String, annotation: String): Int = - builder.pushStringAnnotation(tag = tag, annotation = annotation) + fun pushStringAnnotation( + tag: String, + annotation: String, + ): Int = builder.pushStringAnnotation(tag = tag, annotation = annotation) - fun pushComposableStyle(style: @Composable () -> SpanStyle): Int = - builder.pushComposableStyle(style) + fun pushComposableStyle(style: @Composable () -> SpanStyle): Int = builder.pushComposableStyle(style) - fun popComposableStyle(index: Int) = - builder.popComposableStyle(index) + fun popComposableStyle(index: Int) = builder.popComposableStyle(index) - fun pushTextStyle(style: TextStyler) = - textStyleStack.add(style) + fun pushTextStyle(style: TextStyler) = textStyleStack.add(style) - fun popTextStyle() = - textStyleStack.removeLastOrNull() + fun popTextStyle() = textStyleStack.removeLastOrNull() - fun popSpan() = - spanStack.removeLast() + fun popSpan() = spanStack.removeLast() protected fun findClosestLink(): String? { for (span in spanStack.reversed()) { @@ -85,9 +76,7 @@ inline fun HtmlComposer.withTextStyle( } } -inline fun HtmlParser.withParagraph( - crossinline block: HtmlParser.() -> R, -): R { +inline fun HtmlParser.withParagraph(crossinline block: HtmlParser.() -> R): R { emitParagraph() return block(this).also { emitParagraph() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToAnnotatedString.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToAnnotatedString.kt index aa97e6fd01..0d8ffa25b5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToAnnotatedString.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToAnnotatedString.kt @@ -3,11 +3,11 @@ package com.nononsenseapps.feeder.ui.compose.text import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.BaselineShift -import java.io.InputStream import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode +import java.io.InputStream /** * Returns "plain text" with annotations for TTS @@ -308,11 +308,12 @@ private fun AnnotatedStringComposer.appendTextChildren( element.children() element.children() .filter { - it.tagName() in setOf( - "thead", - "tbody", - "tfoot", - ) + it.tagName() in + setOf( + "thead", + "tbody", + "tfoot", + ) } .sortedBy { when (it.tagName()) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt index 0c4153b2f8..9e53c86f5c 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposable.kt @@ -74,15 +74,15 @@ import com.nononsenseapps.feeder.ui.compose.utils.focusableInNonTouchMode import com.nononsenseapps.feeder.ui.text.Video import com.nononsenseapps.feeder.ui.text.getVideo import com.nononsenseapps.feeder.util.asUTF8Sequence -import java.io.InputStream -import kotlin.math.abs -import kotlin.math.roundToInt -import kotlin.random.Random import org.jsoup.Jsoup import org.jsoup.helper.StringUtil import org.jsoup.nodes.Element import org.jsoup.nodes.Node import org.jsoup.nodes.TextNode +import java.io.InputStream +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.random.Random private const val LOG_TAG = "FEEDER_HTMLTOCOM" @@ -133,9 +133,10 @@ private fun ParagraphText( ClickableText( text = paragraph, style = LocalTextStyle.current, - modifier = modifier - .indication(interactionSource, LocalIndication.current) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + modifier + .indication(interactionSource, LocalIndication.current) + .focusableInNonTouchMode(interactionSource = interactionSource), ) { offset -> paragraph.getStringAnnotations("URL", offset, offset) .firstOrNull() @@ -146,9 +147,10 @@ private fun ParagraphText( } else { Text( text = paragraph, - modifier = modifier - .indication(interactionSource, LocalIndication.current) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + modifier + .indication(interactionSource, LocalIndication.current) + .focusableInNonTouchMode(interactionSource = interactionSource), ) } } @@ -161,16 +163,18 @@ private fun LazyListScope.formatBody( keyHolder: ArticleItemKeyHolder, onLinkClick: (String) -> Unit, ) { - val composer = LazyListComposer(this, keyHolder = keyHolder) { paragraphBuilder, textStyler -> - val dimens = LocalDimens.current - ParagraphText( - paragraphBuilder = paragraphBuilder, - textStyler = textStyler, - modifier = Modifier - .width(dimens.maxReaderWidth), - onLinkClick = onLinkClick, - ) - } + val composer = + LazyListComposer(this, keyHolder = keyHolder) { paragraphBuilder, textStyler -> + val dimens = LocalDimens.current + ParagraphText( + paragraphBuilder = paragraphBuilder, + textStyler = textStyler, + modifier = + Modifier + .width(dimens.maxReaderWidth), + onLinkClick = onLinkClick, + ) + } composer.appendTextChildren( element.childNodes(), @@ -401,35 +405,38 @@ private fun HtmlComposer.appendTextChildren( when (this) { is LazyListComposer -> { - val composer = EagerComposer { paragraphBuilder, textStyler -> - val dimens = LocalDimens.current - val scrollState = rememberScrollState() - val interactionSource = - remember { MutableInteractionSource() } - Surface( - color = CodeBlockBackground(), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .horizontalScroll( - state = scrollState, - ) - .width(dimens.maxReaderWidth) - .indication( - interactionSource, - LocalIndication.current, - ) - .focusableInNonTouchMode(interactionSource = interactionSource), - ) { - Box(modifier = Modifier.padding(all = 4.dp)) { - Text( - text = paragraphBuilder.rememberComposableAnnotatedString(), - style = textStyler?.textStyle() - ?: CodeBlockStyle(), - softWrap = false, - ) + val composer = + EagerComposer { paragraphBuilder, textStyler -> + val dimens = LocalDimens.current + val scrollState = rememberScrollState() + val interactionSource = + remember { MutableInteractionSource() } + Surface( + color = CodeBlockBackground(), + shape = MaterialTheme.shapes.medium, + modifier = + Modifier + .horizontalScroll( + state = scrollState, + ) + .width(dimens.maxReaderWidth) + .indication( + interactionSource, + LocalIndication.current, + ) + .focusableInNonTouchMode(interactionSource = interactionSource), + ) { + Box(modifier = Modifier.padding(all = 4.dp)) { + Text( + text = paragraphBuilder.rememberComposableAnnotatedString(), + style = + textStyler?.textStyle() + ?: CodeBlockStyle(), + softWrap = false, + ) + } } } - } with(composer) { item(keyHolder) { @@ -513,24 +520,27 @@ private fun HtmlComposer.appendTextChildren( val imgElement = element.firstBestDescendantImg(baseUrl = baseUrl) if (imgElement != null) { - val composer = EagerComposer { paragraphBuilder, textStyler -> - val dimens = LocalDimens.current - ParagraphText( - paragraphBuilder = paragraphBuilder, - textStyler = textStyler, - modifier = Modifier - .width(dimens.maxReaderWidth), - onLinkClick = onLinkClick, - ) - } + val composer = + EagerComposer { paragraphBuilder, textStyler -> + val dimens = LocalDimens.current + ParagraphText( + paragraphBuilder = paragraphBuilder, + textStyler = textStyler, + modifier = + Modifier + .width(dimens.maxReaderWidth), + onLinkClick = onLinkClick, + ) + } item(keyHolder) { with(composer) { val dimens = LocalDimens.current Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .width(dimens.maxReaderWidth), + modifier = + Modifier + .width(dimens.maxReaderWidth), ) { withTextStyle(NestedTextStyle.CAPTION) { appendTextChildren( @@ -560,8 +570,9 @@ private fun HtmlComposer.appendTextChildren( val dimens = LocalDimens.current Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .width(dimens.maxReaderWidth), + modifier = + Modifier + .width(dimens.maxReaderWidth), ) { renderImage( baseUrl = baseUrl, @@ -624,8 +635,9 @@ private fun HtmlComposer.appendTextChildren( appendImage(onLinkClick = onLinkClick) { val dimens = LocalDimens.current Column( - modifier = Modifier - .width(dimens.maxReaderWidth), + modifier = + Modifier + .width(dimens.maxReaderWidth), ) { DisableSelection { BoxWithConstraints( @@ -633,29 +645,32 @@ private fun HtmlComposer.appendTextChildren( ) { val imageWidth by rememberMaxImageWidth() AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .placeholder(R.drawable.youtube_icon) - .error(R.drawable.youtube_icon) - .scale(Scale.FIT) - .size(imageWidth) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .placeholder(R.drawable.youtube_icon) + .error(R.drawable.youtube_icon) + .scale(Scale.FIT) + .size(imageWidth) + .precision(Precision.INEXACT) + .build(), contentDescription = stringResource(R.string.touch_to_play_video), - contentScale = if (dimens.hasImageAspectRatioInReader) { - ContentScale.Fit - } else { - ContentScale.FillWidth - }, - modifier = Modifier - .clickable { - onLinkClick(video.link) - } - .fillMaxWidth() - .run { - dimens.imageAspectRatioInReader?.let { ratio -> - aspectRatio(ratio) - } ?: this + contentScale = + if (dimens.hasImageAspectRatioInReader) { + ContentScale.Fit + } else { + ContentScale.FillWidth }, + modifier = + Modifier + .clickable { + onLinkClick(video.link) + } + .fillMaxWidth() + .run { + dimens.imageAspectRatioInReader?.let { ratio -> + aspectRatio(ratio) + } ?: this + }, ) } } @@ -671,13 +686,14 @@ private fun HtmlComposer.appendTextChildren( remember { MutableInteractionSource() } Text( text = stringResource(R.string.touch_to_play_video), - modifier = Modifier - .fillMaxWidth() - .indication( - interactionSource, - LocalIndication.current, - ) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + Modifier + .fillMaxWidth() + .indication( + interactionSource, + LocalIndication.current, + ) + .focusableInNonTouchMode(interactionSource = interactionSource), ) } } @@ -735,14 +751,15 @@ private fun ColumnScope.renderImage( DisableSelection { BoxWithConstraints( - modifier = Modifier - .clip(RectangleShape) - .clickable( - enabled = onClick != null, - ) { - onClick?.invoke() - } - .fillMaxWidth(), + modifier = + Modifier + .clip(RectangleShape) + .clickable( + enabled = onClick != null, + ) { + onClick?.invoke() + } + .fillMaxWidth(), ) { val imageWidth by rememberMaxImageWidth() WithTooltipIfNotBlank(tooltip = alt) { modifier -> @@ -750,18 +767,20 @@ private fun ColumnScope.renderImage( Image( rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), contentDescription = alt, - contentScale = if (dimens.hasImageAspectRatioInReader) { - ContentScale.Fit - } else { - ContentScale.FillWidth - }, - modifier = modifier - .fillMaxWidth() - .run { - dimens.imageAspectRatioInReader?.let { ratio -> - aspectRatio(ratio) - } ?: this + contentScale = + if (dimens.hasImageAspectRatioInReader) { + ContentScale.Fit + } else { + ContentScale.FillWidth }, + modifier = + modifier + .fillMaxWidth() + .run { + dimens.imageAspectRatioInReader?.let { ratio -> + aspectRatio(ratio) + } ?: this + }, ) } else { val pixelDensity = LocalDensity.current.density @@ -774,29 +793,33 @@ private fun ColumnScope.renderImage( } } AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(bestImage) - .scale(Scale.FIT) - .size(imageWidth) - .precision(Precision.INEXACT) - .build(), + model = + ImageRequest.Builder(LocalContext.current) + .data(bestImage) + .scale(Scale.FIT) + .size(imageWidth) + .precision(Precision.INEXACT) + .build(), contentDescription = alt, - placeholder = rememberTintedVectorPainter( - Icons.Outlined.Terrain, - ), + placeholder = + rememberTintedVectorPainter( + Icons.Outlined.Terrain, + ), error = rememberTintedVectorPainter(Icons.Outlined.ErrorOutline), - contentScale = if (dimens.hasImageAspectRatioInReader) { - ContentScale.Fit - } else { - ContentScale.FillWidth - }, - modifier = modifier - .fillMaxWidth() - .run { - dimens.imageAspectRatioInReader?.let { ratio -> - aspectRatio(ratio) - } ?: this + contentScale = + if (dimens.hasImageAspectRatioInReader) { + ContentScale.Fit + } else { + ContentScale.FillWidth }, + modifier = + modifier + .fillMaxWidth() + .run { + dimens.imageAspectRatioInReader?.let { ratio -> + aspectRatio(ratio) + } ?: this + }, ) } } @@ -819,10 +842,11 @@ private fun ColumnScope.renderImage( val interactionSource = remember { MutableInteractionSource() } Text( alt, - modifier = Modifier - .fillMaxWidth() - .indication(interactionSource, LocalIndication.current) - .focusableInNonTouchMode(interactionSource = interactionSource), + modifier = + Modifier + .fillMaxWidth() + .indication(interactionSource, LocalIndication.current) + .focusableInNonTouchMode(interactionSource = interactionSource), ) } } @@ -848,14 +872,15 @@ private fun LazyListComposer.appendTable( ) } else { item(keyHolder) { - val composer = EagerComposer { paragraphBuilder, textStyler -> - ParagraphText( - paragraphBuilder = paragraphBuilder, - textStyler = textStyler, - modifier = Modifier, - onLinkClick = onLinkClick, - ) - } + val composer = + EagerComposer { paragraphBuilder, textStyler -> + ParagraphText( + paragraphBuilder = paragraphBuilder, + textStyler = textStyler, + modifier = Modifier, + onLinkClick = onLinkClick, + ) + } with(composer) { tableColFirst( baseUrl = baseUrl, @@ -912,8 +937,9 @@ private fun EagerComposer.tableColFirst( val dimens = LocalDimens.current Column( verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .width(dimens.maxReaderWidth), + modifier = + Modifier + .width(dimens.maxReaderWidth), ) { key(element, baseUrl, onLinkClick) { element.children() @@ -935,11 +961,12 @@ private fun EagerComposer.tableColFirst( derivedStateOf { element.children() .filter { - it.tagName() in setOf( - "thead", - "tbody", - "tfoot", - ) + it.tagName() in + setOf( + "thead", + "tbody", + "tfoot", + ) } .sortedBy { when (it.tagName()) { @@ -963,9 +990,10 @@ private fun EagerComposer.tableColFirst( if (rowCount > 0 && colCount > 0) { LazyRow( horizontalArrangement = Arrangement.spacedBy(32.dp), - modifier = Modifier - .horizontalScroll(rememberScrollState()) - .width(dimens.maxReaderWidth), + modifier = + Modifier + .horizontalScroll(rememberScrollState()) + .width(dimens.maxReaderWidth), ) { items( count = colCount, @@ -978,12 +1006,13 @@ private fun EagerComposer.tableColFirst( val (section, rowElement) = rowData.getOrNull(rowIndex) ?: break var emptyCell = false Surface( - tonalElevation = when (section) { - "thead" -> 3.dp - "tbody" -> 0.dp - "tfoot" -> 1.dp - else -> 0.dp - }, + tonalElevation = + when (section) { + "thead" -> 3.dp + "tbody" -> 0.dp + "tfoot" -> 1.dp + else -> 0.dp + }, ) { rowElement.children() .filter { it.tagName() in setOf("th", "td") } @@ -1035,11 +1064,12 @@ fun Iterable.elementAtOrNullWithSpans(index: Int): Element? { return it } val spans = it.attr("colspan") ?: "1" - currentColumn += when (val spanCount = spans.toIntOrNull()) { - null, 1 -> (spanCount ?: 1) - 0 -> return null // Firefox special - spans to end - else -> spanCount.coerceAtLeast(1) - } + currentColumn += + when (val spanCount = spans.toIntOrNull()) { + null, 1 -> (spanCount ?: 1) + 0 -> return null // Firefox special - spans to end + else -> spanCount.coerceAtLeast(1) + } } return null } @@ -1091,19 +1121,20 @@ private fun Element.notAncestorOf(tagName: String): Boolean { while (current != null) { val parent = current.parent() - current = when { - parent == null || parent.tagName() == "#root" -> { - null - } + current = + when { + parent == null || parent.tagName() == "#root" -> { + null + } - parent.tagName() == tagName -> { - return false - } + parent.tagName() == tagName -> { + return false + } - else -> { - parent + else -> { + parent + } } - } } return true @@ -1112,54 +1143,62 @@ private fun Element.notAncestorOf(tagName: String): Boolean { private enum class NestedTextStyle : TextStyler { CAPTION { @Composable - override fun textStyle() = MaterialTheme.typography.labelMedium.merge( - TextStyle(color = MaterialTheme.colorScheme.onBackground), - ) + override fun textStyle() = + MaterialTheme.typography.labelMedium.merge( + TextStyle(color = MaterialTheme.colorScheme.onBackground), + ) }, } -private fun String.asFontFamily(): FontFamily? = when (this.lowercase()) { - "monospace" -> FontFamily.Monospace - "serif" -> FontFamily.Serif - "sans-serif" -> FontFamily.SansSerif - else -> null -} +private fun String.asFontFamily(): FontFamily? = + when (this.lowercase()) { + "monospace" -> FontFamily.Monospace + "serif" -> FontFamily.Serif + "sans-serif" -> FontFamily.SansSerif + else -> null + } @Preview @Composable private fun TestIt() { - val html = """ + val html = + """

In Gimp you go to Image in the top menu bar and select Mode followed by Indexed. Now you see a popup where you can select the number of colors for a generated optimum palette.

You’ll have to experiment a little because it will depend on your image.

I used this approach to shrink the size of the cover image in the_zopfli post from a 37KB (JPG) to just 15KB (PNG, all PNG sizes listed include Zopfli compression btw).

Straight JPG to PNG conversion: 124KB

PNG version RGB colors

First off, I exported the JPG file as a PNG file. This PNG file had a whopping 124KB! Clearly there was some bloat being stored.

256 colors: 40KB

Reducing from RGB to only 256 colors has no visible effect to my eyes.

256 colors

128 colors: 34KB

Still no difference.

128 colors

64 colors: 25KB

You can start to see some artifacting in the shadow behind the text.

64 colors

32 colors: 15KB

In my opinion this is the sweet spot. The shadow artifacting is barely noticable but the size is significantly reduced.

32 colors

16 colors: 11KB

Clear artifacting in the text shadow and the yellow (fire?) in the background has developed an outline.

16 colors

8 colors: 7.3KB

The broom has shifted in color from a clear brown to almost grey. Text shadow is just a grey blob at this point. Even clearer outline developed on the yellow background.

8 colors

4 colors: 4.3KB

Interestingly enough, I think 4 colors looks better than 8 colors. The outline in the background has disappeared because there’s not enough color spectrum to render it. The broom is now black and filled areas tend to get a white separator to the outlines.

4 colors

2 colors: 2.4KB

Well, at least the silhouette is well defined at this point I guess.

2 colors


Other posts in the Migrating from Ghost to Hugo series:

- """.trimIndent() + """.trimIndent() html.byteInputStream().use { stream -> LazyColumn { htmlFormattedText( inputStream = stream, baseUrl = "https://cowboyprogrammer.org", - keyHolder = object : ArticleItemKeyHolder { - override fun getAndIncrementKey(): Long { - return Random.nextLong() - } - }, + keyHolder = + object : ArticleItemKeyHolder { + override fun getAndIncrementKey(): Long { + return Random.nextLong() + } + }, ) {} } } } @Composable -fun BoxWithConstraintsScope.rememberMaxImageWidth() = with(LocalDensity.current) { - remember { - derivedStateOf { - maxWidth.toPx().roundToInt().coerceAtMost(2000) +fun BoxWithConstraintsScope.rememberMaxImageWidth() = + with(LocalDensity.current) { + remember { + derivedStateOf { + maxWidth.toPx().roundToInt().coerceAtMost(2000) + } } } -} /** * Gets the url to the image in the tag - could be from srcset or from src */ -internal fun getImageSource(baseUrl: String, element: Element) = ImageCandidates( +internal fun getImageSource( + baseUrl: String, + element: Element, +) = ImageCandidates( baseUrl = baseUrl, srcSet = element.attr("srcset") ?: "", absSrc = element.attr("abs:src") ?: "", @@ -1176,39 +1215,45 @@ internal class ImageCandidates( /** * Might throw if hasImage returns false */ - fun getBestImageForMaxSize(maxWidth: Int, pixelDensity: Float): String { - val setCandidate = srcSet.splitToSequence(", ") - .map { it.trim() } - .map { it.split(spaceRegex).take(2).map { x -> x.trim() } } - .fold(100f to "") { acc, candidate -> - val candidateSize = if (candidate.size == 1) { - // Assume it corresponds to 1x pixel density - 1.0f / pixelDensity - } else { - val descriptor = candidate.last() - when { - descriptor.endsWith("w", ignoreCase = true) -> { - descriptor.substringBefore("w").toFloat() / maxWidth - .toFloat() - } + fun getBestImageForMaxSize( + maxWidth: Int, + pixelDensity: Float, + ): String { + val setCandidate = + srcSet.splitToSequence(", ") + .map { it.trim() } + .map { it.split(spaceRegex).take(2).map { x -> x.trim() } } + .fold(100f to "") { acc, candidate -> + val candidateSize = + if (candidate.size == 1) { + // Assume it corresponds to 1x pixel density + 1.0f / pixelDensity + } else { + val descriptor = candidate.last() + when { + descriptor.endsWith("w", ignoreCase = true) -> { + descriptor.substringBefore("w").toFloat() / + maxWidth + .toFloat() + } - descriptor.endsWith("x", ignoreCase = true) -> { - descriptor.substringBefore("x").toFloat() / pixelDensity - } + descriptor.endsWith("x", ignoreCase = true) -> { + descriptor.substringBefore("x").toFloat() / pixelDensity + } - else -> { - return@fold acc + else -> { + return@fold acc + } + } } - } - } - if (abs(candidateSize - 1.0f) < abs(acc.first - 1.0f)) { - candidateSize to candidate.first() - } else { - acc + if (abs(candidateSize - 1.0f) < abs(acc.first - 1.0f)) { + candidateSize to candidate.first() + } else { + acc + } } - } - .second + .second return (setCandidate.takeIf { it.isNotBlank() } ?: absSrc.takeIf { it.isNotBlank() })?.let { StringUtil.resolve(baseUrl, it) @@ -1254,10 +1299,11 @@ fun Element.appendCorrectlyNormalizedWhiteSpaceRecursively( for (child in childNodes()) { when (child) { is TextNode -> child.appendCorrectlyNormalizedWhiteSpace(builder, stripLeading) - is Element -> child.appendCorrectlyNormalizedWhiteSpaceRecursively( - builder, - stripLeading, - ) + is Element -> + child.appendCorrectlyNormalizedWhiteSpaceRecursively( + builder, + stripLeading, + ) } } } @@ -1273,8 +1319,7 @@ private const val formFeed = 12.toChar() // 160 is   (non-breaking space). Not in the spec but expected. private const val nonBreakableSpace = 160.toChar() -private fun isCollapsableWhiteSpace(c: String) = - c.firstOrNull()?.let { isCollapsableWhiteSpace(it) } ?: false +private fun isCollapsableWhiteSpace(c: String) = c.firstOrNull()?.let { isCollapsableWhiteSpace(it) } ?: false private fun isCollapsableWhiteSpace(c: Char) = c == space || c == tab || c == linefeed || c == carriageReturn || c == formFeed || c == nonBreakableSpace diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/LazyListComposer.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/LazyListComposer.kt index 194821ba24..2c48d37c11 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/LazyListComposer.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/text/LazyListComposer.kt @@ -9,7 +9,6 @@ class LazyListComposer( private val keyHolder: ArticleItemKeyHolder, private val paragraphEmitter: @Composable (AnnotatedParagraphStringBuilder, TextStyler?) -> Unit, ) : HtmlComposer() { - override fun emitParagraph(): Boolean { // List items emit dots and non-breaking space. Don't newline after that if (builder.isEmpty() || builder.endsWithNonBreakingSpace) { @@ -37,16 +36,17 @@ class LazyListComposer( emitParagraph() val url = link ?: findClosestLink() - val onClick: (() -> Unit) = when { - url?.isNotBlank() == true -> { - { - onLinkClick(url) + val onClick: (() -> Unit) = + when { + url?.isNotBlank() == true -> { + { + onLinkClick(url) + } + } + else -> { + {} } } - else -> { - {} - } - } item(keyHolder = keyHolder) { block(onClick) @@ -57,7 +57,10 @@ class LazyListComposer( * Key is necessary or when you switch between default and full text - the initial items * will have the same index and will not recompose. */ - fun item(keyHolder: ArticleItemKeyHolder, block: @Composable () -> Unit) { + fun item( + keyHolder: ArticleItemKeyHolder, + block: @Composable () -> Unit, + ) { lazyListScope.item(key = keyHolder.getAndIncrementKey()) { block() } @@ -69,10 +72,11 @@ class LazyListComposer( for (span in spanStack) { when (span) { is SpanWithStyle -> builder.pushStyle(span.spanStyle) - is SpanWithAnnotation -> builder.pushStringAnnotation( - tag = span.tag, - annotation = span.annotation, - ) + is SpanWithAnnotation -> + builder.pushStringAnnotation( + tag = span.tag, + annotation = span.annotation, + ) is SpanWithComposableStyle -> builder.pushComposableStyle(span.spanStyle) is SpanWithVerbatim -> builder.pushVerbatimTtsAnnotation(span.verbatim) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Dimensions.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Dimensions.kt index 1c3e7e80af..7146c6e799 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Dimensions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Dimensions.kt @@ -54,25 +54,27 @@ class Dimensions( val Dimensions.hasImageAspectRatioInReader: Boolean get() = imageAspectRatioInReader != null -val phoneDimensions = Dimensions( - maxContentWidth = 840.dp, - maxReaderWidth = 840.dp, - imageAspectRatioInReader = null, - navIconMargin = 16.dp, - margin = 16.dp, - gutter = 16.dp, - layoutColumns = 4, - feedScreenColumns = 1, -) +val phoneDimensions = + Dimensions( + maxContentWidth = 840.dp, + maxReaderWidth = 840.dp, + imageAspectRatioInReader = null, + navIconMargin = 16.dp, + margin = 16.dp, + gutter = 16.dp, + layoutColumns = 4, + feedScreenColumns = 1, + ) fun tabletDimensions(screenWidthDp: Int): Dimensions { // Items look good at around 300dp width. Account for 32dp margin at the sides, and the gutters // 3 columns: 3*300 + 4*32 = 1028 - val columns = when { - screenWidthDp > 1360 -> 4 - screenWidthDp > 1028 -> 3 - else -> 2 - } + val columns = + when { + screenWidthDp > 1360 -> 4 + screenWidthDp > 1028 -> 3 + else -> 2 + } return Dimensions( maxContentWidth = 840.dp, maxReaderWidth = 640.dp, @@ -85,32 +87,33 @@ fun tabletDimensions(screenWidthDp: Int): Dimensions { ) } -val tvDimensions = Dimensions( - maxContentWidth = 840.dp, - maxReaderWidth = 640.dp, - imageAspectRatioInReader = 16.0f / 9.0f, - navIconMargin = 32.dp, - margin = 32.dp, - gutter = 32.dp, - layoutColumns = 12, - feedScreenColumns = 3, -) +val tvDimensions = + Dimensions( + maxContentWidth = 840.dp, + maxReaderWidth = 640.dp, + imageAspectRatioInReader = 16.0f / 9.0f, + navIconMargin = 32.dp, + margin = 32.dp, + gutter = 32.dp, + layoutColumns = 12, + feedScreenColumns = 3, + ) -val LocalDimens = staticCompositionLocalOf { - phoneDimensions -} +val LocalDimens = + staticCompositionLocalOf { + phoneDimensions + } @Composable -fun ProvideDimens( - content: @Composable () -> Unit, -) { +fun ProvideDimens(content: @Composable () -> Unit) { val config = LocalConfiguration.current - val dimensionSet = remember { - when { - config.screenWidthDp == 960 && config.screenHeightDp == 540 -> tvDimensions - config.smallestScreenWidthDp >= 600 -> tabletDimensions(config.screenWidthDp) - else -> phoneDimensions + val dimensionSet = + remember { + when { + config.screenWidthDp == 960 && config.screenHeightDp == 540 -> tvDimensions + config.smallestScreenWidthDp >= 600 -> tabletDimensions(config.screenWidthDp) + else -> phoneDimensions + } } - } CompositionLocalProvider(LocalDimens provides dimensionSet, content = content) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/SensibleTopAppBar.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/SensibleTopAppBar.kt index 92e74a0a5f..7fa538cc0a 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/SensibleTopAppBar.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/SensibleTopAppBar.kt @@ -44,9 +44,10 @@ fun SensibleTopAppBar( navigationIcon = navigationIcon, actions = actions, modifier = modifier, - colors = topAppBarColors( - containerColor = MaterialTheme.colorScheme.background, - scrolledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), - ), + colors = + topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + ), ) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/StatusBarColor.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/StatusBarColor.kt index 2be5faa546..b426ae7da7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/StatusBarColor.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/StatusBarColor.kt @@ -17,9 +17,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SetStatusBarColorToMatchScrollableTopAppBar( - scrollBehavior: TopAppBarScrollBehavior, -) { +fun SetStatusBarColorToMatchScrollableTopAppBar(scrollBehavior: TopAppBarScrollBehavior) { // This is what is changed by Black theme val surfaceColor = MaterialTheme.colorScheme.background val surfaceScrolledColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) @@ -28,11 +26,12 @@ fun SetStatusBarColorToMatchScrollableTopAppBar( val fraction = if (colorTransitionFraction > 0.01f) 1f else 0f val appBarContainerColor by animateColorAsState( - targetValue = lerp( - surfaceColor, - surfaceScrolledColor, - FastOutLinearInEasing.transform(fraction), - ), + targetValue = + lerp( + surfaceColor, + surfaceScrolledColor, + FastOutLinearInEasing.transform(fraction), + ), animationSpec = spring(stiffness = Spring.StiffnessMediumLow), ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Theme.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Theme.kt index 4661b72161..8749ac1354 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Theme.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Theme.kt @@ -16,95 +16,98 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.nononsenseapps.feeder.archmodel.DarkThemePreferences import com.nononsenseapps.feeder.archmodel.ThemeOptions -private val lightColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, -) +private val lightColors = + lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + ) -private val darkColors = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, -) +private val darkColors = + darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + ) -private val eInkColors = lightColorScheme( - primary = md_theme_eink_primary, - onPrimary = md_theme_eink_onPrimary, - primaryContainer = md_theme_eink_primaryContainer, - onPrimaryContainer = md_theme_eink_onPrimaryContainer, - secondary = md_theme_eink_secondary, - onSecondary = md_theme_eink_onSecondary, - secondaryContainer = md_theme_eink_secondaryContainer, - onSecondaryContainer = md_theme_eink_onSecondaryContainer, - tertiary = md_theme_eink_tertiary, - onTertiary = md_theme_eink_onTertiary, - tertiaryContainer = md_theme_eink_tertiaryContainer, - onTertiaryContainer = md_theme_eink_onTertiaryContainer, - error = md_theme_eink_error, - errorContainer = md_theme_eink_errorContainer, - onError = md_theme_eink_onError, - onErrorContainer = md_theme_eink_onErrorContainer, - background = md_theme_eink_background, - onBackground = md_theme_eink_onBackground, - surface = md_theme_eink_surface, - onSurface = md_theme_eink_onSurface, - surfaceVariant = md_theme_eink_surfaceVariant, - onSurfaceVariant = md_theme_eink_onSurfaceVariant, - outline = md_theme_eink_outline, - inverseOnSurface = md_theme_eink_inverseOnSurface, - inverseSurface = md_theme_eink_inverseSurface, - inversePrimary = md_theme_eink_inversePrimary, - surfaceTint = md_theme_eink_surfaceTint, -) +private val eInkColors = + lightColorScheme( + primary = md_theme_eink_primary, + onPrimary = md_theme_eink_onPrimary, + primaryContainer = md_theme_eink_primaryContainer, + onPrimaryContainer = md_theme_eink_onPrimaryContainer, + secondary = md_theme_eink_secondary, + onSecondary = md_theme_eink_onSecondary, + secondaryContainer = md_theme_eink_secondaryContainer, + onSecondaryContainer = md_theme_eink_onSecondaryContainer, + tertiary = md_theme_eink_tertiary, + onTertiary = md_theme_eink_onTertiary, + tertiaryContainer = md_theme_eink_tertiaryContainer, + onTertiaryContainer = md_theme_eink_onTertiaryContainer, + error = md_theme_eink_error, + errorContainer = md_theme_eink_errorContainer, + onError = md_theme_eink_onError, + onErrorContainer = md_theme_eink_onErrorContainer, + background = md_theme_eink_background, + onBackground = md_theme_eink_onBackground, + surface = md_theme_eink_surface, + onSurface = md_theme_eink_onSurface, + surfaceVariant = md_theme_eink_surfaceVariant, + onSurfaceVariant = md_theme_eink_onSurfaceVariant, + outline = md_theme_eink_outline, + inverseOnSurface = md_theme_eink_inverseOnSurface, + inverseSurface = md_theme_eink_inverseSurface, + inversePrimary = md_theme_eink_inversePrimary, + surfaceTint = md_theme_eink_surfaceTint, + ) /** * Only use this in the root of the activity @@ -146,13 +149,14 @@ fun FeederTheme( @Composable private fun ThemeOptions.isDarkSystemIcons(): Boolean { - val isDarkTheme = when (this) { - ThemeOptions.DAY, - ThemeOptions.E_INK, - -> false - ThemeOptions.NIGHT -> true - ThemeOptions.SYSTEM -> isSystemInDarkTheme() - } + val isDarkTheme = + when (this) { + ThemeOptions.DAY, + ThemeOptions.E_INK, + -> false + ThemeOptions.NIGHT -> true + ThemeOptions.SYSTEM -> isSystemInDarkTheme() + } return !isDarkTheme } @@ -180,32 +184,35 @@ private fun ThemeOptions.getColorScheme( darkThemePreference: DarkThemePreferences, dynamicColors: Boolean, ): ColorScheme { - val dark = when (this) { - ThemeOptions.DAY, - ThemeOptions.E_INK, - -> false - ThemeOptions.NIGHT -> true - ThemeOptions.SYSTEM -> { - isSystemInDarkTheme() + val dark = + when (this) { + ThemeOptions.DAY, + ThemeOptions.E_INK, + -> false + ThemeOptions.NIGHT -> true + ThemeOptions.SYSTEM -> { + isSystemInDarkTheme() + } } - } - val colorScheme = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColors && dark -> { - dynamicDarkColorScheme(LocalContext.current) - } + val colorScheme = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColors && dark -> { + dynamicDarkColorScheme(LocalContext.current) + } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColors && !dark -> { - dynamicLightColorScheme(LocalContext.current) - } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && dynamicColors && !dark -> { + dynamicLightColorScheme(LocalContext.current) + } - dark -> darkColors - else -> if (this == ThemeOptions.E_INK) { - eInkColors - } else { - lightColors + dark -> darkColors + else -> + if (this == ThemeOptions.E_INK) { + eInkColors + } else { + lightColors + } } - } return if (dark && darkThemePreference == DarkThemePreferences.BLACK) { colorScheme.copy( diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Typography.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Typography.kt index 2e089bfb81..ddc3342731 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Typography.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/theme/Typography.kt @@ -20,39 +20,45 @@ object FeederTypography { val typography: Typography = materialTypography.copy( - headlineLarge = materialTypography.headlineLarge.merge( - TextStyle( - lineBreak = LineBreak.Paragraph, + headlineLarge = + materialTypography.headlineLarge.merge( + TextStyle( + lineBreak = LineBreak.Paragraph, + ), ), - ), - headlineMedium = materialTypography.headlineMedium.merge( - TextStyle( - lineBreak = LineBreak.Paragraph, + headlineMedium = + materialTypography.headlineMedium.merge( + TextStyle( + lineBreak = LineBreak.Paragraph, + ), ), - ), - headlineSmall = materialTypography.headlineSmall.merge( - TextStyle( - lineBreak = LineBreak.Paragraph, + headlineSmall = + materialTypography.headlineSmall.merge( + TextStyle( + lineBreak = LineBreak.Paragraph, + ), ), - ), - bodyLarge = materialTypography.bodyLarge.merge( - TextStyle( - hyphens = Hyphens.Auto, - lineBreak = LineBreak.Paragraph, + bodyLarge = + materialTypography.bodyLarge.merge( + TextStyle( + hyphens = Hyphens.Auto, + lineBreak = LineBreak.Paragraph, + ), ), - ), - bodyMedium = materialTypography.bodyMedium.merge( - TextStyle( - hyphens = Hyphens.Auto, - lineBreak = LineBreak.Paragraph, + bodyMedium = + materialTypography.bodyMedium.merge( + TextStyle( + hyphens = Hyphens.Auto, + lineBreak = LineBreak.Paragraph, + ), ), - ), - bodySmall = materialTypography.bodySmall.merge( - TextStyle( - hyphens = Hyphens.Auto, - lineBreak = LineBreak.Paragraph, + bodySmall = + materialTypography.bodySmall.merge( + TextStyle( + hyphens = Hyphens.Auto, + lineBreak = LineBreak.Paragraph, + ), ), - ), ) } @@ -80,25 +86,22 @@ fun FeedListItemTitleTextStyle(): TextStyle = fun FeedListItemSnippetTextStyle(): TextStyle = MaterialTheme.typography.titleSmall.merge( TextStyle( - lineBreak = LineBreak.Paragraph, hyphens = Hyphens.Auto, + lineBreak = LineBreak.Paragraph, + hyphens = Hyphens.Auto, ), ) @Composable -fun FeedListItemStyle(): TextStyle = - MaterialTheme.typography.bodyLarge +fun FeedListItemStyle(): TextStyle = MaterialTheme.typography.bodyLarge @Composable -fun FeedListItemFeedTitleStyle(): TextStyle = - FeedListItemDateStyle() +fun FeedListItemFeedTitleStyle(): TextStyle = FeedListItemDateStyle() @Composable -fun FeedListItemDateStyle(): TextStyle = - MaterialTheme.typography.labelMedium +fun FeedListItemDateStyle(): TextStyle = MaterialTheme.typography.labelMedium @Composable -fun TTSPlayerStyle(): TextStyle = - MaterialTheme.typography.titleMedium +fun TTSPlayerStyle(): TextStyle = MaterialTheme.typography.titleMedium @Composable fun CodeInlineStyle(): SpanStyle = @@ -119,8 +122,7 @@ fun CodeBlockStyle(): TextStyle = ) @Composable -fun CodeBlockBackground(): Color = - MaterialTheme.colorScheme.surfaceVariant +fun CodeBlockBackground(): Color = MaterialTheme.colorScheme.surfaceVariant @Composable fun BlockQuoteStyle(): SpanStyle = @@ -135,17 +137,19 @@ data class TypographySettings( val fontScale: Float = 1.0f, ) -val LocalTypographySettings = staticCompositionLocalOf { - TypographySettings() -} +val LocalTypographySettings = + staticCompositionLocalOf { + TypographySettings() + } @Composable fun ProvideFontScale( fontScale: Float, content: @Composable () -> Unit, ) { - val typographySettings = TypographySettings( - fontScale = fontScale, - ) + val typographySettings = + TypographySettings( + fontScale = fontScale, + ) CompositionLocalProvider(LocalTypographySettings provides typographySettings, content = content) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt index 4b1d6ed43f..98ce8606df 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/ComposeProviders.kt @@ -11,9 +11,7 @@ import com.nononsenseapps.feeder.ui.compose.theme.ProvideFontScale import org.kodein.di.compose.withDI @Composable -fun DIAwareComponentActivity.withAllProviders( - content: @Composable () -> Unit, -) { +fun DIAwareComponentActivity.withAllProviders(content: @Composable () -> Unit) { withDI { val viewModel: CommonActivityViewModel = diAwareViewModel() val currentTheme by viewModel.currentTheme.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/FeederTextToolbar.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/FeederTextToolbar.kt index 03e10f85bc..db610d7cdc 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/FeederTextToolbar.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/FeederTextToolbar.kt @@ -36,13 +36,14 @@ fun WithFeederTextToolbar(content: @Composable () -> Unit) { class FeederTextToolbar(private val view: View, activityLauncher: ActivityLauncher) : TextToolbar { private var actionMode: ActionMode? = null - private val textActionModeCallback: FeederTextActionModeCallback = FeederTextActionModeCallback( - context = view.context, - activityLauncher = activityLauncher, - onActionModeDestroy = { - actionMode = null - }, - ) + private val textActionModeCallback: FeederTextActionModeCallback = + FeederTextActionModeCallback( + context = view.context, + activityLauncher = activityLauncher, + onActionModeDestroy = { + actionMode = null + }, + ) override var status: TextToolbarStatus = TextToolbarStatus.Hidden private set @@ -66,10 +67,11 @@ class FeederTextToolbar(private val view: View, activityLauncher: ActivityLaunch textActionModeCallback.onSelectAllRequested = onSelectAllRequested if (actionMode == null) { status = TextToolbarStatus.Shown - actionMode = view.startActionMode( - FloatingTextActionModeCallback(textActionModeCallback), - ActionMode.TYPE_FLOATING, - ) + actionMode = + view.startActionMode( + FloatingTextActionModeCallback(textActionModeCallback), + ActionMode.TYPE_FLOATING, + ) } else { actionMode?.invalidate() } @@ -98,7 +100,10 @@ class FeederTextActionModeCallback( private val textProcessors = mutableListOf() - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + override fun onCreateActionMode( + mode: ActionMode?, + menu: Menu?, + ): Boolean { requireNotNull(menu) requireNotNull(mode) @@ -121,14 +126,20 @@ class FeederTextActionModeCallback( return true } - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + override fun onPrepareActionMode( + mode: ActionMode?, + menu: Menu?, + ): Boolean { if (mode == null || menu == null) return false updateMenuItems(menu) // should return true so that new menu items are populated return true } - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + override fun onActionItemClicked( + mode: ActionMode?, + item: MenuItem?, + ): Boolean { when (val itemId = item!!.itemId) { MenuItemOption.Copy.id -> onCopyRequested?.invoke() MenuItemOption.Paste.id -> onPasteRequested?.invoke() @@ -146,11 +157,12 @@ class FeederTextActionModeCallback( textProcessors.getOrNull(itemId - 100)?.let { cn -> activityLauncher.startActivity( openAdjacentIfSuitable = true, - intent = Intent(Intent.ACTION_PROCESS_TEXT).apply { - type = "text/plain" - component = cn - putExtra(Intent.EXTRA_PROCESS_TEXT, clip.getItemAt(0).text) - }, + intent = + Intent(Intent.ACTION_PROCESS_TEXT).apply { + type = "text/plain" + component = cn + putExtra(Intent.EXTRA_PROCESS_TEXT, clip.getItemAt(0).text) + }, ) } } @@ -175,9 +187,10 @@ class FeederTextActionModeCallback( private fun addTextProcessors(menu: Menu) { textProcessors.clear() - val intent = Intent(Intent.ACTION_PROCESS_TEXT).apply { - type = "text/plain" - } + val intent = + Intent(Intent.ACTION_PROCESS_TEXT).apply { + type = "text/plain" + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0L)) @@ -215,7 +228,10 @@ class FeederTextActionModeCallback( } } - private fun addMenuItem(menu: Menu, item: MenuItemOption) { + private fun addMenuItem( + menu: Menu, + item: MenuItemOption, + ) { menu.add(0, item.id, item.order, item.titleResource) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) } @@ -240,12 +256,13 @@ internal enum class MenuItemOption(val id: Int) { ; val titleResource: Int - get() = when (this) { - Copy -> android.R.string.copy - Paste -> android.R.string.paste - Cut -> android.R.string.cut - SelectAll -> android.R.string.selectAll - } + get() = + when (this) { + Copy -> android.R.string.copy + Paste -> android.R.string.paste + Cut -> android.R.string.cut + SelectAll -> android.R.string.selectAll + } /** * This item will be shown before all items that have order greater than this value. @@ -256,15 +273,24 @@ internal enum class MenuItemOption(val id: Int) { internal class FloatingTextActionModeCallback( private val callback: FeederTextActionModeCallback, ) : ActionMode.Callback2() { - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + override fun onActionItemClicked( + mode: ActionMode?, + item: MenuItem?, + ): Boolean { return callback.onActionItemClicked(mode, item) } - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + override fun onCreateActionMode( + mode: ActionMode?, + menu: Menu?, + ): Boolean { return callback.onCreateActionMode(mode, menu) } - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + override fun onPrepareActionMode( + mode: ActionMode?, + menu: Menu?, + ): Boolean { return callback.onPrepareActionMode(mode, menu) } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt index 4eb4667ff7..0843b7ead2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Focusable.kt @@ -14,35 +14,36 @@ import androidx.compose.ui.platform.LocalInputModeManager import androidx.compose.ui.platform.debugInspectorInfo @OptIn(ExperimentalComposeUiApi::class) -fun Modifier.onKeyEventLikeEscape( - action: () -> Unit, -) = composed( - inspectorInfo = debugInspectorInfo { - name = "onEscapeLikeKeyPress" - properties["action"] = action - }, -) { - onKeyEvent { - when (it.key) { - Key.Escape, Key.Back, Key.NavigateOut -> { - action() - true - } +fun Modifier.onKeyEventLikeEscape(action: () -> Unit) = + composed( + inspectorInfo = + debugInspectorInfo { + name = "onEscapeLikeKeyPress" + properties["action"] = action + }, + ) { + onKeyEvent { + when (it.key) { + Key.Escape, Key.Back, Key.NavigateOut -> { + action() + true + } - else -> false + else -> false + } } } -} fun Modifier.focusableInNonTouchMode( enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, ) = composed( - inspectorInfo = debugInspectorInfo { - name = "focusableInNonTouchMode" - properties["enabled"] = enabled - properties["interactionSource"] = interactionSource - }, + inspectorInfo = + debugInspectorInfo { + name = "focusableInNonTouchMode" + properties["enabled"] = enabled + properties["interactionSource"] = interactionSource + }, ) { val inputModeManager = LocalInputModeManager.current Modifier diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Foldables.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Foldables.kt index 7f39402eaa..a5a477de63 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Foldables.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/Foldables.kt @@ -22,13 +22,15 @@ val LocalFoldableHinge: ProvidableCompositionLocal = @Composable fun Activity.withFoldableHinge(content: @Composable () -> Unit) { val displayFeatures = calculateDisplayFeatures(this) - val fold = displayFeatures.find { - it is FoldingFeature - } as FoldingFeature? + val fold = + displayFeatures.find { + it is FoldingFeature + } as FoldingFeature? - val foldableHinge = fold?.let { - FoldableHinge(it.bounds.toComposeRect()) - } + val foldableHinge = + fold?.let { + FoldableHinge(it.bounds.toComposeRect()) + } CompositionLocalProvider(LocalFoldableHinge provides foldableHinge) { content() diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LazyList.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LazyList.kt index 8476062376..b313fbf8f2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LazyList.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LazyList.kt @@ -16,9 +16,10 @@ import kotlinx.coroutines.flow.distinctUntilChanged */ @Composable fun LazyListState.rememberIsItemVisible(key: Any): State { - val isVisible = remember { - mutableStateOf(layoutInfo.visibleItemsInfo.any { it.key == key }) - } + val isVisible = + remember { + mutableStateOf(layoutInfo.visibleItemsInfo.any { it.key == key }) + } LaunchedEffect(this, key) { snapshotFlow { layoutInfo.visibleItemsInfo.any { it.key == key } @@ -39,9 +40,10 @@ fun LazyListState.rememberIsItemVisible(key: Any): State { @OptIn(ExperimentalFoundationApi::class) @Composable fun LazyStaggeredGridState.rememberIsItemVisible(key: Any): State { - val isVisible = remember { - mutableStateOf(layoutInfo.visibleItemsInfo.any { it.key == key }) - } + val isVisible = + remember { + mutableStateOf(layoutInfo.visibleItemsInfo.any { it.key == key }) + } LaunchedEffect(this, key) { snapshotFlow { layoutInfo.visibleItemsInfo.any { it.key == key } @@ -58,14 +60,19 @@ fun LazyStaggeredGridState.rememberIsItemVisible(key: Any): State { * Becomes true the item is mostly visible on screen. */ @Composable -fun LazyListState.rememberIsItemMostlyVisible(key: Any, screenHeightPx: Int): State { - val result = remember { - mutableStateOf(false) - } +fun LazyListState.rememberIsItemMostlyVisible( + key: Any, + screenHeightPx: Int, +): State { + val result = + remember { + mutableStateOf(false) + } LaunchedEffect(this, key) { snapshotFlow { - val item = layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } - ?: return@snapshotFlow false + val item = + layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } + ?: return@snapshotFlow false /* // constrained height @@ -82,11 +89,13 @@ fun LazyListState.rememberIsItemMostlyVisible(key: Any, screenHeightPx: Int): St val vhp = (vh * 100) / h */ - val visibleHeightPercentage = ( + val visibleHeightPercentage = ( - (item.offset + item.size).coerceAtMost(screenHeightPx) - item.offset.coerceAtLeast( - 0, - ) + ( + (item.offset + item.size).coerceAtMost(screenHeightPx) - + item.offset.coerceAtLeast( + 0, + ) ) * 100 ) / item.size.coerceAtMost(screenHeightPx) @@ -112,19 +121,23 @@ fun LazyStaggeredGridState.rememberIsItemMostlyVisible( key: Any, screenHeightPx: Int, ): State { - val result = remember { - mutableStateOf(false) - } + val result = + remember { + mutableStateOf(false) + } LaunchedEffect(this, key) { snapshotFlow { - val item = layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } - ?: return@snapshotFlow false + val item = + layoutInfo.visibleItemsInfo.firstOrNull { it.key == key } + ?: return@snapshotFlow false - val visibleHeightPercentage = ( + val visibleHeightPercentage = ( - (item.offset.y + item.size.height).coerceAtMost(screenHeightPx) - item.offset.y.coerceAtLeast( - 0, - ) + ( + (item.offset.y + item.size.height).coerceAtMost(screenHeightPx) - + item.offset.y.coerceAtLeast( + 0, + ) ) * 100 ) / item.size.height.coerceAtMost(screenHeightPx) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LogCompositions.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LogCompositions.kt index af38fbe3b6..73da7b1bfe 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LogCompositions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/LogCompositions.kt @@ -13,7 +13,10 @@ class Ref(var value: Int) // original call site. @Suppress("NOTHING_TO_INLINE") @Composable -inline fun LogCompositions(tag: String, msg: String) { +inline fun LogCompositions( + tag: String, + msg: String, +) { if (BuildConfig.DEBUG) { val ref = remember { Ref(0) } SideEffect { ref.value++ } diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/MutableSavedState.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/MutableSavedState.kt index 1a4d3e3f0a..3ae5031d30 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/MutableSavedState.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/MutableSavedState.kt @@ -16,7 +16,10 @@ class DelegatedMutableSavedState( private var initialized: Boolean = false private var value: T by mutableStateOf(defaultValue) - operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + operator fun getValue( + thisRef: Any?, + property: KProperty<*>, + ): T { if (!initialized) { value = savedStateHandle[property.name] ?: value initialized = true @@ -25,7 +28,11 @@ class DelegatedMutableSavedState( return value } - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + operator fun setValue( + thisRef: Any?, + property: KProperty<*>, + value: T, + ) { savedStateHandle[property.name] = value this.value = value onChange?.invoke(value) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowInsets.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowInsets.kt index 7f0650f5ef..d5df442cc2 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowInsets.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/compose/utils/WindowInsets.kt @@ -8,9 +8,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -fun WindowInsets.addMargin( - all: Dp = 0.dp, -) = addMargin(vertical = all, horizontal = all) +fun WindowInsets.addMargin(all: Dp = 0.dp) = addMargin(vertical = all, horizontal = all) fun WindowInsets.addMargin( vertical: Dp = 0.dp, @@ -31,14 +29,16 @@ fun WindowInsets.addMarginLayout( ): WindowInsets { val layoutDirection = LocalLayoutDirection.current return addMargin( - left = when (layoutDirection) { - LayoutDirection.Ltr -> start - LayoutDirection.Rtl -> end - }, - right = when (layoutDirection) { - LayoutDirection.Ltr -> end - LayoutDirection.Rtl -> start - }, + left = + when (layoutDirection) { + LayoutDirection.Ltr -> start + LayoutDirection.Rtl -> end + }, + right = + when (layoutDirection) { + LayoutDirection.Ltr -> end + LayoutDirection.Rtl -> start + }, top = top, bottom = bottom, ) diff --git a/app/src/main/java/com/nononsenseapps/feeder/ui/text/HtmlToPlainTextConverter.kt b/app/src/main/java/com/nononsenseapps/feeder/ui/text/HtmlToPlainTextConverter.kt index eb1f0a5232..77a55cba18 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/ui/text/HtmlToPlainTextConverter.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/ui/text/HtmlToPlainTextConverter.kt @@ -1,8 +1,5 @@ package com.nononsenseapps.feeder.ui.text -import java.io.IOException -import java.io.StringReader -import java.util.Stack import org.ccil.cowan.tagsoup.HTMLSchema import org.ccil.cowan.tagsoup.Parser import org.xml.sax.Attributes @@ -12,6 +9,9 @@ import org.xml.sax.Locator import org.xml.sax.SAXException import org.xml.sax.SAXNotRecognizedException import org.xml.sax.SAXNotSupportedException +import java.io.IOException +import java.io.StringReader +import java.util.Stack /** * Intended primarily to convert HTML into plaintext snippets, useful for previewing content in list. @@ -77,7 +77,10 @@ class HtmlToPlainTextConverter : ContentHandler { } @Throws(SAXException::class) - override fun startPrefixMapping(prefix: String, uri: String) { + override fun startPrefixMapping( + prefix: String, + uri: String, + ) { } @Throws(SAXException::class) @@ -85,11 +88,19 @@ class HtmlToPlainTextConverter : ContentHandler { } @Throws(SAXException::class) - override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { + override fun startElement( + uri: String, + localName: String, + qName: String, + attributes: Attributes, + ) { handleStartTag(localName, attributes) } - private fun handleStartTag(tag: String, attributes: Attributes) { + private fun handleStartTag( + tag: String, + attributes: Attributes, + ) { when { tag.equals("br", ignoreCase = true) -> { // We don't need to handle this. TagSoup will ensure that there's a
for each
@@ -116,7 +127,10 @@ class HtmlToPlainTextConverter : ContentHandler { } } - private fun startImg(text: StringBuilder?, attributes: Attributes) { + private fun startImg( + text: StringBuilder?, + attributes: Attributes, + ) { // Ensure whitespace ensureSpace(text) @@ -172,12 +186,19 @@ class HtmlToPlainTextConverter : ContentHandler { listings.pop() } - private fun startA(builder: StringBuilder?, attributes: Attributes) {} + private fun startA( + builder: StringBuilder?, + attributes: Attributes, + ) {} private fun endA(builder: StringBuilder?) {} @Throws(SAXException::class) - override fun endElement(uri: String, localName: String, qName: String) { + override fun endElement( + uri: String, + localName: String, + qName: String, + ) { handleEndTag(localName) } @@ -221,7 +242,11 @@ class HtmlToPlainTextConverter : ContentHandler { } @Throws(SAXException::class) - override fun characters(ch: CharArray, start: Int, length: Int) { + override fun characters( + ch: CharArray, + start: Int, + length: Int, + ) { if (ignoreCount > 0) { return } @@ -241,17 +266,18 @@ class HtmlToPlainTextConverter : ContentHandler { if (c == ' ' || c == '\n') { var len = sb.length - val prev: Char = if (len == 0) { - len = builder!!.length - + val prev: Char = if (len == 0) { - '\n' + len = builder!!.length + + if (len == 0) { + '\n' + } else { + builder!![len - 1] + } } else { - builder!![len - 1] + sb[len - 1] } - } else { - sb[len - 1] - } if (prev != ' ' && prev != '\n') { sb.append(' ') @@ -265,11 +291,18 @@ class HtmlToPlainTextConverter : ContentHandler { } @Throws(SAXException::class) - override fun ignorableWhitespace(ch: CharArray, start: Int, length: Int) { + override fun ignorableWhitespace( + ch: CharArray, + start: Int, + length: Int, + ) { } @Throws(SAXException::class) - override fun processingInstruction(target: String, data: String) { + override fun processingInstruction( + target: String, + data: String, + ) { } @Throws(SAXException::class) @@ -317,7 +350,10 @@ class HtmlToPlainTextConverter : ContentHandler { private class Header(var mLevel: Int) } -fun repeated(string: String, count: Int): String { +fun repeated( + string: String, + count: Int, +): String { val sb = StringBuilder() for (i in 0 until count) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/ActivityLauncher.kt b/app/src/main/java/com/nononsenseapps/feeder/util/ActivityLauncher.kt index 8b06689c26..90f59a3f8a 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/ActivityLauncher.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/ActivityLauncher.kt @@ -37,7 +37,10 @@ class ActivityLauncher( /** * Returns true if activity was launched, false if no such activity */ - fun startActivity(openAdjacentIfSuitable: Boolean, intent: Intent): Boolean { + fun startActivity( + openAdjacentIfSuitable: Boolean, + intent: Intent, + ): Boolean { return try { activity.startActivity(intent.openAdjacentIfSuitable(openAdjacentIfSuitable)) true @@ -67,12 +70,16 @@ class ActivityLauncher( /** * Returns true if activity was launched, false if no such activity */ - fun openLinkInBrowser(link: String, openAdjacentIfSuitable: Boolean = true): Boolean { + fun openLinkInBrowser( + link: String, + openAdjacentIfSuitable: Boolean = true, + ): Boolean { return startActivity( openAdjacentIfSuitable = openAdjacentIfSuitable, - intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)).also { - it.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true) - }, + intent = + Intent(Intent.ACTION_VIEW, Uri.parse(link)).also { + it.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true) + }, ) } @@ -86,12 +93,13 @@ class ActivityLauncher( ): Boolean { return try { val uri = Uri.parse(link) - val intent = CustomTabsIntent.Builder().apply { - setToolbarColor(toolbarColor) - addDefaultShareMenuItem() - }.build().intent.apply { - data = uri - } + val intent = + CustomTabsIntent.Builder().apply { + setToolbarColor(toolbarColor) + addDefaultShareMenuItem() + }.build().intent.apply { + data = uri + } return startActivity(openAdjacentIfSuitable, intent) } catch (e: ActivityNotFoundException) { Log.e(LOG_TAG, "Failed to custom tab", e) diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt b/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt index 5febf86528..b3bb3df531 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/BugReport.kt @@ -9,12 +9,13 @@ import android.net.Uri import android.os.Build import com.nononsenseapps.feeder.BuildConfig -private fun deviceInfoBlock(): String = """ +private fun deviceInfoBlock(): String = + """ Application: ${BuildConfig.APPLICATION_ID} (flavor ${BuildConfig.BUILD_TYPE.ifBlank { "None" }}) Version: ${BuildConfig.VERSION_NAME} (code ${BuildConfig.VERSION_CODE}) Android: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) Device: ${Build.MANUFACTURER} ${Build.MODEL} -""".trimIndent() + """.trimIndent() private fun bugBody(): String = """ @@ -27,12 +28,13 @@ private fun bugBody(): String = private const val emailReportAddress: String = "feeder@nononsenseapps.com" -fun emailBugReportIntent(): Intent = Intent(ACTION_SENDTO).apply { - data = Uri.parse("mailto:$emailReportAddress") - putExtra(EXTRA_SUBJECT, "Bug report for Feeder") - putExtra(EXTRA_EMAIL, emailReportAddress) - putExtra(Intent.EXTRA_TEXT, bugBody()) -} +fun emailBugReportIntent(): Intent = + Intent(ACTION_SENDTO).apply { + data = Uri.parse("mailto:$emailReportAddress") + putExtra(EXTRA_SUBJECT, "Bug report for Feeder") + putExtra(EXTRA_EMAIL, emailReportAddress) + putExtra(Intent.EXTRA_TEXT, bugBody()) + } private const val crashReportAddress: String = "crashes@nononsenseapps.com" @@ -45,20 +47,23 @@ private fun crashBody(throwable: Throwable): String = ${throwable.stackTraceToString()} """.trimIndent() -fun emailCrashReportIntent(throwable: Throwable): Intent = Intent(ACTION_SENDTO).apply { - data = Uri.parse("mailto:$crashReportAddress") - putExtra(EXTRA_SUBJECT, "Crash report for Feeder") - putExtra(EXTRA_EMAIL, crashReportAddress) - putExtra(Intent.EXTRA_TEXT, crashBody(throwable)) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) -} +fun emailCrashReportIntent(throwable: Throwable): Intent = + Intent(ACTION_SENDTO).apply { + data = Uri.parse("mailto:$crashReportAddress") + putExtra(EXTRA_SUBJECT, "Crash report for Feeder") + putExtra(EXTRA_EMAIL, crashReportAddress) + putExtra(Intent.EXTRA_TEXT, crashBody(throwable)) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } -fun openGitlabIssues(): Intent = Intent(ACTION_VIEW).also { - it.data = Uri.parse("https://gitlab.com/spacecowboy/Feeder/issues") -} +fun openGitlabIssues(): Intent = + Intent(ACTION_VIEW).also { + it.data = Uri.parse("https://gitlab.com/spacecowboy/Feeder/issues") + } const val KOFI_URL = "https://ko-fi.com/spacecowboy" -fun openKoFiIntent(): Intent = Intent(ACTION_VIEW).also { - it.data = Uri.parse(KOFI_URL) -} +fun openKoFiIntent(): Intent = + Intent(ACTION_VIEW).also { + it.data = Uri.parse(KOFI_URL) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/ContentValuesExtensions.kt b/app/src/main/java/com/nononsenseapps/feeder/util/ContentValuesExtensions.kt index 384a1946dc..78f1a1b6c1 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/ContentValuesExtensions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/ContentValuesExtensions.kt @@ -2,20 +2,15 @@ package com.nononsenseapps.feeder.util import android.content.ContentValues -fun ContentValues.setBoolean(pair: Pair) = - put(pair.first, pair.second) +fun ContentValues.setBoolean(pair: Pair) = put(pair.first, pair.second) -fun ContentValues.setLong(pair: Pair) = - put(pair.first, pair.second) +fun ContentValues.setLong(pair: Pair) = put(pair.first, pair.second) -fun ContentValues.setInt(pair: Pair) = - put(pair.first, pair.second) +fun ContentValues.setInt(pair: Pair) = put(pair.first, pair.second) -fun ContentValues.setString(pair: Pair) = - put(pair.first, pair.second) +fun ContentValues.setString(pair: Pair) = put(pair.first, pair.second) -fun ContentValues.setNull(column: String) = - putNull(column) +fun ContentValues.setNull(column: String) = putNull(column) fun ContentValues.setStringMaybe(pair: Pair) { if (pair.second == null) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/ContextExtensions.kt b/app/src/main/java/com/nononsenseapps/feeder/util/ContextExtensions.kt index 09a8b26b8e..94dd5889a0 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/ContextExtensions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/ContextExtensions.kt @@ -16,7 +16,10 @@ import java.util.Locale interface ToastMaker { suspend fun makeToast(text: String) - suspend fun makeToast(@StringRes resId: Int) + + suspend fun makeToast( + @StringRes resId: Int, + ) } val Context.notificationManager: NotificationManagerCompat @@ -27,40 +30,46 @@ val Context.notificationManager: NotificationManagerCompat * Ensures that a maximum number of shortcuts is available at any time with the last used being bumped out of the list * first. */ -fun Context.addDynamicShortcutToFeed(label: String, id: Long, icon: Icon? = null) { +fun Context.addDynamicShortcutToFeed( + label: String, + id: Long, + icon: Icon? = null, +) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { val shortcutManager = getSystemService(ShortcutManager::class.java) ?: return - val intent = Intent( - Intent.ACTION_VIEW, - "$DEEP_LINK_BASE_URI/feed?id=$id".toUri(), - this, - MainActivity::class.java, - ).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + val intent = + Intent( + Intent.ACTION_VIEW, + "$DEEP_LINK_BASE_URI/feed?id=$id".toUri(), + this, + MainActivity::class.java, + ).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } val current = shortcutManager.dynamicShortcuts.toMutableList() // Update shortcuts - val shortcut: ShortcutInfo = ShortcutInfo.Builder(this, "$id") - .setShortLabel(label) - .setLongLabel(label) - .setIcon( - icon - ?: Icon.createWithBitmap( - getLetterIcon( - label, - id, - radius = shortcutManager.iconMaxHeight, + val shortcut: ShortcutInfo = + ShortcutInfo.Builder(this, "$id") + .setShortLabel(label) + .setLongLabel(label) + .setIcon( + icon + ?: Icon.createWithBitmap( + getLetterIcon( + label, + id, + radius = shortcutManager.iconMaxHeight, + ), ), - ), - ) - .setIntent(intent) - .setDisabledMessage("Feed deleted") - .setRank(0) - .build() + ) + .setIntent(intent) + .setDisabledMessage("Feed deleted") + .setRank(0) + .build() if (current.map { it.id }.contains(shortcut.id)) { // Just update existing one @@ -98,8 +107,7 @@ fun Context.reportShortcutToFeedUsed(id: Any) { } } -fun Context.unicodeWrap(text: String): String = - BidiFormatter.getInstance(getLocale()).unicodeWrap(text) +fun Context.unicodeWrap(text: String): String = BidiFormatter.getInstance(getLocale()).unicodeWrap(text) fun Context.getLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/Either.kt b/app/src/main/java/com/nononsenseapps/feeder/util/Either.kt index 8c7daad06f..8dfb407be9 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/Either.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/Either.kt @@ -119,7 +119,10 @@ sealed class Either { * @param ifRight transform the [Either.Right] type [B] to [C]. * @return the transformed value [C] by applying [ifLeft] or [ifRight] to [A] or [B] respectively. */ - inline fun fold(ifLeft: (left: A) -> C, ifRight: (right: B) -> C): C { + inline fun fold( + ifLeft: (left: A) -> C, + ifRight: (right: B) -> C, + ): C { contract { callsInPlace(ifLeft, InvocationKind.AT_MOST_ONCE) callsInPlace(ifRight, InvocationKind.AT_MOST_ONCE) @@ -145,8 +148,7 @@ sealed class Either { * * */ - fun swap(): Either = - fold({ Right(it) }, { Left(it) }) + fun swap(): Either = fold({ Right(it) }, { Left(it) }) /** * Map, or transform, the right value [B] of this [Either] to a new value [C]. @@ -283,10 +285,11 @@ sealed class Either { return fold(::identity) { null } } - override fun toString(): String = fold( - { "Either.Left($it)" }, - { "Either.Right($it)" }, - ) + override fun toString(): String = + fold( + { "Either.Left($it)" }, + { "Either.Right($it)" }, + ) data class Left( val value: A, @@ -301,7 +304,10 @@ sealed class Either { } companion object { - inline fun catching(onCatch: (t: Throwable) -> E, block: () -> A): Either { + inline fun catching( + onCatch: (t: Throwable) -> E, + block: () -> A, + ): Either { contract { callsInPlace(onCatch, InvocationKind.AT_MOST_ONCE) callsInPlace(block, InvocationKind.AT_MOST_ONCE) @@ -330,8 +336,7 @@ inline fun Either.flatMap(f: (right: B) -> Either): Either } } -fun Either>.flatten(): Either = - flatMap(::identity) +fun Either>.flatten(): Either = flatMap(::identity) /** * Get the right value [B] of this [Either], @@ -371,8 +376,7 @@ inline infix fun Either.getOrElse(default: (A) -> B): B { * * */ -inline fun Either.merge(): A = - fold(::identity, ::identity) +inline fun Either.merge(): A = fold(::identity, ::identity) fun A.left(): Either = Either.Left(this) @@ -395,15 +399,17 @@ fun Either.combine( combineRight: (B, B) -> B, ): Either = when (val one = this) { - is Either.Left -> when (other) { - is Either.Left -> Either.Left(combineLeft(one.value, other.value)) - is Either.Right -> one - } + is Either.Left -> + when (other) { + is Either.Left -> Either.Left(combineLeft(one.value, other.value)) + is Either.Right -> one + } - is Either.Right -> when (other) { - is Either.Left -> other - is Either.Right -> Either.Right(combineRight(one.value, other.value)) - } + is Either.Right -> + when (other) { + is Either.Left -> other + is Either.Right -> Either.Right(combineRight(one.value, other.value)) + } } /** @@ -423,10 +429,8 @@ fun Either.combine( * ``` * */ -fun Either.widen(): Either = - this +fun Either.widen(): Either = this -fun Either.leftWiden(): Either = - this +fun Either.leftWiden(): Either = this fun identity(value: (A)): A = value diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/FilePathProvider.kt b/app/src/main/java/com/nononsenseapps/feeder/util/FilePathProvider.kt index 8f33c675f2..f095a19cd7 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/FilePathProvider.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/FilePathProvider.kt @@ -40,5 +40,7 @@ private class FilePathProviderImpl( override val imageCacheDir: File = cacheDir.resolve("image_cache") } -fun filePathProvider(cacheDir: File, filesDir: File): FilePathProvider = - FilePathProviderImpl(cacheDir = cacheDir, filesDir = filesDir) +fun filePathProvider( + cacheDir: File, + filesDir: File, +): FilePathProvider = FilePathProviderImpl(cacheDir = cacheDir, filesDir = filesDir) diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/HtmlUtils.kt b/app/src/main/java/com/nononsenseapps/feeder/util/HtmlUtils.kt index 8fa93cd91f..4a613c8ac1 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/HtmlUtils.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/HtmlUtils.kt @@ -3,11 +3,15 @@ package com.nononsenseapps.feeder.util import org.jsoup.Jsoup import org.jsoup.parser.Parser.unescapeEntities -fun findFirstImageLinkInHtml(text: String?, baseUrl: String?): String? = +fun findFirstImageLinkInHtml( + text: String?, + baseUrl: String?, +): String? = if (text != null) { - val doc = unescapeEntities(text, true).byteInputStream().use { - Jsoup.parse(it, "UTF-8", baseUrl ?: "") - } + val doc = + unescapeEntities(text, true).byteInputStream().use { + Jsoup.parse(it, "UTF-8", baseUrl ?: "") + } doc.getElementsByTag("img").asSequence() .filterNot { it.attr("width") == "1" || it.attr("height") == "1" } diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/LetterIconProvider.kt b/app/src/main/java/com/nononsenseapps/feeder/util/LetterIconProvider.kt index 527cdf1628..f03ec2da53 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/LetterIconProvider.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/LetterIconProvider.kt @@ -9,12 +9,13 @@ import android.graphics.Typeface import android.text.TextPaint import kotlin.math.abs -private val colors = arrayOf( - 0xffe57373, 0xfff06292, 0xffba68c8, 0xff9575cd, - 0xff7986cb, 0xff64b5f6, 0xff4fc3f7, 0xff4dd0e1, 0xff4db6ac, 0xff81c784, - 0xffaed581, 0xffff8a65, 0xffd4e157, 0xffffd54f, 0xffffb74d, 0xffa1887f, - 0xff90a4ae, -) +private val colors = + arrayOf( + 0xffe57373, 0xfff06292, 0xffba68c8, 0xff9575cd, + 0xff7986cb, 0xff64b5f6, 0xff4fc3f7, 0xff4dd0e1, 0xff4db6ac, 0xff81c784, + 0xffaed581, 0xffff8a65, 0xffd4e157, 0xffffd54f, 0xffffb74d, 0xffa1887f, + 0xff90a4ae, + ) private const val NUM_OF_TILE_COLORS = 8 @@ -23,7 +24,11 @@ private const val NUM_OF_TILE_COLORS = 8 * key: Anything which denotes the stable ID of the item. * radius: Pixel size of icon. Default should be suitable in most cases. */ -fun getLetterIcon(text: String, key: Any, radius: Int = 128): Bitmap { +fun getLetterIcon( + text: String, + key: Any, + radius: Int = 128, +): Bitmap { val paint = TextPaint() val bounds = Rect() val canvas = Canvas() @@ -40,11 +45,12 @@ fun getLetterIcon(text: String, key: Any, radius: Int = 128): Bitmap { paint.textAlign = Paint.Align.CENTER val firstChar = CharArray(1) - firstChar[0] = if (text.isBlank()) { - 'F' - } else { - text[0].uppercaseChar() - } + firstChar[0] = + if (text.isBlank()) { + 'F' + } else { + text[0].uppercaseChar() + } val fontSize = radius * 0.8f paint.textSize = fontSize diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/LinkUtils.kt b/app/src/main/java/com/nononsenseapps/feeder/util/LinkUtils.kt index 0768964b35..7d6d7de498 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/LinkUtils.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/LinkUtils.kt @@ -27,59 +27,74 @@ fun sloppyLinkToStrictURL(url: String): URL { } } -fun sloppyLinktoURIOrNull(text: String): URI? = try { - URI(text) -} catch (_: URISyntaxException) { - null -} +fun sloppyLinktoURIOrNull(text: String): URI? = + try { + URI(text) + } catch (_: URISyntaxException) { + null + } -fun sloppyLinkToStrictURLOrNull(url: String): URL? = try { - sloppyLinkToStrictURL(url) -} catch (_: MalformedURLException) { - null -} +fun sloppyLinkToStrictURLOrNull(url: String): URL? = + try { + sloppyLinkToStrictURL(url) + } catch (_: MalformedURLException) { + null + } /** * Returns a URL but does not guarantee that it accurately represents the input string if the input string is an invalid URL. * This is used to ensure that migrations to versions where Feeds have URL and not strings don't crash. */ -fun sloppyLinkToStrictURLNoThrows(url: String): URL = try { - sloppyLinkToStrictURL(url) -} catch (_: MalformedURLException) { - URL("http://") -} +fun sloppyLinkToStrictURLNoThrows(url: String): URL = + try { + sloppyLinkToStrictURL(url) + } catch (_: MalformedURLException) { + URL("http://") + } /** * On error, this method simply returns the original link. It does *not* throw exceptions. */ -fun relativeLinkIntoAbsolute(base: URL, link: String): String = try { - // If no exception, it's valid - relativeLinkIntoAbsoluteOrThrow(base, link).toString() -} catch (_: MalformedURLException) { - link -} +fun relativeLinkIntoAbsolute( + base: URL, + link: String, +): String = + try { + // If no exception, it's valid + relativeLinkIntoAbsoluteOrThrow(base, link).toString() + } catch (_: MalformedURLException) { + link + } /** * On error, this method simply returns the original link. It does *not* throw exceptions. */ -fun relativeLinkIntoAbsoluteOrNull(base: URL, link: String?): String? = try { - // If no exception, it's valid - if (link != null) { - relativeLinkIntoAbsoluteOrThrow(base, link).toString() - } else { - null +fun relativeLinkIntoAbsoluteOrNull( + base: URL, + link: String?, +): String? = + try { + // If no exception, it's valid + if (link != null) { + relativeLinkIntoAbsoluteOrThrow(base, link).toString() + } else { + null + } + } catch (_: MalformedURLException) { + link } -} catch (_: MalformedURLException) { - link -} /** * On error, throws MalformedURLException. */ @Throws(MalformedURLException::class) -fun relativeLinkIntoAbsoluteOrThrow(base: URL, link: String): URL = try { - // If no exception, it's valid - URL(link) -} catch (_: MalformedURLException) { - URL(base, link) -} +fun relativeLinkIntoAbsoluteOrThrow( + base: URL, + link: String, +): URL = + try { + // If no exception, it's valid + URL(link) + } catch (_: MalformedURLException) { + URL(base, link) + } diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/PrefUtils.kt b/app/src/main/java/com/nononsenseapps/feeder/util/PrefUtils.kt index f49049fbc2..99123fb8f5 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/PrefUtils.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/PrefUtils.kt @@ -7,5 +7,7 @@ import android.content.SharedPreferences */ const val PREF_MAX_ITEM_COUNT_PER_FEED = "pref_max_item_count_per_feed" -fun SharedPreferences.getStringNonNull(key: String, defaultValue: String): String = - getString(key, defaultValue) ?: defaultValue +fun SharedPreferences.getStringNonNull( + key: String, + defaultValue: String, +): String = getString(key, defaultValue) ?: defaultValue diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/RomeExtensions.kt b/app/src/main/java/com/nononsenseapps/feeder/util/RomeExtensions.kt index afcc5024d3..1db6c9d2eb 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/RomeExtensions.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/RomeExtensions.kt @@ -24,34 +24,37 @@ import java.time.ZonedDateTime fun SyndFeed.asFeed(baseUrl: URL): Feed { val feedAuthor: Author? = this.authors?.firstOrNull()?.asAuthor() - val siteUrl = relativeLinkIntoAbsoluteOrNull( - baseUrl, - this.links?.firstOrNull { - "alternate" == it.rel && "text/html" == it.type - }?.href ?: this.link, - ) + val siteUrl = + relativeLinkIntoAbsoluteOrNull( + baseUrl, + this.links?.firstOrNull { + "alternate" == it.rel && "text/html" == it.type + }?.href ?: this.link, + ) // Base64 encoded images can be quite large - and crash database cursors - val icon = try { - image?.url?.let { url -> - when { - url.startsWith("http") -> url - else -> null + val icon = + try { + image?.url?.let { url -> + when { + url.startsWith("http") -> url + else -> null + } } + } catch (e: Exception) { + Log.e("FEEDER_ROME", "Unable to find feed icon", e) + null } - } catch (e: Exception) { - Log.e("FEEDER_ROME", "Unable to find feed icon", e) - null - } try { return Feed( title = plainTitle(), home_page_url = siteUrl, - feed_url = relativeLinkIntoAbsoluteOrNull( - baseUrl, - this.links?.firstOrNull { "self" == it.rel }?.href, - ), + feed_url = + relativeLinkIntoAbsoluteOrNull( + baseUrl, + this.links?.firstOrNull { "self" == it.rel }?.href, + ), description = this.description, icon = icon, author = feedAuthor, @@ -62,22 +65,28 @@ fun SyndFeed.asFeed(baseUrl: URL): Feed { } } -fun SyndEntry.asItem(baseUrl: URL, feedAuthor: Author? = null): Item { +fun SyndEntry.asItem( + baseUrl: URL, + feedAuthor: Author? = null, +): Item { try { - val contentText = contentText().orIfBlank { - mediaDescription() ?: "" - } + val contentText = + contentText().orIfBlank { + mediaDescription() ?: "" + } // Base64 encoded images can be quite large - and crash database cursors - val image = thumbnail(baseUrl)?.let { img -> - when { - img.startsWith("data:") -> null - else -> img + val image = + thumbnail(baseUrl)?.let { img -> + when { + img.startsWith("data:") -> null + else -> img + } + } + val writer = + when (author?.isNotBlank()) { + true -> Author(name = author) + else -> feedAuthor } - } - val writer = when (author?.isNotBlank()) { - true -> Author(name = author) - else -> feedAuthor - } return Item( id = relativeLinkIntoAbsoluteOrNull(baseUrl, this.uri), @@ -128,21 +137,23 @@ fun SyndEntry.linkToHtml(feedBaseUrl: URL): String? { fun SyndEnclosure.asAttachment(baseUrl: URL): Attachment { return Attachment( - url = relativeLinkIntoAbsoluteOrNull( - baseUrl, - this.url, - ), + url = + relativeLinkIntoAbsoluteOrNull( + baseUrl, + this.url, + ), mime_type = this.type, size_in_bytes = this.length, ) } fun SyndPerson.asAuthor(): Author { - val url: String? = when { - this.uri != null -> this.uri - this.email != null -> "mailto:${this.email}" - else -> null - } + val url: String? = + when { + this.uri != null -> this.uri + this.email != null -> "mailto:${this.email}" + else -> null + } return Author( name = this.name, url = url, @@ -150,31 +161,32 @@ fun SyndPerson.asAuthor(): Author { } fun SyndEntry.contentText(): String { - val possiblyHtml = when { - contents != null && contents.isNotEmpty() -> { // Atom - val contents = contents - var possiblyHtml: String? = null - - for (c in contents) { - if ("text" == c.type && c.value != null) { - return c.value - } else if (null == c.type && c.value != null) { - // Suspect it might be text as per the Rome docs - // https://github.com/ralph-tice/rome/blob/master/src/main/java/com/sun/syndication/feed/synd/SyndContent.java - possiblyHtml = c.value - break - } else if (("html" == c.type || "xhtml" == c.type) && c.value != null) { - possiblyHtml = c.value - } else if (possiblyHtml == null && c.value != null) { - possiblyHtml = c.value + val possiblyHtml = + when { + contents != null && contents.isNotEmpty() -> { // Atom + val contents = contents + var possiblyHtml: String? = null + + for (c in contents) { + if ("text" == c.type && c.value != null) { + return c.value + } else if (null == c.type && c.value != null) { + // Suspect it might be text as per the Rome docs + // https://github.com/ralph-tice/rome/blob/master/src/main/java/com/sun/syndication/feed/synd/SyndContent.java + possiblyHtml = c.value + break + } else if (("html" == c.type || "xhtml" == c.type) && c.value != null) { + possiblyHtml = c.value + } else if (possiblyHtml == null && c.value != null) { + possiblyHtml = c.value + } } - } - possiblyHtml + possiblyHtml + } + else -> // Rss + description?.value } - else -> // Rss - description?.value - } val result = HtmlToPlainTextConverter().convert(possiblyHtml ?: "") @@ -186,11 +198,15 @@ fun SyndEntry.contentText(): String { } } -private fun convertAtomContentToPlainText(content: SyndContent?, fallback: String?): String { +private fun convertAtomContentToPlainText( + content: SyndContent?, + fallback: String?, +): String { return HtmlToPlainTextConverter().convert(content?.value ?: fallback ?: "") } fun SyndFeed.plainTitle(): String = convertAtomContentToPlainText(titleEx, title) + fun SyndEntry.plainTitle(): String = convertAtomContentToPlainText(titleEx, title) fun SyndEntry.contentHtml(): String? { @@ -224,16 +240,17 @@ fun SyndEntry.mediaDescription(): String? { fun SyndEntry.thumbnail(feedBaseUrl: URL): String? { val media = this.getModule(MediaModule.URI) as MediaEntryModule? - val thumbnailCandidates = sequence { - media?.findThumbnailCandidates()?.let { - yieldAll(it) - } - enclosures?.asSequence() - ?.mapNotNull { it.findThumbnailCandidate() } - ?.let { + val thumbnailCandidates = + sequence { + media?.findThumbnailCandidates()?.let { yieldAll(it) } - } + enclosures?.asSequence() + ?.mapNotNull { it.findThumbnailCandidate() } + ?.let { + yieldAll(it) + } + } val thumbnail = thumbnailCandidates.maxByOrNull { it.width ?: -1 } @@ -278,13 +295,14 @@ private fun SyndEnclosure.findThumbnailCandidate(): ThumbnailCandidate? { return null } -private fun MediaGroup.findThumbnailCandidates(): Sequence = sequence { - metadata.thumbnail?.forEach { thumbnail -> - thumbnail.findThumbnailCandidate()?.let { thumbnailCandidate -> - yield(thumbnailCandidate) +private fun MediaGroup.findThumbnailCandidates(): Sequence = + sequence { + metadata.thumbnail?.forEach { thumbnail -> + thumbnail.findThumbnailCandidate()?.let { thumbnailCandidate -> + yield(thumbnailCandidate) + } } } -} private fun Thumbnail.findThumbnailCandidate(): ThumbnailCandidate? { return url?.let { url -> @@ -341,25 +359,25 @@ private fun pointsToImage(url: String): Boolean { fun SyndEntry.publishedRFC3339ZonedDateTime(): ZonedDateTime? = when (publishedDate != null) { - true -> ZonedDateTime.ofInstant( - Instant.ofEpochMilli(publishedDate.time), - ZoneOffset.systemDefault(), - ) + true -> + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(publishedDate.time), + ZoneOffset.systemDefault(), + ) // This is the required element in atom feeds so it is a good fallback else -> modifiedRFC3339ZonedDateTime() } fun SyndEntry.modifiedRFC3339ZonedDateTime(): ZonedDateTime? = when (updatedDate != null) { - true -> ZonedDateTime.ofInstant( - Instant.ofEpochMilli(updatedDate.time), - ZoneOffset.systemDefault(), - ) + true -> + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(updatedDate.time), + ZoneOffset.systemDefault(), + ) else -> null } -fun SyndEntry.publishedRFC3339Date(): String? = - publishedRFC3339ZonedDateTime()?.toString() +fun SyndEntry.publishedRFC3339Date(): String? = publishedRFC3339ZonedDateTime()?.toString() -fun SyndEntry.modifiedRFC3339Date(): String? = - modifiedRFC3339ZonedDateTime()?.toString() +fun SyndEntry.modifiedRFC3339Date(): String? = modifiedRFC3339ZonedDateTime()?.toString() diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/SystemUtils.kt b/app/src/main/java/com/nononsenseapps/feeder/util/SystemUtils.kt index 559f818219..259324eead 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/SystemUtils.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/SystemUtils.kt @@ -56,5 +56,4 @@ fun currentlyConnected(context: Context): Boolean { } ?: false } -fun String.urlEncode(): String = - URLEncoder.encode(this, "UTF-8") +fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8") diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/Time.kt b/app/src/main/java/com/nononsenseapps/feeder/util/Time.kt index 092a6e1f09..0a245eaee6 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/Time.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/Time.kt @@ -3,5 +3,4 @@ package com.nononsenseapps.feeder.util import java.time.Instant import java.time.temporal.ChronoUnit -fun Instant.minusMinutes(minutes: Int): Instant = - minus(minutes.toLong(), ChronoUnit.MINUTES) +fun Instant.minusMinutes(minutes: Int): Instant = minus(minutes.toLong(), ChronoUnit.MINUTES) diff --git a/app/src/main/java/com/nononsenseapps/feeder/util/Unicode.kt b/app/src/main/java/com/nononsenseapps/feeder/util/Unicode.kt index 881ab4f29b..814cf20b73 100644 --- a/app/src/main/java/com/nononsenseapps/feeder/util/Unicode.kt +++ b/app/src/main/java/com/nononsenseapps/feeder/util/Unicode.kt @@ -1,11 +1,12 @@ package com.nononsenseapps.feeder.util -fun String.asUTF8Sequence(): Sequence = sequence { - var i = 0 - while (i < length) { - val code = codePointAt(i) - i += Character.charCount(code) - // Unicode smileys are an example of where toChar() won't work. Needs to be String. - yield(String(intArrayOf(code), 0, 1)) +fun String.asUTF8Sequence(): Sequence = + sequence { + var i = 0 + while (i < length) { + val code = codePointAt(i) + i += Character.charCount(code) + // Unicode smileys are an example of where toChar() won't work. Needs to be String. + yield(String(intArrayOf(code), 0, 1)) + } } -} diff --git a/app/src/main/java/com/nononsenseapps/jsonfeed/JsonFeedParser.kt b/app/src/main/java/com/nononsenseapps/jsonfeed/JsonFeedParser.kt index 7239283643..cf8508737c 100644 --- a/app/src/main/java/com/nononsenseapps/jsonfeed/JsonFeedParser.kt +++ b/app/src/main/java/com/nononsenseapps/jsonfeed/JsonFeedParser.kt @@ -3,13 +3,13 @@ package com.nononsenseapps.jsonfeed import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import java.io.File -import java.io.IOException -import java.util.concurrent.TimeUnit import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.ResponseBody +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit fun cachingHttpClient( cacheDirectory: File? = null, @@ -41,8 +41,7 @@ fun cachingHttpClient( return builder.build() } -fun feedAdapter(): JsonAdapter = - Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build().adapter(Feed::class.java) +fun feedAdapter(): JsonAdapter = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build().adapter(Feed::class.java) /** * A parser for JSONFeeds. CacheDirectory and CacheSize are only relevant if feeds are downloaded. They are not used @@ -52,7 +51,6 @@ class JsonFeedParser( private val httpClient: OkHttpClient, private val jsonFeedAdapter: JsonAdapter, ) { - constructor( cacheDirectory: File? = null, cacheSize: Long = 10L * 1024L * 1024L, @@ -76,9 +74,10 @@ class JsonFeedParser( fun parseUrl(url: String): Feed { val request: Request try { - request = Request.Builder() - .url(url) - .build() + request = + Request.Builder() + .url(url) + .build() } catch (error: Throwable) { throw IllegalArgumentException( "Bad URL. Perhaps it is missing an http:// prefix?", @@ -117,14 +116,14 @@ class JsonFeedParser( /** * Parse a JSONFeed */ - fun parseJson(responseBody: ResponseBody): Feed = - parseJson(responseBody.string()) + fun parseJson(responseBody: ResponseBody): Feed = parseJson(responseBody.string()) /** * Parse a JSONFeed */ - fun parseJson(json: String): Feed = jsonFeedAdapter.fromJson(json) - ?: throw IOException("Failed to parse JSONFeed") + fun parseJson(json: String): Feed = + jsonFeedAdapter.fromJson(json) + ?: throw IOException("Failed to parse JSONFeed") } data class Feed( diff --git a/app/src/main/java/com/nononsenseapps/jsonfeed/OkHttpBuilderExtensions.kt b/app/src/main/java/com/nononsenseapps/jsonfeed/OkHttpBuilderExtensions.kt index 32b96dcb88..3c66965a6a 100644 --- a/app/src/main/java/com/nononsenseapps/jsonfeed/OkHttpBuilderExtensions.kt +++ b/app/src/main/java/com/nononsenseapps/jsonfeed/OkHttpBuilderExtensions.kt @@ -1,5 +1,6 @@ package com.nononsenseapps.jsonfeed +import okhttp3.OkHttpClient import java.security.KeyManagementException import java.security.NoSuchAlgorithmException import java.security.cert.X509Certificate @@ -7,19 +8,25 @@ import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -import okhttp3.OkHttpClient fun OkHttpClient.Builder.trustAllCerts() { try { - val trustManager = object : X509TrustManager { - override fun checkClientTrusted(chain: Array?, authType: String?) { - } + val trustManager = + object : X509TrustManager { + override fun checkClientTrusted( + chain: Array?, + authType: String?, + ) { + } - override fun checkServerTrusted(chain: Array?, authType: String?) { - } + override fun checkServerTrusted( + chain: Array?, + authType: String?, + ) { + } - override fun getAcceptedIssuers(): Array = emptyArray() - } + override fun getAcceptedIssuers(): Array = emptyArray() + } val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, arrayOf(trustManager), null) diff --git a/app/src/main/java/org/kodein/di/compose/Retreiving.kt b/app/src/main/java/org/kodein/di/compose/Retreiving.kt index dc7360c331..a01aa62a2d 100644 --- a/app/src/main/java/org/kodein/di/compose/Retreiving.kt +++ b/app/src/main/java/org/kodein/di/compose/Retreiving.kt @@ -16,9 +16,10 @@ import org.kodein.di.* * @throws DI.DependencyLoopException If the instance construction triggered a dependency loop. */ @Composable -inline fun instance(tag: Any? = null): DIProperty = with(LocalDI.current) { - remember { instance(tag) } -} +inline fun instance(tag: Any? = null): DIProperty = + with(LocalDI.current) { + remember { instance(tag) } + } /** * Gets an instance of [T] for the given type and tag, curried from a factory that takes an argument [A]. @@ -34,9 +35,13 @@ inline fun instance(tag: Any? = null): DIProperty = with(Lo * @throws DI.DependencyLoopException If the value construction triggered a dependency loop. */ @Composable -inline fun instance(tag: Any? = null, arg: A): DIProperty = with(LocalDI.current) { - remember { instance(tag, arg) } -} +inline fun instance( + tag: Any? = null, + arg: A, +): DIProperty = + with(LocalDI.current) { + remember { instance(tag, arg) } + } /** * Gets a factory of `T` for the given argument type, return type and tag. @@ -51,9 +56,10 @@ inline fun instance(tag: Any? = null, arg: A) * @throws DI.DependencyLoopException When calling the factory function, if the instance construction triggered a dependency loop. */ @Composable -inline fun factory(tag: Any? = null): DIProperty<(A) -> T> = with(LocalDI.current) { - remember { factory(tag) } -} +inline fun factory(tag: Any? = null): DIProperty<(A) -> T> = + with(LocalDI.current) { + remember { factory(tag) } + } /** * Gets a provider of `T` for the given type and tag. @@ -67,9 +73,10 @@ inline fun factory(tag: Any? = null): DIPrope * @throws DI.DependencyLoopException When calling the provider function, if the instance construction triggered a dependency loop. */ @Composable -inline fun provider(tag: Any? = null): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag) } -} +inline fun provider(tag: Any? = null): DIProperty<() -> T> = + with(LocalDI.current) { + remember { provider(tag) } + } /** * Gets a provider of [T] for the given type and tag, curried from a factory that takes an argument [A]. @@ -85,9 +92,13 @@ inline fun provider(tag: Any? = null): DIProp * @throws DI.DependencyLoopException When calling the provider, if the value construction triggered a dependency loop. */ @Composable -inline fun provider(tag: Any? = null, arg: A): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag, arg) } -} +inline fun provider( + tag: Any? = null, + arg: A, +): DIProperty<() -> T> = + with(LocalDI.current) { + remember { provider(tag, arg) } + } /** * Gets a provider of [T] for the given type and tag, curried from a factory that takes an argument [A]. @@ -103,6 +114,10 @@ inline fun provider(tag: Any? = null, arg: A) * @throws DI.DependencyLoopException When calling the provider, if the value construction triggered a dependency loop. */ @Composable -inline fun provider(tag: Any? = null, noinline fArg: () -> A): DIProperty<() -> T> = with(LocalDI.current) { - remember { provider(tag, fArg) } -} +inline fun provider( + tag: Any? = null, + noinline fArg: () -> A, +): DIProperty<() -> T> = + with(LocalDI.current) { + remember { provider(tag, fArg) } + } diff --git a/app/src/main/java/org/kodein/di/compose/SubDI.kt b/app/src/main/java/org/kodein/di/compose/SubDI.kt index 827e9abeec..cda9d772db 100644 --- a/app/src/main/java/org/kodein/di/compose/SubDI.kt +++ b/app/src/main/java/org/kodein/di/compose/SubDI.kt @@ -23,7 +23,9 @@ fun subDI( allowSilentOverride: Boolean = false, copy: Copy = Copy.NonCached, diBuilder: DI.MainBuilder.() -> Unit, - content: @Composable() () -> Unit + content: + @Composable() + () -> Unit, ) { val di = org.kodein.di.subDI(parentDI, allowSilentOverride, copy) { diBuilder() } CompositionLocalProvider(LocalDI provides di) { content() } @@ -45,5 +47,7 @@ fun subDI( allowSilentOverride: Boolean = false, copy: Copy = Copy.NonCached, diBuilder: DI.MainBuilder.() -> Unit, - content: @Composable() () -> Unit + content: + @Composable() + () -> Unit, ) = subDI(LocalDI.current, allowSilentOverride, copy, diBuilder, content) diff --git a/app/src/main/java/org/kodein/di/compose/WithDI.kt b/app/src/main/java/org/kodein/di/compose/WithDI.kt index 369e35ee05..1dca827583 100644 --- a/app/src/main/java/org/kodein/di/compose/WithDI.kt +++ b/app/src/main/java/org/kodein/di/compose/WithDI.kt @@ -11,8 +11,10 @@ import org.kodein.di.DI * @param content underlying [Composable] tree that will be able to consume the [DI] container */ @Composable -fun withDI(builder: DI.MainBuilder.() -> Unit, content: @Composable () -> Unit): Unit = - CompositionLocalProvider(LocalDI provides DI { builder() }) { content() } +fun withDI( + builder: DI.MainBuilder.() -> Unit, + content: @Composable () -> Unit, +): Unit = CompositionLocalProvider(LocalDI provides DI { builder() }) { content() } /** * Creates a [DI] container and imports [DI.Module]s before attaching it to the underlying [Composable] tree @@ -21,8 +23,10 @@ fun withDI(builder: DI.MainBuilder.() -> Unit, content: @Composable () -> Unit): * @param content underlying [Composable] tree that will be able to consume the [DI] container */ @Composable -fun withDI(vararg diModules: DI.Module, content: @Composable () -> Unit): Unit = - CompositionLocalProvider(LocalDI provides DI { importAll(*diModules) }) { content() } +fun withDI( + vararg diModules: DI.Module, + content: @Composable () -> Unit, +): Unit = CompositionLocalProvider(LocalDI provides DI { importAll(*diModules) }) { content() } /** * Attaches a [DI] container to the underlying [Composable] tree @@ -31,5 +35,7 @@ fun withDI(vararg diModules: DI.Module, content: @Composable () -> Unit): Unit = * @param content underlying [Composable] tree that will be able to consume the [DI] container */ @Composable -fun withDI(di: DI, content: @Composable () -> Unit): Unit = - CompositionLocalProvider(LocalDI provides di) { content() } +fun withDI( + di: DI, + content: @Composable () -> Unit, +): Unit = CompositionLocalProvider(LocalDI provides di) { content() } diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedItemStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedItemStoreTest.kt index a2eeb909a6..21d488dd24 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedItemStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedItemStoreTest.kt @@ -8,10 +8,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import java.net.URL -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList @@ -23,6 +19,10 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.net.URL +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue class FeedItemStoreTest : DIAware { private val store: FeedItemStore by instance() @@ -88,9 +88,10 @@ class FeedItemStoreTest : DIAware { fun getFeedItem() { coEvery { dao.loadFeedItemFlow(5L) } returns flowOf(FeedItemWithFeed(id = 5L)) - val feedItem = runBlocking { - store.getFeedItem(5L).toList().first() - } + val feedItem = + runBlocking { + store.getFeedItem(5L).toList().first() + } assertEquals(5L, feedItem?.id) } @@ -99,9 +100,10 @@ class FeedItemStoreTest : DIAware { fun getLink() { coEvery { dao.getLink(5L) } returns "foo" - val link = runBlocking { - store.getLink(5L) - } + val link = + runBlocking { + store.getLink(5L) + } assertEquals("foo", link) } @@ -110,9 +112,10 @@ class FeedItemStoreTest : DIAware { fun getArticleOpener() { coEvery { dao.getOpenArticleWith(5L) } returns "foo" - val result = runBlocking { - store.getArticleOpener(5L) - } + val result = + runBlocking { + store.getArticleOpener(5L) + } assertEquals("foo", result) } @@ -152,15 +155,17 @@ class FeedItemStoreTest : DIAware { @Test fun getFeedsItemsWithDefaultFullTextParse() { - val expected = listOf( - FeedItemIdWithLink(5L, "google.com"), - FeedItemIdWithLink(6L, "cowboy.com"), - ) + val expected = + listOf( + FeedItemIdWithLink(5L, "google.com"), + FeedItemIdWithLink(6L, "cowboy.com"), + ) every { dao.getFeedsItemsWithDefaultFullTextNeedingDownload() } returns flowOf(expected) - val items = runBlocking { - store.getFeedsItemsWithDefaultFullTextNeedingDownload().first() - } + val items = + runBlocking { + store.getFeedsItemsWithDefaultFullTextNeedingDownload().first() + } assertEquals( expected.size, @@ -180,9 +185,10 @@ class FeedItemStoreTest : DIAware { val expected = listOf(1L, 2L) every { dao.getFeedItemsNeedingNotifying() } returns flowOf(expected) - val items = runBlocking { - store.getFeedItemsNeedingNotifying().first() - } + val items = + runBlocking { + store.getFeedItemsNeedingNotifying().first() + } assertEquals( expected.size, @@ -203,9 +209,10 @@ class FeedItemStoreTest : DIAware { val guid = "foobar" coEvery { dao.getItemWith(url, guid) } returns 5L - val id = runBlocking { - store.getFeedItemId(url, guid) - } + val id = + runBlocking { + store.getFeedItemId(url, guid) + } assertEquals(5L, id) } @@ -214,9 +221,10 @@ class FeedItemStoreTest : DIAware { fun loadFeedItem() { coEvery { dao.loadFeedItem(any(), any()) } returns null - val result = runBlocking { - store.loadFeedItem("foo", 5L) - } + val result = + runBlocking { + store.loadFeedItem("foo", 5L) + } assertNull(result) @@ -227,9 +235,10 @@ class FeedItemStoreTest : DIAware { fun getItemsToBeCleanedFromFeed() { coEvery { dao.getItemsToBeCleanedFromFeed(any(), any()) } returns listOf(5L) - val result = runBlocking { - store.getItemsToBeCleanedFromFeed(6L, 50) - } + val result = + runBlocking { + store.getItemsToBeCleanedFromFeed(6L, 50) + } assertEquals(5L, result.first()) diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedStoreTest.kt index 890460e7c0..c191a6ac27 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/FeedStoreTest.kt @@ -13,7 +13,6 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify -import java.time.Instant import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList @@ -26,6 +25,7 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.time.Instant class FeedStoreTest : DIAware { private val store: FeedStore by instance() @@ -47,9 +47,10 @@ class FeedStoreTest : DIAware { fun getFeed() { coEvery { dao.loadFeed(5L) } returns Feed(id = 5L) - val feed = runBlocking { - store.getFeed(5L) - } + val feed = + runBlocking { + store.getFeed(5L) + } assertEquals( Feed(id = 5L), @@ -84,9 +85,10 @@ class FeedStoreTest : DIAware { fun allTags() { every { dao.loadAllTags() } returns flowOf(listOf("one", "two")) - val tags = runBlocking { - store.allTags.toList().first() - } + val tags = + runBlocking { + store.allTags.toList().first() + } assertEquals( listOf("one", "two"), @@ -98,9 +100,10 @@ class FeedStoreTest : DIAware { fun saveNewFeed() { coEvery { dao.insertFeed(any()) } returns 5L - val id = runBlocking { - store.saveFeed(Feed()) - } + val id = + runBlocking { + store.saveFeed(Feed()) + } assertEquals( 5L, @@ -110,9 +113,10 @@ class FeedStoreTest : DIAware { @Test fun saveExistingFeed() { - val feedId = runBlocking { - store.saveFeed(Feed(id = 5L)) - } + val feedId = + runBlocking { + store.saveFeed(Feed(id = 5L)) + } assertEquals(5L, feedId) @@ -123,19 +127,21 @@ class FeedStoreTest : DIAware { @Test fun drawerItemsWithUnreadCounts() { - every { dao.loadFlowOfFeedsWithUnreadCounts() } returns flow { - emit( - listOf( - FeedUnreadCount(id = 1, title = "zob", unreadCount = 3, currentlySyncing = true), - FeedUnreadCount(id = 2, title = "bob", tag = "zork", unreadCount = 4, currentlySyncing = false), - FeedUnreadCount(id = 3, title = "alice", tag = "alpha", unreadCount = 5, currentlySyncing = true), - FeedUnreadCount(id = 4, title = "argh", tag = "alpha", unreadCount = 7, currentlySyncing = false), - ), - ) - } - val drawerItems = runBlocking { - store.drawerItemsWithUnreadCounts.toList().first() - } + every { dao.loadFlowOfFeedsWithUnreadCounts() } returns + flow { + emit( + listOf( + FeedUnreadCount(id = 1, title = "zob", unreadCount = 3, currentlySyncing = true), + FeedUnreadCount(id = 2, title = "bob", tag = "zork", unreadCount = 4, currentlySyncing = false), + FeedUnreadCount(id = 3, title = "alice", tag = "alpha", unreadCount = 5, currentlySyncing = true), + FeedUnreadCount(id = 4, title = "argh", tag = "alpha", unreadCount = 7, currentlySyncing = false), + ), + ) + } + val drawerItems = + runBlocking { + store.drawerItemsWithUnreadCounts.toList().first() + } assertEquals( listOf( @@ -153,24 +159,27 @@ class FeedStoreTest : DIAware { @Test fun getFeedTitles() { - every { dao.getFeedTitlesWithId(5L) } returns flowOf( - listOf( - FeedTitle(5L, "fejd"), - ), - ) - every { dao.getFeedTitlesWithTag("foo") } returns flowOf( - listOf( - FeedTitle(5L, "fejd"), - FeedTitle(7L, "axv"), - ), - ) - every { dao.getAllFeedTitles() } returns flowOf( - listOf( - FeedTitle(5L, "fejd"), - FeedTitle(7L, "axv"), - FeedTitle(8L, "zzz"), - ), - ) + every { dao.getFeedTitlesWithId(5L) } returns + flowOf( + listOf( + FeedTitle(5L, "fejd"), + ), + ) + every { dao.getFeedTitlesWithTag("foo") } returns + flowOf( + listOf( + FeedTitle(5L, "fejd"), + FeedTitle(7L, "axv"), + ), + ) + every { dao.getAllFeedTitles() } returns + flowOf( + listOf( + FeedTitle(5L, "fejd"), + FeedTitle(7L, "axv"), + FeedTitle(8L, "zzz"), + ), + ) assertEquals( listOf(FeedTitle(5L, "fejd")), @@ -199,9 +208,10 @@ class FeedStoreTest : DIAware { fun getCurrentlySyncingLatestTimestamp() { every { dao.getCurrentlySyncingLatestTimestamp() } returns flowOf(null) - val result = runBlocking { - store.getCurrentlySyncingLatestTimestamp().toList() - } + val result = + runBlocking { + store.getCurrentlySyncingLatestTimestamp().toList() + } assertEquals(null, result.first()) @@ -226,9 +236,10 @@ class FeedStoreTest : DIAware { fun upsertFeed() { coEvery { dao.upsert(any()) } returns 6L - val result = runBlocking { - store.upsertFeed(Feed()) - } + val result = + runBlocking { + store.upsertFeed(Feed()) + } assertEquals(6L, result) diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt index 38ad213f93..abab2cb01c 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/RepositoryTest.kt @@ -25,10 +25,6 @@ import io.mockk.just import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import java.net.URL -import java.time.Instant -import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -41,6 +37,10 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.net.URL +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertTrue class RepositoryTest : DIAware { private val repository: Repository by instance() @@ -280,27 +280,30 @@ class RepositoryTest : DIAware { @Test fun getScreenTitleForCurrentFeedOrTagAll() { - val result = runBlocking { - repository.getScreenTitleForFeedOrTag(ID_ALL_FEEDS, "").toList().first() - } + val result = + runBlocking { + repository.getScreenTitleForFeedOrTag(ID_ALL_FEEDS, "").toList().first() + } assertEquals(ScreenTitle(title = null, type = FeedType.ALL_FEEDS), result) } @Test fun getScreenTitleForCurrentFeedOrTagSavedArticles() { - val result = runBlocking { - repository.getScreenTitleForFeedOrTag(ID_SAVED_ARTICLES, "").toList().first() - } + val result = + runBlocking { + repository.getScreenTitleForFeedOrTag(ID_SAVED_ARTICLES, "").toList().first() + } assertEquals(ScreenTitle(title = null, type = FeedType.SAVED_ARTICLES), result) } @Test fun getScreenTitleForCurrentFeedOrTagTag() { - val result = runBlocking { - repository.getScreenTitleForFeedOrTag(ID_UNSET, "fwr").toList().first() - } + val result = + runBlocking { + repository.getScreenTitleForFeedOrTag(ID_UNSET, "fwr").toList().first() + } assertEquals(ScreenTitle(title = "fwr", type = FeedType.TAG), result) } @@ -309,9 +312,10 @@ class RepositoryTest : DIAware { fun getScreenTitleForCurrentFeedOrTagFeed() { coEvery { feedStore.getDisplayTitle(5L) } returns "floppa" - val result = runBlocking { - repository.getScreenTitleForFeedOrTag(5L, "fwr").toList().first() - } + val result = + runBlocking { + repository.getScreenTitleForFeedOrTag(5L, "fwr").toList().first() + } assertEquals(ScreenTitle(title = "floppa", type = FeedType.FEED), result) @@ -377,13 +381,15 @@ class RepositoryTest : DIAware { @Test fun getFeedsItemsWithDefaultFullTextParse() { - coEvery { feedItemStore.getFeedsItemsWithDefaultFullTextNeedingDownload() } returns flowOf( - emptyList(), - ) + coEvery { feedItemStore.getFeedsItemsWithDefaultFullTextNeedingDownload() } returns + flowOf( + emptyList(), + ) - val result = runBlocking { - repository.getFeedsItemsWithDefaultFullTextNeedingDownload().first() - } + val result = + runBlocking { + repository.getFeedsItemsWithDefaultFullTextNeedingDownload().first() + } assertTrue { result.isEmpty() @@ -398,9 +404,10 @@ class RepositoryTest : DIAware { fun currentlySyncingLatestTimestamp() { every { feedStore.getCurrentlySyncingLatestTimestamp() } returns flowOf(null) - val result = runBlocking { - repository.currentlySyncingLatestTimestamp.toList() - } + val result = + runBlocking { + repository.currentlySyncingLatestTimestamp.toList() + } assertEquals(1, result.size) assertEquals(Instant.EPOCH, result.first()) @@ -412,10 +419,11 @@ class RepositoryTest : DIAware { @Test fun applyRemoteReadMarks() { - coEvery { syncRemoteStore.getRemoteReadMarksReadyToBeApplied() } returns listOf( - RemoteReadMarkReadyToBeApplied(1L, 2L), - RemoteReadMarkReadyToBeApplied(3L, 4L), - ) + coEvery { syncRemoteStore.getRemoteReadMarksReadyToBeApplied() } returns + listOf( + RemoteReadMarkReadyToBeApplied(1L, 2L), + RemoteReadMarkReadyToBeApplied(3L, 4L), + ) runBlocking { repository.applyRemoteReadMarks() diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt index 1e63f59c7d..1401accbc8 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SettingsStoreTest.kt @@ -14,9 +14,6 @@ import io.mockk.confirmVerified import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify -import java.time.Instant -import kotlin.test.assertEquals -import kotlin.test.assertTrue import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test @@ -25,6 +22,9 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertTrue class SettingsStoreTest : DIAware { private val store: SettingsStore by instance() @@ -377,15 +377,16 @@ class SettingsStoreTest : DIAware { @Test fun getAllSettingsForOPMLExport() { - every { sp.all } returns mapOf( - PREF_SHOW_FAB to false, - "Not a pref" to true, - // Not OK if imported on a fresh device - PREF_LAST_FEED_TAG to "foo", - PREF_LAST_FEED_ID to 1L, - PREF_LAST_ARTICLE_ID to 2L, - PREF_IS_ARTICLE_OPEN to true, - ) + every { sp.all } returns + mapOf( + PREF_SHOW_FAB to false, + "Not a pref" to true, + // Not OK if imported on a fresh device + PREF_LAST_FEED_TAG to "foo", + PREF_LAST_FEED_ID to 1L, + PREF_LAST_ARTICLE_ID to 2L, + PREF_IS_ARTICLE_OPEN to true, + ) val allSettings = store.getAllSettings() assertEquals(1, allSettings.size) } diff --git a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStoreTest.kt b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStoreTest.kt index 4905b9a888..f147d98a7c 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStoreTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/archmodel/SyncRemoteStoreTest.kt @@ -11,8 +11,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.impl.annotations.MockK -import java.time.Instant -import kotlin.test.assertEquals import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -23,6 +21,8 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.time.Instant +import kotlin.test.assertEquals class SyncRemoteStoreTest : DIAware { private val store: SyncRemoteStore by instance() @@ -52,9 +52,10 @@ class SyncRemoteStoreTest : DIAware { fun getNewSyncRemoteInsertsDefault() { coEvery { dao.getSyncRemote() } returns null - val result = runBlocking { - store.getSyncRemote() - } + val result = + runBlocking { + store.getSyncRemote() + } coVerify { dao.getSyncRemote() @@ -71,9 +72,10 @@ class SyncRemoteStoreTest : DIAware { val expected = SyncRemote(id = 1L) coEvery { dao.getSyncRemote() } returns expected - val result = runBlocking { - store.getSyncRemote() - } + val result = + runBlocking { + store.getSyncRemote() + } coVerify { dao.getSyncRemote() @@ -112,9 +114,10 @@ class SyncRemoteStoreTest : DIAware { fun getNextFeedItemWithoutSyncedReadMark() { coEvery { readStatusDao.getNextFeedItemWithoutSyncedReadMark() } returns emptyFlow() - val result = runBlocking { - store.getNextFeedItemWithoutSyncedReadMark().toList() - } + val result = + runBlocking { + store.getNextFeedItemWithoutSyncedReadMark().toList() + } assertEquals(emptyList(), result) diff --git a/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt b/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt index f6e1e61fea..61b594cd6a 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/db/FeedItemTest.kt @@ -35,7 +35,10 @@ class FeedItemTest { @Test fun magnetLinkGivesNullFilename() { - val fi = FeedItem(enclosureLink = "magnet:?xt=urn:btih:E6F5537982306CF703E5016B2BBD36C9B3E3CDD0&dn=Game+of+Thrones+S07E01+PROPER+WEBRip+x264+RARBG&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce") + val fi = + FeedItem( + enclosureLink = "magnet:?xt=urn:btih:E6F5537982306CF703E5016B2BBD36C9B3E3CDD0&dn=Game+of+Thrones+S07E01+PROPER+WEBRip+x264+RARBG&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.trackerfix.com%3A80%2Fannounce", + ) assertNull(fi.enclosureFilename) } } diff --git a/app/src/test/java/com/nononsenseapps/feeder/db/room/ConvertersTest.kt b/app/src/test/java/com/nononsenseapps/feeder/db/room/ConvertersTest.kt index 93afc40ad9..38b46d8254 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/db/room/ConvertersTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/db/room/ConvertersTest.kt @@ -1,10 +1,10 @@ package com.nononsenseapps.feeder.db.room -import java.time.Instant -import java.time.ZonedDateTime import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test +import java.time.Instant +import java.time.ZonedDateTime class ConvertersTest { @Test diff --git a/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserClientTest.kt b/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserClientTest.kt index 6c352ea6d9..516b58e04d 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserClientTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserClientTest.kt @@ -2,10 +2,6 @@ package com.nononsenseapps.feeder.model import com.nononsenseapps.feeder.di.networkModule import com.nononsenseapps.jsonfeed.cachingHttpClient -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -21,15 +17,20 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue class FeedParserClientTest : DIAware { override val di by DI.lazy { - bind() with singleton { - cachingHttpClient() - .newBuilder() - .addNetworkInterceptor(UserAgentInterceptor) - .build() - } + bind() with + singleton { + cachingHttpClient() + .newBuilder() + .addNetworkInterceptor(UserAgentInterceptor) + .build() + } import(networkModule) } val server = MockWebServer() @@ -59,10 +60,10 @@ class FeedParserClientTest : DIAware { addHeader("Content-Type", "application/xml") this.setBody( """ - - - No auth - + + + No auth + """.trimIndent(), ) }, @@ -108,9 +109,10 @@ class FeedParserClientTest : DIAware { } } - val headers = withContext(Dispatchers.IO) { - server.takeRequest().headers - } + val headers = + withContext(Dispatchers.IO) { + server.takeRequest().headers + } val userAgents = headers.toMultimap()["User-Agent"] @@ -125,46 +127,47 @@ class FeedParserClientTest : DIAware { } @Test - fun badProtocolInLinksAreHandled() = runBlocking { - server.enqueue( - MockResponse().apply { - setResponseCode(200) - addHeader("Content-Type", "application/xml") - this.setBody( - """ - - - - QC RSS - http://www.questionablecontent.net - The Official QC RSS Feed - Feeder 4.3.2(5732); Mac OS X Version 12.4 (Build 21F79) - https://reinventedsoftware.com/feeder/ - - http://blogs.law.harvard.edu/tech/rss - en - Wed, 01 Jun 2022 22:09:29 -0300 - Wed, 01 Jun 2022 22:09:29 -0300 - - - Callout Post - ttp://questionablecontent.net/view.php?comic=4776 - - ]]> - Sun, 01 May 2022 22:06:19 -0300 - 325BE5B5-8206-4C4A-9E94-828EE3DD7763 - - - - """.trimIndent(), - ) - }, - ) + fun badProtocolInLinksAreHandled() = + runBlocking { + server.enqueue( + MockResponse().apply { + setResponseCode(200) + addHeader("Content-Type", "application/xml") + this.setBody( + """ + + + + QC RSS + http://www.questionablecontent.net + The Official QC RSS Feed + Feeder 4.3.2(5732); Mac OS X Version 12.4 (Build 21F79) + https://reinventedsoftware.com/feeder/ + + http://blogs.law.harvard.edu/tech/rss + en + Wed, 01 Jun 2022 22:09:29 -0300 + Wed, 01 Jun 2022 22:09:29 -0300 + + + Callout Post + ttp://questionablecontent.net/view.php?comic=4776 + + ]]> + Sun, 01 May 2022 22:06:19 -0300 + 325BE5B5-8206-4C4A-9E94-828EE3DD7763 + + + + """.trimIndent(), + ) + }, + ) - val url = server.url("/foo").toUrl() - // This should not crash - val result = feedParser.parseFeedUrl(url) - assertEquals("http://www.questionablecontent.net/comics/4776.png", result.getOrNull()?.items?.first()?.image) - } + val url = server.url("/foo").toUrl() + // This should not crash + val result = feedParser.parseFeedUrl(url) + assertEquals("http://www.questionablecontent.net/comics/4776.png", result.getOrNull()?.items?.first()?.image) + } } diff --git a/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserTest.kt b/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserTest.kt index dec2e23757..6b86c64fd1 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/model/FeedParserTest.kt @@ -3,13 +3,6 @@ package com.nononsenseapps.feeder.model import com.nononsenseapps.feeder.di.networkModule import com.nononsenseapps.jsonfeed.Author import com.nononsenseapps.jsonfeed.cachingHttpClient -import java.io.BufferedReader -import java.net.URL -import kotlin.test.Ignore -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue import kotlinx.coroutines.runBlocking import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -27,6 +20,13 @@ import org.kodein.di.DIAware import org.kodein.di.bind import org.kodein.di.instance import org.kodein.di.singleton +import java.io.BufferedReader +import java.net.URL +import kotlin.test.Ignore +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue class FeedParserTest : DIAware { @Rule @@ -58,28 +58,33 @@ class FeedParserTest : DIAware { } @Test - fun dcCreatorEndsUpAsAuthor() = runBlocking { - readResource("openstreetmap.xml") { - val feed = feedParser.parseFeedResponse( - URL("http://https://www.openstreetmap.org/diary/rss"), - it, - null, - ) - val item = feed.getOrNull()?.items!!.first() - - assertEquals(Author(name = "0235"), item.author) + fun dcCreatorEndsUpAsAuthor() = + runBlocking { + readResource("openstreetmap.xml") { + val feed = + feedParser.parseFeedResponse( + URL("http://https://www.openstreetmap.org/diary/rss"), + it, + null, + ) + val item = feed.getOrNull()?.items!!.first() + + assertEquals(Author(name = "0235"), item.author) + } } - } @Test @Throws(Exception::class) fun htmlAtomContentGetsUnescaped() { readResource("atom_hnapp.xml") { - val feed = feedParser.parseFeedResponse( - URL("http://hnapp.com/rss?q=type%3Astory%20score%3E36%20-bitcoin%20-ethereum%20-cryptocurrency%20-blockchain%20-snowden%20-hiring%20-ask"), - it, - null, - ) + val feed = + feedParser.parseFeedResponse( + URL( + "http://hnapp.com/rss?q=type%3Astory%20score%3E36%20-bitcoin%20-ethereum%20-cryptocurrency%20-blockchain%20-snowden%20-hiring%20-ask", + ), + it, + null, + ) val item = feed.getOrNull()?.items!![0] assertEquals( @@ -101,11 +106,12 @@ class FeedParserTest : DIAware { @Throws(Exception::class) fun enclosedImageIsUsedAsThumbnail() { readResource("rss_lemonde.xml") { - val feed = feedParser.parseFeedResponse( - URL("http://www.lemonde.fr/rss/une.xml"), - it, - null, - ) + val feed = + feedParser.parseFeedResponse( + URL("http://www.lemonde.fr/rss/une.xml"), + it, + null, + ) val item = feed.getOrNull()?.items!![0] assertEquals( @@ -116,113 +122,125 @@ class FeedParserTest : DIAware { } @Test - fun parsesYoutubeMediaInfo() = runBlocking { - val feed = readResource("atom_youtube.xml") { - feedParser.parseFeedResponse( - URL("http://www.youtube.com/feeds/videos.xml"), - it, - null, - ) - } + fun parsesYoutubeMediaInfo() = + runBlocking { + val feed = + readResource("atom_youtube.xml") { + feedParser.parseFeedResponse( + URL("http://www.youtube.com/feeds/videos.xml"), + it, + null, + ) + } - val item = feed.getOrNull()?.items!!.first() + val item = feed.getOrNull()?.items!!.first() - assertEquals("Can You Observe a Typical Universe?", item.title) - assertEquals("https://i2.ytimg.com/vi/q-6oU3jXAho/hqdefault.jpg", item.image) - assertTrue { - item.content_text!!.startsWith("Sign Up on Patreon to get access to the Space Time Discord!") + assertEquals("Can You Observe a Typical Universe?", item.title) + assertEquals("https://i2.ytimg.com/vi/q-6oU3jXAho/hqdefault.jpg", item.image) + assertTrue { + item.content_text!!.startsWith("Sign Up on Patreon to get access to the Space Time Discord!") + } } - } @Test - fun parsesPeertubeMediaInfo() = runBlocking { - val feed = readResource("rss_peertube.xml") { - feedParser.parseFeedResponse(URL("https://framatube.org/feeds/videos.xml"), it, null) - } - - val item = feed.getOrNull()?.items!!.first() + fun parsesPeertubeMediaInfo() = + runBlocking { + val feed = + readResource("rss_peertube.xml") { + feedParser.parseFeedResponse(URL("https://framatube.org/feeds/videos.xml"), it, null) + } - assertEquals("1.4. Et les réseaux sociaux ?", item.title) - assertEquals( - "https://framatube.org/static/thumbnails/ed5c048d-01f3-4ceb-97db-6e278de512b0.jpg", - item.image, - ) - assertTrue { - item.content_text!!.startsWith("MOOC CHATONS#1 - Internet") - } - } + val item = feed.getOrNull()?.items!!.first() - @Test - fun parsesYahooMediaRss() = runBlocking { - val feed = readResource("rss_mediarss.xml") { - feedParser.parseFeedResponse( - URL("https://rutube.ru/mrss/video/person/11234072/"), - it, - null, + assertEquals("1.4. Et les réseaux sociaux ?", item.title) + assertEquals( + "https://framatube.org/static/thumbnails/ed5c048d-01f3-4ceb-97db-6e278de512b0.jpg", + item.image, ) + assertTrue { + item.content_text!!.startsWith("MOOC CHATONS#1 - Internet") + } } - val item = feed.getOrNull()?.items!!.first() + @Test + fun parsesYahooMediaRss() = + runBlocking { + val feed = + readResource("rss_mediarss.xml") { + feedParser.parseFeedResponse( + URL("https://rutube.ru/mrss/video/person/11234072/"), + it, + null, + ) + } - assertEquals("Камеди Клаб: «3 сентября»", item.title) - assertEquals( - "https://pic.rutubelist.ru/video/93/24/93245691f0e18d063da5fa5cd60fa6de.jpg?size=l", - item.image, - ) - } + val item = feed.getOrNull()?.items!!.first() - @Test - fun parsesYahooMediaRss2() = runBlocking { - val feed = readResource("rss_myanimelist.xml") { - feedParser.parseFeedResponse( - URL("https://myanimelist.net/rss/news.xml"), - it, - null, + assertEquals("Камеди Клаб: «3 сентября»", item.title) + assertEquals( + "https://pic.rutubelist.ru/video/93/24/93245691f0e18d063da5fa5cd60fa6de.jpg?size=l", + item.image, ) } - val item = feed.getOrNull()?.items!!.first() + @Test + fun parsesYahooMediaRss2() = + runBlocking { + val feed = + readResource("rss_myanimelist.xml") { + feedParser.parseFeedResponse( + URL("https://myanimelist.net/rss/news.xml"), + it, + null, + ) + } - assertEquals( - "https://cdn.myanimelist.net/s/common/uploaded_files/1664092688-dd34666e64d7ae624e6e2c70087c181f.jpeg", - item.image, - ) - } + val item = feed.getOrNull()?.items!!.first() - @Test - fun parsesYahooMediaRssPicksLargestThumbnail() = runBlocking { - val feed = readResource("rss_theguardian.xml") { - feedParser.parseFeedResponse( - URL("https://www.theguardian.com/world/rss"), - it, - null, + assertEquals( + "https://cdn.myanimelist.net/s/common/uploaded_files/1664092688-dd34666e64d7ae624e6e2c70087c181f.jpeg", + item.image, ) } - val item = feed.getOrNull()?.items!!.first() + @Test + fun parsesYahooMediaRssPicksLargestThumbnail() = + runBlocking { + val feed = + readResource("rss_theguardian.xml") { + feedParser.parseFeedResponse( + URL("https://www.theguardian.com/world/rss"), + it, + null, + ) + } - assertEquals( - "https://i.guim.co.uk/img/media/c4d7049b24ee34d1c4c630c751094cabc57c54f6/0_32_6000_3601/master/6000.jpg?width=460&quality=85&auto=format&fit=max&s=919d72fef6d4f3469aff69e94964126c", - item.image, - ) - } + val item = feed.getOrNull()?.items!!.first() - @Test - fun encodingTestWithSmileys() = runBlocking { - val feed = readResource("rss_lawnchair.xml") { - feedParser.parseFeedResponse( - URL("https://nitter.weiler.rocks/lawnchairapp/rss"), - it, - null, + assertEquals( + "https://i.guim.co.uk/img/media/c4d7049b24ee34d1c4c630c751094cabc57c54f6/0_32_6000_3601/master/6000.jpg?width=460&quality=85&auto=format&fit=max&s=919d72fef6d4f3469aff69e94964126c", + item.image, ) } - val item = feed.getOrNull()?.items!!.first() + @Test + fun encodingTestWithSmileys() = + runBlocking { + val feed = + readResource("rss_lawnchair.xml") { + feedParser.parseFeedResponse( + URL("https://nitter.weiler.rocks/lawnchairapp/rss"), + it, + null, + ) + } + + val item = feed.getOrNull()?.items!!.first() - assertTrue { - "\uD83D\uDE0D\uD83E\uDD29" in item.content_html!! + assertTrue { + "\uD83D\uDE0D\uD83E\uDD29" in item.content_html!! + } } - } @Test @Throws(Exception::class) @@ -240,31 +258,37 @@ class FeedParserTest : DIAware { } @Test - fun findsAppleTouchIconForFeed() = runBlocking { - val metadata = readResource("fz.html") { - feedParser.getSiteMetaDataInHtml(URL("https://www.fz.se"), it) - } + fun findsAppleTouchIconForFeed() = + runBlocking { + val metadata = + readResource("fz.html") { + feedParser.getSiteMetaDataInHtml(URL("https://www.fz.se"), it) + } - assertEquals("https://www.fz.se/apple-touch-icon.png", metadata.getOrNull()!!.feedImage) - } + assertEquals("https://www.fz.se/apple-touch-icon.png", metadata.getOrNull()!!.feedImage) + } @Test - fun findsAppleTouchIconInHtml() = runBlocking { - val icon = readResource("fz.html") { - feedParser.getFeedIconInHtml(it, URL("https://www.fz.se")) - } + fun findsAppleTouchIconInHtml() = + runBlocking { + val icon = + readResource("fz.html") { + feedParser.getFeedIconInHtml(it, URL("https://www.fz.se")) + } - assertEquals("https://www.fz.se/apple-touch-icon.png", icon) - } + assertEquals("https://www.fz.se/apple-touch-icon.png", icon) + } @Test - fun findsFaviconInHtml() = runBlocking { - val icon = readResource("slashdot.html") { - feedParser.getFeedIconInHtml(it, URL("https://slashdot.org")) - } + fun findsFaviconInHtml() = + runBlocking { + val icon = + readResource("slashdot.html") { + feedParser.getFeedIconInHtml(it, URL("https://slashdot.org")) + } - assertEquals("https://slashdot.org/favicon.ico", icon) - } + assertEquals("https://slashdot.org/favicon.ico", icon) + } @Test @Throws(Exception::class) @@ -295,537 +319,574 @@ class FeedParserTest : DIAware { @Test @Throws(Exception::class) - fun encodingIsHandledInAtomRss() = runBlocking { - val feed = golemDe.use { feedParser.parseFeedResponse(it) } + fun encodingIsHandledInAtomRss() = + runBlocking { + val feed = golemDe.use { feedParser.parseFeedResponse(it) } - assertEquals(true, feed.getOrNull()?.items?.get(0)?.content_text?.contains("größte")) - } + assertEquals(true, feed.getOrNull()?.items?.get(0)?.content_text?.contains("größte")) + } // Bug in Rome which I am working around, this will crash if not worked around @Test @Throws(Exception::class) - fun emptySlashCommentsDontCrashParsingAndEncodingIsStillRespected() = runBlocking { - val feed = emptySlashComment.use { feedParser.parseFeedResponse(it) } - - assertEquals(1, feed.getOrNull()?.items?.size) - assertEquals( - true, - feed.getOrNull()?.items?.get(0)?.content_text?.contains("größte"), - feed.getOrNull()?.items?.get(0)?.content_text!!, - ) - } + fun emptySlashCommentsDontCrashParsingAndEncodingIsStillRespected() = + runBlocking { + val feed = emptySlashComment.use { feedParser.parseFeedResponse(it) } + + assertEquals(1, feed.getOrNull()?.items?.size) + assertEquals( + true, + feed.getOrNull()?.items?.get(0)?.content_text?.contains("größte"), + feed.getOrNull()?.items?.get(0)?.content_text!!, + ) + } @Test @Throws(Exception::class) - fun correctAlternateLinkInAtomIsUsedForUrl() = runBlocking { - val feed = utdelningsSeglarenAtom.use { feedParser.parseFeedResponse(it) } + fun correctAlternateLinkInAtomIsUsedForUrl() = + runBlocking { + val feed = utdelningsSeglarenAtom.use { feedParser.parseFeedResponse(it) } - assertEquals( - "http://utdelningsseglaren.blogspot.com/2017/12/tips-pa-6-podcasts.html", - feed.getOrNull()?.items?.first()?.url, - ) - } + assertEquals( + "http://utdelningsseglaren.blogspot.com/2017/12/tips-pa-6-podcasts.html", + feed.getOrNull()?.items?.first()?.url, + ) + } @Test @Throws(Exception::class) - fun relativeLinksAreMadeAbsoluteAtom() = runBlocking { - val feed = feedParser.parseFeedResponse( - URL("http://cowboyprogrammer.org/feed.atom"), - atomRelative, - null, - ) - assertTrue { feed.isRight() } - - assertEquals("http://cowboyprogrammer.org/feed.atom", feed.getOrNull()!!.feed_url) - } + fun relativeLinksAreMadeAbsoluteAtom() = + runBlocking { + val feed = + feedParser.parseFeedResponse( + URL("http://cowboyprogrammer.org/feed.atom"), + atomRelative, + null, + ) + assertTrue { feed.isRight() } + + assertEquals("http://cowboyprogrammer.org/feed.atom", feed.getOrNull()!!.feed_url) + } @Test @Throws(Exception::class) - fun relativeLinksAreMadeAbsoluteAtomNoBase() = runBlocking { - val feed = feedParser.parseFeedResponse( - URL("http://cowboyprogrammer.org/feed.atom"), - atomRelativeNoBase, - null, - ) - assertTrue { feed.isRight() } - - assertEquals("http://cowboyprogrammer.org/feed.atom", feed.getOrNull()!!.feed_url) - } + fun relativeLinksAreMadeAbsoluteAtomNoBase() = + runBlocking { + val feed = + feedParser.parseFeedResponse( + URL("http://cowboyprogrammer.org/feed.atom"), + atomRelativeNoBase, + null, + ) + assertTrue { feed.isRight() } + + assertEquals("http://cowboyprogrammer.org/feed.atom", feed.getOrNull()!!.feed_url) + } @Test @Throws(Exception::class) - fun relativeFeedLinkInRssIsMadeAbsolute() = runBlocking { - val feed = lineageosRss.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } - - assertEquals("https://lineageos.org/", feed.getOrNull()!!.home_page_url) - assertEquals("https://lineageos.org/feed.xml", feed.getOrNull()!!.feed_url) - - assertEquals("https://lineageos.org/Changelog-16", feed.getOrNull()?.items?.get(0)?.id) - assertEquals("https://lineageos.org/Changelog-16/", feed.getOrNull()?.items?.get(0)?.url) - assertEquals( - "https://lineageos.org/images/2018-02-25/lineageos-15.1-hero.png", - feed.getOrNull()?.items?.get(0)?.image, - ) - } + fun relativeFeedLinkInRssIsMadeAbsolute() = + runBlocking { + val feed = lineageosRss.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } + + assertEquals("https://lineageos.org/", feed.getOrNull()!!.home_page_url) + assertEquals("https://lineageos.org/feed.xml", feed.getOrNull()!!.feed_url) + + assertEquals("https://lineageos.org/Changelog-16", feed.getOrNull()?.items?.get(0)?.id) + assertEquals("https://lineageos.org/Changelog-16/", feed.getOrNull()?.items?.get(0)?.url) + assertEquals( + "https://lineageos.org/images/2018-02-25/lineageos-15.1-hero.png", + feed.getOrNull()?.items?.get(0)?.image, + ) + } @Test @Throws(Exception::class) - fun noStyles() = runBlocking { - val feed = researchRsc.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } + fun noStyles() = + runBlocking { + val feed = researchRsc.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } - assertEquals("http://research.swtch.com/feed.atom", feed.getOrNull()!!.feed_url) + assertEquals("http://research.swtch.com/feed.atom", feed.getOrNull()!!.feed_url) - assertEquals(17, feed.getOrNull()?.items!!.size) + assertEquals(17, feed.getOrNull()?.items!!.size) - val (_, _, _, title, _, _, summary, image) = feed.getOrNull()?.items!![9] + val (_, _, _, title, _, _, summary, image) = feed.getOrNull()?.items!![9] - assertEquals("http://research.swtch.com/qr-bbc.png", image) + assertEquals("http://research.swtch.com/qr-bbc.png", image) - assertEquals( - "QArt Codes", - title, - ) + assertEquals( + "QArt Codes", + title, + ) - // Style tags should be ignored - assertEquals( - "QR codes are 2-dimensional bar codes that encode arbitrary text strings. A common use of QR codes is to encode URLs so that people can scan a QR code (for example, on an advertising poster, building r", - summary, - ) - } + // Style tags should be ignored + assertEquals( + "QR codes are 2-dimensional bar codes that encode arbitrary text strings. A common use of QR codes is to encode URLs so that people can scan a QR code (for example, on an advertising poster, building r", + summary, + ) + } @Test @Throws(Exception::class) - fun feedAuthorIsUsedAsFallback() = runBlocking { - val feed = researchRsc.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } + fun feedAuthorIsUsedAsFallback() = + runBlocking { + val feed = researchRsc.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } - assertEquals("http://research.swtch.com/feed.atom", feed.getOrNull()!!.feed_url) + assertEquals("http://research.swtch.com/feed.atom", feed.getOrNull()!!.feed_url) - assertEquals(17, feed.getOrNull()?.items!!.size) + assertEquals(17, feed.getOrNull()?.items!!.size) - val (_, _, _, _, _, _, _, _, _, _, _, author) = feed.getOrNull()?.items!![9] + val (_, _, _, _, _, _, _, _, _, _, _, author) = feed.getOrNull()?.items!![9] - assertEquals("Russ Cox", feed.getOrNull()!!.author!!.name) - assertEquals(feed.getOrNull()!!.author, author) - } + assertEquals("Russ Cox", feed.getOrNull()!!.author!!.name) + assertEquals(feed.getOrNull()!!.author, author) + } @Test - fun nixos() = runBlocking { - val feed = nixosRss.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } + fun nixos() = + runBlocking { + val feed = nixosRss.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } - assertEquals("http://nixos.org/news-rss.xml", feed.getOrNull()!!.feed_url) + assertEquals("http://nixos.org/news-rss.xml", feed.getOrNull()!!.feed_url) - assertEquals(99, feed.getOrNull()?.items!!.size) + assertEquals(99, feed.getOrNull()?.items!!.size) - val (_, _, _, title, _, _, _, image) = feed.getOrNull()?.items!![0] + val (_, _, _, title, _, _, _, image) = feed.getOrNull()?.items!![0] - assertEquals("https://nixos.org/logo/nixos-logo-18.09-jellyfish-lores.png", image) - assertEquals("NixOS 18.09 released", title) - } + assertEquals("https://nixos.org/logo/nixos-logo-18.09-jellyfish-lores.png", image) + assertEquals("NixOS 18.09 released", title) + } @Test @Throws(Exception::class) - fun nixers() = runBlocking { - val feed = nixersRss.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } + fun nixers() = + runBlocking { + val feed = nixersRss.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } - assertEquals("https://newsletter.nixers.net/feed.xml", feed.getOrNull()!!.feed_url) + assertEquals("https://newsletter.nixers.net/feed.xml", feed.getOrNull()!!.feed_url) - assertEquals(111, feed.getOrNull()?.items!!.size) + assertEquals(111, feed.getOrNull()?.items!!.size) - val item = feed.getOrNull()?.items!![0] + val item = feed.getOrNull()?.items!![0] - // Timezone issues - so only verify date - assertTrue(message = "Expected a pubdate to have been parsed") { - item.date_published!!.startsWith("2019-01-25") + // Timezone issues - so only verify date + assertTrue(message = "Expected a pubdate to have been parsed") { + item.date_published!!.startsWith("2019-01-25") + } } - } @Test - fun doesNotFetchVideos(): Unit = runBlocking { - val result = videoResponse.use { feedParser.parseFeedResponse(it) } - assertTrue { - result.isLeft() - } - assertTrue { - result.leftOrNull()!!.description.contains("video/mp4") + fun doesNotFetchVideos(): Unit = + runBlocking { + val result = videoResponse.use { feedParser.parseFeedResponse(it) } + assertTrue { + result.isLeft() + } + assertTrue { + result.leftOrNull()!!.description.contains("video/mp4") + } } - } @Test @Throws(Exception::class) - fun cyklist() = runBlocking { - val feed = cyklistBloggen.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } + fun cyklist() = + runBlocking { + val feed = cyklistBloggen.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } - assertEquals("http://www.cyklistbloggen.se/feed/", feed.getOrNull()!!.feed_url) + assertEquals("http://www.cyklistbloggen.se/feed/", feed.getOrNull()!!.feed_url) - assertEquals(10, feed.getOrNull()?.items!!.size) + assertEquals(10, feed.getOrNull()?.items!!.size) - val (_, _, _, title, _, _, summary, image) = feed.getOrNull()?.items!![0] + val (_, _, _, title, _, _, summary, image) = feed.getOrNull()?.items!![0] - assertEquals( - "http://www.cyklistbloggen.se/wp-content/uploads/2014/01/Danviksklippan-skyltad.jpg", - image, - ) + assertEquals( + "http://www.cyklistbloggen.se/wp-content/uploads/2014/01/Danviksklippan-skyltad.jpg", + image, + ) - assertEquals( - "Ingen ombyggning av Danvikstull", - title, - ) + assertEquals( + "Ingen ombyggning av Danvikstull", + title, + ) - // Make sure character 160 (non-breaking space) is trimmed - assertEquals( - "För mer än tre år sedan aviserade dåvarande Allians-styrda Stockholms Stad att man äntligen skulle bredda den extremt smala passagen på pendlingsstråket vid Danvikstull: I smalaste passagen är gångdel", - summary, - ) - } + // Make sure character 160 (non-breaking space) is trimmed + assertEquals( + "För mer än tre år sedan aviserade dåvarande Allians-styrda Stockholms Stad att man äntligen skulle bredda den extremt smala passagen på pendlingsstråket vid Danvikstull: I smalaste passagen är gångdel", + summary, + ) + } @Test @Throws(Exception::class) - fun cowboy() = runBlocking { - val feed = cowboyRss.use { feedParser.parseFeedResponse(it) } - assertTrue { feed.isRight() } - - assertEquals("http://cowboyprogrammer.org/index.xml", feed.getOrNull()!!.feed_url) - - assertEquals(15, feed.getOrNull()?.items!!.size) - - var entry = feed.getOrNull()?.items!![1] - - assertEquals( - "https://cowboyprogrammer.org/images/zopfli_all_the_things.jpg", - entry.image, - ) - - // Snippet should not contain images - entry = feed.getOrNull()?.items!![4] - assertEquals("Fixing the up button in Python shell history", entry.title) - assertEquals( - "In case your python/ipython shell doesn’t have a working history, e.g. pressing ↑ only prints some nonsensical ^[[A, then you are missing either the readline or ncurses library. Ipython is more descri", - entry.summary, - ) - // Snippet should not contain links - entry = feed.getOrNull()?.items!![1] - assertEquals("Compress all the images!", entry.title) - assertEquals( - "Update 2016-11-22: Made the Makefile compatible with BSD sed (MacOS) One advantage that static sites, such as those built by Hugo, provide is fast loading times. Because there is no processing to be d", - entry.summary, - ) - } + fun cowboy() = + runBlocking { + val feed = cowboyRss.use { feedParser.parseFeedResponse(it) } + assertTrue { feed.isRight() } + + assertEquals("http://cowboyprogrammer.org/index.xml", feed.getOrNull()!!.feed_url) + + assertEquals(15, feed.getOrNull()?.items!!.size) + + var entry = feed.getOrNull()?.items!![1] + + assertEquals( + "https://cowboyprogrammer.org/images/zopfli_all_the_things.jpg", + entry.image, + ) + + // Snippet should not contain images + entry = feed.getOrNull()?.items!![4] + assertEquals("Fixing the up button in Python shell history", entry.title) + assertEquals( + "In case your python/ipython shell doesn’t have a working history, e.g. pressing ↑ only prints some nonsensical ^[[A, then you are missing either the readline or ncurses library. Ipython is more descri", + entry.summary, + ) + // Snippet should not contain links + entry = feed.getOrNull()?.items!![1] + assertEquals("Compress all the images!", entry.title) + assertEquals( + "Update 2016-11-22: Made the Makefile compatible with BSD sed (MacOS) One advantage that static sites, such as those built by Hugo, provide is fast loading times. Because there is no processing to be d", + entry.summary, + ) + } @Test @Throws(Exception::class) - fun rss() = runBlocking { - val feed = cornucopiaRss.use { feedParser.parseFeedResponse(it) } - val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! - - assertEquals("http://cornucopia.cornubot.se/", home_page_url) - assertEquals("https://cornucopia.cornubot.se/feeds/posts/default?alt=rss", feed_url) - - assertEquals(25, items!!.size) - val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0] - - assertEquals( - "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", - title, - ) - assertEquals( - "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", - title, - ) - - assertEquals( - "För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k", - summary, - ) - assertTrue(content_html!!.startsWith("För tredje månaden på raken")) - assertEquals( - "https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png", - image, - ) - - assertEquals?>(emptyList(), attachments) - } + fun rss() = + runBlocking { + val feed = cornucopiaRss.use { feedParser.parseFeedResponse(it) } + val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! + + assertEquals("http://cornucopia.cornubot.se/", home_page_url) + assertEquals("https://cornucopia.cornubot.se/feeds/posts/default?alt=rss", feed_url) + + assertEquals(25, items!!.size) + val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0] + + assertEquals( + "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", + title, + ) + assertEquals( + "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", + title, + ) + + assertEquals( + "För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k", + summary, + ) + assertTrue(content_html!!.startsWith("För tredje månaden på raken")) + assertEquals( + "https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png", + image, + ) + + assertEquals?>(emptyList(), attachments) + } @Test @Throws(Exception::class) - fun atom() = runBlocking { - val feed = cornucopiaAtom.use { feedParser.parseFeedResponse(it) } - val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! - - assertEquals("http://cornucopia.cornubot.se/", home_page_url) - assertEquals("http://www.blogger.com/feeds/8354057230547055221/posts/default", feed_url) - - assertEquals(25, items!!.size) - val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0] - - assertEquals( - "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", - title, - ) - assertEquals( - "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", - title, - ) - - assertEquals( - "För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k", - summary, - ) - assertTrue(content_html!!.startsWith("För tredje månaden på raken")) - assertEquals( - "https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png", - image, - ) - - assertEquals?>(emptyList(), attachments) - } + fun atom() = + runBlocking { + val feed = cornucopiaAtom.use { feedParser.parseFeedResponse(it) } + val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! + + assertEquals("http://cornucopia.cornubot.se/", home_page_url) + assertEquals("http://www.blogger.com/feeds/8354057230547055221/posts/default", feed_url) + + assertEquals(25, items!!.size) + val (_, _, _, title, content_html, _, summary, image, _, _, _, _, _, attachments) = items[0] + + assertEquals( + "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", + title, + ) + assertEquals( + "Tredje månaden med överhettad svensk ekonomi - tydlig säljsignal för börsen", + title, + ) + + assertEquals( + "För tredje månaden på raken ligger Konjunkturinsitutets barometerindikator (\"konjunkturbarometern\") kvar i överhettat läge. Det råder alltså en klart och tydligt långsiktig säljsignal i enlighet med k", + summary, + ) + assertTrue(content_html!!.startsWith("För tredje månaden på raken")) + assertEquals( + "https://1.bp.blogspot.com/-hD_mqKJx-XY/WLwTIKSEt6I/AAAAAAAAqfI/sztWEjwSYAoN22y_YfnZ-yotKjQsypZHACLcB/s72-c/konj.png", + image, + ) + + assertEquals?>(emptyList(), attachments) + } @Test @Throws(Exception::class) - fun atomCowboy() = runBlocking { - val feed = cowboyAtom.use { feedParser.parseFeedResponse(it) } - val (_, _, _, _, _, _, _, icon, _, _, _, _, items) = feed.getOrNull()!! + fun atomCowboy() = + runBlocking { + val feed = cowboyAtom.use { feedParser.parseFeedResponse(it) } + val (_, _, _, _, _, _, _, icon, _, _, _, _, items) = feed.getOrNull()!! - assertEquals(15, items!!.size) - val (id, _, _, _, _, _, _, image, _, date_published) = items[1] + assertEquals(15, items!!.size) + val (id, _, _, _, _, _, _, image, _, date_published) = items[1] - assertEquals("http://cowboyprogrammer.org/dummy-id-to-distinguis-from-alternate-link", id) - assertTrue(date_published!!.contains("2016"), "Should take the updated timestamp") - assertEquals( - "http://localhost:1313/images/zopfli_all_the_things.jpg", - image, - ) + assertEquals("http://cowboyprogrammer.org/dummy-id-to-distinguis-from-alternate-link", id) + assertTrue(date_published!!.contains("2016"), "Should take the updated timestamp") + assertEquals( + "http://localhost:1313/images/zopfli_all_the_things.jpg", + image, + ) - assertEquals("http://localhost:1313/css/images/logo.png", icon) - } + assertEquals("http://localhost:1313/css/images/logo.png", icon) + } @Test @Throws(Exception::class) - fun morningPaper() = runBlocking { - val feed = morningPaper.use { feedParser.parseFeedResponse(it) } - val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! + fun morningPaper() = + runBlocking { + val feed = morningPaper.use { feedParser.parseFeedResponse(it) } + val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! - assertEquals("https://blog.acolyer.org", home_page_url) - assertEquals("https://blog.acolyer.org/feed/", feed_url) + assertEquals("https://blog.acolyer.org", home_page_url) + assertEquals("https://blog.acolyer.org/feed/", feed_url) - assertEquals(10, items!!.size) - val (_, _, _, title, _, _, _, image) = items[0] + assertEquals(10, items!!.size) + val (_, _, _, title, _, _, _, image) = items[0] - assertEquals( - "Thou shalt not depend on me: analysing the use of outdated JavaScript libraries on the web", - title, - ) + assertEquals( + "Thou shalt not depend on me: analysing the use of outdated JavaScript libraries on the web", + title, + ) - assertEquals( - "http://1.gravatar.com/avatar/a795b4f89a6d096f314fc0a2c80479c1?s=96&d=identicon&r=G", - image, - ) - } + assertEquals( + "http://1.gravatar.com/avatar/a795b4f89a6d096f314fc0a2c80479c1?s=96&d=identicon&r=G", + image, + ) + } @Test @Throws(Exception::class) - fun londoner() = runBlocking { - val feed = londoner.use { feedParser.parseFeedResponse(it) } - val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! + fun londoner() = + runBlocking { + val feed = londoner.use { feedParser.parseFeedResponse(it) } + val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!! - assertEquals("http://londonist.com/", home_page_url) - assertEquals("http://londonist.com/feed", feed_url) + assertEquals("http://londonist.com/", home_page_url) + assertEquals("http://londonist.com/feed", feed_url) - assertEquals(40, items!!.size) - val (_, _, _, title, _, _, _, image) = items[0] + assertEquals(40, items!!.size) + val (_, _, _, title, _, _, _, image) = items[0] - assertEquals( - "Make The Most Of London's Offerings With Chip", - title, - ) + assertEquals( + "Make The Most Of London's Offerings With Chip", + title, + ) - assertEquals( - "http://assets.londonist.com/uploads/2017/06/chip_2.jpg", - image, - ) - } + assertEquals( + "http://assets.londonist.com/uploads/2017/06/chip_2.jpg", + image, + ) + } @Test @Throws(Exception::class) - fun noLinkShouldBeNull() = runBlocking { - val feed = anon.use { feedParser.parseFeedResponse(it) } + fun noLinkShouldBeNull() = + runBlocking { + val feed = anon.use { feedParser.parseFeedResponse(it) } - assertEquals("http://ANON.com/sub", feed.getOrNull()!!.home_page_url) - assertEquals("http://anon.com/rss", feed.getOrNull()!!.feed_url) - assertEquals("ANON", feed.getOrNull()!!.title) - assertEquals("ANON", feed.getOrNull()!!.description) + assertEquals("http://ANON.com/sub", feed.getOrNull()!!.home_page_url) + assertEquals("http://anon.com/rss", feed.getOrNull()!!.feed_url) + assertEquals("ANON", feed.getOrNull()!!.title) + assertEquals("ANON", feed.getOrNull()!!.description) - assertEquals(1, feed.getOrNull()?.items!!.size) - val item = feed.getOrNull()?.items!![0] + assertEquals(1, feed.getOrNull()?.items!!.size) + val item = feed.getOrNull()?.items!![0] - assertNull(item.url) + assertNull(item.url) - assertEquals("ANON", item.title) - assertEquals("ANON", item.content_text) - assertEquals("ANON", item.content_html) - assertEquals("ANON", item.summary) + assertEquals("ANON", item.title) + assertEquals("ANON", item.content_text) + assertEquals("ANON", item.content_html) + assertEquals("ANON", item.summary) /*assertEquals("2018-12-13 00:00:00", item.date_published)*/ - } + } @Test - fun golem2ShouldBeParsedDespiteEmptySlashComments() = runBlocking { - val feed = golemDe2.use { feedParser.parseFeedResponse(it) } + fun golem2ShouldBeParsedDespiteEmptySlashComments() = + runBlocking { + val feed = golemDe2.use { feedParser.parseFeedResponse(it) } - assertEquals("Golem.de", feed.getOrNull()?.title) - } + assertEquals("Golem.de", feed.getOrNull()?.title) + } @Test @Throws(Exception::class) @Ignore - fun cowboyAuthenticated() = runBlocking { + fun cowboyAuthenticated() = runBlocking { - val feed = - feedParser.parseFeedUrl(URL("https://test:test@cowboyprogrammer.org/auth_basic/index.xml")) - assertEquals("Cowboy Programmer", feed.getOrNull()?.title) + runBlocking { + val feed = + feedParser.parseFeedUrl(URL("https://test:test@cowboyprogrammer.org/auth_basic/index.xml")) + assertEquals("Cowboy Programmer", feed.getOrNull()?.title) + } } - } @Test - fun diskuse() = runBlocking { + fun diskuse() = runBlocking { - val feed = diskuse.use { feedParser.parseFeedResponse(it) } - val entry = feed.getOrNull()?.items!!.first() - - assertEquals( - "Kajman, O této diskusi: test
 in  and bold in title",
-                entry.title,
-            )
+            runBlocking {
+                val feed = diskuse.use { feedParser.parseFeedResponse(it) }
+                val entry = feed.getOrNull()?.items!!.first()
+
+                assertEquals(
+                    "Kajman, O této diskusi: test 
 in  and bold in title",
+                    entry.title,
+                )
+            }
         }
-    }
 
     @Test
     @Ignore
     @Throws(Exception::class)
-    fun fz() = runBlocking {
-        val feed = fz.use { feedParser.parseFeedResponse(it) }
-        val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!!
+    fun fz() =
+        runBlocking {
+            val feed = fz.use { feedParser.parseFeedResponse(it) }
+            val (_, _, home_page_url, feed_url, _, _, _, _, _, _, _, _, items) = feed.getOrNull()!!
 
-        assertEquals("http://www.fz.se/nyheter/", home_page_url)
-        assertNull(feed_url)
+            assertEquals("http://www.fz.se/nyheter/", home_page_url)
+            assertNull(feed_url)
 
-        assertEquals(20, items!!.size)
-        val (_, _, _, title, _, _, _, image) = items[0]
+            assertEquals(20, items!!.size)
+            val (_, _, _, title, _, _, _, image) = items[0]
 
-        assertEquals(
-            "Nier: Automata bjuder på maffig lanseringstrailer",
-            title,
-        )
+            assertEquals(
+                "Nier: Automata bjuder på maffig lanseringstrailer",
+                title,
+            )
 
-        assertEquals(
-            "http://d2ihp3fq52ho68.cloudfront.net/YTo2OntzOjI6ImlkIjtpOjEzOTI3OTM7czoxOiJ3IjtpOjUwMDtzOjE6ImgiO2k6OTk5OTtzOjE6ImMiO2k6MDtzOjE6InMiO2k6MDtzOjE6ImsiO3M6NDA6IjU5YjA2YjgyZjkyY2IxZjBiMDZjZmI5MmE3NTk5NjMzMjIyMmU4NGMiO30=",
-            image,
-        )
-    }
+            assertEquals(
+                "http://d2ihp3fq52ho68.cloudfront.net/YTo2OntzOjI6ImlkIjtpOjEzOTI3OTM7czoxOiJ3IjtpOjUwMDtzOjE6ImgiO2k6OTk5OTtzOjE6ImMiO2k6MDtzOjE6InMiO2k6MDtzOjE6ImsiO3M6NDA6IjU5YjA2YjgyZjkyY2IxZjBiMDZjZmI5MmE3NTk5NjMzMjIyMmU4NGMiO30=",
+                image,
+            )
+        }
 
     @Test
-    fun geekpark() = runBlocking {
-        val feed = geekpark.use { feedParser.parseFeedResponse(it) }
+    fun geekpark() =
+        runBlocking {
+            val feed = geekpark.use { feedParser.parseFeedResponse(it) }
 
-        assertEquals("极客公园(!)", feed.getOrNull()!!.title)
+            assertEquals("极客公园(!)", feed.getOrNull()!!.title)
 
-        assertEquals(30, feed.getOrNull()?.items?.size)
-    }
+            assertEquals(30, feed.getOrNull()?.items?.size)
+        }
 
     @Test
-    fun contentTypeHtmlIsNotUnescapedTwice() = runBlocking {
-        val feed = contentTypeHtml.use { feedParser.parseFeedResponse(it) }
+    fun contentTypeHtmlIsNotUnescapedTwice() =
+        runBlocking {
+            val feed = contentTypeHtml.use { feedParser.parseFeedResponse(it) }
 
-        val item = feed.getOrNull()?.items!!.single()
+            val item = feed.getOrNull()?.items!!.single()
 
-        assertFalse(
-            item.content_html!!.contains(" 
obs.lon <- ncvar_get(nc.obs, 'lon')"),
-        )
+            assertFalse(
+                item.content_html!!.contains(" 
obs.lon <- ncvar_get(nc.obs, 'lon')"),
+            )
 
-        assertTrue(
-            item.content_html!!.contains(" 
obs.lon <- ncvar_get(nc.obs, 'lon')"),
-        )
-    }
+            assertTrue(
+                item.content_html!!.contains(" 
obs.lon <- ncvar_get(nc.obs, 'lon')"),
+            )
+        }
 
     @Test
-    fun escapedRssDescriptionIsProperlyUnescaped() = runBlocking {
-        val feed = feedParser.parseFeedResponse(
-            URL("http://cowboyprogrammer.org"),
-            rssWithHtmlEscapedDescription,
-            null,
-        )
-
-        val item = feed.getOrNull()?.items!!.single()
-
-        assertEquals(
-            "http://cowboyprogrammer.org/hello.jpg&cached=true",
-            item.image,
-        )
-        assertEquals(
-            "",
-            item.content_html,
-        )
-    }
+    fun escapedRssDescriptionIsProperlyUnescaped() =
+        runBlocking {
+            val feed =
+                feedParser.parseFeedResponse(
+                    URL("http://cowboyprogrammer.org"),
+                    rssWithHtmlEscapedDescription,
+                    null,
+                )
+
+            val item = feed.getOrNull()?.items!!.single()
+
+            assertEquals(
+                "http://cowboyprogrammer.org/hello.jpg&cached=true",
+                item.image,
+            )
+            assertEquals(
+                "",
+                item.content_html,
+            )
+        }
 
     @Test
-    fun escapedAtomContentIsProperlyUnescaped() = runBlocking {
-        val feed = feedParser.parseFeedResponse(
-            URL("http://cowboyprogrammer.org"),
-            atomWithHtmlEscapedContents,
-            null,
-        )
-
-        val text = feed.getOrNull()?.items!!.first()
-        assertEquals(
-            "http://cowboyprogrammer.org/hello.jpg&cached=true",
-            text.image,
-        )
-        assertEquals(
-            "",
-            text.content_html,
-        )
-
-        val html = feed.getOrNull()?.items!![1]
-        assertEquals(
-            "http://cowboyprogrammer.org/hello.jpg&cached=true",
-            html.image,
-        )
-        assertEquals(
-            "",
-            html.content_html,
-        )
-
-        val xhtml = feed.getOrNull()?.items!![2]
-        assertEquals(
-            "http://cowboyprogrammer.org/hello.jpg&cached=true",
-            xhtml.image,
-        )
-        assertTrue("Actual:\n${xhtml.content_html}") {
-            "" in xhtml.content_html!!
+    fun escapedAtomContentIsProperlyUnescaped() =
+        runBlocking {
+            val feed =
+                feedParser.parseFeedResponse(
+                    URL("http://cowboyprogrammer.org"),
+                    atomWithHtmlEscapedContents,
+                    null,
+                )
+
+            val text = feed.getOrNull()?.items!!.first()
+            assertEquals(
+                "http://cowboyprogrammer.org/hello.jpg&cached=true",
+                text.image,
+            )
+            assertEquals(
+                "",
+                text.content_html,
+            )
+
+            val html = feed.getOrNull()?.items!![1]
+            assertEquals(
+                "http://cowboyprogrammer.org/hello.jpg&cached=true",
+                html.image,
+            )
+            assertEquals(
+                "",
+                html.content_html,
+            )
+
+            val xhtml = feed.getOrNull()?.items!![2]
+            assertEquals(
+                "http://cowboyprogrammer.org/hello.jpg&cached=true",
+                xhtml.image,
+            )
+            assertTrue("Actual:\n${xhtml.content_html}") {
+                "" in xhtml.content_html!!
+            }
         }
-    }
 
     @Test
-    fun handlesUnknownProtocols() = runBlocking {
-        val feed = feedParser.parseFeedResponse(
-            URL("https://gemini.circumlunar.space"),
-            atomWithUnknownProtocol,
-            null,
-        )
-
-        assertEquals(8, feed.getOrNull()?.items!!.size)
-    }
+    fun handlesUnknownProtocols() =
+        runBlocking {
+            val feed =
+                feedParser.parseFeedResponse(
+                    URL("https://gemini.circumlunar.space"),
+                    atomWithUnknownProtocol,
+                    null,
+                )
 
-    private fun  readResource(asdf: String, block: suspend (String) -> T): T {
-        val body = javaClass.getResourceAsStream(asdf)!!
-            .bufferedReader()
-            .use(BufferedReader::readText)
+            assertEquals(8, feed.getOrNull()?.items!!.size)
+        }
+
+    private fun  readResource(
+        asdf: String,
+        block: suspend (String) -> T,
+    ): T {
+        val body =
+            javaClass.getResourceAsStream(asdf)!!
+                .bufferedReader()
+                .use(BufferedReader::readText)
 
         return runBlocking {
             block(body)
@@ -840,10 +901,11 @@ class FeedParserTest : DIAware {
             }
 
     private val emptySlashComment: Response
-        get() = bytesToResponse(
-            "empty_slash_comment.xml",
-            "https://rss.golem.de/rss.php?feed=RSS2.0",
-        )
+        get() =
+            bytesToResponse(
+                "empty_slash_comment.xml",
+                "https://rss.golem.de/rss.php?feed=RSS2.0",
+            )
 
     private val golemDe: Response
         get() = bytesToResponse("golem-de.xml", "https://rss.golem.de/rss.php?feed=RSS2.0")
@@ -852,25 +914,28 @@ class FeedParserTest : DIAware {
         get() = bytesToResponse("rss_golem_2.xml", "https://rss.golem.de/rss.php?feed=RSS2.0")
 
     private val utdelningsSeglarenAtom: Response
-        get() = bytesToResponse(
-            "atom_utdelningsseglaren.xml",
-            "http://utdelningsseglaren.blogspot.com/feeds/posts/default",
-        )
+        get() =
+            bytesToResponse(
+                "atom_utdelningsseglaren.xml",
+                "http://utdelningsseglaren.blogspot.com/feeds/posts/default",
+            )
 
     private val lineageosRss: Response
         get() = bytesToResponse("rss_lineageos.xml", "https://lineageos.org/feed.xml")
 
     private val cornucopiaAtom: Response
-        get() = bytesToResponse(
-            "atom_cornucopia.xml",
-            "https://cornucopia.cornubot.se/feeds/posts/default",
-        )
+        get() =
+            bytesToResponse(
+                "atom_cornucopia.xml",
+                "https://cornucopia.cornubot.se/feeds/posts/default",
+            )
 
     private val cornucopiaRss: Response
-        get() = bytesToResponse(
-            "rss_cornucopia.xml",
-            "https://cornucopia.cornubot.se/feeds/posts/default?alt=rss",
-        )
+        get() =
+            bytesToResponse(
+                "rss_cornucopia.xml",
+                "https://cornucopia.cornubot.se/feeds/posts/default?alt=rss",
+            )
 
     private val cowboyRss: Response
         get() = bytesToResponse("rss_cowboy.xml", "http://cowboyprogrammer.org/index.xml")
@@ -900,32 +965,36 @@ class FeedParserTest : DIAware {
         get() = bytesToResponse("rss_nixos.xml", "http://nixos.org/news-rss.xml")
 
     private val nixersRss: Response
-        get() = bytesToResponse(
-            "rss_nixers_newsletter.xml",
-            "https://newsletter.nixers.net/feed.xml",
-        )
+        get() =
+            bytesToResponse(
+                "rss_nixers_newsletter.xml",
+                "https://newsletter.nixers.net/feed.xml",
+            )
 
     private val videoResponse: Response
-        get() = bytesToResponse(
-            "rss_nixers_newsletter.xml",
-            "https://foo.bar/video.mp4",
-            "video/mp4",
-        )
+        get() =
+            bytesToResponse(
+                "rss_nixers_newsletter.xml",
+                "https://foo.bar/video.mp4",
+                "video/mp4",
+            )
 
     private val diskuse: Response
-        get() = bytesToResponse(
-            "rss_diskuse.xml",
-            "https://diskuse.jakpsatweb.cz/rss2.php?topic=173233",
-        )
+        get() =
+            bytesToResponse(
+                "rss_diskuse.xml",
+                "https://diskuse.jakpsatweb.cz/rss2.php?topic=173233",
+            )
 
     private val geekpark: Response
         get() = bytesToResponse("rss_geekpark.xml", "http://main_test.geekpark.net/rss.rss")
 
     private val contentTypeHtml: Response
-        get() = bytesToResponse(
-            "atom_content_type_html.xml",
-            "http://www.zoocoop.com/contentoob/o1.atom",
-        )
+        get() =
+            bytesToResponse(
+                "atom_content_type_html.xml",
+                "http://www.zoocoop.com/contentoob/o1.atom",
+            )
 
     private fun bytesToResponse(
         resourceName: String,
diff --git a/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlParserTest.kt b/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlParserTest.kt
index 97ecb17b38..8f6748c90d 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlParserTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlParserTest.kt
@@ -30,108 +30,113 @@ import org.kodein.di.singleton
 class OpmlParserTest : DIAware {
     private val feedDao: FeedDao = mockk()
     private val settingsStore: SettingsStore = mockk(relaxUnitFun = true)
-    override val di = DI.lazy {
-        bind() with instance(feedDao)
-        bind() with instance(settingsStore)
-        bind() with singleton { OPMLImporter(di) }
-    }
+    override val di =
+        DI.lazy {
+            bind() with instance(feedDao)
+            bind() with instance(settingsStore)
+            bind() with singleton { OPMLImporter(di) }
+        }
     private val opmlImporter: OPMLParserHandler by instance()
 
     private suspend fun setAllSettings() {
         for (userSetting in UserSettings.values()) {
             opmlImporter.saveSetting(
                 key = userSetting.key,
-                value = when (userSetting) {
-                    UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
-                    UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true"
-                    UserSettings.SETTING_THEME -> "night"
-                    UserSettings.SETTING_DARK_THEME -> "DaRk"
-                    UserSettings.SETTING_DYNAMIC_THEME -> "false"
-                    UserSettings.SETTING_SORT -> "oldest_first"
-                    UserSettings.SETTING_SHOW_FAB -> "false"
-                    UserSettings.SETTING_FEED_ITEM_STYLE -> "super_compact"
-                    UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED"
-                    UserSettings.SETTING_SYNC_ON_RESUME -> "true"
-                    UserSettings.SETTING_SYNC_ONLY_WIFI -> "false"
-                    UserSettings.SETTING_IMG_ONLY_WIFI -> "true"
-                    UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false"
-                    UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
-                    UserSettings.SETTING_TEXT_SCALE -> "1.6"
-                    UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true"
-                    UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true"
-                    UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true"
-                    UserSettings.SETTING_SYNC_FREQ -> "720"
-                    UserSettings.SETTING_MAX_LINES -> "6"
-                    UserSettings.SETTINGS_FILTER_SAVED -> "true"
-                    UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true"
-                    UserSettings.SETTINGS_FILTER_READ -> "false"
-                    UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true"
-                    UserSettings.SETTING_OPEN_ADJACENT -> "true"
-                },
+                value =
+                    when (userSetting) {
+                        UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
+                        UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true"
+                        UserSettings.SETTING_THEME -> "night"
+                        UserSettings.SETTING_DARK_THEME -> "DaRk"
+                        UserSettings.SETTING_DYNAMIC_THEME -> "false"
+                        UserSettings.SETTING_SORT -> "oldest_first"
+                        UserSettings.SETTING_SHOW_FAB -> "false"
+                        UserSettings.SETTING_FEED_ITEM_STYLE -> "super_compact"
+                        UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED"
+                        UserSettings.SETTING_SYNC_ON_RESUME -> "true"
+                        UserSettings.SETTING_SYNC_ONLY_WIFI -> "false"
+                        UserSettings.SETTING_IMG_ONLY_WIFI -> "true"
+                        UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false"
+                        UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
+                        UserSettings.SETTING_TEXT_SCALE -> "1.6"
+                        UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true"
+                        UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true"
+                        UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true"
+                        UserSettings.SETTING_SYNC_FREQ -> "720"
+                        UserSettings.SETTING_MAX_LINES -> "6"
+                        UserSettings.SETTINGS_FILTER_SAVED -> "true"
+                        UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true"
+                        UserSettings.SETTINGS_FILTER_READ -> "false"
+                        UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true"
+                        UserSettings.SETTING_OPEN_ADJACENT -> "true"
+                    },
             )
         }
     }
 
     @Test
-    fun handlesAllSettings(): Unit = runBlocking {
-        setAllSettings()
-        verify {
-            settingsStore.setLinkOpener(LinkOpener.CUSTOM_TAB)
-            settingsStore.setAddedFeederNews(true)
-            settingsStore.setCurrentTheme(ThemeOptions.NIGHT)
-            settingsStore.setDarkThemePreference(DarkThemePreferences.DARK)
-            settingsStore.setUseDynamicTheme(false)
-            settingsStore.setCurrentSorting(SortingOptions.OLDEST_FIRST)
-            settingsStore.setShowFab(false)
-            settingsStore.setFeedItemStyle(FeedItemStyle.SUPER_COMPACT)
-            settingsStore.setSwipeAsRead(SwipeAsRead.DISABLED)
-            settingsStore.setSyncOnResume(true)
-            settingsStore.setLoadImageOnlyOnWifi(true)
-            settingsStore.setShowThumbnails(false)
-            settingsStore.setItemOpener(ItemOpener.CUSTOM_TAB)
-            settingsStore.setTextScale(1.6f)
-            settingsStore.setIsMarkAsReadOnScroll(true)
-            settingsStore.setUseDetectLanguage(true)
-            settingsStore.setSyncOnlyWhenCharging(true)
-            settingsStore.setSyncOnlyOnWifi(false)
-            settingsStore.setSyncFrequency(SyncFrequency.EVERY_12_HOURS)
-            settingsStore.setMaxLines(6)
-            settingsStore.setFeedListFilterRecentlyRead(true)
-            settingsStore.setFeedListFilterRead(false)
-            settingsStore.setFeedListFilterSaved(true)
-            settingsStore.setShowOnlyTitles(true)
-            settingsStore.setOpenAdjacent(true)
-        }
+    fun handlesAllSettings(): Unit =
+        runBlocking {
+            setAllSettings()
+            verify {
+                settingsStore.setLinkOpener(LinkOpener.CUSTOM_TAB)
+                settingsStore.setAddedFeederNews(true)
+                settingsStore.setCurrentTheme(ThemeOptions.NIGHT)
+                settingsStore.setDarkThemePreference(DarkThemePreferences.DARK)
+                settingsStore.setUseDynamicTheme(false)
+                settingsStore.setCurrentSorting(SortingOptions.OLDEST_FIRST)
+                settingsStore.setShowFab(false)
+                settingsStore.setFeedItemStyle(FeedItemStyle.SUPER_COMPACT)
+                settingsStore.setSwipeAsRead(SwipeAsRead.DISABLED)
+                settingsStore.setSyncOnResume(true)
+                settingsStore.setLoadImageOnlyOnWifi(true)
+                settingsStore.setShowThumbnails(false)
+                settingsStore.setItemOpener(ItemOpener.CUSTOM_TAB)
+                settingsStore.setTextScale(1.6f)
+                settingsStore.setIsMarkAsReadOnScroll(true)
+                settingsStore.setUseDetectLanguage(true)
+                settingsStore.setSyncOnlyWhenCharging(true)
+                settingsStore.setSyncOnlyOnWifi(false)
+                settingsStore.setSyncFrequency(SyncFrequency.EVERY_12_HOURS)
+                settingsStore.setMaxLines(6)
+                settingsStore.setFeedListFilterRecentlyRead(true)
+                settingsStore.setFeedListFilterRead(false)
+                settingsStore.setFeedListFilterSaved(true)
+                settingsStore.setShowOnlyTitles(true)
+                settingsStore.setOpenAdjacent(true)
+            }
 
-        confirmVerified(settingsStore)
-    }
+            confirmVerified(settingsStore)
+        }
 
     @Test
-    fun handlesBlockedPatterns(): Unit = runBlocking {
-        every { settingsStore.blockListPreference } returns flowOf(
-            listOf("existing"),
-        )
+    fun handlesBlockedPatterns(): Unit =
+        runBlocking {
+            every { settingsStore.blockListPreference } returns
+                flowOf(
+                    listOf("existing"),
+                )
 
-        opmlImporter.saveBlocklistPatterns(
-            listOf(
-                "foo",
-                "existing",
-                "foo",
-                "injection break';",
-                "",
-                " ",
-            ),
-        )
+            opmlImporter.saveBlocklistPatterns(
+                listOf(
+                    "foo",
+                    "existing",
+                    "foo",
+                    "injection break';",
+                    "",
+                    " ",
+                ),
+            )
 
-        verify(exactly = 1) {
-            settingsStore.blockListPreference
-        }
+            verify(exactly = 1) {
+                settingsStore.blockListPreference
+            }
 
-        coVerify(exactly = 1) {
-            settingsStore.addBlocklistPattern("foo")
-            settingsStore.addBlocklistPattern("injection break';")
-        }
+            coVerify(exactly = 1) {
+                settingsStore.addBlocklistPattern("foo")
+                settingsStore.addBlocklistPattern("injection break';")
+            }
 
-        confirmVerified(settingsStore)
-    }
+            confirmVerified(settingsStore)
+        }
 }
diff --git a/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlWriterKtTest.kt b/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlWriterKtTest.kt
index a4b9c4f491..81cc143e4e 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlWriterKtTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/model/opml/OpmlWriterKtTest.kt
@@ -3,11 +3,11 @@ package com.nononsenseapps.feeder.model.opml
 import com.nononsenseapps.feeder.archmodel.PREF_VAL_OPEN_WITH_CUSTOM_TAB
 import com.nononsenseapps.feeder.archmodel.UserSettings
 import com.nononsenseapps.feeder.db.room.Feed
-import java.io.ByteArrayOutputStream
-import java.net.URL
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.net.URL
 
 class OpmlWriterKtTest {
     @Test
@@ -23,61 +23,66 @@ class OpmlWriterKtTest {
     }
 
     @Test
-    fun shouldEscapeStrings() = runBlocking {
-        val bos = ByteArrayOutputStream()
-        writeOutputStream(bos, emptyMap(), emptyList(), listOf("quoted \"tag\"")) { tag ->
-            val result = mutableListOf()
-            val feed = Feed(
-                id = 1L,
-                title = "A \"feeditem\" with id '9' > 0 & < 10",
-                customTitle = "A custom \"title\" with id '9' > 0 & < 1e",
-                url = URL("http://example.com/rss.xml?format=feed&type=rss"),
-                tag = tag,
-                notify = true,
-                imageUrl = URL("https://example.com/feedImage"),
-                fullTextByDefault = true,
-                openArticlesWith = "reader",
-                alternateId = true,
-            )
+    fun shouldEscapeStrings() =
+        runBlocking {
+            val bos = ByteArrayOutputStream()
+            writeOutputStream(bos, emptyMap(), emptyList(), listOf("quoted \"tag\"")) { tag ->
+                val result = mutableListOf()
+                val feed =
+                    Feed(
+                        id = 1L,
+                        title = "A \"feeditem\" with id '9' > 0 & < 10",
+                        customTitle = "A custom \"title\" with id '9' > 0 & < 1e",
+                        url = URL("http://example.com/rss.xml?format=feed&type=rss"),
+                        tag = tag,
+                        notify = true,
+                        imageUrl = URL("https://example.com/feedImage"),
+                        fullTextByDefault = true,
+                        openArticlesWith = "reader",
+                        alternateId = true,
+                    )
 
-            result.add(feed)
-            result
+                result.add(feed)
+                result
+            }
+            val output = String(bos.toByteArray())
+            assertEquals(expected, output.trimEnd())
         }
-        val output = String(bos.toByteArray())
-        assertEquals(expected, output.trimEnd())
-    }
 
     @Test
-    fun exportsSettings() = runBlocking {
-        val bos = ByteArrayOutputStream()
-        writeOutputStream(
-            os = bos,
-            settings = ALL_SETTINGS_WITH_VALUES,
-            blockedPatterns = listOf("foo", "break \"xml id '9' > 0 & < 10"),
-            tags = listOf("news"),
-        ) { tag ->
-            val result = mutableListOf()
-            val feed = Feed(
-                id = 1L,
-                title = "title",
-                customTitle = "customTitle",
-                url = URL("http://example.com/rss.xml?format=feed&type=rss"),
-                tag = tag,
-                notify = true,
-                imageUrl = URL("https://example.com/feedImage"),
-                fullTextByDefault = true,
-                openArticlesWith = "reader",
-                alternateId = true,
-            )
+    fun exportsSettings() =
+        runBlocking {
+            val bos = ByteArrayOutputStream()
+            writeOutputStream(
+                os = bos,
+                settings = ALL_SETTINGS_WITH_VALUES,
+                blockedPatterns = listOf("foo", "break \"xml id '9' > 0 & < 10"),
+                tags = listOf("news"),
+            ) { tag ->
+                val result = mutableListOf()
+                val feed =
+                    Feed(
+                        id = 1L,
+                        title = "title",
+                        customTitle = "customTitle",
+                        url = URL("http://example.com/rss.xml?format=feed&type=rss"),
+                        tag = tag,
+                        notify = true,
+                        imageUrl = URL("https://example.com/feedImage"),
+                        fullTextByDefault = true,
+                        openArticlesWith = "reader",
+                        alternateId = true,
+                    )
 
-            result.add(feed)
-            result
+                result.add(feed)
+                result
+            }
+            val output = String(bos.toByteArray())
+            assertEquals(expectedWithSettings, output.trimEnd())
         }
-        val output = String(bos.toByteArray())
-        assertEquals(expectedWithSettings, output.trimEnd())
-    }
 
-    private val expected = """
+    private val expected =
+        """
         
         
           
@@ -91,9 +96,10 @@ class OpmlWriterKtTest {
             
           
         
-    """.trimIndent()
+        """.trimIndent()
 
-    private val expectedWithSettings = """
+    private val expectedWithSettings =
+        """
         
         
           
@@ -136,38 +142,39 @@ class OpmlWriterKtTest {
             
           
         
-    """.trimIndent()
+        """.trimIndent()
 
     companion object {
         private val ALL_SETTINGS_WITH_VALUES: Map =
             UserSettings.values().associate { userSetting ->
-                userSetting.key to when (userSetting) {
-                    UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
-                    UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true"
-                    UserSettings.SETTING_THEME -> "night"
-                    UserSettings.SETTING_DARK_THEME -> "DaRk"
-                    UserSettings.SETTING_DYNAMIC_THEME -> "false"
-                    UserSettings.SETTING_SORT -> "oldest_first"
-                    UserSettings.SETTING_SHOW_FAB -> "false"
-                    UserSettings.SETTING_FEED_ITEM_STYLE -> "super_compact"
-                    UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED"
-                    UserSettings.SETTING_SYNC_ON_RESUME -> "true"
-                    UserSettings.SETTING_SYNC_ONLY_WIFI -> "false"
-                    UserSettings.SETTING_IMG_ONLY_WIFI -> "true"
-                    UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false"
-                    UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
-                    UserSettings.SETTING_TEXT_SCALE -> "1.6"
-                    UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true"
-                    UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true"
-                    UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true"
-                    UserSettings.SETTING_SYNC_FREQ -> "720"
-                    UserSettings.SETTING_MAX_LINES -> "6"
-                    UserSettings.SETTINGS_FILTER_SAVED -> "true"
-                    UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true"
-                    UserSettings.SETTINGS_FILTER_READ -> "false"
-                    UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true"
-                    UserSettings.SETTING_OPEN_ADJACENT -> "true"
-                }
+                userSetting.key to
+                    when (userSetting) {
+                        UserSettings.SETTING_OPEN_LINKS_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
+                        UserSettings.SETTING_ADDED_FEEDER_NEWS -> "true"
+                        UserSettings.SETTING_THEME -> "night"
+                        UserSettings.SETTING_DARK_THEME -> "DaRk"
+                        UserSettings.SETTING_DYNAMIC_THEME -> "false"
+                        UserSettings.SETTING_SORT -> "oldest_first"
+                        UserSettings.SETTING_SHOW_FAB -> "false"
+                        UserSettings.SETTING_FEED_ITEM_STYLE -> "super_compact"
+                        UserSettings.SETTING_SWIPE_AS_READ -> "DISABLED"
+                        UserSettings.SETTING_SYNC_ON_RESUME -> "true"
+                        UserSettings.SETTING_SYNC_ONLY_WIFI -> "false"
+                        UserSettings.SETTING_IMG_ONLY_WIFI -> "true"
+                        UserSettings.SETTING_IMG_SHOW_THUMBNAILS -> "false"
+                        UserSettings.SETTING_DEFAULT_OPEN_ITEM_WITH -> PREF_VAL_OPEN_WITH_CUSTOM_TAB
+                        UserSettings.SETTING_TEXT_SCALE -> "1.6"
+                        UserSettings.SETTING_IS_MARK_AS_READ_ON_SCROLL -> "true"
+                        UserSettings.SETTING_READALOUD_USE_DETECT_LANGUAGE -> "true"
+                        UserSettings.SETTING_SYNC_ONLY_CHARGING -> "true"
+                        UserSettings.SETTING_SYNC_FREQ -> "720"
+                        UserSettings.SETTING_MAX_LINES -> "6"
+                        UserSettings.SETTINGS_FILTER_SAVED -> "true"
+                        UserSettings.SETTINGS_FILTER_RECENTLY_READ -> "true"
+                        UserSettings.SETTINGS_FILTER_READ -> "false"
+                        UserSettings.SETTINGS_LIST_SHOW_ONLY_TITLES -> "true"
+                        UserSettings.SETTING_OPEN_ADJACENT -> "true"
+                    }
             }
     }
 }
diff --git a/app/src/test/java/com/nononsenseapps/feeder/notifications/NotificationsWorkerTest.kt b/app/src/test/java/com/nononsenseapps/feeder/notifications/NotificationsWorkerTest.kt
index b70f728666..f900b451b6 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/notifications/NotificationsWorkerTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/notifications/NotificationsWorkerTest.kt
@@ -43,7 +43,7 @@ class NotificationsWorkerTest : DIAware {
         runBlocking {
             notificationsWorker.unNotifyForMissingItems(
                 prev = listOf(1L, 2L, 3L, 4L),
-                current = listOf(1L, 3L)
+                current = listOf(1L, 3L),
             )
         }
 
diff --git a/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt b/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt
index 376bb372ec..381ab63a36 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/sync/EncryptedFeedTest.kt
@@ -1,9 +1,9 @@
 package com.nononsenseapps.feeder.sync
 
-import java.net.URL
 import org.intellij.lang.annotations.Language
 import org.junit.Assert.*
 import org.junit.Test
+import java.net.URL
 
 class EncryptedFeedTest {
     private val moshi = getMoshi()
@@ -13,14 +13,15 @@ class EncryptedFeedTest {
         val adapter = moshi.adapter()
 
         @Language("JSON")
-        val json = """
+        val json =
+            """
             {
                "url": "https://foo.bar",
                "title": "foo",
                "alternateId": true,
                "notARealField": 1
             }
-        """.trimIndent()
+            """.trimIndent()
         val feed = adapter.fromJson(json)!!
 
         assertEquals(URL("https://foo.bar"), feed.url)
diff --git a/app/src/test/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposableUnitTest.kt b/app/src/test/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposableUnitTest.kt
index e521040107..c7af4eac86 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposableUnitTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/ui/compose/text/HtmlToComposableUnitTest.kt
@@ -2,14 +2,13 @@ package com.nononsenseapps.feeder.ui.compose.text
 
 import io.mockk.every
 import io.mockk.mockk
+import org.jsoup.nodes.Element
+import org.junit.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
-import org.jsoup.nodes.Element
-import org.junit.Test
 
 class HtmlToComposableUnitTest {
-
     private val element = mockk()
 
     @Test
@@ -163,8 +162,12 @@ class HtmlToComposableUnitTest {
 
     @Test
     fun findImageBestPoliticoSrcSet() {
-        every { element.attr("srcset") } returns "https://www.politico.eu/cdn-cgi/image/width=1024,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 1024w, https://www.politico.eu/cdn-cgi/image/width=300,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 300w, https://www.politico.eu/cdn-cgi/image/width=1280,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 1280w"
-        every { element.attr("abs:src") } returns "https://www.politico.eu/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd-1024x683.jpeg"
+        every {
+            element.attr("srcset")
+        } returns "https://www.politico.eu/cdn-cgi/image/width=1024,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 1024w, https://www.politico.eu/cdn-cgi/image/width=300,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 300w, https://www.politico.eu/cdn-cgi/image/width=1280,quality=80,onerror=redirect,format=auto/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd.jpeg 1280w"
+        every {
+            element.attr("abs:src")
+        } returns "https://www.politico.eu/wp-content/uploads/2022/10/07/thumbnail_Kal-econ-cartoon-10-7-22synd-1024x683.jpeg"
         every { element.attr("width") } returns "1024"
         every { element.attr("height") } returns "683"
 
@@ -188,8 +191,12 @@ class HtmlToComposableUnitTest {
         A pen pointing to a piece of LK-99 standing on its side above a magnet.
          */
 
-        every { element.attr("srcset") } returns "https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/16x11/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 16w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/32x21/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 32w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/48x32/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 48w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/64x43/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 64w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/96x64/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 96w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/128x85/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 128w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/256x171/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 256w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/376x251/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 376w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/384x256/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 384w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/415x277/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 415w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/480x320/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 480w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/540x360/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 540w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/640x427/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 640w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/750x500/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 750w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/828x552/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 828w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1080x720/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1080w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1200x800/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1200w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1440x960/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1440w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1920x1280/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1920w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2048x1365/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 2048w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2400x1600/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 2400w"
-        every { element.attr("abs:src") } returns "https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2400x1600/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png"
+        every {
+            element.attr("srcset")
+        } returns "https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/16x11/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 16w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/32x21/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 32w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/48x32/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 48w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/64x43/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 64w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/96x64/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 96w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/128x85/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 128w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/256x171/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 256w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/376x251/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 376w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/384x256/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 384w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/415x277/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 415w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/480x320/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 480w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/540x360/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 540w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/640x427/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 640w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/750x500/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 750w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/828x552/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 828w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1080x720/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1080w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1200x800/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1200w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1440x960/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1440w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/1920x1280/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 1920w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2048x1365/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 2048w, https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2400x1600/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png 2400w"
+        every {
+            element.attr("abs:src")
+        } returns "https://duet-cdn.vox-cdn.com/thumbor/184x0:2614x1535/2400x1600/filters:focal(1847x240:1848x241):format(webp)/cdn.vox-cdn.com/uploads/chorus_asset/file/24842461/Screenshot_2023_08_10_at_12.22.58_PM.png"
         every { element.attr("width") } returns null
         every { element.attr("height") } returns null
 
diff --git a/app/src/test/java/com/nononsenseapps/feeder/util/HtmlUtilsKtTest.kt b/app/src/test/java/com/nononsenseapps/feeder/util/HtmlUtilsKtTest.kt
index 5264345bd6..26b36c6749 100644
--- a/app/src/test/java/com/nononsenseapps/feeder/util/HtmlUtilsKtTest.kt
+++ b/app/src/test/java/com/nononsenseapps/feeder/util/HtmlUtilsKtTest.kt
@@ -1,8 +1,8 @@
 package com.nononsenseapps.feeder.util
 
+import org.junit.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertNull
-import org.junit.Test
 
 class HtmlUtilsKtTest {
     @Test
@@ -26,7 +26,7 @@ ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
         val text = "<img src=\"https://imgs.xkcd.com/comics/interstellar_asteroid.png\" title=\"Every time we detect an asteroid from outside the Solar System, we should immediately launch a mission to fling one of our asteroids back in the direction it came from.\" alt=\"Every time we detect an asteroid from outside the Solar System, we should immediately launch a mission to fling one of our asteroids back in the direction it came from.\" />"
         assertEquals(
             "https://imgs.xkcd.com/comics/interstellar_asteroid.png",
-            findFirstImageLinkInHtml(text, null)
+            findFirstImageLinkInHtml(text, null),
         )
     }
 
@@ -35,7 +35,7 @@ ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
         val text = "<img title=\"Every time we detect an asteroid from outside the Solar System, we should immediately launch a mission to fling one of our asteroids back in the direction it came from.\" alt=\"Every time we detect an asteroid from outside the Solar System, we should immediately launch a mission to fling one of our asteroids back in the direction it came from.\" src=\"https://imgs.xkcd.com/comics/interstellar_asteroid.png\" />"
         assertEquals(
             "https://imgs.xkcd.com/comics/interstellar_asteroid.png",
-            findFirstImageLinkInHtml(text, null)
+            findFirstImageLinkInHtml(text, null),
         )
     }
 
@@ -52,11 +52,21 @@ ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
 
     @Test
     fun returnsNullForTwitterAndFacebookIcons() {
-        assertNull(findFirstImageLinkInHtml("There's a tiny, black-freckled toad that likes the water in hot springs. Unfortunately, the only place in the world where the species is found is on 760 acres of wetlands about 100 miles east of Reno, Nevada, according to the New York Times. And that's near the site for two renewable-energy geothermal plants which poses \"significant risk to the well-being of the species,\" according to America's Fish and Wildlife Service — which just announced an emergency measure declaring it an endangered species. The temporary protection, which went into effect immediately and lasts for 240 days, was imposed to ward off the toad's potential extinction, the U.S. Fish and Wildlife Service said in a statement, adding that it would consider public comments about whether to extend the toad's emergency listing. The designation would add another hurdle for a plan to build two power plants with the encouragement of the U.S. Bureau of Land Management. The project is already the subject of a lawsuit filed by conservationists and a nearby Native American tribe. They hope the emergency listing can be used to block construction, which recently resumed.... The suit contended that the geothermal plants would dry up nearby hot springs sacred to the tribe and wipe out the Dixie Valley toad species. The U.S. Fish and Wildlife Service argues that \"protecting small population species like this ensures the continued biodiversity necessary to maintain climate-resilient landscapes in one of the driest states in the country.\" They were only recently scientifically described — or declared a unique species — in 2017, making the Dixie Valley toad \">the first new toad species to be described in the U.S. in nearly 50 years. And they are truly unique. When they were described, scientists analyzed 14 different morphological characteristics like size, shape, and markings. Dixie Valley toads scored \"significantly different\" from other western toad species in all categories. Thanks to long-time Slashdot reader walterbyrd for sharing the link!

Read more of this story at Slashdot.

", null)) + assertNull( + findFirstImageLinkInHtml( + "There's a tiny, black-freckled toad that likes the water in hot springs. Unfortunately, the only place in the world where the species is found is on 760 acres of wetlands about 100 miles east of Reno, Nevada, according to the New York Times. And that's near the site for two renewable-energy geothermal plants which poses \"significant risk to the well-being of the species,\" according to America's Fish and Wildlife Service — which just announced an emergency measure declaring it an endangered species. The temporary protection, which went into effect immediately and lasts for 240 days, was imposed to ward off the toad's potential extinction, the U.S. Fish and Wildlife Service said in a statement, adding that it would consider public comments about whether to extend the toad's emergency listing. The designation would add another hurdle for a plan to build two power plants with the encouragement of the U.S. Bureau of Land Management. The project is already the subject of a lawsuit filed by conservationists and a nearby Native American tribe. They hope the emergency listing can be used to block construction, which recently resumed.... The suit contended that the geothermal plants would dry up nearby hot springs sacred to the tribe and wipe out the Dixie Valley toad species. The U.S. Fish and Wildlife Service argues that \"protecting small population species like this ensures the continued biodiversity necessary to maintain climate-resilient landscapes in one of the driest states in the country.\" They were only recently scientifically described — or declared a unique species — in 2017, making the Dixie Valley toad \">the first new toad species to be described in the U.S. in nearly 50 years. And they are truly unique. When they were described, scientists analyzed 14 different morphological characteristics like size, shape, and markings. Dixie Valley toads scored \"significantly different\" from other western toad species in all categories. Thanks to long-time Slashdot reader walterbyrd for sharing the link!

Read more of this story at Slashdot.

", + null, + ), + ) } @Test fun returnsNullForTrackingPixels() { - assertNull(findFirstImageLinkInHtml("""asdfasd asdf asd""", null)) + assertNull( + findFirstImageLinkInHtml( + """asdfasd asdf asd""", + null, + ), + ) } } diff --git a/app/src/test/java/com/nononsenseapps/feeder/util/LinkUtilsKtTest.kt b/app/src/test/java/com/nononsenseapps/feeder/util/LinkUtilsKtTest.kt index 135b5c4f58..1d45c2dfa1 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/util/LinkUtilsKtTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/util/LinkUtilsKtTest.kt @@ -1,11 +1,11 @@ package com.nononsenseapps.feeder.util +import org.junit.Test import java.net.MalformedURLException import java.net.URL import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertFailsWith -import org.junit.Test class LinkUtilsKtTest { @Test diff --git a/app/src/test/java/com/nononsenseapps/feeder/util/RomeExtensionsKtTest.kt b/app/src/test/java/com/nononsenseapps/feeder/util/RomeExtensionsKtTest.kt index 3f657086a8..96278b54da 100644 --- a/app/src/test/java/com/nononsenseapps/feeder/util/RomeExtensionsKtTest.kt +++ b/app/src/test/java/com/nononsenseapps/feeder/util/RomeExtensionsKtTest.kt @@ -16,6 +16,10 @@ import com.rometools.rome.feed.synd.SyndEntry import com.rometools.rome.feed.synd.SyndFeed import com.rometools.rome.feed.synd.SyndLink import com.rometools.rome.feed.synd.SyndPerson +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import java.net.URI import java.net.URL import java.time.Instant @@ -24,10 +28,6 @@ import java.time.ZonedDateTime import java.util.Date import java.util.Random import kotlin.test.assertEquals -import kotlinx.coroutines.runBlocking -import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` class RomeExtensionsKtTest { @Test @@ -39,55 +39,61 @@ class RomeExtensionsKtTest { } @Test - fun feedLinkButNoLinks() = runBlocking { - assertEquals( - Feed(home_page_url = "$baseUrl/homepage", title = "", items = emptyList()), - mockSyndFeed(link = "homepage").asFeed(baseUrl), - ) - } + fun feedLinkButNoLinks() = + runBlocking { + assertEquals( + Feed(home_page_url = "$baseUrl/homepage", title = "", items = emptyList()), + mockSyndFeed(link = "homepage").asFeed(baseUrl), + ) + } @Test - fun feedLinks() = runBlocking { - assertEquals( - Feed(home_page_url = "$baseUrl/homepage", title = "", items = emptyList()), - mockSyndFeed( - links = listOf( - mockSyndLink( - href = "homepage", - rel = "alternate", - type = "text/html", - ), - ), - ).asFeed(baseUrl), - ) - } + fun feedLinks() = + runBlocking { + assertEquals( + Feed(home_page_url = "$baseUrl/homepage", title = "", items = emptyList()), + mockSyndFeed( + links = + listOf( + mockSyndLink( + href = "homepage", + rel = "alternate", + type = "text/html", + ), + ), + ).asFeed(baseUrl), + ) + } @Test - fun itemFallsBackToFeedAuthor() = runBlocking { - assertEquals( - Feed( - author = Author(name = "bob"), - title = "", - items = listOf( - Item( - id = "$baseUrl/id", - author = Author(name = "bob"), - content_text = "", - url = null, - summary = "", - title = "", - attachments = emptyList(), - ), - ), - ), - mockSyndFeed( - author = mockSyndPerson(name = "bob"), - entries = listOf( - mockSyndEntry(uri = "id"), + fun itemFallsBackToFeedAuthor() = + runBlocking { + assertEquals( + Feed( + author = Author(name = "bob"), + title = "", + items = + listOf( + Item( + id = "$baseUrl/id", + author = Author(name = "bob"), + content_text = "", + url = null, + summary = "", + title = "", + attachments = emptyList(), + ), + ), ), - ).asFeed(baseUrl), - ) - } + mockSyndFeed( + author = mockSyndPerson(name = "bob"), + entries = + listOf( + mockSyndEntry(uri = "id"), + ), + ).asFeed(baseUrl), + ) + } // Essentially a test for XKCD @Test @@ -184,10 +190,11 @@ class RomeExtensionsKtTest { mockSyndEntry( uri = "id", description = mockSyndContent(value = ""), - links = listOf( - mockSyndLink(href = "abc", rel = "self"), - mockSyndLink(href = "bcd"), - ), + links = + listOf( + mockSyndLink(href = "abc", rel = "self"), + mockSyndLink(href = "bcd"), + ), ).asItem(baseUrl), ) } @@ -214,12 +221,13 @@ class RomeExtensionsKtTest { ), mockSyndEntry( uri = "id", - contents = listOf( - mockSyndContent(value = "PLAIN", type = "text"), - mockSyndContent(value = "html", type = "html"), - mockSyndContent(value = null, type = "xhtml"), - mockSyndContent(value = "bah", type = null), - ), + contents = + listOf( + mockSyndContent(value = "PLAIN", type = "text"), + mockSyndContent(value = "html", type = "html"), + mockSyndContent(value = null, type = "xhtml"), + mockSyndContent(value = "bah", type = null), + ), ).asItem(baseUrl), ) } @@ -238,11 +246,12 @@ class RomeExtensionsKtTest { ), mockSyndEntry( uri = "id", - contents = listOf( - mockSyndContent(value = "html", type = "html"), - mockSyndContent(value = null, type = "xhtml"), - mockSyndContent(value = "bah", type = null), - ), + contents = + listOf( + mockSyndContent(value = "html", type = "html"), + mockSyndContent(value = null, type = "xhtml"), + mockSyndContent(value = "bah", type = null), + ), ).asItem(baseUrl), ) } @@ -261,10 +270,11 @@ class RomeExtensionsKtTest { ), mockSyndEntry( uri = "id", - contents = listOf( - mockSyndContent(value = "html", type = "html"), - mockSyndContent(value = null, type = "xhtml"), - ), + contents = + listOf( + mockSyndContent(value = "html", type = "html"), + mockSyndContent(value = null, type = "xhtml"), + ), ).asItem(baseUrl), ) } @@ -283,9 +293,10 @@ class RomeExtensionsKtTest { ), mockSyndEntry( uri = "id", - contents = listOf( - mockSyndContent(value = "foo"), - ), + contents = + listOf( + mockSyndContent(value = "foo"), + ), ).asItem(baseUrl), ) } @@ -396,16 +407,18 @@ class RomeExtensionsKtTest { ), mockSyndEntry( uri = "id", - thumbnails = arrayOf( - mockThumbnail( - url = URI.create( - "data:image/png;base64,iVBORw0KGgoAAA" + - "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" + - "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" + - "5ErkJggg==", + thumbnails = + arrayOf( + mockThumbnail( + url = + URI.create( + "data:image/png;base64,iVBORw0KGgoAAA" + + "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" + + "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" + + "5ErkJggg==", + ), ), ), - ), ).asItem(baseUrl), ) } @@ -431,17 +444,20 @@ class RomeExtensionsKtTest { @Test fun thumbnailFromHtmlDescriptionIsUnescaped() { - val description = mockSyndContent( - value = """ + val description = + mockSyndContent( + value = + """ Google didn't completely scrap its robotic dreams after it sold off Boston Dynamics and shuttered the other robotic start-ups it acquired over the past decade. Now, the tech giant has given us a glimpse of how the program has changed in a blog post a... - """.trimIndent(), - type = null, - ) + """.trimIndent(), + type = null, + ) - val item = mockSyndEntry( - uri = "id", - description = description, - ).asItem(baseUrl) + val item = + mockSyndEntry( + uri = "id", + description = description, + ).asItem(baseUrl) assertEquals( "https://o.aolcdn.com/images/dims?crop=1200%2C627%2C0%2C0&quality=85&format=jpg&resize=1600%2C836&image_uri=https%3A%2F%2Fs.yimg.com%2Fos%2Fcreatr-uploaded-images%2F2019-03%2Ffa057c20-5050-11e9-bfef-d1614983d7cc&client=a1acac3e1b3290917d92&signature=351348aa11c53a569d5ad40f3a7ef697471b645a", @@ -451,17 +467,20 @@ class RomeExtensionsKtTest { @Test fun thumbnailFromTypeTextIsFound() { - val description = mockSyndContent( - value = """ - Google didn't completely scrap its robotic dreams after it sold off Boston Dynamics and shuttered the other robotic start-ups it acquired over the past decade. Now, the tech giant has given us a glimpse of how the program has changed in a blog post a... - """.trimIndent(), - type = "text", - ) + val description = + mockSyndContent( + value = + """ + Google didn't completely scrap its robotic dreams after it sold off Boston Dynamics and shuttered the other robotic start-ups it acquired over the past decade. Now, the tech giant has given us a glimpse of how the program has changed in a blog post a... + """.trimIndent(), + type = "text", + ) - val item = mockSyndEntry( - uri = "id", - description = description, - ).asItem(baseUrl) + val item = + mockSyndEntry( + uri = "id", + description = description, + ).asItem(baseUrl) assertEquals( "https://o.aolcdn.com/images/dims?crop=1200%2C627%2C0%2C0&quality=85&format=jpg&resize=1600%2C836&image_uri=https%3A%2F%2Fs.yimg.com%2Fos%2Fcreatr-uploaded-images%2F2019-03%2Ffa057c20-5050-11e9-bfef-d1614983d7cc&client=a1acac3e1b3290917d92&signature=351348aa11c53a569d5ad40f3a7ef697471b645a", @@ -471,17 +490,20 @@ class RomeExtensionsKtTest { @Test fun thumbnailFromTypeHtmlIsFound() { - val description = mockSyndContent( - value = """ - Google didn't completely scrap its robotic dreams after it sold off Boston Dynamics and shuttered the other robotic start-ups it acquired over the past decade. Now, the tech giant has given us a glimpse of how the program has changed in a blog post a... - """.trimIndent(), - type = "html", - ) + val description = + mockSyndContent( + value = + """ + Google didn't completely scrap its robotic dreams after it sold off Boston Dynamics and shuttered the other robotic start-ups it acquired over the past decade. Now, the tech giant has given us a glimpse of how the program has changed in a blog post a... + """.trimIndent(), + type = "html", + ) - val item = mockSyndEntry( - uri = "id", - description = description, - ).asItem(baseUrl) + val item = + mockSyndEntry( + uri = "id", + description = description, + ).asItem(baseUrl) assertEquals( "https://o.aolcdn.com/images/dims?crop=1200%2C627%2C0%2C0&quality=85&format=jpg&resize=1600%2C836&image_uri=https%3A%2F%2Fs.yimg.com%2Fos%2Fcreatr-uploaded-images%2F2019-03%2Ffa057c20-5050-11e9-bfef-d1614983d7cc&client=a1acac3e1b3290917d92&signature=351348aa11c53a569d5ad40f3a7ef697471b645a", @@ -491,15 +513,17 @@ class RomeExtensionsKtTest { @Test fun thumbnailFromEnclosureIsFound() { - val item = mockSyndEntry( - uri = "id", - enclosures = listOf( - mockSyndEnclosure( - url = "http://foo/bar.png", - type = "image/png", - ), - ), - ).asItem(baseUrl) + val item = + mockSyndEntry( + uri = "id", + enclosures = + listOf( + mockSyndEnclosure( + url = "http://foo/bar.png", + type = "image/png", + ), + ), + ).asItem(baseUrl) assertEquals( "http://foo/bar.png", @@ -612,7 +636,11 @@ class RomeExtensionsKtTest { return mock } - private fun mockSyndLink(href: String, rel: String? = null, type: String? = null): SyndLink { + private fun mockSyndLink( + href: String, + rel: String? = null, + type: String? = null, + ): SyndLink { val mock = mock(SyndLink::class.java) `when`(mock.href).thenReturn(href) @@ -666,7 +694,10 @@ class RomeExtensionsKtTest { return mock } - private fun mockSyndContent(value: String? = null, type: String? = null): SyndContent { + private fun mockSyndContent( + value: String? = null, + type: String? = null, + ): SyndContent { val mock = mock(SyndContent::class.java) `when`(mock.value).thenReturn(value) @@ -698,7 +729,10 @@ class RomeExtensionsKtTest { } @Suppress("SameParameterValue") - private fun mockMediaContent(url: String? = null, medium: String? = null): MediaContent { + private fun mockMediaContent( + url: String? = null, + medium: String? = null, + ): MediaContent { val mock = mock(MediaContent::class.java) var mockRef: Reference? = null diff --git a/app/src/test/java/com/nononsenseapps/jsonfeed/JsonFeedParserTest.kt b/app/src/test/java/com/nononsenseapps/jsonfeed/JsonFeedParserTest.kt index 25f3cc100d..909b742a06 100644 --- a/app/src/test/java/com/nononsenseapps/jsonfeed/JsonFeedParserTest.kt +++ b/app/src/test/java/com/nononsenseapps/jsonfeed/JsonFeedParserTest.kt @@ -1,17 +1,17 @@ package com.nononsenseapps.jsonfeed -import kotlin.test.assertEquals import org.junit.Assert import org.junit.Test +import kotlin.test.assertEquals class JsonFeedParserTest { - @Test fun basic() { val parser = JsonFeedParser() - val feed = parser.parseJson( - """ + val feed = + parser.parseJson( + """ { "version": "https://jsonfeed.org/version/1", "title": "My Example Feed", @@ -31,7 +31,7 @@ class JsonFeedParserTest { ] } """, - ) + ) assertEquals("https://jsonfeed.org/version/1", feed.version) assertEquals("My Example Feed", feed.title) @@ -53,8 +53,9 @@ class JsonFeedParserTest { fun dateParsing() { val parser = JsonFeedParser() - val feed = parser.parseJson( - """ + val feed = + parser.parseJson( + """ { "version": "https://jsonfeed.org/version/1", "title": "My Example Feed", @@ -71,7 +72,7 @@ class JsonFeedParserTest { ] } """, - ) + ) assertEquals("https://jsonfeed.org/version/1", feed.version) assertEquals("My Example Feed", feed.title) diff --git a/scripts/changelog-to-hugo.main.kts b/scripts/changelog-to-hugo.main.kts index fbf8c42ee3..34b7864f9a 100755 --- a/scripts/changelog-to-hugo.main.kts +++ b/scripts/changelog-to-hugo.main.kts @@ -2,14 +2,13 @@ @file:DependsOn("org.jetbrains:markdown-jvm:0.4.1") @file:DependsOn("net.pwall.mustache:kotlin-mustache:0.10") -import java.io.File -import java.util.concurrent.TimeUnit import net.pwall.mustache.parser.Parser import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor -import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser +import java.io.File +import java.util.concurrent.TimeUnit val flavour = CommonMarkFlavourDescriptor() @@ -74,13 +73,14 @@ fun recurseMarkdown( if (ignoreContent) { for (child in node.children) { - newVersion = recurseMarkdown( - node = child, - src = src, - version = newVersion, - sb = sb, - entries = entries, - ) + newVersion = + recurseMarkdown( + node = child, + src = src, + version = newVersion, + sb = sb, + entries = entries, + ) } } else { val content = src.slice(node.startOffset until node.endOffset) @@ -89,8 +89,12 @@ fun recurseMarkdown( return newVersion } -fun generateHugoEntries(targetDir: File, entries: List) { - val hugoTemplateString = """ +fun generateHugoEntries( + targetDir: File, + entries: List, +) { + val hugoTemplateString = + """ --- title: "{{title}}" date: {{timestamp}} @@ -98,7 +102,7 @@ fun generateHugoEntries(targetDir: File, entries: List) { thumbnail: "feature.png" --- {{&content}} - """.trimIndent() + """.trimIndent() println("${entries.size} entries") @@ -120,11 +124,12 @@ fun generateHugoEntries(targetDir: File, entries: List) { fun String.runCommand(): String { val parts = this.split("\\s".toRegex()) - val proc = ProcessBuilder(*parts.toTypedArray()) - .directory(File("/home/jonas/workspace/feeder")) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() + val proc = + ProcessBuilder(*parts.toTypedArray()) + .directory(File("/home/jonas/workspace/feeder")) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() proc.waitFor(2, TimeUnit.SECONDS) return proc.inputStream.bufferedReader().readText().trim() @@ -132,9 +137,10 @@ fun String.runCommand(): String { // $args -val targetDir = args.firstOrNull() - ?.let { File(it) } - ?: error("Expects target directory as first argument") +val targetDir = + args.firstOrNull() + ?.let { File(it) } + ?: error("Expects target directory as first argument") if (!targetDir.isDirectory) { error("$targetDir does not exist or is not a directory!") @@ -147,7 +153,8 @@ val entries = parseChangelog() generateHugoEntries( targetDir = targetDir, - entries = entries.filter { - tag == null || it.version == tag - }, + entries = + entries.filter { + tag == null || it.version == tag + }, ) diff --git a/scripts/convert-changelog.main.kts b/scripts/convert-changelog.main.kts index da0b1b5927..cf1937cab8 100755 --- a/scripts/convert-changelog.main.kts +++ b/scripts/convert-changelog.main.kts @@ -2,14 +2,14 @@ @file:DependsOn("org.jetbrains:markdown-jvm:0.4.1") @file:DependsOn("net.pwall.mustache:kotlin-mustache:0.10") -import java.io.File -import java.util.concurrent.TimeUnit import net.pwall.mustache.parser.Parser import org.intellij.markdown.MarkdownElementTypes import org.intellij.markdown.ast.ASTNode import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor import org.intellij.markdown.html.HtmlGenerator import org.intellij.markdown.parser.MarkdownParser +import java.io.File +import java.util.concurrent.TimeUnit val flavour = CommonMarkFlavourDescriptor() @@ -85,7 +85,8 @@ fun recurseMarkdown( } fun generateAtomChangelog(entries: List) { - val atomTemplateString = """ + val atomTemplateString = + """ https://github.com/spacecowboy/Feeder/blob/master/CHANGELOG.md @@ -122,23 +123,25 @@ fun generateAtomChangelog(entries: List) { val parser = Parser() val atomTemplate = parser.parse(atomTemplateString) - val y = atomTemplate.processToString( - mapOf( - "timestamp" to entries.first().timestamp, - "entry" to entries, - ), - ) + val y = + atomTemplate.processToString( + mapOf( + "timestamp" to entries.first().timestamp, + "entry" to entries, + ), + ) println(y) } fun String.runCommand(): String { val parts = this.split("\\s".toRegex()) - val proc = ProcessBuilder(*parts.toTypedArray()) - .directory(File("/home/jonas/workspace/feeder")) - .redirectOutput(ProcessBuilder.Redirect.PIPE) - .redirectError(ProcessBuilder.Redirect.PIPE) - .start() + val proc = + ProcessBuilder(*parts.toTypedArray()) + .directory(File("/home/jonas/workspace/feeder")) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() proc.waitFor(2, TimeUnit.SECONDS) return proc.inputStream.bufferedReader().readText().trim()