Compare commits
	
		
			4 Commits
		
	
	
		
			v125010241
			...
			64dec53a7a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 64dec53a7a | |||
| 55a6b83837 | |||
| 4fbebf2954 | |||
| 5035392aff | 
							
								
								
									
										36
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
 | 
			
		||||
[.editorconfig]
 | 
			
		||||
insert_final_newline = false
 | 
			
		||||
ij_kotlin_line_break_after_multiline_when_entry = false
 | 
			
		||||
 | 
			
		||||
[*.{kt,kts}]
 | 
			
		||||
#  Disable wildcard imports entirely
 | 
			
		||||
ij_kotlin_name_count_to_use_star_import = 2147483647
 | 
			
		||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
ij_kotlin_allow_trailing_comma = true
 | 
			
		||||
ij_kotlin_allow_trailing_comma_on_call_site = true
 | 
			
		||||
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
 | 
			
		||||
ij_kotlin_indent_before_arrow_on_new_line = false
 | 
			
		||||
ij_kotlin_line_break_after_multiline_when_entry = true
 | 
			
		||||
ij_kotlin_packages_to_use_import_on_demand = unset
 | 
			
		||||
indent_size = 4
 | 
			
		||||
indent_style = space
 | 
			
		||||
ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = unset
 | 
			
		||||
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
 | 
			
		||||
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
 | 
			
		||||
ktlint_code_style = ktlint_official
 | 
			
		||||
ktlint_enum_entry_name_casing = upper_or_camel_cases
 | 
			
		||||
ktlint_function_naming_ignore_when_annotated_with = unset
 | 
			
		||||
ktlint_function_signature_body_expression_wrapping = multiline
 | 
			
		||||
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
 | 
			
		||||
ktlint_ignore_back_ticked_identifier = false
 | 
			
		||||
ktlint_property_naming_constant_naming = screaming_snake_case
 | 
			
		||||
max_line_length = 140
 | 
			
		||||
 | 
			
		||||
[**/build]
 | 
			
		||||
ktlint = disabled
 | 
			
		||||
@@ -16,13 +16,14 @@ jobs:
 | 
			
		||||
          java-version: '17'
 | 
			
		||||
          cache: gradle
 | 
			
		||||
      - name: Install klint
 | 
			
		||||
        run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
 | 
			
		||||
        run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
 | 
			
		||||
      - name: Install detekt
 | 
			
		||||
        run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
 | 
			
		||||
        run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip  unzip detekt-cli-1.23.7.zip
 | 
			
		||||
      - name: Linting...
 | 
			
		||||
        run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
 | 
			
		||||
        run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
 | 
			
		||||
      - name: Detecting...
 | 
			
		||||
        run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
 | 
			
		||||
        run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
 | 
			
		||||
  build:
 | 
			
		||||
    needs: Lint
 | 
			
		||||
    uses: ./.gitea/workflows/common_build.yml
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: ./.gitea/workflows/common_build.yml
 | 
			
		||||
 
 | 
			
		||||
@@ -20,65 +20,72 @@ import org.hamcrest.Matchers.hasToString
 | 
			
		||||
fun performLogin(someUrl: String? = null) {
 | 
			
		||||
    onView(withId(R.id.urlView)).perform(click()).perform(
 | 
			
		||||
        typeTextIntoFocusedView(
 | 
			
		||||
            if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888"
 | 
			
		||||
        )
 | 
			
		||||
            if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun loginAndInitHome() {
 | 
			
		||||
 | 
			
		||||
    performLogin()
 | 
			
		||||
    onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
 | 
			
		||||
    onView(withText("OK")).perform(click())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
 | 
			
		||||
fun changeAndCancelSetting(
 | 
			
		||||
    oldValue: String,
 | 
			
		||||
    newValue: String,
 | 
			
		||||
    openSettingItem: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    openSettingItem()
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.edit)
 | 
			
		||||
        withId(android.R.id.edit),
 | 
			
		||||
    ).perform(replaceText(newValue))
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.button2)
 | 
			
		||||
        withId(android.R.id.button2),
 | 
			
		||||
    ).perform(click())
 | 
			
		||||
    openSettingItem()
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.edit)
 | 
			
		||||
        withId(android.R.id.edit),
 | 
			
		||||
    ).check(matches(withText(oldValue)))
 | 
			
		||||
    onView(
 | 
			
		||||
        withText(newValue)
 | 
			
		||||
        withText(newValue),
 | 
			
		||||
    ).check(doesNotExist())
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.button2)
 | 
			
		||||
        withId(android.R.id.button2),
 | 
			
		||||
    ).perform(click())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
 | 
			
		||||
fun changeAndSaveSetting(
 | 
			
		||||
    oldValue: String,
 | 
			
		||||
    newValue: String,
 | 
			
		||||
    openSettingItem: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    openSettingItem()
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.edit)
 | 
			
		||||
        withId(android.R.id.edit),
 | 
			
		||||
    ).perform(replaceText(newValue))
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.button1)
 | 
			
		||||
        withId(android.R.id.button1),
 | 
			
		||||
    ).perform(click())
 | 
			
		||||
    openSettingItem()
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.edit)
 | 
			
		||||
        withId(android.R.id.edit),
 | 
			
		||||
    ).check(matches(withText(newValue)))
 | 
			
		||||
    if (oldValue.isNotEmpty()) {
 | 
			
		||||
        onView(
 | 
			
		||||
            withText(oldValue)
 | 
			
		||||
            withText(oldValue),
 | 
			
		||||
        ).check(doesNotExist())
 | 
			
		||||
    }
 | 
			
		||||
    onView(
 | 
			
		||||
        withId(android.R.id.button2)
 | 
			
		||||
        withId(android.R.id.button2),
 | 
			
		||||
    ).perform(click())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun testPreferencesFromArray(
 | 
			
		||||
    context: Context,
 | 
			
		||||
    @ArrayRes arrayRes: Int,
 | 
			
		||||
    openSettingItem: () -> Unit
 | 
			
		||||
    openSettingItem: () -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    openSettingItem()
 | 
			
		||||
    context.resources.getStringArray(arrayRes).forEach { res ->
 | 
			
		||||
@@ -90,20 +97,25 @@ fun testPreferencesFromArray(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun testAddSourceWithUrl(url: String, sourceName: String) {
 | 
			
		||||
fun testAddSourceWithUrl(
 | 
			
		||||
    url: String,
 | 
			
		||||
    sourceName: String,
 | 
			
		||||
) {
 | 
			
		||||
    onView(withId(R.id.fab))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
    onView(withId(R.id.nameInput))
 | 
			
		||||
        .perform(click()).perform(typeTextIntoFocusedView(sourceName))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
        .perform(typeTextIntoFocusedView(sourceName))
 | 
			
		||||
    onView(withId(R.id.sourceUri))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
        .perform(typeTextIntoFocusedView(url))
 | 
			
		||||
    onView(withId(R.id.tags))
 | 
			
		||||
        .perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
        .perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
 | 
			
		||||
    onView(withId(R.id.spoutsSpinner))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
    onData(hasToString("RSS Feed")).perform(click())
 | 
			
		||||
    onView(withId(R.id.saveBtn))
 | 
			
		||||
        .perform(click())
 | 
			
		||||
    onView(withText(sourceName)).check(matches(isDisplayed()))
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,38 +25,35 @@ import org.hamcrest.Matcher
 | 
			
		||||
import org.hamcrest.Matchers
 | 
			
		||||
import org.hamcrest.TypeSafeMatcher
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fun withError(@StringRes id: Int): TypeSafeMatcher<View?> {
 | 
			
		||||
fun withError(
 | 
			
		||||
    @StringRes id: Int,
 | 
			
		||||
): TypeSafeMatcher<View?> {
 | 
			
		||||
    return object : TypeSafeMatcher<View?>() {
 | 
			
		||||
        override fun matchesSafely(view: View?): Boolean {
 | 
			
		||||
            if (view == null) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            val context = view.context
 | 
			
		||||
            if (view !is EditText) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            if (view.error == null) {
 | 
			
		||||
            if (view != null && (view !is EditText || view.error == null)) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            val context = view!!.context
 | 
			
		||||
 | 
			
		||||
            return view.error.toString() == context.getString(id)
 | 
			
		||||
            return (view as EditText).error.toString() == context.getString(id)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun describeTo(description: Description?) {
 | 
			
		||||
            // Nothing
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun isPopupWindow(): Matcher<Root> {
 | 
			
		||||
    return isPlatformPopup()
 | 
			
		||||
}
 | 
			
		||||
fun isPopupWindow(): Matcher<Root> = isPlatformPopup()
 | 
			
		||||
 | 
			
		||||
fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
 | 
			
		||||
fun withDrawable(
 | 
			
		||||
    @DrawableRes id: Int,
 | 
			
		||||
) = object : TypeSafeMatcher<View>() {
 | 
			
		||||
    override fun describeTo(description: Description) {
 | 
			
		||||
        description.appendText("ImageView with drawable same as drawable with id $id")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    override fun matchesSafely(view: View): Boolean {
 | 
			
		||||
        val context = view.context
 | 
			
		||||
        val expectedBitmap = context.getDrawable(id)!!.toBitmap()
 | 
			
		||||
@@ -68,43 +65,46 @@ fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun hasBottombarItemText(@StringRes id: Int): Matcher<View>? {
 | 
			
		||||
    return allOf(
 | 
			
		||||
fun hasBottombarItemText(
 | 
			
		||||
    @StringRes id: Int,
 | 
			
		||||
): Matcher<View>? =
 | 
			
		||||
    allOf(
 | 
			
		||||
        withResourceName("fixed_bottom_navigation_icon"),
 | 
			
		||||
        withParent(
 | 
			
		||||
            allOf(
 | 
			
		||||
                withResourceName("fixed_bottom_navigation_icon_container"),
 | 
			
		||||
                hasSibling(withText(id))
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
                hasSibling(withText(id)),
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
 | 
			
		||||
    return allOf(
 | 
			
		||||
fun withSettingsCheckboxWidget(
 | 
			
		||||
    @StringRes id: Int,
 | 
			
		||||
): Matcher<View>? =
 | 
			
		||||
    allOf(
 | 
			
		||||
        withId(android.R.id.switch_widget),
 | 
			
		||||
        withParent(
 | 
			
		||||
            withSettingsCheckboxFrame(id)
 | 
			
		||||
        )
 | 
			
		||||
            withSettingsCheckboxFrame(id),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun withSettingsCheckboxFrame(@StringRes id: Int): Matcher<View>? {
 | 
			
		||||
    return allOf(
 | 
			
		||||
fun withSettingsCheckboxFrame(
 | 
			
		||||
    @StringRes id: Int,
 | 
			
		||||
): Matcher<View>? =
 | 
			
		||||
    allOf(
 | 
			
		||||
        withId(android.R.id.widget_frame),
 | 
			
		||||
        hasSibling(
 | 
			
		||||
            allOf(
 | 
			
		||||
                withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
 | 
			
		||||
                withChild(
 | 
			
		||||
                    withText(id)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
                    withText(id),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun openMenu() {
 | 
			
		||||
    openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
        ApplicationProvider.getApplicationContext<Context>()
 | 
			
		||||
        ApplicationProvider.getApplicationContext<Context>(),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -23,7 +23,6 @@ import org.junit.runner.RunWith
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class HomeActivityTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
@@ -36,13 +35,13 @@ class HomeActivityTest {
 | 
			
		||||
    fun testMenu() {
 | 
			
		||||
        onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isClickable()
 | 
			
		||||
            )
 | 
			
		||||
                isClickable(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isClickable()
 | 
			
		||||
            )
 | 
			
		||||
                isClickable(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        openMenu()
 | 
			
		||||
        onView(withText(R.string.readAll)).check(matches(isDisplayed()))
 | 
			
		||||
@@ -57,19 +56,19 @@ class HomeActivityTest {
 | 
			
		||||
    fun testMenuActions() {
 | 
			
		||||
        onView(withId(R.id.action_search)).perform(click())
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(R.id.search_src_text)
 | 
			
		||||
            withId(R.id.search_src_text),
 | 
			
		||||
        ).check(matches(isFocused()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.action_filter)).perform(click())
 | 
			
		||||
        onView(
 | 
			
		||||
            withText(R.string.filter_item_sources)
 | 
			
		||||
            withText(R.string.filter_item_sources),
 | 
			
		||||
        ).check(matches(isDisplayed()))
 | 
			
		||||
        onView(
 | 
			
		||||
            withText(R.string.filter_item_tags)
 | 
			
		||||
            withText(R.string.filter_item_tags),
 | 
			
		||||
        ).check(matches(isDisplayed()))
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(R.id.floatingActionButton2)
 | 
			
		||||
            withId(R.id.floatingActionButton2),
 | 
			
		||||
        ).check(matches(isDisplayed()))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
 | 
			
		||||
@@ -107,14 +106,13 @@ class HomeActivityTest {
 | 
			
		||||
    fun testEmptyView() {
 | 
			
		||||
        onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(
 | 
			
		||||
            hasBottombarItemText(R.string.tab_new)
 | 
			
		||||
            hasBottombarItemText(R.string.tab_new),
 | 
			
		||||
        ).check(matches(isDisplayed())).check(matches(isSelected()))
 | 
			
		||||
        onView(
 | 
			
		||||
            hasBottombarItemText(R.string.tab_read)
 | 
			
		||||
            hasBottombarItemText(R.string.tab_read),
 | 
			
		||||
        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
			
		||||
        onView(
 | 
			
		||||
            hasBottombarItemText(R.string.tab_favs)
 | 
			
		||||
            hasBottombarItemText(R.string.tab_favs),
 | 
			
		||||
        ).check(matches(isDisplayed())).check(matches(not(isSelected())))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import androidx.test.espresso.Espresso.onView
 | 
			
		||||
import androidx.test.espresso.IdlingRegistry
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.click
 | 
			
		||||
@@ -23,40 +22,37 @@ import org.junit.runner.RunWith
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class LoginActivityTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
    private fun getActivity(): Activity? {
 | 
			
		||||
        var activity: Activity? = null
 | 
			
		||||
        activityRule.scenario.onActivity {
 | 
			
		||||
            activity = it
 | 
			
		||||
        }
 | 
			
		||||
        return activity
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    fun registerIdlingResource() {
 | 
			
		||||
        IdlingRegistry.getInstance()
 | 
			
		||||
        IdlingRegistry
 | 
			
		||||
            .getInstance()
 | 
			
		||||
            .register(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
    fun unregisterIdlingResource() {
 | 
			
		||||
        IdlingRegistry.getInstance()
 | 
			
		||||
        IdlingRegistry
 | 
			
		||||
            .getInstance()
 | 
			
		||||
            .unregister(CountingIdlingResourceSingleton.countingIdlingResource)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun viewIsInitialized() {
 | 
			
		||||
        onView(withId(R.id.urlView)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
 | 
			
		||||
        onView(withId(R.id.selfSigned))
 | 
			
		||||
            .check(matches(isDisplayed()))
 | 
			
		||||
            .check(matches(isNotChecked()))
 | 
			
		||||
            .check(
 | 
			
		||||
                matches(isClickable())
 | 
			
		||||
                matches(isClickable()),
 | 
			
		||||
            )
 | 
			
		||||
        onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
 | 
			
		||||
            .check(matches(isNotChecked())).check(
 | 
			
		||||
                matches(isClickable())
 | 
			
		||||
        onView(withId(R.id.withLogin))
 | 
			
		||||
            .check(matches(isDisplayed()))
 | 
			
		||||
            .check(matches(isNotChecked()))
 | 
			
		||||
            .check(
 | 
			
		||||
                matches(isClickable()),
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -80,4 +76,4 @@ class LoginActivityTest {
 | 
			
		||||
        performLogin()
 | 
			
		||||
        onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,9 @@ import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class SettingsActivityGeneralTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
@@ -38,81 +36,90 @@ class SettingsActivityGeneralTest {
 | 
			
		||||
    fun init() {
 | 
			
		||||
        loginAndInitHome()
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext()
 | 
			
		||||
            ApplicationProvider.getApplicationContext(),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_header_general)).perform(click())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod")
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testGeneral() {
 | 
			
		||||
        onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(
 | 
			
		||||
            withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title)
 | 
			
		||||
            withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
 | 
			
		||||
        ).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), isChecked()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    isChecked(),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                not(isEnabled())
 | 
			
		||||
            )
 | 
			
		||||
                not(isEnabled()),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), isChecked()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    isChecked(),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withId(R.id.settings)).perform(swipeUp())
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:ForbiddenComment")
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testGeneralActionsNumberItems() {
 | 
			
		||||
        onView(withText(R.string.pref_api_items_number_title)).perform(click())
 | 
			
		||||
@@ -120,25 +127,25 @@ class SettingsActivityGeneralTest {
 | 
			
		||||
 | 
			
		||||
        // Value check
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.edit)
 | 
			
		||||
            withId(android.R.id.edit),
 | 
			
		||||
        ).perform(replaceText("AVC"))
 | 
			
		||||
            .check(matches(withText("")))
 | 
			
		||||
        // TODO: should check message error. Not working for api level 30+
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.edit)
 | 
			
		||||
            withId(android.R.id.edit),
 | 
			
		||||
        ).perform(replaceText("-1"))
 | 
			
		||||
            .check(matches(withText("")))
 | 
			
		||||
        // TODO: should check message error. Not working for api level 30+
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.edit)
 | 
			
		||||
            withId(android.R.id.edit),
 | 
			
		||||
        ).perform(replaceText("300"))
 | 
			
		||||
            .check(matches(withText("")))
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.edit)
 | 
			
		||||
            withId(android.R.id.edit),
 | 
			
		||||
        ).perform(typeTextIntoFocusedView("300"))
 | 
			
		||||
            .check(matches(withText("30")))
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.edit)
 | 
			
		||||
            withId(android.R.id.edit),
 | 
			
		||||
        ).perform(replaceText("10"))
 | 
			
		||||
            .check(matches(withText("10")))
 | 
			
		||||
        onView(isRoot()).perform(ViewActions.pressBack())
 | 
			
		||||
@@ -157,18 +164,18 @@ class SettingsActivityGeneralTest {
 | 
			
		||||
        // article viewer settings
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                not(isEnabled())
 | 
			
		||||
            )
 | 
			
		||||
                not(isEnabled()),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,9 @@ import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class SettingsActivityOfflineTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
@@ -38,72 +36,79 @@ class SettingsActivityOfflineTest {
 | 
			
		||||
        }
 | 
			
		||||
        loginAndInitHome()
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext()
 | 
			
		||||
            ApplicationProvider.getApplicationContext(),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_header_offline)).perform(click())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod")
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testOffline() {
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(isNotEnabled(), isDisplayed())
 | 
			
		||||
            )
 | 
			
		||||
                allOf(isNotEnabled(), isDisplayed()),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isNotEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isNotEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(isChecked())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isChecked()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isNotEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isNotEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), isChecked()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    isChecked(),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod")
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testOfflineActions() {
 | 
			
		||||
        onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
 | 
			
		||||
@@ -111,50 +116,50 @@ class SettingsActivityOfflineTest {
 | 
			
		||||
        onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isNotEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isNotEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isNotEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isNotEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isNotEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isNotEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isDisplayed()
 | 
			
		||||
            )
 | 
			
		||||
                isDisplayed(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isDisplayed()
 | 
			
		||||
            )
 | 
			
		||||
                isDisplayed(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isEnabled()
 | 
			
		||||
            )
 | 
			
		||||
                isEnabled(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        changeAndCancelSetting("360", "123") {
 | 
			
		||||
            onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
 | 
			
		||||
@@ -166,4 +171,4 @@ class SettingsActivityOfflineTest {
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
 | 
			
		||||
        onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,9 @@ import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class SettingsActivityReaderTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
@@ -36,7 +34,7 @@ class SettingsActivityReaderTest {
 | 
			
		||||
        }
 | 
			
		||||
        loginAndInitHome()
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(
 | 
			
		||||
            ApplicationProvider.getApplicationContext()
 | 
			
		||||
            ApplicationProvider.getApplicationContext(),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_header_viewer)).perform(click())
 | 
			
		||||
@@ -47,11 +45,12 @@ class SettingsActivityReaderTest {
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(), not(
 | 
			
		||||
                        isChecked()
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(
 | 
			
		||||
                        isChecked(),
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
 | 
			
		||||
@@ -61,14 +60,14 @@ class SettingsActivityReaderTest {
 | 
			
		||||
    fun testReaderActions() {
 | 
			
		||||
        onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isDisplayed()
 | 
			
		||||
            )
 | 
			
		||||
                isDisplayed(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
 | 
			
		||||
        onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
 | 
			
		||||
            matches(
 | 
			
		||||
                isDisplayed()
 | 
			
		||||
            )
 | 
			
		||||
                isDisplayed(),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.pref_content_reader_font_size)).perform(click())
 | 
			
		||||
@@ -83,4 +82,4 @@ class SettingsActivityReaderTest {
 | 
			
		||||
            onView(withText(R.string.settings_reader_font)).perform(click())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,6 @@ import org.junit.runner.RunWith
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
@LargeTest
 | 
			
		||||
class SettingsActivityTest {
 | 
			
		||||
 | 
			
		||||
    @get:Rule
 | 
			
		||||
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)
 | 
			
		||||
    lateinit var context: Context
 | 
			
		||||
@@ -35,10 +34,8 @@ class SettingsActivityTest {
 | 
			
		||||
        onView(withText(R.string.title_activity_settings)).perform(click())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testAllSettings() {
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
 | 
			
		||||
@@ -48,14 +45,13 @@ class SettingsActivityTest {
 | 
			
		||||
            matches(
 | 
			
		||||
                allOf(
 | 
			
		||||
                    isDisplayed(),
 | 
			
		||||
                    not(isSelected())
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
                    not(isSelected()),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        onView(withText(R.string.action_about)).check(matches(isDisplayed()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testThemes() {
 | 
			
		||||
        testPreferencesFromArray(context, R.array.ModeTitles) {
 | 
			
		||||
@@ -63,7 +59,6 @@ class SettingsActivityTest {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testExperimentail() {
 | 
			
		||||
        onView(withText(R.string.pref_header_experimental)).perform(click())
 | 
			
		||||
@@ -75,13 +70,11 @@ class SettingsActivityTest {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testBugReports() {
 | 
			
		||||
        onView(withText(R.string.pref_switch_disable_acra)).perform(click())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testLinks() {
 | 
			
		||||
        onView(withText(R.string.pref_header_links)).perform(click())
 | 
			
		||||
@@ -91,10 +84,9 @@ class SettingsActivityTest {
 | 
			
		||||
        onView(withText(R.string.translation)).check(matches(isDisplayed()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun testAbout() {
 | 
			
		||||
        onView(withText(R.string.action_about)).perform(click())
 | 
			
		||||
        onView(withText("ACRA")).check(matches(isDisplayed()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,11 +41,11 @@ class SourcesActivityTest {
 | 
			
		||||
    fun addSource() {
 | 
			
		||||
        testAddSourceWithUrl(
 | 
			
		||||
            "https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
 | 
			
		||||
            sourceName
 | 
			
		||||
            sourceName,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    @Test
 | 
			
		||||
    fun addSourceCheckContent() {
 | 
			
		||||
        testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
 | 
			
		||||
@@ -54,7 +54,7 @@ class SourcesActivityTest {
 | 
			
		||||
        onView(withText(R.string.menu_home_refresh)).perform(click())
 | 
			
		||||
        onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
 | 
			
		||||
        onView(
 | 
			
		||||
            withId(android.R.id.button1)
 | 
			
		||||
            withId(android.R.id.button1),
 | 
			
		||||
        ).perform(click())
 | 
			
		||||
        Thread.sleep(10000)
 | 
			
		||||
        onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
 | 
			
		||||
@@ -74,10 +74,9 @@ class SourcesActivityTest {
 | 
			
		||||
        onView(withText(sourceName)).check(doesNotExist())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private fun goToSources() {
 | 
			
		||||
        openMenu()
 | 
			
		||||
        onView(withText(R.string.menu_home_sources))
 | 
			
		||||
            .perform(click())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,12 @@ import org.kodein.di.instance
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
 | 
			
		||||
private const val MIN_WIDTH_CARD_DP = 300
 | 
			
		||||
 | 
			
		||||
class HomeActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    SearchView.OnQueryTextListener,
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private var items: ArrayList<SelfossModel.Item> = ArrayList()
 | 
			
		||||
 | 
			
		||||
    private var elementsShown: ItemType = ItemType.UNREAD
 | 
			
		||||
@@ -171,11 +176,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                            getElementsAccordingToTab()
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@HomeActivity,
 | 
			
		||||
                            "Found null when swiping at positon $position.",
 | 
			
		||||
                            Toast.LENGTH_LONG,
 | 
			
		||||
                        ).show()
 | 
			
		||||
                        Toast
 | 
			
		||||
                            .makeText(
 | 
			
		||||
                                this@HomeActivity,
 | 
			
		||||
                                "Found null when swiping at positon $position.",
 | 
			
		||||
                                Toast.LENGTH_LONG,
 | 
			
		||||
                            ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -196,19 +202,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod")
 | 
			
		||||
    private fun handleBottomBar() {
 | 
			
		||||
        tabNewBadge =
 | 
			
		||||
            TextBadgeItem()
 | 
			
		||||
                .setText("")
 | 
			
		||||
                .setHideOnSelect(false).hide(false)
 | 
			
		||||
                .setHideOnSelect(false)
 | 
			
		||||
                .hide(false)
 | 
			
		||||
        tabArchiveBadge =
 | 
			
		||||
            TextBadgeItem()
 | 
			
		||||
                .setText("")
 | 
			
		||||
                .setHideOnSelect(false).hide(false)
 | 
			
		||||
                .setHideOnSelect(false)
 | 
			
		||||
                .hide(false)
 | 
			
		||||
        tabStarredBadge =
 | 
			
		||||
            TextBadgeItem()
 | 
			
		||||
                .setText("")
 | 
			
		||||
                .setHideOnSelect(false).hide(false)
 | 
			
		||||
                .setHideOnSelect(false)
 | 
			
		||||
                .hide(false)
 | 
			
		||||
 | 
			
		||||
        if (appSettingsService.isDisplayUnreadCountEnabled()) {
 | 
			
		||||
            lifecycleScope.launch {
 | 
			
		||||
@@ -236,14 +246,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
            BottomNavigationItem(
 | 
			
		||||
                R.drawable.ic_tab_fiber_new_black_24dp,
 | 
			
		||||
                getString(R.string.tab_new),
 | 
			
		||||
            )
 | 
			
		||||
                .setBadgeItem(tabNewBadge)
 | 
			
		||||
            ).setBadgeItem(tabNewBadge)
 | 
			
		||||
        val tabArchive =
 | 
			
		||||
            BottomNavigationItem(
 | 
			
		||||
                R.drawable.ic_tab_archive_black_24dp,
 | 
			
		||||
                getString(R.string.tab_read),
 | 
			
		||||
            )
 | 
			
		||||
                .setBadgeItem(tabArchiveBadge)
 | 
			
		||||
            ).setBadgeItem(tabArchiveBadge)
 | 
			
		||||
        val tabStarred =
 | 
			
		||||
            BottomNavigationItem(
 | 
			
		||||
                R.drawable.ic_tab_favorite_black_24dp,
 | 
			
		||||
@@ -277,7 +285,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
 | 
			
		||||
        handleBottomBarActions()
 | 
			
		||||
 | 
			
		||||
        handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
 | 
			
		||||
        handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
 | 
			
		||||
 | 
			
		||||
        handleRecurringTask()
 | 
			
		||||
        CountingIdlingResourceSingleton.increment()
 | 
			
		||||
@@ -289,10 +297,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        getElementsAccordingToTab()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleGDPRDialog(GDPRShown: Boolean) {
 | 
			
		||||
    private fun handleGdprDialog(gdprShown: Boolean) {
 | 
			
		||||
        val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
 | 
			
		||||
        messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
 | 
			
		||||
        if (!GDPRShown) {
 | 
			
		||||
        if (!gdprShown) {
 | 
			
		||||
            val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
            alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
 | 
			
		||||
            alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
 | 
			
		||||
@@ -425,17 +433,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getLastVisibleItem(): Int {
 | 
			
		||||
        return when (val manager = binding.recyclerView.layoutManager) {
 | 
			
		||||
    private fun getLastVisibleItem(): Int =
 | 
			
		||||
        when (val manager = binding.recyclerView.layoutManager) {
 | 
			
		||||
            is StaggeredGridLayoutManager ->
 | 
			
		||||
                manager.findLastCompletelyVisibleItemPositions(
 | 
			
		||||
                    null,
 | 
			
		||||
                ).last()
 | 
			
		||||
                manager
 | 
			
		||||
                    .findLastCompletelyVisibleItemPositions(
 | 
			
		||||
                        null,
 | 
			
		||||
                    ).last()
 | 
			
		||||
 | 
			
		||||
            is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
 | 
			
		||||
            else -> 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun mayBeEmpty() =
 | 
			
		||||
        if (items.isEmpty()) {
 | 
			
		||||
@@ -538,7 +546,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
    private fun calculateNoOfColumns(): Int {
 | 
			
		||||
        val displayMetrics = resources.displayMetrics
 | 
			
		||||
        val dpWidth = displayMetrics.widthPixels / displayMetrics.density
 | 
			
		||||
        return (dpWidth / 300).toInt()
 | 
			
		||||
        return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onQueryTextChange(p0: String?): Boolean {
 | 
			
		||||
@@ -577,7 +585,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
        messageRes: Int,
 | 
			
		||||
        doFn: () -> Unit,
 | 
			
		||||
    ) {
 | 
			
		||||
        AlertDialog.Builder(this@HomeActivity)
 | 
			
		||||
        AlertDialog
 | 
			
		||||
            .Builder(this@HomeActivity)
 | 
			
		||||
            .setMessage(messageRes)
 | 
			
		||||
            .setTitle(titleRes)
 | 
			
		||||
            .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
 | 
			
		||||
@@ -586,10 +595,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
            .show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:ReturnCount", "detekt:LongMethod")
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.issue_tracker -> {
 | 
			
		||||
                baseContext.openUrlInBrowser(AppSettingsService.trackerUrl)
 | 
			
		||||
                baseContext.openUrlInBrowser(AppSettingsService.BUG_URL)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -606,18 +616,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                    CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                        val updatedRemote = repository.updateRemote()
 | 
			
		||||
                        if (updatedRemote) {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                this@HomeActivity,
 | 
			
		||||
                                R.string.refresh_success_response,
 | 
			
		||||
                                Toast.LENGTH_LONG,
 | 
			
		||||
                            )
 | 
			
		||||
                                .show()
 | 
			
		||||
                            Toast
 | 
			
		||||
                                .makeText(
 | 
			
		||||
                                    this@HomeActivity,
 | 
			
		||||
                                    R.string.refresh_success_response,
 | 
			
		||||
                                    Toast.LENGTH_LONG,
 | 
			
		||||
                                ).show()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                this@HomeActivity,
 | 
			
		||||
                                R.string.refresh_failer_message,
 | 
			
		||||
                                Toast.LENGTH_SHORT,
 | 
			
		||||
                            ).show()
 | 
			
		||||
                            Toast
 | 
			
		||||
                                .makeText(
 | 
			
		||||
                                    this@HomeActivity,
 | 
			
		||||
                                    R.string.refresh_failer_message,
 | 
			
		||||
                                    Toast.LENGTH_SHORT,
 | 
			
		||||
                                ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                        CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
                    }
 | 
			
		||||
@@ -633,25 +644,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
                        CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                            val success = repository.markAllAsRead(items)
 | 
			
		||||
                            if (success) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    this@HomeActivity,
 | 
			
		||||
                                    R.string.all_posts_read,
 | 
			
		||||
                                    Toast.LENGTH_SHORT,
 | 
			
		||||
                                ).show()
 | 
			
		||||
                                Toast
 | 
			
		||||
                                    .makeText(
 | 
			
		||||
                                        this@HomeActivity,
 | 
			
		||||
                                        R.string.all_posts_read,
 | 
			
		||||
                                        Toast.LENGTH_SHORT,
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                                tabNewBadge.removeBadge()
 | 
			
		||||
 | 
			
		||||
                                getElementsAccordingToTab()
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    this@HomeActivity,
 | 
			
		||||
                                    R.string.all_posts_not_read,
 | 
			
		||||
                                    Toast.LENGTH_SHORT,
 | 
			
		||||
                                ).show()
 | 
			
		||||
                                Toast
 | 
			
		||||
                                    .makeText(
 | 
			
		||||
                                        this@HomeActivity,
 | 
			
		||||
                                        R.string.all_posts_not_read,
 | 
			
		||||
                                        Toast.LENGTH_SHORT,
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                            handleListResult()
 | 
			
		||||
                            binding.swipeRefreshLayout.isRefreshing = false
 | 
			
		||||
                            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
@@ -661,7 +673,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
            R.id.action_disconnect -> {
 | 
			
		||||
                needsConfirmation(
 | 
			
		||||
                    R.string.confirm_disconnect_title,
 | 
			
		||||
                    R.string.confirm_disconnect_description
 | 
			
		||||
                    R.string.confirm_disconnect_description,
 | 
			
		||||
                ) {
 | 
			
		||||
                    runBlocking {
 | 
			
		||||
                        repository.logout()
 | 
			
		||||
@@ -702,7 +714,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
    private fun handleRecurringTask() {
 | 
			
		||||
        if (appSettingsService.isPeriodicRefreshEnabled()) {
 | 
			
		||||
            val myConstraints =
 | 
			
		||||
                Constraints.Builder()
 | 
			
		||||
                Constraints
 | 
			
		||||
                    .Builder()
 | 
			
		||||
                    .setRequiresBatteryNotLow(true)
 | 
			
		||||
                    .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
 | 
			
		||||
                    .setRequiresStorageNotLow(true)
 | 
			
		||||
@@ -711,19 +724,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
 | 
			
		||||
            val backgroundWork =
 | 
			
		||||
                PeriodicWorkRequestBuilder<LoadingWorker>(
 | 
			
		||||
                    appSettingsService.getRefreshMinutes(),
 | 
			
		||||
                    TimeUnit.MINUTES
 | 
			
		||||
                )
 | 
			
		||||
                    .setConstraints(myConstraints)
 | 
			
		||||
                    TimeUnit.MINUTES,
 | 
			
		||||
                ).setConstraints(myConstraints)
 | 
			
		||||
                    .addTag("selfoss-loading")
 | 
			
		||||
                    .build()
 | 
			
		||||
 | 
			
		||||
            WorkManager.getInstance(
 | 
			
		||||
                baseContext,
 | 
			
		||||
            ).enqueueUniquePeriodicWork(
 | 
			
		||||
                "selfoss-loading",
 | 
			
		||||
                ExistingPeriodicWorkPolicy.KEEP,
 | 
			
		||||
                backgroundWork
 | 
			
		||||
            )
 | 
			
		||||
            WorkManager
 | 
			
		||||
                .getInstance(
 | 
			
		||||
                    baseContext,
 | 
			
		||||
                ).enqueueUniquePeriodicWork(
 | 
			
		||||
                    "selfoss-loading",
 | 
			
		||||
                    ExistingPeriodicWorkPolicy.KEEP,
 | 
			
		||||
                    backgroundWork,
 | 
			
		||||
                )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,9 @@ class ImageActivity : AppCompatActivity() {
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
 | 
			
		||||
    private inner class ScreenSlidePagerAdapter(
 | 
			
		||||
        fa: FragmentActivity,
 | 
			
		||||
    ) : FragmentStateAdapter(fa) {
 | 
			
		||||
        override fun getItemCount(): Int = allImages.size
 | 
			
		||||
 | 
			
		||||
        override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,11 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
 | 
			
		||||
 | 
			
		||||
class LoginActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private var inValidCount: Int = 0
 | 
			
		||||
    private var isWithLogin = false
 | 
			
		||||
 | 
			
		||||
@@ -108,7 +112,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            repository.updateApiInformation()
 | 
			
		||||
            ACRA.errorReporter.putCustomData(
 | 
			
		||||
                "SELFOSS_API_VERSION",
 | 
			
		||||
                appSettingsService.getApiVersion().toString()
 | 
			
		||||
                appSettingsService.getApiVersion().toString(),
 | 
			
		||||
            )
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
@@ -132,9 +136,18 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        binding.passwordView.error = null
 | 
			
		||||
 | 
			
		||||
        // Store values at the time of the login attempt.
 | 
			
		||||
        val url = binding.urlView.text.toString().trim()
 | 
			
		||||
        val login = binding.loginView.text.toString().trim()
 | 
			
		||||
        val password = binding.passwordView.text.toString().trim()
 | 
			
		||||
        val url =
 | 
			
		||||
            binding.urlView.text
 | 
			
		||||
                .toString()
 | 
			
		||||
                .trim()
 | 
			
		||||
        val login =
 | 
			
		||||
            binding.loginView.text
 | 
			
		||||
                .toString()
 | 
			
		||||
                .trim()
 | 
			
		||||
        val password =
 | 
			
		||||
            binding.passwordView.text
 | 
			
		||||
                .toString()
 | 
			
		||||
                .trim()
 | 
			
		||||
 | 
			
		||||
        failInvalidUrl(url)
 | 
			
		||||
        failLoginDetails(password, login)
 | 
			
		||||
@@ -151,11 +164,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                repository.updateApiInformation()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                if (e.message?.startsWith("No transformation found") == true) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        applicationContext,
 | 
			
		||||
                        R.string.application_selfoss_only,
 | 
			
		||||
                        Toast.LENGTH_LONG,
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    Toast
 | 
			
		||||
                        .makeText(
 | 
			
		||||
                            applicationContext,
 | 
			
		||||
                            R.string.application_selfoss_only,
 | 
			
		||||
                            Toast.LENGTH_LONG,
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    preferenceError()
 | 
			
		||||
                    showProgress(false)
 | 
			
		||||
                }
 | 
			
		||||
@@ -205,7 +219,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            cancel = true
 | 
			
		||||
            binding.urlView.error = getString(R.string.login_url_problem)
 | 
			
		||||
            inValidCount++
 | 
			
		||||
            if (inValidCount == 3) {
 | 
			
		||||
            if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
 | 
			
		||||
                val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
                alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
			
		||||
                alertDialog.setMessage(getString(R.string.text_wrong_url))
 | 
			
		||||
@@ -270,7 +284,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        return when (item.itemId) {
 | 
			
		||||
            R.id.issue_tracker -> {
 | 
			
		||||
                val browserIntent =
 | 
			
		||||
                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
 | 
			
		||||
                    Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL))
 | 
			
		||||
                startActivity(browserIntent)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
@@ -280,9 +294,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                    .withAboutIconShown(true)
 | 
			
		||||
                    .withAboutVersionShown(true)
 | 
			
		||||
                    .withAboutSpecial2("Bug reports")
 | 
			
		||||
                    .withAboutSpecial2Description(AppSettingsService.trackerUrl)
 | 
			
		||||
                    .withAboutSpecial2Description(AppSettingsService.BUG_URL)
 | 
			
		||||
                    .withAboutSpecial1("Project Page")
 | 
			
		||||
                    .withAboutSpecial1Description(AppSettingsService.sourceUrl)
 | 
			
		||||
                    .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
 | 
			
		||||
                    .start(this)
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
@@ -290,4 +304,4 @@ class LoginActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            else -> super.onOptionsItemSelected(item)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,11 +9,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner
 | 
			
		||||
import androidx.lifecycle.ProcessLifecycleOwner
 | 
			
		||||
import androidx.multidex.MultiDexApplication
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.di.networkModule
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.repository.Repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import com.github.ln_12.library.ConnectivityStatus
 | 
			
		||||
@@ -36,21 +36,23 @@ import org.kodein.di.bind
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import org.kodein.di.singleton
 | 
			
		||||
 | 
			
		||||
class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
class MyApp :
 | 
			
		||||
    MultiDexApplication(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    override val di by DI.lazy {
 | 
			
		||||
        bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
 | 
			
		||||
        import(networkModule)
 | 
			
		||||
        bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
 | 
			
		||||
        bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
 | 
			
		||||
        bind<Repository>() with
 | 
			
		||||
                singleton {
 | 
			
		||||
                    Repository(
 | 
			
		||||
                        instance(),
 | 
			
		||||
                        instance(),
 | 
			
		||||
                        isConnectionAvailable,
 | 
			
		||||
                        instance(),
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            singleton {
 | 
			
		||||
                Repository(
 | 
			
		||||
                    instance(),
 | 
			
		||||
                    instance(),
 | 
			
		||||
                    isConnectionAvailable,
 | 
			
		||||
                    instance(),
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
 | 
			
		||||
        bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
 | 
			
		||||
    }
 | 
			
		||||
@@ -60,6 +62,7 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
    private val connectivityStatus: ConnectivityStatus by instance()
 | 
			
		||||
    private val driverFactory: DriverFactory by instance()
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:ForbiddenComment")
 | 
			
		||||
    // TODO: handle with the "previous" way
 | 
			
		||||
    private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
 | 
			
		||||
 | 
			
		||||
@@ -89,11 +92,12 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
                            R.string.network_connectivity_lost
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        applicationContext,
 | 
			
		||||
                        toastMessage,
 | 
			
		||||
                        Toast.LENGTH_SHORT,
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    Toast
 | 
			
		||||
                        .makeText(
 | 
			
		||||
                            applicationContext,
 | 
			
		||||
                            toastMessage,
 | 
			
		||||
                            Toast.LENGTH_SHORT,
 | 
			
		||||
                        ).show()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -151,13 +155,13 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
 | 
			
		||||
            val name = getString(R.string.notification_channel_sync)
 | 
			
		||||
            val importance = NotificationManager.IMPORTANCE_LOW
 | 
			
		||||
            val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
 | 
			
		||||
            val mChannel = NotificationChannel(AppSettingsService.SYNC_CHANNEL_ID, name, importance)
 | 
			
		||||
 | 
			
		||||
            val newItemsChannelname = getString(R.string.new_items_channel_sync)
 | 
			
		||||
            val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
 | 
			
		||||
            val newItemsChannelmChannel =
 | 
			
		||||
                NotificationChannel(
 | 
			
		||||
                    AppSettingsService.newItemsChannelId,
 | 
			
		||||
                    AppSettingsService.NEW_ITEMS_CHANNEL,
 | 
			
		||||
                    newItemsChannelname,
 | 
			
		||||
                    newItemsChannelimportance,
 | 
			
		||||
                )
 | 
			
		||||
@@ -199,4 +203,4 @@ class MyApp : MultiDexApplication(), DIAware {
 | 
			
		||||
            super.onPause(owner)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,9 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
class ReaderActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private var currentItem: Int = 0
 | 
			
		||||
 | 
			
		||||
    private lateinit var toolbarMenu: Menu
 | 
			
		||||
@@ -51,6 +53,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        showMenuItem(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        binding = ActivityReaderBinding.inflate(layoutInflater)
 | 
			
		||||
@@ -99,8 +102,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        oldInstanceState.clear()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
 | 
			
		||||
        FragmentStateAdapter(fa) {
 | 
			
		||||
    private inner class ScreenSlidePagerAdapter(
 | 
			
		||||
        fa: FragmentActivity,
 | 
			
		||||
    ) : FragmentStateAdapter(fa) {
 | 
			
		||||
        override fun getItemCount(): Int = allItems.size
 | 
			
		||||
 | 
			
		||||
        override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
 | 
			
		||||
@@ -109,25 +113,26 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
    override fun onKeyDown(
 | 
			
		||||
        keyCode: Int,
 | 
			
		||||
        event: KeyEvent?,
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        return when (keyCode) {
 | 
			
		||||
    ): Boolean =
 | 
			
		||||
        when (keyCode) {
 | 
			
		||||
            KeyEvent.KEYCODE_VOLUME_DOWN -> {
 | 
			
		||||
                val currentFragment =
 | 
			
		||||
                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
			
		||||
                currentFragment.scrollDown()
 | 
			
		||||
                currentFragment.volumeButtonScrollDown()
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            KeyEvent.KEYCODE_VOLUME_UP -> {
 | 
			
		||||
                val currentFragment =
 | 
			
		||||
                    supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
 | 
			
		||||
                currentFragment.scrollUp()
 | 
			
		||||
                currentFragment.volumeButtonScrollUp()
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> {
 | 
			
		||||
                super.onKeyDown(keyCode, event)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun alignmentMenu() {
 | 
			
		||||
        val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
 | 
			
		||||
@@ -187,6 +192,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                onBackPressedDispatcher.onBackPressed()
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.star -> {
 | 
			
		||||
                if (allItems[binding.pager.currentItem].starred) {
 | 
			
		||||
                    CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
@@ -200,10 +206,12 @@ class ReaderActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                    afterSave()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.align_left -> {
 | 
			
		||||
                switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            R.id.align_justify -> {
 | 
			
		||||
                switchAlignmentSetting(AppSettingsService.JUSTIFY)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,9 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
class SourcesActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private lateinit var binding: ActivitySourcesBinding
 | 
			
		||||
 | 
			
		||||
    override val di by closestDI()
 | 
			
		||||
@@ -68,11 +70,12 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                binding.recyclerView.adapter = mAdapter
 | 
			
		||||
                mAdapter.notifyDataSetChanged()
 | 
			
		||||
            } else {
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    this@SourcesActivity,
 | 
			
		||||
                    R.string.cant_get_sources,
 | 
			
		||||
                    Toast.LENGTH_SHORT,
 | 
			
		||||
                ).show()
 | 
			
		||||
                Toast
 | 
			
		||||
                    .makeText(
 | 
			
		||||
                        this@SourcesActivity,
 | 
			
		||||
                        R.string.cant_get_sources,
 | 
			
		||||
                        Toast.LENGTH_SHORT,
 | 
			
		||||
                    ).show()
 | 
			
		||||
            }
 | 
			
		||||
            CountingIdlingResourceSingleton.decrement()
 | 
			
		||||
        }
 | 
			
		||||
@@ -81,4 +84,4 @@ class SourcesActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,9 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
class UpsertSourceActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private var existingSource: SelfossModel.SourceDetail? = null
 | 
			
		||||
    private var mSpoutsValue: String? = null
 | 
			
		||||
 | 
			
		||||
@@ -83,6 +85,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    private fun handleSpoutsSpinner() {
 | 
			
		||||
        val spoutsKV = HashMap<String, String>()
 | 
			
		||||
        binding.spoutsSpinner.onItemSelectedListener =
 | 
			
		||||
@@ -105,11 +108,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        fun handleSpoutFailure(networkIssue: Boolean = false) {
 | 
			
		||||
            Toast.makeText(
 | 
			
		||||
                this@UpsertSourceActivity,
 | 
			
		||||
                if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
 | 
			
		||||
                Toast.LENGTH_SHORT,
 | 
			
		||||
            ).show()
 | 
			
		||||
            Toast
 | 
			
		||||
                .makeText(
 | 
			
		||||
                    this@UpsertSourceActivity,
 | 
			
		||||
                    if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
 | 
			
		||||
                    Toast.LENGTH_SHORT,
 | 
			
		||||
                ).show()
 | 
			
		||||
            binding.progress.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -170,6 +174,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
            sourceDetailsUnavailable -> {
 | 
			
		||||
                Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> {
 | 
			
		||||
                CoroutineScope(Dispatchers.Main).launch {
 | 
			
		||||
                    val successfullyAddedSource =
 | 
			
		||||
@@ -192,11 +197,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
 | 
			
		||||
                    if (successfullyAddedSource) {
 | 
			
		||||
                        finish()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@UpsertSourceActivity,
 | 
			
		||||
                            R.string.cant_create_source,
 | 
			
		||||
                            Toast.LENGTH_SHORT,
 | 
			
		||||
                        ).show()
 | 
			
		||||
                        Toast
 | 
			
		||||
                            .makeText(
 | 
			
		||||
                                this@UpsertSourceActivity,
 | 
			
		||||
                                R.string.cant_create_source,
 | 
			
		||||
                                Toast.LENGTH_SHORT,
 | 
			
		||||
                            ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,10 @@ class ItemCardAdapter(
 | 
			
		||||
        return ViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
 | 
			
		||||
    private fun handleClickListeners(
 | 
			
		||||
        holderBinding: CardItemBinding,
 | 
			
		||||
        position: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        holderBinding.favButton.setOnClickListener {
 | 
			
		||||
            val item = items[position]
 | 
			
		||||
            if (item.starred) {
 | 
			
		||||
@@ -96,12 +99,13 @@ class ItemCardAdapter(
 | 
			
		||||
 | 
			
		||||
            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
			
		||||
 | 
			
		||||
            binding.sourceTitleAndDate.text = try {
 | 
			
		||||
                itm.sourceAuthorAndDate()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
 | 
			
		||||
                itm.sourceAuthorOnly()
 | 
			
		||||
            }
 | 
			
		||||
            binding.sourceTitleAndDate.text =
 | 
			
		||||
                try {
 | 
			
		||||
                    itm.sourceAuthorAndDate()
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
 | 
			
		||||
                    itm.sourceAuthorOnly()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if (!appSettingsService.isFullHeightCardsEnabled()) {
 | 
			
		||||
                binding.itemImage.maxHeight = imageMaxHeight
 | 
			
		||||
@@ -125,5 +129,7 @@ class ItemCardAdapter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
    inner class ViewHolder(
 | 
			
		||||
        val binding: CardItemBinding,
 | 
			
		||||
    ) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -53,12 +53,13 @@ class ItemListAdapter(
 | 
			
		||||
 | 
			
		||||
            binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
 | 
			
		||||
 | 
			
		||||
            binding.sourceTitleAndDate.text = try {
 | 
			
		||||
                itm.sourceAuthorAndDate()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
 | 
			
		||||
                itm.sourceAuthorOnly()
 | 
			
		||||
            }
 | 
			
		||||
            binding.sourceTitleAndDate.text =
 | 
			
		||||
                try {
 | 
			
		||||
                    itm.sourceAuthorAndDate()
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
 | 
			
		||||
                    itm.sourceAuthorOnly()
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
 | 
			
		||||
                if (itm.getIcon(repository.baseUrl).isEmpty()) {
 | 
			
		||||
@@ -72,5 +73,7 @@ class ItemListAdapter(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
    inner class ViewHolder(
 | 
			
		||||
        val binding: ListItemBinding,
 | 
			
		||||
    ) : RecyclerView.ViewHolder(binding.root)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,9 @@ import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
 | 
			
		||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
 | 
			
		||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
 | 
			
		||||
    RecyclerView.Adapter<VH>(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    abstract val items: ArrayList<SelfossModel.Item>
 | 
			
		||||
    abstract val repository: Repository
 | 
			
		||||
    abstract val binding: ViewBinding
 | 
			
		||||
@@ -45,8 +47,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
                    app.findViewById(R.id.coordLayout),
 | 
			
		||||
                    R.string.marked_as_read,
 | 
			
		||||
                    Snackbar.LENGTH_LONG,
 | 
			
		||||
                )
 | 
			
		||||
                .setAction(R.string.undo_string) {
 | 
			
		||||
                ).setAction(R.string.undo_string) {
 | 
			
		||||
                    unreadItemAtIndex(item, position, false)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -66,8 +67,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
                    app.findViewById(R.id.coordLayout),
 | 
			
		||||
                    R.string.marked_as_unread,
 | 
			
		||||
                    Snackbar.LENGTH_LONG,
 | 
			
		||||
                )
 | 
			
		||||
                .setAction(R.string.undo_string) {
 | 
			
		||||
                ).setAction(R.string.undo_string) {
 | 
			
		||||
                    readItemAtIndex(item, position, false)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -77,7 +77,10 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
 | 
			
		||||
    protected fun handleLinkOpening(
 | 
			
		||||
        holderBinding: ViewBinding,
 | 
			
		||||
        position: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        holderBinding.root.setOnClickListener {
 | 
			
		||||
            repository.setReaderItems(items)
 | 
			
		||||
            c.openItemUrl(
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,8 @@ import org.kodein.di.instance
 | 
			
		||||
class SourcesListAdapter(
 | 
			
		||||
    private val app: Activity,
 | 
			
		||||
    private val items: ArrayList<SelfossModel.SourceDetail>,
 | 
			
		||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
 | 
			
		||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private lateinit var binding: SourceListItemBinding
 | 
			
		||||
 | 
			
		||||
@@ -61,11 +62,12 @@ class SourcesListAdapter(
 | 
			
		||||
                    notifyItemRemoved(position)
 | 
			
		||||
                    notifyItemRangeChanged(position, itemCount)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        app,
 | 
			
		||||
                        R.string.can_delete_source,
 | 
			
		||||
                        Toast.LENGTH_SHORT,
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    Toast
 | 
			
		||||
                        .makeText(
 | 
			
		||||
                            app,
 | 
			
		||||
                            R.string.can_delete_source,
 | 
			
		||||
                            Toast.LENGTH_SHORT,
 | 
			
		||||
                        ).show()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -99,5 +101,7 @@ class SourcesListAdapter(
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
 | 
			
		||||
    inner class ViewHolder(
 | 
			
		||||
        val mView: ConstraintLayout,
 | 
			
		||||
    ) : RecyclerView.ViewHolder(mView)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,15 @@ import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.Timer
 | 
			
		||||
import kotlin.concurrent.schedule
 | 
			
		||||
 | 
			
		||||
class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
			
		||||
    Worker(context, params),
 | 
			
		||||
private const val NOTIFICATION_DELAY = 4000L
 | 
			
		||||
 | 
			
		||||
class LoadingWorker(
 | 
			
		||||
    val context: Context,
 | 
			
		||||
    params: WorkerParameters,
 | 
			
		||||
) : Worker(context, params),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    override val di by lazy { (applicationContext as MyApp).di }
 | 
			
		||||
    private val repository: Repository by instance()
 | 
			
		||||
@@ -40,12 +44,13 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
			
		||||
                    applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
			
		||||
 | 
			
		||||
                val notification =
 | 
			
		||||
                    NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
 | 
			
		||||
                    NotificationCompat
 | 
			
		||||
                        .Builder(applicationContext, AppSettingsService.SYNC_CHANNEL_ID)
 | 
			
		||||
                        .setContentTitle(context.getString(R.string.loading_notification_title))
 | 
			
		||||
                        .setContentText(context.getString(R.string.loading_notification_text))
 | 
			
		||||
                        .setOngoing(true)
 | 
			
		||||
                        .setPriority(PRIORITY_LOW)
 | 
			
		||||
                        .setChannelId(AppSettingsService.syncChannelId)
 | 
			
		||||
                        .setChannelId(AppSettingsService.SYNC_CHANNEL_ID)
 | 
			
		||||
                        .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
 | 
			
		||||
 | 
			
		||||
                notificationManager.notify(1, notification.build())
 | 
			
		||||
@@ -87,28 +92,27 @@ class LoadingWorker(val context: Context, params: WorkerParameters) :
 | 
			
		||||
                    PendingIntent.getActivity(context, 0, intent, pflags)
 | 
			
		||||
 | 
			
		||||
                val newItemsNotification =
 | 
			
		||||
                    NotificationCompat.Builder(
 | 
			
		||||
                        applicationContext,
 | 
			
		||||
                        AppSettingsService.newItemsChannelId,
 | 
			
		||||
                    )
 | 
			
		||||
                        .setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
			
		||||
                    NotificationCompat
 | 
			
		||||
                        .Builder(
 | 
			
		||||
                            applicationContext,
 | 
			
		||||
                            AppSettingsService.NEW_ITEMS_CHANNEL,
 | 
			
		||||
                        ).setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
			
		||||
                        .setContentText(
 | 
			
		||||
                            context.getString(
 | 
			
		||||
                                R.string.new_items_notification_text,
 | 
			
		||||
                                newSize,
 | 
			
		||||
                            ),
 | 
			
		||||
                        )
 | 
			
		||||
                        .setPriority(PRIORITY_DEFAULT)
 | 
			
		||||
                        .setChannelId(AppSettingsService.newItemsChannelId)
 | 
			
		||||
                        ).setPriority(PRIORITY_DEFAULT)
 | 
			
		||||
                        .setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL)
 | 
			
		||||
                        .setContentIntent(pendingIntent)
 | 
			
		||||
                        .setAutoCancel(true)
 | 
			
		||||
                        .setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
 | 
			
		||||
 | 
			
		||||
                Timer("", false).schedule(4000) {
 | 
			
		||||
                Timer("", false).schedule(NOTIFICATION_DELAY) {
 | 
			
		||||
                    notificationManager.notify(2, newItemsNotification.build())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Timer("", false).schedule(4000) {
 | 
			
		||||
            Timer("", false).schedule(NOTIFICATION_DELAY) {
 | 
			
		||||
                notificationManager.cancel(1)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.android.fragments
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.content.res.TypedArray
 | 
			
		||||
@@ -8,6 +9,7 @@ import android.graphics.Typeface
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.util.TypedValue.DATA_NULL_UNDEFINED
 | 
			
		||||
import android.view.GestureDetector
 | 
			
		||||
import android.view.InflateException
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
@@ -64,8 +66,14 @@ import java.util.concurrent.ExecutionException
 | 
			
		||||
 | 
			
		||||
private const val IMAGE_JPG = "image/jpg"
 | 
			
		||||
 | 
			
		||||
class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
    private var fontSize: Int = 16
 | 
			
		||||
private const val WHITE_COLOR_HEX = 0xFFFFFF
 | 
			
		||||
 | 
			
		||||
private const val DEFAULT_FONT_SIZE = 16
 | 
			
		||||
 | 
			
		||||
class ArticleFragment :
 | 
			
		||||
    Fragment(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private var fontSize: Int = DEFAULT_FONT_SIZE
 | 
			
		||||
    private lateinit var item: SelfossModel.Item
 | 
			
		||||
    private lateinit var url: String
 | 
			
		||||
    private lateinit var contentText: String
 | 
			
		||||
@@ -96,6 +104,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        item = pi.toModel()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod")
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
@@ -113,12 +122,13 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
            contentText = item.content
 | 
			
		||||
            contentTitle = item.title.getHtmlDecoded()
 | 
			
		||||
            contentImage = item.getThumbnail(repository.baseUrl)
 | 
			
		||||
            contentSource = try {
 | 
			
		||||
                item.sourceAuthorAndDate()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("Article Fragment parse date")
 | 
			
		||||
                item.sourceAuthorOnly()
 | 
			
		||||
            }
 | 
			
		||||
            contentSource =
 | 
			
		||||
                try {
 | 
			
		||||
                    item.sourceAuthorAndDate()
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("Article Fragment parse date")
 | 
			
		||||
                    item.sourceAuthorOnly()
 | 
			
		||||
                }
 | 
			
		||||
            allImages = item.getImages()
 | 
			
		||||
 | 
			
		||||
            fontSize = appSettingsService.getFontSize()
 | 
			
		||||
@@ -164,7 +174,8 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        } catch (e: InflateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("webview not available")
 | 
			
		||||
            try {
 | 
			
		||||
                AlertDialog.Builder(requireContext())
 | 
			
		||||
                AlertDialog
 | 
			
		||||
                    .Builder(requireContext())
 | 
			
		||||
                    .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
 | 
			
		||||
                    .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
 | 
			
		||||
                    .setPositiveButton(
 | 
			
		||||
@@ -172,8 +183,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                    ) { _, _ ->
 | 
			
		||||
                        appSettingsService.disableArticleViewer()
 | 
			
		||||
                        requireActivity().finish()
 | 
			
		||||
                    }
 | 
			
		||||
                    .create()
 | 
			
		||||
                    }.create()
 | 
			
		||||
                    .show()
 | 
			
		||||
            } catch (e: IllegalStateException) {
 | 
			
		||||
                e.sendSilentlyWithAcraWithName("Context required is null")
 | 
			
		||||
@@ -232,21 +242,23 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                                        repository.markAsRead(this@ArticleFragment.item)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    this@ArticleFragment.item.unread = false
 | 
			
		||||
                                    Toast.makeText(
 | 
			
		||||
                                        requireContext(),
 | 
			
		||||
                                        R.string.marked_as_read,
 | 
			
		||||
                                        Toast.LENGTH_LONG,
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                                    Toast
 | 
			
		||||
                                        .makeText(
 | 
			
		||||
                                            requireContext(),
 | 
			
		||||
                                            R.string.marked_as_read,
 | 
			
		||||
                                            Toast.LENGTH_LONG,
 | 
			
		||||
                                        ).show()
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    CoroutineScope(Dispatchers.IO).launch {
 | 
			
		||||
                                        repository.unmarkAsRead(this@ArticleFragment.item)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    this@ArticleFragment.item.unread = true
 | 
			
		||||
                                    Toast.makeText(
 | 
			
		||||
                                        context,
 | 
			
		||||
                                        R.string.marked_as_unread,
 | 
			
		||||
                                        Toast.LENGTH_LONG,
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                                    Toast
 | 
			
		||||
                                        .makeText(
 | 
			
		||||
                                            context,
 | 
			
		||||
                                            R.string.marked_as_unread,
 | 
			
		||||
                                            Toast.LENGTH_LONG,
 | 
			
		||||
                                        ).show()
 | 
			
		||||
                                }
 | 
			
		||||
                            } catch (e: IllegalStateException) {
 | 
			
		||||
                                e.sendSilentlyWithAcraWithName("Context required is null")
 | 
			
		||||
@@ -273,6 +285,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    private fun getContentFromMercury() {
 | 
			
		||||
        binding.progressBar.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
@@ -311,16 +324,15 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleLeadImage(lead_image_url: String?) {
 | 
			
		||||
        if (!lead_image_url.isNullOrEmpty() && context != null) {
 | 
			
		||||
    private fun handleLeadImage(leadImageUrl: String?) {
 | 
			
		||||
        if (!leadImageUrl.isNullOrEmpty() && context != null) {
 | 
			
		||||
            binding.imageView.visibility = View.VISIBLE
 | 
			
		||||
            Glide
 | 
			
		||||
                .with(requireContext())
 | 
			
		||||
                .asBitmap()
 | 
			
		||||
                .load(
 | 
			
		||||
                    lead_image_url,
 | 
			
		||||
                )
 | 
			
		||||
                .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                    leadImageUrl,
 | 
			
		||||
                ).apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                .into(binding.imageView)
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.imageView.visibility = View.GONE
 | 
			
		||||
@@ -334,84 +346,133 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                override fun shouldOverrideUrlLoading(
 | 
			
		||||
                    view: WebView?,
 | 
			
		||||
                    url: String,
 | 
			
		||||
                ): Boolean {
 | 
			
		||||
                    return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
 | 
			
		||||
                ): Boolean =
 | 
			
		||||
                    if (context != null &&
 | 
			
		||||
                        url.isUrlValid() &&
 | 
			
		||||
                        binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
			
		||||
                    ) {
 | 
			
		||||
                        requireContext().openUrlInBrowser(url)
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Suppress("detekt:LongMethod", "detekt:SwallowedException")
 | 
			
		||||
                @Deprecated("Deprecated in Java")
 | 
			
		||||
                override fun shouldInterceptRequest(
 | 
			
		||||
                    view: WebView,
 | 
			
		||||
                    url: String,
 | 
			
		||||
                ): WebResourceResponse? {
 | 
			
		||||
                    val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
 | 
			
		||||
                    var glideResource: WebResourceResponse? = null
 | 
			
		||||
                    if (url.lowercase(Locale.US).contains(".jpg") ||
 | 
			
		||||
                        url.lowercase(Locale.US)
 | 
			
		||||
                        url
 | 
			
		||||
                            .lowercase(Locale.US)
 | 
			
		||||
                            .contains(".jpeg")
 | 
			
		||||
                    ) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            val image =
 | 
			
		||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
			
		||||
                                Glide
 | 
			
		||||
                                    .with(view)
 | 
			
		||||
                                    .asBitmap()
 | 
			
		||||
                                    .apply(glideOptions)
 | 
			
		||||
                                    .load(url)
 | 
			
		||||
                                    .submit()
 | 
			
		||||
                                    .get()
 | 
			
		||||
                            return WebResourceResponse(
 | 
			
		||||
                                IMAGE_JPG,
 | 
			
		||||
                                "UTF-8",
 | 
			
		||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
 | 
			
		||||
                            )
 | 
			
		||||
                            glideResource =
 | 
			
		||||
                                WebResourceResponse(
 | 
			
		||||
                                    IMAGE_JPG,
 | 
			
		||||
                                    "UTF-8",
 | 
			
		||||
                                    getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
 | 
			
		||||
                                )
 | 
			
		||||
                        } catch (e: ExecutionException) {
 | 
			
		||||
                            // Do nothing
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (url.lowercase(Locale.US).contains(".png")) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            val image =
 | 
			
		||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
			
		||||
                                Glide
 | 
			
		||||
                                    .with(view)
 | 
			
		||||
                                    .asBitmap()
 | 
			
		||||
                                    .apply(glideOptions)
 | 
			
		||||
                                    .load(url)
 | 
			
		||||
                                    .submit()
 | 
			
		||||
                                    .get()
 | 
			
		||||
                            return WebResourceResponse(
 | 
			
		||||
                                IMAGE_JPG,
 | 
			
		||||
                                "UTF-8",
 | 
			
		||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
 | 
			
		||||
                            )
 | 
			
		||||
                            glideResource =
 | 
			
		||||
                                WebResourceResponse(
 | 
			
		||||
                                    IMAGE_JPG,
 | 
			
		||||
                                    "UTF-8",
 | 
			
		||||
                                    getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
 | 
			
		||||
                                )
 | 
			
		||||
                        } catch (e: ExecutionException) {
 | 
			
		||||
                            // Do nothing
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (url.lowercase(Locale.US).contains(".webp")) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            val image =
 | 
			
		||||
                                Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
 | 
			
		||||
                                Glide
 | 
			
		||||
                                    .with(view)
 | 
			
		||||
                                    .asBitmap()
 | 
			
		||||
                                    .apply(glideOptions)
 | 
			
		||||
                                    .load(url)
 | 
			
		||||
                                    .submit()
 | 
			
		||||
                                    .get()
 | 
			
		||||
                            return WebResourceResponse(
 | 
			
		||||
                                IMAGE_JPG,
 | 
			
		||||
                                "UTF-8",
 | 
			
		||||
                                getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
 | 
			
		||||
                            )
 | 
			
		||||
                            glideResource =
 | 
			
		||||
                                WebResourceResponse(
 | 
			
		||||
                                    IMAGE_JPG,
 | 
			
		||||
                                    "UTF-8",
 | 
			
		||||
                                    getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
 | 
			
		||||
                                )
 | 
			
		||||
                        } catch (e: ExecutionException) {
 | 
			
		||||
                            // Do nothing
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return super.shouldInterceptRequest(view, url)
 | 
			
		||||
                    return glideResource ?: super.shouldInterceptRequest(view, url)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
 | 
			
		||||
    private fun htmlToWebview() {
 | 
			
		||||
        val context: Context
 | 
			
		||||
        try {
 | 
			
		||||
            context = requireContext()
 | 
			
		||||
        } catch (e: IllegalStateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("Context required is null")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val colorOnSurface = TypedValue()
 | 
			
		||||
        val colorSurface = TypedValue()
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
 | 
			
		||||
            val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
 | 
			
		||||
            val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
 | 
			
		||||
 | 
			
		||||
            binding.webcontent.settings.standardFontFamily = a.getString(0)
 | 
			
		||||
            binding.webcontent.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
            val colorOnSurface = TypedValue()
 | 
			
		||||
            requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
 | 
			
		||||
            context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
 | 
			
		||||
 | 
			
		||||
            val colorSurface = TypedValue()
 | 
			
		||||
            requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
 | 
			
		||||
            context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
 | 
			
		||||
        } catch (e: IllegalStateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val colorSurfaceString =
 | 
			
		||||
            String.format(
 | 
			
		||||
                "#%06X",
 | 
			
		||||
                WHITE_COLOR_HEX and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else WHITE_COLOR_HEX),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        val colorOnSurfaceString =
 | 
			
		||||
            String.format(
 | 
			
		||||
                "#%06X",
 | 
			
		||||
                WHITE_COLOR_HEX and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            binding.webcontent.settings.useWideViewPort = true
 | 
			
		||||
            binding.webcontent.settings.loadWithOverviewMode = true
 | 
			
		||||
            binding.webcontent.settings.javaScriptEnabled = false
 | 
			
		||||
@@ -422,19 +483,25 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                GestureDetector(
 | 
			
		||||
                    activity,
 | 
			
		||||
                    object : GestureDetector.SimpleOnGestureListener() {
 | 
			
		||||
                        override fun onSingleTapUp(e: MotionEvent): Boolean {
 | 
			
		||||
                            return performClick()
 | 
			
		||||
                        }
 | 
			
		||||
                        override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
 | 
			
		||||
            binding.webcontent.setOnTouchListener { _, event ->
 | 
			
		||||
                gestureDetector.onTouchEvent(
 | 
			
		||||
                    event,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            binding.webcontent.settings.layoutAlgorithm =
 | 
			
		||||
                WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
			
		||||
        } catch (e: IllegalStateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var baseUrl: String? = null
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                val itemUrl = URL(url)
 | 
			
		||||
                baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
			
		||||
@@ -484,12 +551,12 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |        color: ${
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and resources.getColor(R.color.colorAccent),
 | 
			
		||||
                        WHITE_COLOR_HEX and context.resources.getColor(R.color.colorAccent),
 | 
			
		||||
                    )
 | 
			
		||||
                } !important;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      *:not(a) {
 | 
			
		||||
                |        color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
 | 
			
		||||
                |        color: $colorOnSurfaceString;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      * {
 | 
			
		||||
                |        font-size: ${fontSize}px;
 | 
			
		||||
@@ -497,26 +564,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |        word-break: break-word;
 | 
			
		||||
                |        overflow:hidden;
 | 
			
		||||
                |        line-height: 1.5em;
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data,
 | 
			
		||||
                    )
 | 
			
		||||
                };
 | 
			
		||||
                |        background-color: $colorSurfaceString;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      body, html {
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data,
 | 
			
		||||
                    )
 | 
			
		||||
                } !important;
 | 
			
		||||
                |        border-color: ${
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data,
 | 
			
		||||
                    )
 | 
			
		||||
                }  !important;
 | 
			
		||||
                |        background-color: $colorSurfaceString !important;
 | 
			
		||||
                |        border-color: $colorSurfaceString  !important;
 | 
			
		||||
                |        padding: 0 !important;
 | 
			
		||||
                |        margin: 0 !important;
 | 
			
		||||
                |      }
 | 
			
		||||
@@ -526,12 +578,7 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                |      pre, code {
 | 
			
		||||
                |        white-space: pre-wrap;
 | 
			
		||||
                |        width:100%;
 | 
			
		||||
                |        background-color: ${
 | 
			
		||||
                    String.format(
 | 
			
		||||
                        "#%06X",
 | 
			
		||||
                        0xFFFFFF and colorSurface.data,
 | 
			
		||||
                    )
 | 
			
		||||
                };
 | 
			
		||||
                |        background-color: $colorSurfaceString;
 | 
			
		||||
                |      }
 | 
			
		||||
                |   </style>
 | 
			
		||||
                |   $fontLinkAndStyle
 | 
			
		||||
@@ -545,16 +592,16 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
                null,
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: IllegalStateException) {
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("Context required is null")
 | 
			
		||||
            e.sendSilentlyWithAcraWithName("Context required is still null ?")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scrollDown() {
 | 
			
		||||
    fun volumeButtonScrollDown() {
 | 
			
		||||
        val height = binding.nestedScrollView.measuredHeight
 | 
			
		||||
        binding.nestedScrollView.smoothScrollBy(0, height / 2)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scrollUp() {
 | 
			
		||||
    fun volumeButtonScrollUp() {
 | 
			
		||||
        val height = binding.nestedScrollView.measuredHeight
 | 
			
		||||
        binding.nestedScrollView.smoothScrollBy(0, -height / 2)
 | 
			
		||||
    }
 | 
			
		||||
@@ -581,10 +628,11 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun performClick(): Boolean {
 | 
			
		||||
        if (allImages != null && (
 | 
			
		||||
                    binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
			
		||||
                            binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
			
		||||
                    )
 | 
			
		||||
        if (allImages != null &&
 | 
			
		||||
            (
 | 
			
		||||
                binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
 | 
			
		||||
                    binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
 | 
			
		||||
            )
 | 
			
		||||
        ) {
 | 
			
		||||
            val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
 | 
			
		||||
 | 
			
		||||
@@ -596,4 +644,4 @@ class ArticleFragment : Fragment(), DIAware {
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,11 @@ import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.x.closestDI
 | 
			
		||||
import org.kodein.di.instance
 | 
			
		||||
 | 
			
		||||
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
private const val DRAWABLE_SIZE = 30
 | 
			
		||||
 | 
			
		||||
class FilterSheetFragment :
 | 
			
		||||
    BottomSheetDialogFragment(),
 | 
			
		||||
    DIAware {
 | 
			
		||||
    private lateinit var binding: FilterFragmentBinding
 | 
			
		||||
    override val di: DI by closestDI()
 | 
			
		||||
    private val repository: Repository by instance()
 | 
			
		||||
@@ -80,7 +84,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
            val c = Chip(context)
 | 
			
		||||
            c.ellipsize = TextUtils.TruncateAt.END
 | 
			
		||||
 | 
			
		||||
            Glide.with(context)
 | 
			
		||||
            Glide
 | 
			
		||||
                .with(context)
 | 
			
		||||
                .load(source.getIcon(repository.baseUrl))
 | 
			
		||||
                .into(
 | 
			
		||||
                    object : ViewTarget<Chip?, Drawable?>(c) {
 | 
			
		||||
@@ -153,8 +158,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
                        }
 | 
			
		||||
                    gd.setColor(gdColor)
 | 
			
		||||
                    gd.shape = GradientDrawable.RECTANGLE
 | 
			
		||||
                    gd.setSize(30, 30)
 | 
			
		||||
                    gd.cornerRadius = 30F
 | 
			
		||||
                    gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
 | 
			
		||||
                    gd.cornerRadius = DRAWABLE_SIZE.toFloat()
 | 
			
		||||
                    c.chipIcon = gd
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
 | 
			
		||||
@@ -190,4 +195,4 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TAG = "FilterModalBottomSheet"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ class ImageFragment : Fragment() {
 | 
			
		||||
    private lateinit var imageUrl: String
 | 
			
		||||
    private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
 | 
			
		||||
    private var _binding: FragmentImageBinding? = null
 | 
			
		||||
    private val binding get() = _binding
 | 
			
		||||
    val binding get() = _binding
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
@@ -31,7 +31,8 @@ class ImageFragment : Fragment() {
 | 
			
		||||
        val view = binding?.root
 | 
			
		||||
 | 
			
		||||
        binding!!.photoView.visibility = View.VISIBLE
 | 
			
		||||
        Glide.with(requireActivity())
 | 
			
		||||
        Glide
 | 
			
		||||
            .with(requireActivity())
 | 
			
		||||
            .asBitmap()
 | 
			
		||||
            .apply(glideOptions)
 | 
			
		||||
            .load(imageUrl)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,17 +9,22 @@ import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
 | 
			
		||||
private const val PRELOAD_IMAGE_TIMEOUT = 10000
 | 
			
		||||
 | 
			
		||||
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
 | 
			
		||||
    val imageUrls = this.getImages()
 | 
			
		||||
 | 
			
		||||
    val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
 | 
			
		||||
    val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT)
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        for (url in imageUrls) {
 | 
			
		||||
            if (URLUtil.isValidUrl(url)) {
 | 
			
		||||
                Glide.with(context).asBitmap()
 | 
			
		||||
                Glide
 | 
			
		||||
                    .with(context)
 | 
			
		||||
                    .asBitmap()
 | 
			
		||||
                    .apply(glideOptions)
 | 
			
		||||
                    .load(url).submit()
 | 
			
		||||
                    .load(url)
 | 
			
		||||
                    .submit()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e: Error) {
 | 
			
		||||
@@ -40,4 +45,4 @@ fun String.toTextDrawableString(): String {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return textDrawable.toString()
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,12 +17,19 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBin
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.API_ITEMS_NUMBER
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.CURRENT_THEME
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.READER_FONT_SIZE
 | 
			
		||||
import com.mikepenz.aboutlibraries.LibsBuilder
 | 
			
		||||
import org.kodein.di.DIAware
 | 
			
		||||
import org.kodein.di.android.closestDI
 | 
			
		||||
 | 
			
		||||
private const val TITLE_TAG = "settingsActivityTitle"
 | 
			
		||||
 | 
			
		||||
const val MAX_ITEMS_NUMBER = 200
 | 
			
		||||
 | 
			
		||||
private const val MIN_ITEMS_NUMBER = 1
 | 
			
		||||
 | 
			
		||||
class SettingsActivity :
 | 
			
		||||
    AppCompatActivity(),
 | 
			
		||||
    PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
 | 
			
		||||
@@ -61,15 +68,14 @@ class SettingsActivity :
 | 
			
		||||
        outState.putCharSequence(TITLE_TAG, title)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSupportNavigateUp(): Boolean {
 | 
			
		||||
        return if (supportFragmentManager.popBackStackImmediate()) {
 | 
			
		||||
    override fun onSupportNavigateUp(): Boolean =
 | 
			
		||||
        if (supportFragmentManager.popBackStackImmediate()) {
 | 
			
		||||
            supportActionBar?.title = getText(R.string.title_activity_settings)
 | 
			
		||||
            false
 | 
			
		||||
        } else {
 | 
			
		||||
            super.onBackPressed()
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPreferenceStartFragment(
 | 
			
		||||
        caller: PreferenceFragmentCompat,
 | 
			
		||||
@@ -78,15 +84,17 @@ class SettingsActivity :
 | 
			
		||||
        // Instantiate the new Fragment
 | 
			
		||||
        val args = pref.extras
 | 
			
		||||
        val fragment =
 | 
			
		||||
            supportFragmentManager.fragmentFactory.instantiate(
 | 
			
		||||
                classLoader,
 | 
			
		||||
                pref.fragment.toString(),
 | 
			
		||||
            ).apply {
 | 
			
		||||
                arguments = args
 | 
			
		||||
                setTargetFragment(caller, 0)
 | 
			
		||||
            }
 | 
			
		||||
            supportFragmentManager.fragmentFactory
 | 
			
		||||
                .instantiate(
 | 
			
		||||
                    classLoader,
 | 
			
		||||
                    pref.fragment.toString(),
 | 
			
		||||
                ).apply {
 | 
			
		||||
                    arguments = args
 | 
			
		||||
                    setTargetFragment(caller, 0)
 | 
			
		||||
                }
 | 
			
		||||
        // Replace the existing Fragment with the new Fragment
 | 
			
		||||
        supportFragmentManager.beginTransaction()
 | 
			
		||||
        supportFragmentManager
 | 
			
		||||
            .beginTransaction()
 | 
			
		||||
            .replace(R.id.settings, fragment)
 | 
			
		||||
            .addToBackStack(null)
 | 
			
		||||
            .commit()
 | 
			
		||||
@@ -102,10 +110,10 @@ class SettingsActivity :
 | 
			
		||||
        ) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_main, rootKey)
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
 | 
			
		||||
            preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener =
 | 
			
		||||
                Preference.OnPreferenceChangeListener { _, newValue ->
 | 
			
		||||
                    AppCompatDelegate.setDefaultNightMode(
 | 
			
		||||
                        newValue.toString().toInt()
 | 
			
		||||
                        newValue.toString().toInt(),
 | 
			
		||||
                    ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
@@ -131,7 +139,7 @@ class SettingsActivity :
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_general, rootKey)
 | 
			
		||||
 | 
			
		||||
            val editTextPreference =
 | 
			
		||||
                preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
 | 
			
		||||
                preferenceManager.findPreference<EditTextPreference>(API_ITEMS_NUMBER)
 | 
			
		||||
            editTextPreference?.setOnBindEditTextListener { editText ->
 | 
			
		||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
			
		||||
                editText.filters =
 | 
			
		||||
@@ -139,13 +147,14 @@ class SettingsActivity :
 | 
			
		||||
                        InputFilter { source, _, _, dest, _, _ ->
 | 
			
		||||
                            try {
 | 
			
		||||
                                val input: Int = (dest.toString() + source.toString()).toInt()
 | 
			
		||||
                                if (input in 1..200) return@InputFilter null
 | 
			
		||||
                                if (input in MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null
 | 
			
		||||
                            } catch (nfe: NumberFormatException) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    activity,
 | 
			
		||||
                                    R.string.items_number_should_be_number,
 | 
			
		||||
                                    Toast.LENGTH_LONG
 | 
			
		||||
                                ).show()
 | 
			
		||||
                                Toast
 | 
			
		||||
                                    .makeText(
 | 
			
		||||
                                        activity,
 | 
			
		||||
                                        R.string.items_number_should_be_number,
 | 
			
		||||
                                        Toast.LENGTH_LONG,
 | 
			
		||||
                                    ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                            ""
 | 
			
		||||
                        },
 | 
			
		||||
@@ -161,7 +170,7 @@ class SettingsActivity :
 | 
			
		||||
        ) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_viewer, rootKey)
 | 
			
		||||
 | 
			
		||||
            val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
 | 
			
		||||
            val fontSize = preferenceManager.findPreference<EditTextPreference>(READER_FONT_SIZE)
 | 
			
		||||
            fontSize?.setOnBindEditTextListener { editText ->
 | 
			
		||||
                editText.inputType = InputType.TYPE_CLASS_NUMBER
 | 
			
		||||
                editText.addTextChangedListener {
 | 
			
		||||
@@ -218,23 +227,6 @@ class SettingsActivity :
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class ThemePreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        override fun onCreatePreferences(
 | 
			
		||||
            savedInstanceState: Bundle?,
 | 
			
		||||
            rootKey: String?,
 | 
			
		||||
        ) {
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_theme, rootKey)
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
 | 
			
		||||
                Preference.OnPreferenceChangeListener { _, newValue ->
 | 
			
		||||
                    AppCompatDelegate.setDefaultNightMode(
 | 
			
		||||
                        newValue.toString().toInt()
 | 
			
		||||
                    ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class LinksPreferenceFragment : PreferenceFragmentCompat() {
 | 
			
		||||
        private fun openUrl(url: String) {
 | 
			
		||||
            context?.openUrlInBrowser(url)
 | 
			
		||||
@@ -248,19 +240,19 @@ class SettingsActivity :
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
 | 
			
		||||
                Preference.OnPreferenceClickListener {
 | 
			
		||||
                    openUrl(AppSettingsService.trackerUrl)
 | 
			
		||||
                    openUrl(AppSettingsService.BUG_URL)
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
 | 
			
		||||
                Preference.OnPreferenceClickListener {
 | 
			
		||||
                    openUrl(AppSettingsService.sourceUrl)
 | 
			
		||||
                    openUrl(AppSettingsService.SOURCE_URL)
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
 | 
			
		||||
                Preference.OnPreferenceClickListener {
 | 
			
		||||
                    openUrl(AppSettingsService.translationUrl)
 | 
			
		||||
                    openUrl(AppSettingsService.TRANSLATION_URL)
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
@@ -274,4 +266,4 @@ class SettingsActivity :
 | 
			
		||||
            setPreferencesFromResource(R.xml.pref_experimental, rootKey)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
 | 
			
		||||
import androidx.test.espresso.idling.CountingIdlingResource
 | 
			
		||||
 | 
			
		||||
object CountingIdlingResourceSingleton {
 | 
			
		||||
 | 
			
		||||
    private const val RESOURCE = "GLOBAL"
 | 
			
		||||
 | 
			
		||||
    @JvmField
 | 
			
		||||
@@ -18,4 +17,4 @@ object CountingIdlingResourceSingleton {
 | 
			
		||||
            countingIdlingResource.decrement()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android.testing
 | 
			
		||||
 | 
			
		||||
import android.os.Build
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestingHelper {
 | 
			
		||||
    fun isUnitTest(): Boolean {
 | 
			
		||||
        var device = Build.DEVICE
 | 
			
		||||
@@ -16,4 +15,4 @@ class TestingHelper {
 | 
			
		||||
        }
 | 
			
		||||
        return device == "robolectric" && product == "robolectric"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,10 @@ fun Context.shareLink(
 | 
			
		||||
    sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
 | 
			
		||||
    sendIntent.type = "text/plain"
 | 
			
		||||
    startActivity(
 | 
			
		||||
        Intent.createChooser(
 | 
			
		||||
            sendIntent,
 | 
			
		||||
            getString(R.string.share),
 | 
			
		||||
        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
 | 
			
		||||
        Intent
 | 
			
		||||
            .createChooser(
 | 
			
		||||
                sendIntent,
 | 
			
		||||
                getString(R.string.share),
 | 
			
		||||
            ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,5 @@ class CircleImageView
 | 
			
		||||
            textView.text = text.toTextDrawableString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun colorFromIdentifier(key: String): Int {
 | 
			
		||||
            return colorScheme[abs(key.hashCode()) % colorScheme.size]
 | 
			
		||||
        }
 | 
			
		||||
        private fun colorFromIdentifier(key: String): Int = colorScheme[abs(key.hashCode()) % colorScheme.size]
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fun Context.openItemUrl(
 | 
			
		||||
    currentItem: Int,
 | 
			
		||||
    linkDecoded: String,
 | 
			
		||||
@@ -26,11 +25,12 @@ fun Context.openItemUrl(
 | 
			
		||||
    app: Activity,
 | 
			
		||||
) {
 | 
			
		||||
    if (!linkDecoded.isUrlValid()) {
 | 
			
		||||
        Toast.makeText(
 | 
			
		||||
            this,
 | 
			
		||||
            this.getString(R.string.cant_open_invalid_url),
 | 
			
		||||
            Toast.LENGTH_LONG,
 | 
			
		||||
        ).show()
 | 
			
		||||
        Toast
 | 
			
		||||
            .makeText(
 | 
			
		||||
                this,
 | 
			
		||||
                this.getString(R.string.cant_open_invalid_url),
 | 
			
		||||
                Toast.LENGTH_LONG,
 | 
			
		||||
            ).show()
 | 
			
		||||
    } else {
 | 
			
		||||
        if (articleViewer) {
 | 
			
		||||
            val intent = Intent(this, ReaderActivity::class.java)
 | 
			
		||||
@@ -42,8 +42,7 @@ fun Context.openItemUrl(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun String.isUrlValid(): Boolean =
 | 
			
		||||
    this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
 | 
			
		||||
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
 | 
			
		||||
 | 
			
		||||
fun String.isBaseUrlInvalid(): Boolean {
 | 
			
		||||
    val baseUrl = this.toHttpUrlOrNull()
 | 
			
		||||
@@ -61,7 +60,6 @@ fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.openUrlInBrowserAsNewTask(url: String) {
 | 
			
		||||
 | 
			
		||||
    val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
    intent.data = Uri.parse(url)
 | 
			
		||||
@@ -74,13 +72,13 @@ fun Context.openUrlInBrowser(url: String) {
 | 
			
		||||
    this.mayBeStartActivity(intent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:SwallowedException")
 | 
			
		||||
fun Context.mayBeStartActivity(intent: Intent) {
 | 
			
		||||
    try {
 | 
			
		||||
        this.startActivity(intent)
 | 
			
		||||
    } catch (e: ActivityNotFoundException) {
 | 
			
		||||
        Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LinkOnTouchListener : View.OnTouchListener {
 | 
			
		||||
@@ -122,4 +120,4 @@ class LinkOnTouchListener : View.OnTouchListener {
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,4 @@ import org.acra.ktx.sendSilentlyWithAcra
 | 
			
		||||
fun Throwable.sendSilentlyWithAcraWithName(name: String) {
 | 
			
		||||
    ACRA.errorReporter.putCustomData("error_source", name)
 | 
			
		||||
    this.sendSilentlyWithAcra()
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,22 +8,19 @@ import org.acra.config.CoreConfiguration
 | 
			
		||||
import org.acra.config.ReportingAdministrator
 | 
			
		||||
import org.acra.data.CrashReportData
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@AutoService(ReportingAdministrator::class)
 | 
			
		||||
class AcraReportingAdministrator : ReportingAdministrator {
 | 
			
		||||
    override fun shouldStartCollecting(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        config: CoreConfiguration,
 | 
			
		||||
        reportBuilder: ReportBuilder
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        return reportBuilder.exception !is DeadSystemException && (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
 | 
			
		||||
    }
 | 
			
		||||
        reportBuilder: ReportBuilder,
 | 
			
		||||
    ): Boolean =
 | 
			
		||||
        reportBuilder.exception !is DeadSystemException &&
 | 
			
		||||
            (reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
 | 
			
		||||
 | 
			
		||||
    override fun shouldSendReport(
 | 
			
		||||
        context: Context,
 | 
			
		||||
        config: CoreConfiguration,
 | 
			
		||||
        crashReportData: CrashReportData
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        return crashReportData.get("BRAND") != "redroid"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
        crashReportData: CrashReportData,
 | 
			
		||||
    ): Boolean = crashReportData.get("BRAND") != "redroid"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ import java.io.InputStream
 | 
			
		||||
fun Context.bitmapCenterCrop(
 | 
			
		||||
    url: String,
 | 
			
		||||
    iv: ImageView,
 | 
			
		||||
) = Glide.with(this)
 | 
			
		||||
) = Glide
 | 
			
		||||
    .with(this)
 | 
			
		||||
    .asBitmap()
 | 
			
		||||
    .load(url)
 | 
			
		||||
    .apply(RequestOptions.centerCropTransform())
 | 
			
		||||
@@ -25,17 +26,20 @@ fun Context.circularDrawable(
 | 
			
		||||
) {
 | 
			
		||||
    view.textView.text = ""
 | 
			
		||||
 | 
			
		||||
    Glide.with(this)
 | 
			
		||||
    Glide
 | 
			
		||||
        .with(this)
 | 
			
		||||
        .load(url)
 | 
			
		||||
        .into(view.imageView)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
 | 
			
		||||
 | 
			
		||||
fun getBitmapInputStream(
 | 
			
		||||
    bitmap: Bitmap,
 | 
			
		||||
    compressFormat: Bitmap.CompressFormat,
 | 
			
		||||
): InputStream {
 | 
			
		||||
    val byteArrayOutputStream = ByteArrayOutputStream()
 | 
			
		||||
    bitmap.compress(compressFormat, 80, byteArrayOutputStream)
 | 
			
		||||
    bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
 | 
			
		||||
    val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
 | 
			
		||||
    return ByteArrayInputStream(bitmapData)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.ConnectivityManager
 | 
			
		||||
import android.net.NetworkCapabilities
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
 | 
			
		||||
lateinit var s: Snackbar
 | 
			
		||||
@@ -11,19 +10,13 @@ lateinit var s: Snackbar
 | 
			
		||||
fun isNetworkAccessible(context: Context): Boolean {
 | 
			
		||||
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 | 
			
		||||
 | 
			
		||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 | 
			
		||||
        val network = connectivityManager.activeNetwork ?: return false
 | 
			
		||||
        val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
 | 
			
		||||
    val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
 | 
			
		||||
 | 
			
		||||
        return when {
 | 
			
		||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
 | 
			
		||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
 | 
			
		||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
 | 
			
		||||
            networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
 | 
			
		||||
            else -> false
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        val network = connectivityManager.activeNetworkInfo ?: return false
 | 
			
		||||
        return network.isConnectedOrConnecting
 | 
			
		||||
    return when {
 | 
			
		||||
        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
 | 
			
		||||
        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
 | 
			
		||||
        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
 | 
			
		||||
        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
 | 
			
		||||
        else -> false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asSharedFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
class AppViewModel(private val repository: Repository) : ViewModel() {
 | 
			
		||||
class AppViewModel(
 | 
			
		||||
    private val repository: Repository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
 | 
			
		||||
    val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
 | 
			
		||||
    private var wasConnected = true
 | 
			
		||||
@@ -19,11 +21,10 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
 | 
			
		||||
                    if (isConnected && !wasConnected && repository.connectionMonitored) {
 | 
			
		||||
                        _networkAvailableProvider.emit(true)
 | 
			
		||||
                        wasConnected = true
 | 
			
		||||
                    } else if (!isConnected && wasConnected && repository.connectionMonitored)
 | 
			
		||||
                        {
 | 
			
		||||
                            _networkAvailableProvider.emit(false)
 | 
			
		||||
                            wasConnected = false
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (!isConnected && wasConnected && repository.connectionMonitored) {
 | 
			
		||||
                        _networkAvailableProvider.emit(false)
 | 
			
		||||
                        wasConnected = false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto">
 | 
			
		||||
 | 
			
		||||
    <ListPreference
 | 
			
		||||
        android:defaultValue="0"
 | 
			
		||||
        android:entries="@array/ModeTitles"
 | 
			
		||||
        android:entryValues="@array/ModeValues"
 | 
			
		||||
        android:key="currentMode"
 | 
			
		||||
        app:iconSpaceReserved="false"
 | 
			
		||||
        android:title="@string/pref_theme_title"
 | 
			
		||||
        app:useSimpleSummaryProvider="false" />
 | 
			
		||||
</PreferenceScreen>
 | 
			
		||||
@@ -11,13 +11,17 @@ fun dialogMessage(): String {
 | 
			
		||||
    return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Menu.assertClickable(@IdRes id: Int) {
 | 
			
		||||
fun Menu.assertClickable(
 | 
			
		||||
    @IdRes id: Int,
 | 
			
		||||
) {
 | 
			
		||||
    this.assertVisible(id)
 | 
			
		||||
    val item = this.findItem(id)
 | 
			
		||||
    assertTrue(item.isEnabled)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Menu.assertVisible(@IdRes id: Int) {
 | 
			
		||||
fun Menu.assertVisible(
 | 
			
		||||
    @IdRes id: Int,
 | 
			
		||||
) {
 | 
			
		||||
    val item = this.findItem(id)
 | 
			
		||||
    assertTrue(item.isVisible)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -11,10 +11,8 @@ import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
import org.robolectric.Robolectric
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@RunWith(RobotElectriqueRunnerclass::class)
 | 
			
		||||
@RunWith(RobotElectriqueRunner::class)
 | 
			
		||||
class LoginActivityTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun login_shouldDisplay() {
 | 
			
		||||
        Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
 | 
			
		||||
@@ -74,4 +72,4 @@ class LoginActivityTest {
 | 
			
		||||
             assertEquals(expectedIntent.component, actual.component)
 | 
			
		||||
         }
 | 
			
		||||
     }*/
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,8 @@ package bou.amine.apps.readerforselfossv2.android.tests.robolectric
 | 
			
		||||
import org.robolectric.RobolectricTestRunner
 | 
			
		||||
import org.robolectric.annotation.Config
 | 
			
		||||
 | 
			
		||||
class RobotElectriqueRunnerclass(testClass: Class<*>?) :
 | 
			
		||||
    RobolectricTestRunner(testClass) {
 | 
			
		||||
 | 
			
		||||
    override fun buildGlobalConfig(): Config {
 | 
			
		||||
        return Config.Builder().setSdk(25, 30, 33).build()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
class RobotElectriqueRunner(
 | 
			
		||||
    testClass: Class<*>?,
 | 
			
		||||
) : RobolectricTestRunner(testClass) {
 | 
			
		||||
    override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("detekt:LargeClass")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.tests.repository
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
@@ -42,14 +44,14 @@ private const val FEED_URL = "https://test.com/feed"
 | 
			
		||||
 | 
			
		||||
private const val TAGS = "Test, New"
 | 
			
		||||
 | 
			
		||||
private const val NUMBER_ARTICLES = 100
 | 
			
		||||
private const val NUMBER_UNREAD = 50
 | 
			
		||||
private const val NUMBER_STARRED = 20
 | 
			
		||||
 | 
			
		||||
class RepositoryTest {
 | 
			
		||||
    private val db = mockk<ReaderForSelfossDB>(relaxed = true)
 | 
			
		||||
    private val appSettingsService = mockk<AppSettingsService>()
 | 
			
		||||
    private val api = mockk<SelfossApi>()
 | 
			
		||||
 | 
			
		||||
    private val NUMBER_ARTICLES = 100
 | 
			
		||||
    private val NUMBER_UNREAD = 50
 | 
			
		||||
    private val NUMBER_STARRED = 20
 | 
			
		||||
    private lateinit var repository: Repository
 | 
			
		||||
 | 
			
		||||
    private fun initializeRepository(
 | 
			
		||||
@@ -75,19 +77,20 @@ class RepositoryTest {
 | 
			
		||||
        every { appSettingsService.isUpdateSourcesEnabled() } returns false
 | 
			
		||||
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data =
 | 
			
		||||
                    SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        coEvery { api.stats() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
 | 
			
		||||
        every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
 | 
			
		||||
@@ -117,7 +120,7 @@ class RepositoryTest {
 | 
			
		||||
    fun get_api_4_date_with_api_1_version_stored() {
 | 
			
		||||
        every { appSettingsService.getApiVersion() } returns 1
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
        every { appSettingsService.updateApiVersion(any()) } returns Unit
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -133,14 +136,15 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data =
 | 
			
		||||
                    SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -153,14 +157,15 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_username_not_empty() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data =
 | 
			
		||||
                    SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true)
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, true),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns "username"
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -173,14 +178,15 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_no_auth() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data =
 | 
			
		||||
                    SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, false)
 | 
			
		||||
                        SelfossModel.ApiConfiguration(true, false),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -193,14 +199,15 @@ class RepositoryTest {
 | 
			
		||||
    fun get_public_access_disabled() {
 | 
			
		||||
        every { appSettingsService.updatePublicAccess(any()) } returns Unit
 | 
			
		||||
        coEvery { api.apiInformation() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = SelfossModel.ApiInformation(
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data =
 | 
			
		||||
                    SelfossModel.ApiInformation(
 | 
			
		||||
                        "2.19-ba1e8e3",
 | 
			
		||||
                        "4.0.0",
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true)
 | 
			
		||||
                        SelfossModel.ApiConfiguration(false, true),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        every { appSettingsService.getUserName() } returns ""
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
@@ -216,10 +223,10 @@ class RepositoryTest {
 | 
			
		||||
        val itemParameters = FakeItemParameters()
 | 
			
		||||
        itemParameters.datetime = "2021-04-23 11:45:32"
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = generateTestApiItem(itemParameters),
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = generateTestApiItem(itemParameters),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        runBlocking {
 | 
			
		||||
@@ -232,7 +239,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        runBlocking {
 | 
			
		||||
@@ -247,7 +254,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_newer_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.ALL
 | 
			
		||||
@@ -263,7 +270,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_newer_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -302,8 +309,8 @@ class RepositoryTest {
 | 
			
		||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
			
		||||
            itemParameter1,
 | 
			
		||||
        ) +
 | 
			
		||||
                generateTestDBItems(itemParameter2) +
 | 
			
		||||
                generateTestDBItems(itemParameter3)
 | 
			
		||||
            generateTestDBItems(itemParameter2) +
 | 
			
		||||
            generateTestDBItems(itemParameter3)
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
 | 
			
		||||
@@ -330,8 +337,8 @@ class RepositoryTest {
 | 
			
		||||
        coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
 | 
			
		||||
            itemParameter1,
 | 
			
		||||
        ) +
 | 
			
		||||
                generateTestDBItems(itemParameter2) +
 | 
			
		||||
                generateTestDBItems(itemParameter3)
 | 
			
		||||
            generateTestDBItems(itemParameter2) +
 | 
			
		||||
            generateTestDBItems(itemParameter3)
 | 
			
		||||
 | 
			
		||||
        every { appSettingsService.isItemCachingEnabled() } returns true
 | 
			
		||||
 | 
			
		||||
@@ -360,7 +367,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -376,7 +383,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_all_older_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.items = ArrayList(generateTestApiItem())
 | 
			
		||||
@@ -393,7 +400,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun get_older_starred_items() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = true, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        repository.displayedItems = ItemType.STARRED
 | 
			
		||||
@@ -833,7 +840,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
            SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -861,7 +868,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_but_response_fails() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
                SuccessResponse(false)
 | 
			
		||||
            SuccessResponse(false)
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -889,7 +896,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun create_source_without_connection() {
 | 
			
		||||
        coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
 | 
			
		||||
                SuccessResponse(true)
 | 
			
		||||
            SuccessResponse(true)
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -962,10 +969,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "finished",
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "finished",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -980,10 +987,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_but_response_fails() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = false,
 | 
			
		||||
                    data = "unallowed access",
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = false,
 | 
			
		||||
                data = "unallowed access",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -998,10 +1005,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_with_unallowed_access() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "unallowed access",
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "unallowed access",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1016,10 +1023,10 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun update_remote_without_connection() {
 | 
			
		||||
        coEvery { api.update() } returns
 | 
			
		||||
                StatusAndData(
 | 
			
		||||
                    success = true,
 | 
			
		||||
                    data = "undocumented...",
 | 
			
		||||
                )
 | 
			
		||||
            StatusAndData(
 | 
			
		||||
                success = true,
 | 
			
		||||
                data = "undocumented...",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        var response: Boolean
 | 
			
		||||
@@ -1109,11 +1116,11 @@ class RepositoryTest {
 | 
			
		||||
                any(),
 | 
			
		||||
            )
 | 
			
		||||
        } returnsMany
 | 
			
		||||
                listOf(
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
                    StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                )
 | 
			
		||||
            listOf(
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
 | 
			
		||||
                StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1127,7 +1134,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_but_response_fails() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository()
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1141,7 +1148,7 @@ class RepositoryTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun cache_items_without_connection() {
 | 
			
		||||
        coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
 | 
			
		||||
                StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
            StatusAndData(success = false, data = generateTestApiItem())
 | 
			
		||||
 | 
			
		||||
        initializeRepository(MutableStateFlow(false))
 | 
			
		||||
        prepareSearch()
 | 
			
		||||
@@ -1168,4 +1175,4 @@ class RepositoryTest {
 | 
			
		||||
        )
 | 
			
		||||
        repository.searchFilter = "search"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@ package bou.amine.apps.readerforselfossv2.tests.repository
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ITEM
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
 | 
			
		||||
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
 | 
			
		||||
    return listOf(
 | 
			
		||||
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> =
 | 
			
		||||
    listOf(
 | 
			
		||||
        ITEM(
 | 
			
		||||
            id = item.id,
 | 
			
		||||
            datetime = item.datetime,
 | 
			
		||||
@@ -20,10 +20,9 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
 | 
			
		||||
            author = item.author,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> {
 | 
			
		||||
    return listOf(
 | 
			
		||||
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> =
 | 
			
		||||
    listOf(
 | 
			
		||||
        SelfossModel.Item(
 | 
			
		||||
            id = item.id.toInt(),
 | 
			
		||||
            datetime = item.datetime,
 | 
			
		||||
@@ -39,7 +38,6 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
 | 
			
		||||
            author = item.author,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FakeItemParameters {
 | 
			
		||||
    var id = "20"
 | 
			
		||||
@@ -56,4 +54,4 @@ class FakeItemParameters {
 | 
			
		||||
    var sourcetitle = "La Chimica e la Società"
 | 
			
		||||
    var tags = "Chimica, Testing"
 | 
			
		||||
    var author = "Someone important"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										786
									
								
								detekt.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										786
									
								
								detekt.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,786 @@
 | 
			
		||||
build:
 | 
			
		||||
  maxIssues: 0
 | 
			
		||||
  excludeCorrectable: false
 | 
			
		||||
  weights:
 | 
			
		||||
  # complexity: 2
 | 
			
		||||
  # LongParameterList: 1
 | 
			
		||||
  # style: 1
 | 
			
		||||
  # comments: 1
 | 
			
		||||
 | 
			
		||||
config:
 | 
			
		||||
  validation: true
 | 
			
		||||
  warningsAsErrors: false
 | 
			
		||||
  checkExhaustiveness: false
 | 
			
		||||
  # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
 | 
			
		||||
  excludes: ''
 | 
			
		||||
 | 
			
		||||
processors:
 | 
			
		||||
  active: true
 | 
			
		||||
  exclude:
 | 
			
		||||
    - 'DetektProgressListener'
 | 
			
		||||
  # - 'KtFileCountProcessor'
 | 
			
		||||
  # - 'PackageCountProcessor'
 | 
			
		||||
  # - 'ClassCountProcessor'
 | 
			
		||||
  # - 'FunctionCountProcessor'
 | 
			
		||||
  # - 'PropertyCountProcessor'
 | 
			
		||||
  # - 'ProjectComplexityProcessor'
 | 
			
		||||
  # - 'ProjectCognitiveComplexityProcessor'
 | 
			
		||||
  # - 'ProjectLLOCProcessor'
 | 
			
		||||
  # - 'ProjectCLOCProcessor'
 | 
			
		||||
  # - 'ProjectLOCProcessor'
 | 
			
		||||
  # - 'ProjectSLOCProcessor'
 | 
			
		||||
  # - 'LicenseHeaderLoaderExtension'
 | 
			
		||||
 | 
			
		||||
console-reports:
 | 
			
		||||
  active: true
 | 
			
		||||
  exclude:
 | 
			
		||||
    - 'ProjectStatisticsReport'
 | 
			
		||||
    - 'ComplexityReport'
 | 
			
		||||
    - 'NotificationReport'
 | 
			
		||||
    - 'FindingsReport'
 | 
			
		||||
    - 'FileBasedFindingsReport'
 | 
			
		||||
  #  - 'LiteFindingsReport'
 | 
			
		||||
 | 
			
		||||
output-reports:
 | 
			
		||||
  active: true
 | 
			
		||||
  exclude:
 | 
			
		||||
  # - 'TxtOutputReport'
 | 
			
		||||
  # - 'XmlOutputReport'
 | 
			
		||||
  # - 'HtmlOutputReport'
 | 
			
		||||
  # - 'MdOutputReport'
 | 
			
		||||
  # - 'SarifOutputReport'
 | 
			
		||||
 | 
			
		||||
comments:
 | 
			
		||||
  active: true
 | 
			
		||||
  AbsentOrWrongFileLicense:
 | 
			
		||||
    active: false
 | 
			
		||||
    licenseTemplateFile: 'license.template'
 | 
			
		||||
    licenseTemplateIsRegex: false
 | 
			
		||||
  CommentOverPrivateFunction:
 | 
			
		||||
    active: false
 | 
			
		||||
  CommentOverPrivateProperty:
 | 
			
		||||
    active: false
 | 
			
		||||
  DeprecatedBlockTag:
 | 
			
		||||
    active: false
 | 
			
		||||
  EndOfSentenceFormat:
 | 
			
		||||
    active: false
 | 
			
		||||
    endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
 | 
			
		||||
  KDocReferencesNonPublicProperty:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
  OutdatedDocumentation:
 | 
			
		||||
    active: false
 | 
			
		||||
    matchTypeParameters: true
 | 
			
		||||
    matchDeclarationsOrder: true
 | 
			
		||||
    allowParamOnConstructorProperties: false
 | 
			
		||||
  UndocumentedPublicClass:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    searchInNestedClass: true
 | 
			
		||||
    searchInInnerClass: true
 | 
			
		||||
    searchInInnerObject: true
 | 
			
		||||
    searchInInnerInterface: true
 | 
			
		||||
    searchInProtectedClass: false
 | 
			
		||||
  UndocumentedPublicFunction:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    searchProtectedFunction: false
 | 
			
		||||
  UndocumentedPublicProperty:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    searchProtectedProperty: false
 | 
			
		||||
 | 
			
		||||
complexity:
 | 
			
		||||
  active: true
 | 
			
		||||
  CognitiveComplexMethod:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 15
 | 
			
		||||
  ComplexCondition:
 | 
			
		||||
    active: true
 | 
			
		||||
    threshold: 4
 | 
			
		||||
  ComplexInterface:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 10
 | 
			
		||||
    includeStaticDeclarations: false
 | 
			
		||||
    includePrivateDeclarations: false
 | 
			
		||||
    ignoreOverloaded: false
 | 
			
		||||
  CyclomaticComplexMethod:
 | 
			
		||||
    active: true
 | 
			
		||||
    threshold: 15
 | 
			
		||||
    ignoreSingleWhenExpression: false
 | 
			
		||||
    ignoreSimpleWhenEntries: false
 | 
			
		||||
    ignoreNestingFunctions: false
 | 
			
		||||
    nestingFunctions:
 | 
			
		||||
      - 'also'
 | 
			
		||||
      - 'apply'
 | 
			
		||||
      - 'forEach'
 | 
			
		||||
      - 'isNotNull'
 | 
			
		||||
      - 'ifNull'
 | 
			
		||||
      - 'let'
 | 
			
		||||
      - 'run'
 | 
			
		||||
      - 'use'
 | 
			
		||||
      - 'with'
 | 
			
		||||
  LabeledExpression:
 | 
			
		||||
    active: false
 | 
			
		||||
    ignoredLabels: [ ]
 | 
			
		||||
  LargeClass:
 | 
			
		||||
    active: true
 | 
			
		||||
    threshold: 600
 | 
			
		||||
  LongMethod:
 | 
			
		||||
    active: true
 | 
			
		||||
    threshold: 60
 | 
			
		||||
  LongParameterList:
 | 
			
		||||
    active: true
 | 
			
		||||
    functionThreshold: 6
 | 
			
		||||
    constructorThreshold: 7
 | 
			
		||||
    ignoreDefaultParameters: false
 | 
			
		||||
    ignoreDataClasses: true
 | 
			
		||||
    ignoreAnnotatedParameter: [ ]
 | 
			
		||||
  MethodOverloading:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 6
 | 
			
		||||
  NamedArguments:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 3
 | 
			
		||||
    ignoreArgumentsMatchingNames: false
 | 
			
		||||
  NestedBlockDepth:
 | 
			
		||||
    active: true
 | 
			
		||||
    threshold: 4
 | 
			
		||||
  NestedScopeFunctions:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 1
 | 
			
		||||
    functions:
 | 
			
		||||
      - 'kotlin.apply'
 | 
			
		||||
      - 'kotlin.run'
 | 
			
		||||
      - 'kotlin.with'
 | 
			
		||||
      - 'kotlin.let'
 | 
			
		||||
      - 'kotlin.also'
 | 
			
		||||
  ReplaceSafeCallChainWithRun:
 | 
			
		||||
    active: false
 | 
			
		||||
  StringLiteralDuplication:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    threshold: 3
 | 
			
		||||
    ignoreAnnotation: true
 | 
			
		||||
    excludeStringsWithLessThan5Characters: true
 | 
			
		||||
    ignoreStringsRegex: '$^'
 | 
			
		||||
  TooManyFunctions:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/android/*Activity.kt', '**/fragments/*Fragment.kt' ]
 | 
			
		||||
    thresholdInFiles: 11
 | 
			
		||||
    thresholdInClasses: 11
 | 
			
		||||
    thresholdInInterfaces: 11
 | 
			
		||||
    thresholdInObjects: 11
 | 
			
		||||
    thresholdInEnums: 11
 | 
			
		||||
    ignoreDeprecated: false
 | 
			
		||||
    ignorePrivate: false
 | 
			
		||||
    ignoreOverridden: false
 | 
			
		||||
    ignoreAnnotatedFunctions: [ ]
 | 
			
		||||
 | 
			
		||||
coroutines:
 | 
			
		||||
  active: true
 | 
			
		||||
  GlobalCoroutineUsage:
 | 
			
		||||
    active: false
 | 
			
		||||
  InjectDispatcher:
 | 
			
		||||
    active: true
 | 
			
		||||
    dispatcherNames:
 | 
			
		||||
      - 'IO'
 | 
			
		||||
      - 'Default'
 | 
			
		||||
      - 'Unconfined'
 | 
			
		||||
  RedundantSuspendModifier:
 | 
			
		||||
    active: true
 | 
			
		||||
  SleepInsteadOfDelay:
 | 
			
		||||
    active: true
 | 
			
		||||
  SuspendFunSwallowedCancellation:
 | 
			
		||||
    active: false
 | 
			
		||||
  SuspendFunWithCoroutineScopeReceiver:
 | 
			
		||||
    active: false
 | 
			
		||||
  SuspendFunWithFlowReturnType:
 | 
			
		||||
    active: true
 | 
			
		||||
 | 
			
		||||
empty-blocks:
 | 
			
		||||
  active: true
 | 
			
		||||
  EmptyCatchBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
			
		||||
  EmptyClassBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyDefaultConstructor:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyDoWhileBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyElseBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyFinallyBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyForBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyFunctionBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreOverridden: false
 | 
			
		||||
  EmptyIfBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyInitBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyKtFile:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptySecondaryConstructor:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyTryBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyWhenBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  EmptyWhileBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
 | 
			
		||||
exceptions:
 | 
			
		||||
  active: true
 | 
			
		||||
  ExceptionRaisedInUnexpectedLocation:
 | 
			
		||||
    active: true
 | 
			
		||||
    methodNames:
 | 
			
		||||
      - 'equals'
 | 
			
		||||
      - 'finalize'
 | 
			
		||||
      - 'hashCode'
 | 
			
		||||
      - 'toString'
 | 
			
		||||
  InstanceOfCheckForException:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
  NotImplementedDeclaration:
 | 
			
		||||
    active: false
 | 
			
		||||
  ObjectExtendsThrowable:
 | 
			
		||||
    active: false
 | 
			
		||||
  PrintStackTrace:
 | 
			
		||||
    active: true
 | 
			
		||||
  RethrowCaughtException:
 | 
			
		||||
    active: true
 | 
			
		||||
  ReturnFromFinally:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreLabeled: false
 | 
			
		||||
  SwallowedException:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoredExceptionTypes:
 | 
			
		||||
      - 'InterruptedException'
 | 
			
		||||
      - 'MalformedURLException'
 | 
			
		||||
      - 'NumberFormatException'
 | 
			
		||||
      - 'ParseException'
 | 
			
		||||
    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
			
		||||
  ThrowingExceptionFromFinally:
 | 
			
		||||
    active: true
 | 
			
		||||
  ThrowingExceptionInMain:
 | 
			
		||||
    active: false
 | 
			
		||||
  ThrowingExceptionsWithoutMessageOrCause:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    exceptions:
 | 
			
		||||
      - 'ArrayIndexOutOfBoundsException'
 | 
			
		||||
      - 'Exception'
 | 
			
		||||
      - 'IllegalArgumentException'
 | 
			
		||||
      - 'IllegalMonitorStateException'
 | 
			
		||||
      - 'IllegalStateException'
 | 
			
		||||
      - 'IndexOutOfBoundsException'
 | 
			
		||||
      - 'NullPointerException'
 | 
			
		||||
      - 'RuntimeException'
 | 
			
		||||
      - 'Throwable'
 | 
			
		||||
  ThrowingNewInstanceOfSameException:
 | 
			
		||||
    active: true
 | 
			
		||||
  TooGenericExceptionCaught:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    exceptionNames:
 | 
			
		||||
      - 'ArrayIndexOutOfBoundsException'
 | 
			
		||||
      - 'Error'
 | 
			
		||||
      - 'Exception'
 | 
			
		||||
      - 'IllegalMonitorStateException'
 | 
			
		||||
      - 'IndexOutOfBoundsException'
 | 
			
		||||
      - 'NullPointerException'
 | 
			
		||||
      - 'RuntimeException'
 | 
			
		||||
      - 'Throwable'
 | 
			
		||||
    allowedExceptionNameRegex: '_|(ignore|expected).*'
 | 
			
		||||
  TooGenericExceptionThrown:
 | 
			
		||||
    active: true
 | 
			
		||||
    exceptionNames:
 | 
			
		||||
      - 'Error'
 | 
			
		||||
      - 'Exception'
 | 
			
		||||
      - 'RuntimeException'
 | 
			
		||||
      - 'Throwable'
 | 
			
		||||
 | 
			
		||||
naming:
 | 
			
		||||
  active: true
 | 
			
		||||
  BooleanPropertyNaming:
 | 
			
		||||
    active: false
 | 
			
		||||
    allowedPattern: '^(is|has|are)'
 | 
			
		||||
  ClassNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    classPattern: '[A-Z][a-zA-Z0-9]*'
 | 
			
		||||
  ConstructorParameterNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    parameterPattern: '[a-z][A-Za-z0-9]*'
 | 
			
		||||
    privateParameterPattern: '[a-z][A-Za-z0-9]*'
 | 
			
		||||
    excludeClassPattern: '$^'
 | 
			
		||||
  EnumNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
 | 
			
		||||
  ForbiddenClassName:
 | 
			
		||||
    active: false
 | 
			
		||||
    forbiddenName: [ ]
 | 
			
		||||
  FunctionMaxLength:
 | 
			
		||||
    active: false
 | 
			
		||||
    maximumFunctionNameLength: 30
 | 
			
		||||
  FunctionMinLength:
 | 
			
		||||
    active: false
 | 
			
		||||
    minimumFunctionNameLength: 3
 | 
			
		||||
  FunctionNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    functionPattern: '[a-z][a-zA-Z0-9]*'
 | 
			
		||||
    excludeClassPattern: '$^'
 | 
			
		||||
  FunctionParameterNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    parameterPattern: '[a-z][A-Za-z0-9]*'
 | 
			
		||||
    excludeClassPattern: '$^'
 | 
			
		||||
  InvalidPackageDeclaration:
 | 
			
		||||
    active: true
 | 
			
		||||
    rootPackage: ''
 | 
			
		||||
    requireRootInDeclaration: false
 | 
			
		||||
  LambdaParameterNaming:
 | 
			
		||||
    active: false
 | 
			
		||||
    parameterPattern: '[a-z][A-Za-z0-9]*|_'
 | 
			
		||||
  MatchingDeclarationName:
 | 
			
		||||
    active: false # done in ktlint
 | 
			
		||||
    mustBeFirst: true
 | 
			
		||||
  MemberNameEqualsClassName:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreOverridden: true
 | 
			
		||||
  NoNameShadowing:
 | 
			
		||||
    active: true
 | 
			
		||||
  NonBooleanPropertyPrefixedWithIs:
 | 
			
		||||
    active: false
 | 
			
		||||
  ObjectPropertyNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    constantPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
			
		||||
    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
			
		||||
    privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
 | 
			
		||||
  PackageNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
 | 
			
		||||
  TopLevelPropertyNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    constantPattern: '[A-Z][_A-Z0-9]*'
 | 
			
		||||
    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
 | 
			
		||||
    privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
 | 
			
		||||
  VariableMaxLength:
 | 
			
		||||
    active: false
 | 
			
		||||
    maximumVariableNameLength: 64
 | 
			
		||||
  VariableMinLength:
 | 
			
		||||
    active: false
 | 
			
		||||
    minimumVariableNameLength: 1
 | 
			
		||||
  VariableNaming:
 | 
			
		||||
    active: true
 | 
			
		||||
    variablePattern: '[a-z][A-Za-z0-9]*'
 | 
			
		||||
    privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
 | 
			
		||||
    excludeClassPattern: '$^'
 | 
			
		||||
 | 
			
		||||
performance:
 | 
			
		||||
  active: true
 | 
			
		||||
  ArrayPrimitive:
 | 
			
		||||
    active: true
 | 
			
		||||
  CouldBeSequence:
 | 
			
		||||
    active: false
 | 
			
		||||
    threshold: 3
 | 
			
		||||
  ForEachOnRange:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
  SpreadOperator:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
  UnnecessaryPartOfBinaryExpression:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryTemporaryInstantiation:
 | 
			
		||||
    active: true
 | 
			
		||||
 | 
			
		||||
potential-bugs:
 | 
			
		||||
  active: true
 | 
			
		||||
  AvoidReferentialEquality:
 | 
			
		||||
    active: true
 | 
			
		||||
    forbiddenTypePatterns:
 | 
			
		||||
      - 'kotlin.String'
 | 
			
		||||
  CastNullableToNonNullableType:
 | 
			
		||||
    active: false
 | 
			
		||||
  CastToNullableType:
 | 
			
		||||
    active: false
 | 
			
		||||
  Deprecation:
 | 
			
		||||
    active: false
 | 
			
		||||
  DontDowncastCollectionTypes:
 | 
			
		||||
    active: false
 | 
			
		||||
  DoubleMutabilityForCollection:
 | 
			
		||||
    active: true
 | 
			
		||||
    mutableTypes:
 | 
			
		||||
      - 'kotlin.collections.MutableList'
 | 
			
		||||
      - 'kotlin.collections.MutableMap'
 | 
			
		||||
      - 'kotlin.collections.MutableSet'
 | 
			
		||||
      - 'java.util.ArrayList'
 | 
			
		||||
      - 'java.util.LinkedHashSet'
 | 
			
		||||
      - 'java.util.HashSet'
 | 
			
		||||
      - 'java.util.LinkedHashMap'
 | 
			
		||||
      - 'java.util.HashMap'
 | 
			
		||||
  ElseCaseInsteadOfExhaustiveWhen:
 | 
			
		||||
    active: false
 | 
			
		||||
    ignoredSubjectTypes: [ ]
 | 
			
		||||
  EqualsAlwaysReturnsTrueOrFalse:
 | 
			
		||||
    active: true
 | 
			
		||||
  EqualsWithHashCodeExist:
 | 
			
		||||
    active: true
 | 
			
		||||
  ExitOutsideMain:
 | 
			
		||||
    active: false
 | 
			
		||||
  ExplicitGarbageCollectionCall:
 | 
			
		||||
    active: true
 | 
			
		||||
  HasPlatformType:
 | 
			
		||||
    active: true
 | 
			
		||||
  IgnoredReturnValue:
 | 
			
		||||
    active: true
 | 
			
		||||
    restrictToConfig: true
 | 
			
		||||
    returnValueAnnotations:
 | 
			
		||||
      - 'CheckResult'
 | 
			
		||||
      - '*.CheckResult'
 | 
			
		||||
      - 'CheckReturnValue'
 | 
			
		||||
      - '*.CheckReturnValue'
 | 
			
		||||
    ignoreReturnValueAnnotations:
 | 
			
		||||
      - 'CanIgnoreReturnValue'
 | 
			
		||||
      - '*.CanIgnoreReturnValue'
 | 
			
		||||
    returnValueTypes:
 | 
			
		||||
      - 'kotlin.sequences.Sequence'
 | 
			
		||||
      - 'kotlinx.coroutines.flow.*Flow'
 | 
			
		||||
      - 'java.util.stream.*Stream'
 | 
			
		||||
    ignoreFunctionCall: [ ]
 | 
			
		||||
  ImplicitDefaultLocale:
 | 
			
		||||
    active: true
 | 
			
		||||
  ImplicitUnitReturnType:
 | 
			
		||||
    active: false
 | 
			
		||||
    allowExplicitReturnType: true
 | 
			
		||||
  InvalidRange:
 | 
			
		||||
    active: true
 | 
			
		||||
  IteratorHasNextCallsNextMethod:
 | 
			
		||||
    active: true
 | 
			
		||||
  IteratorNotThrowingNoSuchElementException:
 | 
			
		||||
    active: true
 | 
			
		||||
  LateinitUsage:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
    ignoreOnClassesPattern: ''
 | 
			
		||||
  MapGetWithNotNullAssertionOperator:
 | 
			
		||||
    active: true
 | 
			
		||||
  MissingPackageDeclaration:
 | 
			
		||||
    active: false
 | 
			
		||||
    excludes: [ '**/*.kts' ]
 | 
			
		||||
  NullCheckOnMutableProperty:
 | 
			
		||||
    active: false
 | 
			
		||||
  NullableToStringCall:
 | 
			
		||||
    active: false
 | 
			
		||||
  PropertyUsedBeforeDeclaration:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnconditionalJumpStatementInLoop:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryNotNullCheck:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryNotNullOperator:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnnecessarySafeCall:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnreachableCatchBlock:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnreachableCode:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnsafeCallOnNullableType:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
 | 
			
		||||
  UnsafeCast:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnusedUnaryOperator:
 | 
			
		||||
    active: true
 | 
			
		||||
  UselessPostfixExpression:
 | 
			
		||||
    active: true
 | 
			
		||||
  WrongEqualsTypeParameter:
 | 
			
		||||
    active: true
 | 
			
		||||
 | 
			
		||||
style:
 | 
			
		||||
  active: true
 | 
			
		||||
  AlsoCouldBeApply:
 | 
			
		||||
    active: false
 | 
			
		||||
  BracesOnIfStatements:
 | 
			
		||||
    active: false
 | 
			
		||||
    singleLine: 'never'
 | 
			
		||||
    multiLine: 'always'
 | 
			
		||||
  BracesOnWhenStatements:
 | 
			
		||||
    active: false
 | 
			
		||||
    singleLine: 'necessary'
 | 
			
		||||
    multiLine: 'consistent'
 | 
			
		||||
  CanBeNonNullable:
 | 
			
		||||
    active: false
 | 
			
		||||
  CascadingCallWrapping:
 | 
			
		||||
    active: false
 | 
			
		||||
    includeElvis: true
 | 
			
		||||
  ClassOrdering:
 | 
			
		||||
    active: false
 | 
			
		||||
  CollapsibleIfStatements:
 | 
			
		||||
    active: false
 | 
			
		||||
  DataClassContainsFunctions:
 | 
			
		||||
    active: false
 | 
			
		||||
    conversionFunctionPrefix:
 | 
			
		||||
      - 'to'
 | 
			
		||||
    allowOperators: false
 | 
			
		||||
  DataClassShouldBeImmutable:
 | 
			
		||||
    active: false
 | 
			
		||||
  DestructuringDeclarationWithTooManyEntries:
 | 
			
		||||
    active: true
 | 
			
		||||
    maxDestructuringEntries: 3
 | 
			
		||||
  DoubleNegativeLambda:
 | 
			
		||||
    active: false
 | 
			
		||||
    negativeFunctions:
 | 
			
		||||
      - reason: 'Use `takeIf` instead.'
 | 
			
		||||
        value: 'takeUnless'
 | 
			
		||||
      - reason: 'Use `all` instead.'
 | 
			
		||||
        value: 'none'
 | 
			
		||||
    negativeFunctionNameParts:
 | 
			
		||||
      - 'not'
 | 
			
		||||
      - 'non'
 | 
			
		||||
  EqualsNullCall:
 | 
			
		||||
    active: true
 | 
			
		||||
  EqualsOnSignatureLine:
 | 
			
		||||
    active: false
 | 
			
		||||
  ExplicitCollectionElementAccessMethod:
 | 
			
		||||
    active: false
 | 
			
		||||
  ExplicitItLambdaParameter:
 | 
			
		||||
    active: true
 | 
			
		||||
  ExpressionBodySyntax:
 | 
			
		||||
    active: false
 | 
			
		||||
    includeLineWrapping: false
 | 
			
		||||
  ForbiddenAnnotation:
 | 
			
		||||
    active: false
 | 
			
		||||
    annotations:
 | 
			
		||||
      - reason: 'it is a java annotation. Use `Suppress` instead.'
 | 
			
		||||
        value: 'java.lang.SuppressWarnings'
 | 
			
		||||
      - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
 | 
			
		||||
        value: 'java.lang.Deprecated'
 | 
			
		||||
      - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
 | 
			
		||||
        value: 'java.lang.annotation.Documented'
 | 
			
		||||
      - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
 | 
			
		||||
        value: 'java.lang.annotation.Target'
 | 
			
		||||
      - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
 | 
			
		||||
        value: 'java.lang.annotation.Retention'
 | 
			
		||||
      - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
 | 
			
		||||
        value: 'java.lang.annotation.Repeatable'
 | 
			
		||||
      - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
 | 
			
		||||
        value: 'java.lang.annotation.Inherited'
 | 
			
		||||
  ForbiddenComment:
 | 
			
		||||
    active: true
 | 
			
		||||
    comments:
 | 
			
		||||
      - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
 | 
			
		||||
        value: 'FIXME:'
 | 
			
		||||
      - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
 | 
			
		||||
        value: 'STOPSHIP:'
 | 
			
		||||
      - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
 | 
			
		||||
        value: 'TODO:'
 | 
			
		||||
    allowedPatterns: ''
 | 
			
		||||
  ForbiddenImport:
 | 
			
		||||
    active: false
 | 
			
		||||
    imports: [ ]
 | 
			
		||||
    forbiddenPatterns: ''
 | 
			
		||||
  ForbiddenMethodCall:
 | 
			
		||||
    active: false
 | 
			
		||||
    methods:
 | 
			
		||||
      - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
 | 
			
		||||
        value: 'kotlin.io.print'
 | 
			
		||||
      - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
 | 
			
		||||
        value: 'kotlin.io.println'
 | 
			
		||||
  ForbiddenSuppress:
 | 
			
		||||
    active: false
 | 
			
		||||
    rules: [ ]
 | 
			
		||||
  ForbiddenVoid:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreOverridden: false
 | 
			
		||||
    ignoreUsageInGenerics: false
 | 
			
		||||
  FunctionOnlyReturningConstant:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreOverridableFunction: true
 | 
			
		||||
    ignoreActualFunction: true
 | 
			
		||||
    excludedFunctions: [ ]
 | 
			
		||||
  LoopWithTooManyJumpStatements:
 | 
			
		||||
    active: true
 | 
			
		||||
    maxJumpCount: 1
 | 
			
		||||
  MagicNumber:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ]
 | 
			
		||||
    ignoreNumbers:
 | 
			
		||||
      - '-1'
 | 
			
		||||
      - '0'
 | 
			
		||||
      - '1'
 | 
			
		||||
      - '2'
 | 
			
		||||
    ignoreHashCodeFunction: true
 | 
			
		||||
    ignorePropertyDeclaration: false
 | 
			
		||||
    ignoreLocalVariableDeclaration: false
 | 
			
		||||
    ignoreConstantDeclaration: true
 | 
			
		||||
    ignoreCompanionObjectPropertyDeclaration: true
 | 
			
		||||
    ignoreAnnotation: false
 | 
			
		||||
    ignoreNamedArgument: true
 | 
			
		||||
    ignoreEnums: false
 | 
			
		||||
    ignoreRanges: false
 | 
			
		||||
    ignoreExtensionFunctions: true
 | 
			
		||||
  MandatoryBracesLoops:
 | 
			
		||||
    active: false
 | 
			
		||||
  MaxChainedCallsOnSameLine:
 | 
			
		||||
    active: false
 | 
			
		||||
    maxChainedCalls: 5
 | 
			
		||||
  MaxLineLength:
 | 
			
		||||
    active: false # done in ktlint
 | 
			
		||||
    maxLineLength: 140 # default is 120. 140 to match ktlint
 | 
			
		||||
    excludePackageStatements: true
 | 
			
		||||
    excludeImportStatements: true
 | 
			
		||||
    excludeCommentStatements: false
 | 
			
		||||
    excludeRawStrings: true
 | 
			
		||||
  MayBeConst:
 | 
			
		||||
    active: true
 | 
			
		||||
  ModifierOrder:
 | 
			
		||||
    active: true
 | 
			
		||||
  MultilineLambdaItParameter:
 | 
			
		||||
    active: false
 | 
			
		||||
  MultilineRawStringIndentation:
 | 
			
		||||
    active: false
 | 
			
		||||
    indentSize: 4
 | 
			
		||||
    trimmingMethods:
 | 
			
		||||
      - 'trimIndent'
 | 
			
		||||
      - 'trimMargin'
 | 
			
		||||
  NestedClassesVisibility:
 | 
			
		||||
    active: true
 | 
			
		||||
  NewLineAtEndOfFile:
 | 
			
		||||
    active: false # done in ktlint
 | 
			
		||||
  NoTabs:
 | 
			
		||||
    active: false
 | 
			
		||||
  NullableBooleanCheck:
 | 
			
		||||
    active: false
 | 
			
		||||
  ObjectLiteralToLambda:
 | 
			
		||||
    active: true
 | 
			
		||||
  OptionalAbstractKeyword:
 | 
			
		||||
    active: true
 | 
			
		||||
  OptionalUnit:
 | 
			
		||||
    active: false
 | 
			
		||||
  PreferToOverPairSyntax:
 | 
			
		||||
    active: false
 | 
			
		||||
  ProtectedMemberInFinalClass:
 | 
			
		||||
    active: true
 | 
			
		||||
  RedundantExplicitType:
 | 
			
		||||
    active: false
 | 
			
		||||
  RedundantHigherOrderMapUsage:
 | 
			
		||||
    active: true
 | 
			
		||||
  RedundantVisibilityModifierRule:
 | 
			
		||||
    active: false
 | 
			
		||||
  ReturnCount:
 | 
			
		||||
    active: true
 | 
			
		||||
    max: 2
 | 
			
		||||
    excludedFunctions:
 | 
			
		||||
      - 'equals'
 | 
			
		||||
    excludeLabeled: false
 | 
			
		||||
    excludeReturnFromLambda: true
 | 
			
		||||
    excludeGuardClauses: false
 | 
			
		||||
  SafeCast:
 | 
			
		||||
    active: true
 | 
			
		||||
  SerialVersionUIDInSerializableClass:
 | 
			
		||||
    active: true
 | 
			
		||||
  SpacingBetweenPackageAndImports:
 | 
			
		||||
    active: false
 | 
			
		||||
  StringShouldBeRawString:
 | 
			
		||||
    active: false
 | 
			
		||||
    maxEscapedCharacterCount: 2
 | 
			
		||||
    ignoredCharacters: [ ]
 | 
			
		||||
  ThrowsCount:
 | 
			
		||||
    active: true
 | 
			
		||||
    max: 2
 | 
			
		||||
    excludeGuardClauses: false
 | 
			
		||||
  TrailingWhitespace:
 | 
			
		||||
    active: false
 | 
			
		||||
  TrimMultilineRawString:
 | 
			
		||||
    active: false
 | 
			
		||||
    trimmingMethods:
 | 
			
		||||
      - 'trimIndent'
 | 
			
		||||
      - 'trimMargin'
 | 
			
		||||
  UnderscoresInNumericLiterals:
 | 
			
		||||
    active: false
 | 
			
		||||
    acceptableLength: 4
 | 
			
		||||
    allowNonStandardGrouping: false
 | 
			
		||||
  UnnecessaryAbstractClass:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnnecessaryAnnotationUseSiteTarget:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryApply:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnnecessaryBackticks:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryBracesAroundTrailingLambda:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryFilter:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnnecessaryInheritance:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnnecessaryInnerClass:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryLet:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnnecessaryParentheses:
 | 
			
		||||
    active: false
 | 
			
		||||
    allowForUnclearPrecedence: false
 | 
			
		||||
  UntilInsteadOfRangeTo:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnusedImports:
 | 
			
		||||
    active: false
 | 
			
		||||
  UnusedParameter:
 | 
			
		||||
    active: true
 | 
			
		||||
    allowedNames: 'ignored|expected'
 | 
			
		||||
  UnusedPrivateClass:
 | 
			
		||||
    active: true
 | 
			
		||||
  UnusedPrivateMember:
 | 
			
		||||
    active: true
 | 
			
		||||
    allowedNames: ''
 | 
			
		||||
  UnusedPrivateProperty:
 | 
			
		||||
    active: true
 | 
			
		||||
    allowedNames: '_|ignored|expected|serialVersionUID'
 | 
			
		||||
    excludes: [ '**/build.gradle.kts' ]
 | 
			
		||||
  UseAnyOrNoneInsteadOfFind:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseArrayLiteralsInAnnotations:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseCheckNotNull:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseCheckOrError:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseDataClass:
 | 
			
		||||
    active: false
 | 
			
		||||
    allowVars: false
 | 
			
		||||
  UseEmptyCounterpart:
 | 
			
		||||
    active: false
 | 
			
		||||
  UseIfEmptyOrIfBlank:
 | 
			
		||||
    active: false
 | 
			
		||||
  UseIfInsteadOfWhen:
 | 
			
		||||
    active: false
 | 
			
		||||
    ignoreWhenContainingVariableDeclaration: false
 | 
			
		||||
  UseIsNullOrEmpty:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseLet:
 | 
			
		||||
    active: false
 | 
			
		||||
  UseOrEmpty:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseRequire:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseRequireNotNull:
 | 
			
		||||
    active: true
 | 
			
		||||
  UseSumOfInsteadOfFlatMapSize:
 | 
			
		||||
    active: false
 | 
			
		||||
  UselessCallOnNotNull:
 | 
			
		||||
    active: true
 | 
			
		||||
  UtilityClassWithPublicConstructor:
 | 
			
		||||
    active: true
 | 
			
		||||
  VarCouldBeVal:
 | 
			
		||||
    active: true
 | 
			
		||||
    ignoreLateinitVar: false
 | 
			
		||||
  WildcardImport:
 | 
			
		||||
    active: true
 | 
			
		||||
    excludeImports:
 | 
			
		||||
      - 'java.util.*'
 | 
			
		||||
@@ -4,12 +4,13 @@ import android.content.Context
 | 
			
		||||
import app.cash.sqldelight.db.SqlDriver
 | 
			
		||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
 | 
			
		||||
 | 
			
		||||
actual class DriverFactory(private val context: Context) {
 | 
			
		||||
    actual fun createDriver(): SqlDriver {
 | 
			
		||||
        return AndroidSqliteDriver(
 | 
			
		||||
actual class DriverFactory(
 | 
			
		||||
    private val context: Context,
 | 
			
		||||
) {
 | 
			
		||||
    actual fun createDriver(): SqlDriver =
 | 
			
		||||
        AndroidSqliteDriver(
 | 
			
		||||
            ReaderForSelfossDB.Schema,
 | 
			
		||||
            context,
 | 
			
		||||
            "ReaderForSelfossV2-android.db"
 | 
			
		||||
            "ReaderForSelfossV2-android.db",
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -8,16 +8,20 @@ class NaiveTrustManager : X509TrustManager {
 | 
			
		||||
    override fun checkClientTrusted(
 | 
			
		||||
        chain: Array<out X509Certificate>?,
 | 
			
		||||
        authType: String?,
 | 
			
		||||
    ) {}
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun checkServerTrusted(
 | 
			
		||||
        chain: Array<out X509Certificate>?,
 | 
			
		||||
        authType: String?,
 | 
			
		||||
    ) {}
 | 
			
		||||
    ) {
 | 
			
		||||
        // Nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
 | 
			
		||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
 | 
			
		||||
    config.https.trustManager = NaiveTrustManager()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import io.github.aakira.napier.Napier
 | 
			
		||||
import kotlinx.datetime.*
 | 
			
		||||
import kotlinx.datetime.Clock
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
			
		||||
actual class DateUtils {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,46 +4,36 @@ import android.net.Uri
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
actual fun String.getHtmlDecoded(): String {
 | 
			
		||||
    return Html.fromHtml(this).toString()
 | 
			
		||||
}
 | 
			
		||||
actual fun String.getHtmlDecoded(): String = Html.fromHtml(this).toString()
 | 
			
		||||
 | 
			
		||||
actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
 | 
			
		||||
    return constructUrl(baseUrl, "favicons", icon)
 | 
			
		||||
}
 | 
			
		||||
actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
 | 
			
		||||
 | 
			
		||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
 | 
			
		||||
    return constructUrl(baseUrl, "thumbnails", thumbnail)
 | 
			
		||||
}
 | 
			
		||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
 | 
			
		||||
 | 
			
		||||
val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
 | 
			
		||||
 | 
			
		||||
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
 | 
			
		||||
    val allImages = ArrayList<String>()
 | 
			
		||||
 | 
			
		||||
    for (image in Jsoup.parse(content).getElementsByTag("img")) {
 | 
			
		||||
        val url = image.attr("src")
 | 
			
		||||
        if (url.lowercase(Locale.US).contains(".jpg") ||
 | 
			
		||||
            url.lowercase(Locale.US).contains(".jpeg") ||
 | 
			
		||||
            url.lowercase(Locale.US).contains(".png") ||
 | 
			
		||||
            url.lowercase(Locale.US).contains(".webp")
 | 
			
		||||
        ) {
 | 
			
		||||
        if (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) {
 | 
			
		||||
            allImages.add(url)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return allImages
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
 | 
			
		||||
    return constructUrl(baseUrl, "favicons", icon)
 | 
			
		||||
}
 | 
			
		||||
actual fun SelfossModel.Source.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon)
 | 
			
		||||
 | 
			
		||||
actual fun constructUrl(
 | 
			
		||||
    baseUrl: String,
 | 
			
		||||
    path: String,
 | 
			
		||||
    file: String?,
 | 
			
		||||
): String {
 | 
			
		||||
    return if (file == null || file == "null" || file.isEmpty()) {
 | 
			
		||||
): String =
 | 
			
		||||
    if (file == null || file == "null" || file.isEmpty()) {
 | 
			
		||||
        ""
 | 
			
		||||
    } else {
 | 
			
		||||
        val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
 | 
			
		||||
@@ -51,4 +41,3 @@ actual fun constructUrl(
 | 
			
		||||
 | 
			
		||||
        baseUriBuilder.toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,4 +4,4 @@ import app.cash.sqldelight.db.SqlDriver
 | 
			
		||||
 | 
			
		||||
expect class DriverFactory {
 | 
			
		||||
    fun createDriver(): SqlDriver
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.DI
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.di
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
@file:Suppress("detekt:LongParameterList")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.model
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
class MercuryModel {
 | 
			
		||||
    @Suppress("detekt:ConstructorParameterNaming")
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class ParsedContent(
 | 
			
		||||
        val title: String? = null,
 | 
			
		||||
        val content: String? = null,
 | 
			
		||||
        val lead_image_url: String? = null, // NOSONAR
 | 
			
		||||
        val lead_image_url: String? = null,
 | 
			
		||||
        val url: String? = null,
 | 
			
		||||
        val error: Boolean? = null,
 | 
			
		||||
        val message: String? = null,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,20 @@ package bou.amine.apps.readerforselfossv2.model
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class SuccessResponse(val success: Boolean) {
 | 
			
		||||
class SuccessResponse(
 | 
			
		||||
    val success: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    val isSuccess: Boolean
 | 
			
		||||
        get() = success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class StatusAndData<T>(val success: Boolean, val data: T? = null) {
 | 
			
		||||
class StatusAndData<T>(
 | 
			
		||||
    val success: Boolean,
 | 
			
		||||
    val data: T? = null,
 | 
			
		||||
) {
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <T> succes(d: T): StatusAndData<T> {
 | 
			
		||||
            return StatusAndData(true, d)
 | 
			
		||||
        }
 | 
			
		||||
        fun <T> succes(d: T): StatusAndData<T> = StatusAndData(true, d)
 | 
			
		||||
 | 
			
		||||
        fun <T> error(): StatusAndData<T> {
 | 
			
		||||
            return StatusAndData(false)
 | 
			
		||||
        }
 | 
			
		||||
        fun <T> error(): StatusAndData<T> = StatusAndData(false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("detekt:LongParameterList")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.model
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
 | 
			
		||||
@@ -18,6 +20,10 @@ import kotlinx.serialization.json.booleanOrNull
 | 
			
		||||
import kotlinx.serialization.json.int
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
 | 
			
		||||
class ModelException(
 | 
			
		||||
    message: String,
 | 
			
		||||
) : Throwable(message)
 | 
			
		||||
 | 
			
		||||
class SelfossModel {
 | 
			
		||||
    @Serializable
 | 
			
		||||
    data class Tag(
 | 
			
		||||
@@ -141,7 +147,7 @@ class SelfossModel {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (stringUrl.isEmptyOrNullOrNullString()) {
 | 
			
		||||
                throw Exception("Link ${link} was translated to ${stringUrl}, but was empty. Handle this.")
 | 
			
		||||
                throw ModelException("Link $link was translated to $stringUrl, but was empty. Handle this.")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return stringUrl
 | 
			
		||||
@@ -170,14 +176,13 @@ class SelfossModel {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: this seems to be super slow.
 | 
			
		||||
    // this seems to be super slow.
 | 
			
		||||
    object TagsListSerializer : KSerializer<List<String>> {
 | 
			
		||||
        override fun deserialize(decoder: Decoder): List<String> {
 | 
			
		||||
            return when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
 | 
			
		||||
        override fun deserialize(decoder: Decoder): List<String> =
 | 
			
		||||
            when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
 | 
			
		||||
                is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") }
 | 
			
		||||
                else -> json.toString().split(",")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override val descriptor: SerialDescriptor
 | 
			
		||||
            get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
 | 
			
		||||
@@ -188,7 +193,7 @@ class SelfossModel {
 | 
			
		||||
        ) {
 | 
			
		||||
            encoder.encodeCollection(
 | 
			
		||||
                PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING),
 | 
			
		||||
                value.size
 | 
			
		||||
                value.size,
 | 
			
		||||
            ) { this.toString() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -204,10 +209,11 @@ class SelfossModel {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override val descriptor: SerialDescriptor
 | 
			
		||||
            get() = PrimitiveSerialDescriptor(
 | 
			
		||||
                "BooleanOrIntForSomeSelfossVersions",
 | 
			
		||||
                PrimitiveKind.BOOLEAN
 | 
			
		||||
            )
 | 
			
		||||
            get() =
 | 
			
		||||
                PrimitiveSerialDescriptor(
 | 
			
		||||
                    "BooleanOrIntForSomeSelfossVersions",
 | 
			
		||||
                    PrimitiveKind.BOOLEAN,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        override fun serialize(
 | 
			
		||||
            encoder: Encoder,
 | 
			
		||||
@@ -216,4 +222,4 @@ class SelfossModel {
 | 
			
		||||
            TODO("Not yet implemented")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,23 @@
 | 
			
		||||
@file:Suppress("detekt:TooManyFunctions")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.repository
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.*
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ACTION
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ITEM
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.SOURCE
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.dao.TAG
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.*
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toEntity
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.utils.toView
 | 
			
		||||
import io.github.aakira.napier.Napier
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
@@ -14,6 +25,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
 | 
			
		||||
import kotlinx.coroutines.flow.asStateFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
private const val MAX_ITEMS_NUMBER = 200
 | 
			
		||||
 | 
			
		||||
class Repository(
 | 
			
		||||
    private val api: SelfossApi,
 | 
			
		||||
    private val appSettingsService: AppSettingsService,
 | 
			
		||||
@@ -118,7 +131,7 @@ class Repository(
 | 
			
		||||
                    null,
 | 
			
		||||
                    null,
 | 
			
		||||
                    null,
 | 
			
		||||
                    200,
 | 
			
		||||
                    MAX_ITEMS_NUMBER,
 | 
			
		||||
                )
 | 
			
		||||
            return if (items.success && items.data != null) {
 | 
			
		||||
                items.data
 | 
			
		||||
@@ -130,6 +143,7 @@ class Repository(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:ForbiddenComment")
 | 
			
		||||
    suspend fun reloadBadges(): Boolean {
 | 
			
		||||
        var success = false
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
@@ -170,8 +184,8 @@ class Repository(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            val spouts = api.spouts()
 | 
			
		||||
            if (spouts.success && spouts.data != null) {
 | 
			
		||||
                spouts.data
 | 
			
		||||
@@ -181,7 +195,6 @@ class Repository(
 | 
			
		||||
        } else {
 | 
			
		||||
            throw NetworkUnavailableException()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
 | 
			
		||||
        var sources = ArrayList<SelfossModel.Source>()
 | 
			
		||||
@@ -234,14 +247,13 @@ class Repository(
 | 
			
		||||
        return success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun markAsReadById(id: Int): Boolean {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    private suspend fun markAsReadById(id: Int): Boolean =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            api.markAsRead(id.toString()).isSuccess
 | 
			
		||||
        } else {
 | 
			
		||||
            insertDBAction(id.toString(), read = true)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
 | 
			
		||||
        val success = unmarkAsReadById(item.id)
 | 
			
		||||
@@ -252,14 +264,13 @@ class Repository(
 | 
			
		||||
        return success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun unmarkAsReadById(id: Int): Boolean {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    private suspend fun unmarkAsReadById(id: Int): Boolean =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            api.unmarkAsRead(id.toString()).isSuccess
 | 
			
		||||
        } else {
 | 
			
		||||
            insertDBAction(id.toString(), unread = true)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun starr(item: SelfossModel.Item): Boolean {
 | 
			
		||||
        val success = starrById(item.id)
 | 
			
		||||
@@ -270,14 +281,13 @@ class Repository(
 | 
			
		||||
        return success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun starrById(id: Int): Boolean {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    private suspend fun starrById(id: Int): Boolean =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            api.starr(id.toString()).isSuccess
 | 
			
		||||
        } else {
 | 
			
		||||
            insertDBAction(id.toString(), starred = true)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun unstarr(item: SelfossModel.Item): Boolean {
 | 
			
		||||
        val success = unstarrById(item.id)
 | 
			
		||||
@@ -288,14 +298,13 @@ class Repository(
 | 
			
		||||
        return success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun unstarrById(id: Int): Boolean {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    private suspend fun unstarrById(id: Int): Boolean =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            api.unstarr(id.toString()).isSuccess
 | 
			
		||||
        } else {
 | 
			
		||||
            insertDBAction(id.toString(), starred = true)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
 | 
			
		||||
        var success = false
 | 
			
		||||
@@ -361,12 +370,13 @@ class Repository(
 | 
			
		||||
    ): Boolean {
 | 
			
		||||
        var response = false
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            response = api.createSourceForVersion(
 | 
			
		||||
                title,
 | 
			
		||||
                url,
 | 
			
		||||
                spout,
 | 
			
		||||
                tags,
 | 
			
		||||
            ).isSuccess == true
 | 
			
		||||
            response = api
 | 
			
		||||
                .createSourceForVersion(
 | 
			
		||||
                    title,
 | 
			
		||||
                    url,
 | 
			
		||||
                    spout,
 | 
			
		||||
                    tags,
 | 
			
		||||
                ).isSuccess == true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response
 | 
			
		||||
@@ -407,13 +417,12 @@ class Repository(
 | 
			
		||||
        return success
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun updateRemote(): Boolean {
 | 
			
		||||
        return if (isNetworkAvailable()) {
 | 
			
		||||
    suspend fun updateRemote(): Boolean =
 | 
			
		||||
        if (isNetworkAvailable()) {
 | 
			
		||||
            api.update().data.equals("finished")
 | 
			
		||||
        } else {
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun login(): Boolean {
 | 
			
		||||
        var result = false
 | 
			
		||||
@@ -422,7 +431,7 @@ class Repository(
 | 
			
		||||
                val response = api.login()
 | 
			
		||||
                result = response.isSuccess == true
 | 
			
		||||
            } catch (cause: Throwable) {
 | 
			
		||||
                Napier.e("login failed", cause, tag = "RepositoryImpl.login")
 | 
			
		||||
                Napier.e("login failed", cause, tag = "Repository.login")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result
 | 
			
		||||
@@ -436,7 +445,7 @@ class Repository(
 | 
			
		||||
                // a random rss feed, that would throw a NoTransformationFoundException
 | 
			
		||||
                fetchFailed = !api.getItemsWithoutCatch().success
 | 
			
		||||
            } catch (e: Throwable) {
 | 
			
		||||
                Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance")
 | 
			
		||||
                Napier.e("checkIfFetchFails failed", e, tag = "Repository.shouldBeSelfossInstance")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -448,10 +457,10 @@ class Repository(
 | 
			
		||||
            try {
 | 
			
		||||
                val response = api.logout()
 | 
			
		||||
                if (!response.isSuccess) {
 | 
			
		||||
                    Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
 | 
			
		||||
                    Napier.e("Couldn't logout.", tag = "Repository.logout")
 | 
			
		||||
                }
 | 
			
		||||
            } catch (cause: Throwable) {
 | 
			
		||||
                Napier.e("logout failed", cause, tag = "RepositoryImpl.logout")
 | 
			
		||||
                Napier.e("logout failed", cause, tag = "Repository.logout")
 | 
			
		||||
            }
 | 
			
		||||
            appSettingsService.clearAll()
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -555,6 +564,7 @@ class Repository(
 | 
			
		||||
            item.id.toString(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
 | 
			
		||||
        try {
 | 
			
		||||
            val newItems = getMaxItemsForBackground(ItemType.UNREAD)
 | 
			
		||||
@@ -578,16 +588,19 @@ class Repository(
 | 
			
		||||
                        markAsReadById(action.articleid.toInt()),
 | 
			
		||||
                        action,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                action.unread ->
 | 
			
		||||
                    doAndReportOnFail(
 | 
			
		||||
                        unmarkAsReadById(action.articleid.toInt()),
 | 
			
		||||
                        action,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                action.starred ->
 | 
			
		||||
                    doAndReportOnFail(
 | 
			
		||||
                        starrById(action.articleid.toInt()),
 | 
			
		||||
                        action,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                action.unstarred ->
 | 
			
		||||
                    doAndReportOnFail(
 | 
			
		||||
                        unstarrById(action.articleid.toInt()),
 | 
			
		||||
@@ -618,9 +631,7 @@ class Repository(
 | 
			
		||||
        _readerItems = readerItems
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getReaderItems(): ArrayList<SelfossModel.Item> {
 | 
			
		||||
        return _readerItems
 | 
			
		||||
    }
 | 
			
		||||
    fun getReaderItems(): ArrayList<SelfossModel.Item> = _readerItems
 | 
			
		||||
 | 
			
		||||
    fun migrate(driverFactory: DriverFactory) {
 | 
			
		||||
        ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
 | 
			
		||||
@@ -634,7 +645,5 @@ class Repository(
 | 
			
		||||
        _selectedSource = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getSelectedSource(): SelfossModel.SourceDetail? {
 | 
			
		||||
        return _selectedSource
 | 
			
		||||
    }
 | 
			
		||||
    fun getSelectedSource(): SelfossModel.SourceDetail? = _selectedSource
 | 
			
		||||
}
 | 
			
		||||
@@ -17,8 +17,8 @@ import kotlinx.serialization.json.Json
 | 
			
		||||
class MercuryApi {
 | 
			
		||||
    var client = createHttpClient()
 | 
			
		||||
 | 
			
		||||
    private fun createHttpClient(): HttpClient {
 | 
			
		||||
        return HttpClient {
 | 
			
		||||
    private fun createHttpClient(): HttpClient =
 | 
			
		||||
        HttpClient {
 | 
			
		||||
            install(HttpCache)
 | 
			
		||||
            install(ContentNegotiation) {
 | 
			
		||||
                json(
 | 
			
		||||
@@ -40,7 +40,6 @@ class MercuryApi {
 | 
			
		||||
            }
 | 
			
		||||
            expectSuccess = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
 | 
			
		||||
        bodyOrFailure(
 | 
			
		||||
@@ -48,4 +47,4 @@ class MercuryApi {
 | 
			
		||||
                parameter("link", url)
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,28 @@ package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SuccessResponse
 | 
			
		||||
import io.github.aakira.napier.Napier
 | 
			
		||||
import io.ktor.client.*
 | 
			
		||||
import io.ktor.client.call.*
 | 
			
		||||
import io.ktor.client.request.*
 | 
			
		||||
import io.ktor.client.request.forms.*
 | 
			
		||||
import io.ktor.client.statement.*
 | 
			
		||||
import io.ktor.http.*
 | 
			
		||||
import io.ktor.client.HttpClient
 | 
			
		||||
import io.ktor.client.call.body
 | 
			
		||||
import io.ktor.client.request.HttpRequestBuilder
 | 
			
		||||
import io.ktor.client.request.delete
 | 
			
		||||
import io.ktor.client.request.forms.submitForm
 | 
			
		||||
import io.ktor.client.request.get
 | 
			
		||||
import io.ktor.client.request.post
 | 
			
		||||
import io.ktor.client.request.url
 | 
			
		||||
import io.ktor.client.statement.HttpResponse
 | 
			
		||||
import io.ktor.http.HttpStatusCode
 | 
			
		||||
import io.ktor.http.Parameters
 | 
			
		||||
import io.ktor.http.isSuccess
 | 
			
		||||
 | 
			
		||||
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse {
 | 
			
		||||
    return if (r != null && r.status === HttpStatusCode.NotFound) {
 | 
			
		||||
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse =
 | 
			
		||||
    if (r != null && r.status === HttpStatusCode.NotFound) {
 | 
			
		||||
        SuccessResponse(true)
 | 
			
		||||
    } else {
 | 
			
		||||
        maybeResponse(r)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
 | 
			
		||||
    return if (r != null && r.status.isSuccess()) {
 | 
			
		||||
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
 | 
			
		||||
    if (r != null && r.status.isSuccess()) {
 | 
			
		||||
        r.body()
 | 
			
		||||
    } else {
 | 
			
		||||
        if (r != null) {
 | 
			
		||||
@@ -27,8 +32,8 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
 | 
			
		||||
        }
 | 
			
		||||
        SuccessResponse(false)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:SwallowedException")
 | 
			
		||||
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
 | 
			
		||||
    try {
 | 
			
		||||
        return if (r != null && r.status.isSuccess()) {
 | 
			
		||||
@@ -98,4 +103,4 @@ suspend fun HttpClient.tryToSubmitForm(
 | 
			
		||||
            url(url)
 | 
			
		||||
            block()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("detekt:TooManyFunctions", "detekt:LongParameterList", "detekt:LargeClass")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
 | 
			
		||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
 | 
			
		||||
@@ -33,16 +35,20 @@ import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
 | 
			
		||||
expect fun setupInsecureHTTPEngine(config: CIOEngineConfig)
 | 
			
		||||
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
 | 
			
		||||
 | 
			
		||||
class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
private const val VERSION_WHERE_POST_LOGIN_SHOULD_WORK = 5
 | 
			
		||||
 | 
			
		||||
class SelfossApi(
 | 
			
		||||
    private val appSettingsService: AppSettingsService,
 | 
			
		||||
) {
 | 
			
		||||
    var client = createHttpClient()
 | 
			
		||||
 | 
			
		||||
    fun createHttpClient() =
 | 
			
		||||
        HttpClient(CIO) {
 | 
			
		||||
            if (appSettingsService.getSelfSigned()) {
 | 
			
		||||
                engine {
 | 
			
		||||
                    setupInsecureHTTPEngine(this)
 | 
			
		||||
                    setupInsecureHttpEngine(this)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            install(HttpCache)
 | 
			
		||||
@@ -105,12 +111,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
 | 
			
		||||
    private fun hasLoginInfo() =
 | 
			
		||||
        appSettingsService.getUserName().isNotEmpty() &&
 | 
			
		||||
                appSettingsService.getPassword()
 | 
			
		||||
                    .isNotEmpty()
 | 
			
		||||
            appSettingsService
 | 
			
		||||
                .getPassword()
 | 
			
		||||
                .isNotEmpty()
 | 
			
		||||
 | 
			
		||||
    suspend fun login(): SuccessResponse =
 | 
			
		||||
        if (appSettingsService.getUserName().isNotEmpty() &&
 | 
			
		||||
            appSettingsService.getPassword()
 | 
			
		||||
            appSettingsService
 | 
			
		||||
                .getPassword()
 | 
			
		||||
                .isNotEmpty()
 | 
			
		||||
        ) {
 | 
			
		||||
            if (shouldHavePostLogin()) {
 | 
			
		||||
@@ -127,8 +135,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            client.tryToGet(url("/login")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -150,8 +160,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            client.tryToPost(url("/login")) {
 | 
			
		||||
                parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -168,8 +180,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    private fun shouldHaveNewLogout() =
 | 
			
		||||
        appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
 | 
			
		||||
    private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= VERSION_WHERE_POST_LOGIN_SHOULD_WORK // We are missing 4.1.0
 | 
			
		||||
 | 
			
		||||
    suspend fun logout(): SuccessResponse =
 | 
			
		||||
        if (shouldHaveNewLogout()) {
 | 
			
		||||
@@ -181,8 +192,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
    private suspend fun maybeLogoutIfAvailable() =
 | 
			
		||||
        responseOrSuccessIf404(
 | 
			
		||||
            client.tryToGet(url("/logout")) {
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -202,8 +215,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
    private suspend fun doLogout() =
 | 
			
		||||
        maybeResponse(
 | 
			
		||||
            client.tryToDelete(url("/api/session/current")) {
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -242,8 +257,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                parameter("updatedsince", updatedSince)
 | 
			
		||||
                parameter("items", items ?: appSettingsService.getItemsNumber())
 | 
			
		||||
                parameter("offset", offset)
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -269,8 +286,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                }
 | 
			
		||||
                parameter("type", "all")
 | 
			
		||||
                parameter("items", 1)
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -294,8 +313,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -319,8 +340,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -344,8 +367,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -369,8 +394,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -394,8 +421,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -419,8 +448,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -440,8 +471,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
    suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
 | 
			
		||||
        bodyOrFailure(
 | 
			
		||||
            client.tryToGet(url("/api/about")) {
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -465,8 +498,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -490,8 +525,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -515,8 +552,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -540,8 +579,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -563,16 +604,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
            client.tryToSubmitForm(
 | 
			
		||||
                url = url("/mark"),
 | 
			
		||||
                formParameters =
 | 
			
		||||
                Parameters.build {
 | 
			
		||||
                    if (!shouldHavePostLogin()) {
 | 
			
		||||
                        append("username", appSettingsService.getUserName())
 | 
			
		||||
                        append("password", appSettingsService.getPassword())
 | 
			
		||||
                    }
 | 
			
		||||
                    ids.map { append("ids[]", it) }
 | 
			
		||||
                },
 | 
			
		||||
                    Parameters.build {
 | 
			
		||||
                        if (!shouldHavePostLogin()) {
 | 
			
		||||
                            append("username", appSettingsService.getUserName())
 | 
			
		||||
                            append("password", appSettingsService.getPassword())
 | 
			
		||||
                        }
 | 
			
		||||
                        ids.map { append("ids[]", it) }
 | 
			
		||||
                    },
 | 
			
		||||
                block = {
 | 
			
		||||
                    if (appSettingsService.getBasicUserName()
 | 
			
		||||
                            .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                    if (appSettingsService
 | 
			
		||||
                            .getBasicUserName()
 | 
			
		||||
                            .isNotEmpty() &&
 | 
			
		||||
                        appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                    ) {
 | 
			
		||||
                        headers {
 | 
			
		||||
                            append(
 | 
			
		||||
@@ -614,19 +657,21 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
        client.tryToSubmitForm(
 | 
			
		||||
            url = url("/source"),
 | 
			
		||||
            formParameters =
 | 
			
		||||
            Parameters.build {
 | 
			
		||||
                if (!shouldHavePostLogin()) {
 | 
			
		||||
                    append("username", appSettingsService.getUserName())
 | 
			
		||||
                    append("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                append("title", title)
 | 
			
		||||
                append("url", url)
 | 
			
		||||
                append("spout", spout)
 | 
			
		||||
                append(tagsParamName, tags)
 | 
			
		||||
            },
 | 
			
		||||
                Parameters.build {
 | 
			
		||||
                    if (!shouldHavePostLogin()) {
 | 
			
		||||
                        append("username", appSettingsService.getUserName())
 | 
			
		||||
                        append("password", appSettingsService.getPassword())
 | 
			
		||||
                    }
 | 
			
		||||
                    append("title", title)
 | 
			
		||||
                    append("url", url)
 | 
			
		||||
                    append("spout", spout)
 | 
			
		||||
                    append(tagsParamName, tags)
 | 
			
		||||
                },
 | 
			
		||||
            block = {
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -669,19 +714,21 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
        client.tryToSubmitForm(
 | 
			
		||||
            url = url("/source/$id"),
 | 
			
		||||
            formParameters =
 | 
			
		||||
            Parameters.build {
 | 
			
		||||
                if (!shouldHavePostLogin()) {
 | 
			
		||||
                    append("username", appSettingsService.getUserName())
 | 
			
		||||
                    append("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                append("title", title)
 | 
			
		||||
                append("url", url)
 | 
			
		||||
                append("spout", spout)
 | 
			
		||||
                append(tagsParamName, tags)
 | 
			
		||||
            },
 | 
			
		||||
                Parameters.build {
 | 
			
		||||
                    if (!shouldHavePostLogin()) {
 | 
			
		||||
                        append("username", appSettingsService.getUserName())
 | 
			
		||||
                        append("password", appSettingsService.getPassword())
 | 
			
		||||
                    }
 | 
			
		||||
                    append("title", title)
 | 
			
		||||
                    append("url", url)
 | 
			
		||||
                    append("spout", spout)
 | 
			
		||||
                    append(tagsParamName, tags)
 | 
			
		||||
                },
 | 
			
		||||
            block = {
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -705,8 +752,10 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                    parameter("username", appSettingsService.getUserName())
 | 
			
		||||
                    parameter("password", appSettingsService.getPassword())
 | 
			
		||||
                }
 | 
			
		||||
                if (appSettingsService.getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                if (appSettingsService
 | 
			
		||||
                        .getBasicUserName()
 | 
			
		||||
                        .isNotEmpty() &&
 | 
			
		||||
                    appSettingsService.getBasicPassword().isNotEmpty()
 | 
			
		||||
                ) {
 | 
			
		||||
                    headers {
 | 
			
		||||
                        append(
 | 
			
		||||
@@ -722,4 +771,4 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
@file:Suppress("detekt:TooManyFunctions")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.service
 | 
			
		||||
 | 
			
		||||
import com.russhwolf.settings.Settings
 | 
			
		||||
@@ -121,4 +123,4 @@ class ACRASettings : Settings {
 | 
			
		||||
        longs.remove(key)
 | 
			
		||||
        strings.remove(key)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,22 @@
 | 
			
		||||
@file:Suppress("detekt:TooManyFunctions")
 | 
			
		||||
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.service
 | 
			
		||||
 | 
			
		||||
import com.russhwolf.settings.Settings
 | 
			
		||||
 | 
			
		||||
class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
private const val DEFAULT_FONT_SIZE = 16
 | 
			
		||||
 | 
			
		||||
private const val DEFAULT_REFRESH_MINUTES = 360L
 | 
			
		||||
 | 
			
		||||
private const val MIN_REFRESH_MINUTES = 15L
 | 
			
		||||
 | 
			
		||||
private const val DEFAULT_API_TIMEOUT = 60L
 | 
			
		||||
 | 
			
		||||
private const val DEFAULT_ITEMS_NUMBER = 20
 | 
			
		||||
 | 
			
		||||
class AppSettingsService(
 | 
			
		||||
    acraSenderServiceProcess: Boolean = false,
 | 
			
		||||
) {
 | 
			
		||||
    val settings: Settings =
 | 
			
		||||
        if (acraSenderServiceProcess) {
 | 
			
		||||
            ACRASettings()
 | 
			
		||||
@@ -11,37 +25,37 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    // Api related
 | 
			
		||||
    private var _apiVersion: Int = -1
 | 
			
		||||
    private var _publicAccess: Boolean? = null
 | 
			
		||||
    private var _selfSigned: Boolean? = null
 | 
			
		||||
    private var _baseUrl: String = ""
 | 
			
		||||
    private var _userName: String = ""
 | 
			
		||||
    private var _basicUserName: String = ""
 | 
			
		||||
    private var _password: String = ""
 | 
			
		||||
    private var _basicPassword: String = ""
 | 
			
		||||
    private var apiVersion: Int = -1
 | 
			
		||||
    private var publicAccess: Boolean? = null
 | 
			
		||||
    private var selfSigned: Boolean? = null
 | 
			
		||||
    private var baseUrl: String = ""
 | 
			
		||||
    private var userName: String = ""
 | 
			
		||||
    private var basicUserName: String = ""
 | 
			
		||||
    private var password: String = ""
 | 
			
		||||
    private var basicPassword: String = ""
 | 
			
		||||
 | 
			
		||||
    // User settings related
 | 
			
		||||
    private var _itemsCaching: Boolean? = null
 | 
			
		||||
    private var _articleViewer: Boolean? = null
 | 
			
		||||
    private var _shouldBeCardView: Boolean? = null
 | 
			
		||||
    private var _displayUnreadCount: Boolean? = null
 | 
			
		||||
    private var _displayAllCount: Boolean? = null
 | 
			
		||||
    private var _fullHeightCards: Boolean? = null
 | 
			
		||||
    private var _updateSources: Boolean? = null
 | 
			
		||||
    private var _periodicRefresh: Boolean? = null
 | 
			
		||||
    private var _refreshWhenChargingOnly: Boolean? = null
 | 
			
		||||
    private var _infiniteLoading: Boolean? = null
 | 
			
		||||
    private var _notifyNewItems: Boolean? = null
 | 
			
		||||
    private var _itemsNumber: Int? = null
 | 
			
		||||
    private var _apiTimeout: Long? = null
 | 
			
		||||
    private var _refreshMinutes: Long = 360
 | 
			
		||||
    private var _markOnScroll: Boolean? = null
 | 
			
		||||
    private var _activeAlignment: Int? = null
 | 
			
		||||
    private var itemsCaching: Boolean? = null
 | 
			
		||||
    private var articleViewer: Boolean? = null
 | 
			
		||||
    private var shouldBeCardView: Boolean? = null
 | 
			
		||||
    private var displayUnreadCount: Boolean? = null
 | 
			
		||||
    private var displayAllCount: Boolean? = null
 | 
			
		||||
    private var fullHeightCards: Boolean? = null
 | 
			
		||||
    private var updateSources: Boolean? = null
 | 
			
		||||
    private var periodicRefresh: Boolean? = null
 | 
			
		||||
    private var refreshWhenChargingOnly: Boolean? = null
 | 
			
		||||
    private var infiniteLoading: Boolean? = null
 | 
			
		||||
    private var notifyNewItems: Boolean? = null
 | 
			
		||||
    private var itemsNumber: Int? = null
 | 
			
		||||
    private var apiTimeout: Long? = null
 | 
			
		||||
    private var refreshMinutes: Long = DEFAULT_REFRESH_MINUTES
 | 
			
		||||
    private var markOnScroll: Boolean? = null
 | 
			
		||||
    private var activeAlignment: Int? = null
 | 
			
		||||
 | 
			
		||||
    private var _fontSize: Int? = null
 | 
			
		||||
    private var _staticBar: Boolean? = null
 | 
			
		||||
    private var _font: String = ""
 | 
			
		||||
    private var _theme: Int? = null
 | 
			
		||||
    private var fontSize: Int? = null
 | 
			
		||||
    private var staticBar: Boolean? = null
 | 
			
		||||
    private var font: String = ""
 | 
			
		||||
    private var theme: Int? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        refreshApiSettings()
 | 
			
		||||
@@ -49,11 +63,11 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getApiVersion(): Int {
 | 
			
		||||
        if (_apiVersion == -1) {
 | 
			
		||||
        if (apiVersion == -1) {
 | 
			
		||||
            refreshApiVersion()
 | 
			
		||||
            return _apiVersion
 | 
			
		||||
            return apiVersion
 | 
			
		||||
        }
 | 
			
		||||
        return _apiVersion
 | 
			
		||||
        return apiVersion
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateApiVersion(apiMajorVersion: Int) {
 | 
			
		||||
@@ -62,14 +76,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshApiVersion() {
 | 
			
		||||
        _apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
 | 
			
		||||
        apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getPublicAccess(): Boolean {
 | 
			
		||||
        if (_publicAccess == null) {
 | 
			
		||||
        if (publicAccess == null) {
 | 
			
		||||
            refreshPublicAccess()
 | 
			
		||||
        }
 | 
			
		||||
        return _publicAccess!!
 | 
			
		||||
        return publicAccess!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updatePublicAccess(publicAccess: Boolean) {
 | 
			
		||||
@@ -78,14 +92,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshPublicAccess() {
 | 
			
		||||
        _publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
 | 
			
		||||
        publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getSelfSigned(): Boolean {
 | 
			
		||||
        if (_selfSigned == null) {
 | 
			
		||||
        if (selfSigned == null) {
 | 
			
		||||
            refreshSelfSigned()
 | 
			
		||||
        }
 | 
			
		||||
        return _selfSigned!!
 | 
			
		||||
        return selfSigned!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateSelfSigned(selfSigned: Boolean) {
 | 
			
		||||
@@ -94,312 +108,315 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshSelfSigned() {
 | 
			
		||||
        _selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
 | 
			
		||||
        selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBaseUrl(): String {
 | 
			
		||||
        if (_baseUrl.isEmpty()) {
 | 
			
		||||
        if (baseUrl.isEmpty()) {
 | 
			
		||||
            refreshBaseUrl()
 | 
			
		||||
        }
 | 
			
		||||
        return _baseUrl
 | 
			
		||||
        return baseUrl
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getUserName(): String {
 | 
			
		||||
        if (_userName.isEmpty()) {
 | 
			
		||||
        if (userName.isEmpty()) {
 | 
			
		||||
            refreshUsername()
 | 
			
		||||
        }
 | 
			
		||||
        return _userName
 | 
			
		||||
        return userName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getPassword(): String {
 | 
			
		||||
        if (_password.isEmpty()) {
 | 
			
		||||
        if (password.isEmpty()) {
 | 
			
		||||
            refreshPassword()
 | 
			
		||||
        }
 | 
			
		||||
        return _password
 | 
			
		||||
        return password
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBasicUserName(): String {
 | 
			
		||||
        if (_basicUserName.isEmpty()) {
 | 
			
		||||
        if (basicUserName.isEmpty()) {
 | 
			
		||||
            refreshBasicUsername()
 | 
			
		||||
        }
 | 
			
		||||
        return _basicUserName
 | 
			
		||||
        return basicUserName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getBasicPassword(): String {
 | 
			
		||||
        if (_basicPassword.isEmpty()) {
 | 
			
		||||
        if (basicPassword.isEmpty()) {
 | 
			
		||||
            refreshBasicPassword()
 | 
			
		||||
        }
 | 
			
		||||
        return _basicPassword
 | 
			
		||||
        return basicPassword
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getItemsNumber(): Int {
 | 
			
		||||
        if (_itemsNumber == null) {
 | 
			
		||||
        if (itemsNumber == null) {
 | 
			
		||||
            refreshItemsNumber()
 | 
			
		||||
        }
 | 
			
		||||
        return _itemsNumber!!
 | 
			
		||||
        return itemsNumber!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    private fun refreshItemsNumber() {
 | 
			
		||||
        _itemsNumber =
 | 
			
		||||
        itemsNumber =
 | 
			
		||||
            try {
 | 
			
		||||
                settings.getString(API_ITEMS_NUMBER, "20").toInt()
 | 
			
		||||
                settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt()
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                settings.remove(API_ITEMS_NUMBER)
 | 
			
		||||
                20
 | 
			
		||||
                DEFAULT_ITEMS_NUMBER
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getApiTimeout(): Long {
 | 
			
		||||
        if (_apiTimeout == null) {
 | 
			
		||||
        if (apiTimeout == null) {
 | 
			
		||||
            refreshApiTimeout()
 | 
			
		||||
        }
 | 
			
		||||
        return _apiTimeout!!
 | 
			
		||||
        return apiTimeout!!
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:MagicNumber")
 | 
			
		||||
    private fun secToMs(n: Long) = n * 1000
 | 
			
		||||
 | 
			
		||||
    @Suppress("detekt:SwallowedException")
 | 
			
		||||
    private fun refreshApiTimeout() {
 | 
			
		||||
        _apiTimeout =
 | 
			
		||||
        apiTimeout =
 | 
			
		||||
            secToMs(
 | 
			
		||||
                try {
 | 
			
		||||
                    val settingsTimeout = settings.getString(API_TIMEOUT, "60")
 | 
			
		||||
                    val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString())
 | 
			
		||||
                    if (settingsTimeout.toLong() > 0) {
 | 
			
		||||
                        settingsTimeout.toLong()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        settings.remove(API_TIMEOUT)
 | 
			
		||||
                        60
 | 
			
		||||
                        DEFAULT_API_TIMEOUT
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    settings.remove(API_TIMEOUT)
 | 
			
		||||
                    60
 | 
			
		||||
                    DEFAULT_API_TIMEOUT
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshBaseUrl() {
 | 
			
		||||
        _baseUrl = settings.getString(BASE_URL, "")
 | 
			
		||||
        baseUrl = settings.getString(BASE_URL, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshUsername() {
 | 
			
		||||
        _userName = settings.getString(LOGIN, "")
 | 
			
		||||
        userName = settings.getString(LOGIN, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshPassword() {
 | 
			
		||||
        _password = settings.getString(PASSWORD, "")
 | 
			
		||||
        password = settings.getString(PASSWORD, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshBasicUsername() {
 | 
			
		||||
        _basicUserName = settings.getString(BASIC_LOGIN, "")
 | 
			
		||||
        basicUserName = settings.getString(BASIC_LOGIN, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshBasicPassword() {
 | 
			
		||||
        _basicPassword = settings.getString(BASIC_PASSWORD, "")
 | 
			
		||||
        basicPassword = settings.getString(BASIC_PASSWORD, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshArticleViewerEnabled() {
 | 
			
		||||
        _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
 | 
			
		||||
        articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isArticleViewerEnabled(): Boolean {
 | 
			
		||||
        if (_articleViewer != null) {
 | 
			
		||||
        if (articleViewer != null) {
 | 
			
		||||
            refreshArticleViewerEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _articleViewer == true
 | 
			
		||||
        return articleViewer == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshShouldBeCardViewEnabled() {
 | 
			
		||||
        _shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
 | 
			
		||||
        shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isCardViewEnabled(): Boolean {
 | 
			
		||||
        if (_shouldBeCardView != null) {
 | 
			
		||||
        if (shouldBeCardView != null) {
 | 
			
		||||
            refreshShouldBeCardViewEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _shouldBeCardView == true
 | 
			
		||||
        return shouldBeCardView == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshDisplayUnreadCountEnabled() {
 | 
			
		||||
        _displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
 | 
			
		||||
        displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isDisplayUnreadCountEnabled(): Boolean {
 | 
			
		||||
        if (_displayUnreadCount != null) {
 | 
			
		||||
        if (displayUnreadCount != null) {
 | 
			
		||||
            refreshDisplayUnreadCountEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _displayUnreadCount == true
 | 
			
		||||
        return displayUnreadCount == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshDisplayAllCountEnabled() {
 | 
			
		||||
        _displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
 | 
			
		||||
        displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isDisplayAllCountEnabled(): Boolean {
 | 
			
		||||
        if (_displayAllCount != null) {
 | 
			
		||||
        if (displayAllCount != null) {
 | 
			
		||||
            refreshDisplayAllCountEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _displayAllCount == true
 | 
			
		||||
        return displayAllCount == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFullHeightCardsEnabled() {
 | 
			
		||||
        _fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
 | 
			
		||||
        fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isFullHeightCardsEnabled(): Boolean {
 | 
			
		||||
        if (_fullHeightCards != null) {
 | 
			
		||||
        if (fullHeightCards != null) {
 | 
			
		||||
            refreshFullHeightCardsEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _fullHeightCards == true
 | 
			
		||||
        return fullHeightCards == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshUpdateSourcesEnabled() {
 | 
			
		||||
        _updateSources = settings.getBoolean(UPDATE_SOURCES, true)
 | 
			
		||||
        updateSources = settings.getBoolean(UPDATE_SOURCES, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isUpdateSourcesEnabled(): Boolean {
 | 
			
		||||
        if (_updateSources != null) {
 | 
			
		||||
        if (updateSources != null) {
 | 
			
		||||
            refreshUpdateSourcesEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _updateSources == true
 | 
			
		||||
        return updateSources == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshPeriodicRefreshEnabled() {
 | 
			
		||||
        _periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
 | 
			
		||||
        periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isPeriodicRefreshEnabled(): Boolean {
 | 
			
		||||
        if (_periodicRefresh != null) {
 | 
			
		||||
        if (periodicRefresh != null) {
 | 
			
		||||
            refreshPeriodicRefreshEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _periodicRefresh == true
 | 
			
		||||
        return periodicRefresh == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshRefreshWhenChargingOnlyEnabled() {
 | 
			
		||||
        _refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
 | 
			
		||||
        refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isRefreshWhenChargingOnlyEnabled(): Boolean {
 | 
			
		||||
        if (_refreshWhenChargingOnly != null) {
 | 
			
		||||
        if (refreshWhenChargingOnly != null) {
 | 
			
		||||
            refreshRefreshWhenChargingOnlyEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _refreshWhenChargingOnly == true
 | 
			
		||||
        return refreshWhenChargingOnly == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshRefreshMinutes() {
 | 
			
		||||
        _refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
 | 
			
		||||
        if (_refreshMinutes <= 15) {
 | 
			
		||||
            _refreshMinutes = 15
 | 
			
		||||
        refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong()
 | 
			
		||||
        if (refreshMinutes <= MIN_REFRESH_MINUTES) {
 | 
			
		||||
            refreshMinutes = MIN_REFRESH_MINUTES
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getRefreshMinutes(): Long {
 | 
			
		||||
        if (_refreshMinutes != 360L) {
 | 
			
		||||
        if (refreshMinutes != DEFAULT_REFRESH_MINUTES) {
 | 
			
		||||
            refreshRefreshMinutes()
 | 
			
		||||
        }
 | 
			
		||||
        return _refreshMinutes
 | 
			
		||||
        return refreshMinutes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshInfiniteLoadingEnabled() {
 | 
			
		||||
        _infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
 | 
			
		||||
        infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isInfiniteLoadingEnabled(): Boolean {
 | 
			
		||||
        if (_infiniteLoading != null) {
 | 
			
		||||
        if (infiniteLoading != null) {
 | 
			
		||||
            refreshInfiniteLoadingEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _infiniteLoading == true
 | 
			
		||||
        return infiniteLoading == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshItemCachingEnabled() {
 | 
			
		||||
        _itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
 | 
			
		||||
        itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isItemCachingEnabled(): Boolean {
 | 
			
		||||
        if (_itemsCaching != null) {
 | 
			
		||||
        if (itemsCaching != null) {
 | 
			
		||||
            refreshItemCachingEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _itemsCaching == true
 | 
			
		||||
        return itemsCaching == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshNotifyNewItemsEnabled() {
 | 
			
		||||
        _notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
 | 
			
		||||
        notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isNotifyNewItemsEnabled(): Boolean {
 | 
			
		||||
        if (_notifyNewItems != null) {
 | 
			
		||||
        if (notifyNewItems != null) {
 | 
			
		||||
            refreshNotifyNewItemsEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _notifyNewItems == true
 | 
			
		||||
        return notifyNewItems == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshMarkOnScrollEnabled() {
 | 
			
		||||
        _markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
 | 
			
		||||
        markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isMarkOnScrollEnabled(): Boolean {
 | 
			
		||||
        if (_markOnScroll != null) {
 | 
			
		||||
        if (markOnScroll != null) {
 | 
			
		||||
            refreshMarkOnScrollEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _markOnScroll == true
 | 
			
		||||
        return markOnScroll == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshActiveAllignment() {
 | 
			
		||||
        _activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
 | 
			
		||||
        activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getActiveAllignment(): Int {
 | 
			
		||||
        if (_activeAlignment != null) {
 | 
			
		||||
        if (activeAlignment != null) {
 | 
			
		||||
            refreshActiveAllignment()
 | 
			
		||||
        }
 | 
			
		||||
        return _activeAlignment ?: JUSTIFY
 | 
			
		||||
        return activeAlignment ?: JUSTIFY
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun changeAllignment(allignment: Int) {
 | 
			
		||||
        settings.putInt(TEXT_ALIGN, allignment)
 | 
			
		||||
        _activeAlignment = allignment
 | 
			
		||||
        activeAlignment = allignment
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFontSize() {
 | 
			
		||||
        _fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
 | 
			
		||||
        fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getFontSize(): Int {
 | 
			
		||||
        if (_fontSize != null) {
 | 
			
		||||
        if (fontSize != null) {
 | 
			
		||||
            refreshFontSize()
 | 
			
		||||
        }
 | 
			
		||||
        return _fontSize ?: 16
 | 
			
		||||
        return fontSize ?: DEFAULT_FONT_SIZE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshStaticBarEnabled() {
 | 
			
		||||
        _staticBar = settings.getBoolean(READER_STATIC_BAR, false)
 | 
			
		||||
        staticBar = settings.getBoolean(READER_STATIC_BAR, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun isStaticBarEnabled(): Boolean {
 | 
			
		||||
        if (_staticBar != null) {
 | 
			
		||||
        if (staticBar != null) {
 | 
			
		||||
            refreshStaticBarEnabled()
 | 
			
		||||
        }
 | 
			
		||||
        return _staticBar == true
 | 
			
		||||
        return staticBar == true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFont() {
 | 
			
		||||
        _font = settings.getString(READER_FONT, "")
 | 
			
		||||
        font = settings.getString(READER_FONT, "")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getFont(): String {
 | 
			
		||||
        if (_font.isEmpty()) {
 | 
			
		||||
        if (font.isEmpty()) {
 | 
			
		||||
            refreshFont()
 | 
			
		||||
        }
 | 
			
		||||
        return _font
 | 
			
		||||
        return font
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshCurrentTheme() {
 | 
			
		||||
        _theme = settings.getString(CURRENT_THEME, "-1").toInt()
 | 
			
		||||
        theme = settings.getString(CURRENT_THEME, "-1").toInt()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getCurrentTheme(): Int {
 | 
			
		||||
        if (_theme == null) {
 | 
			
		||||
        if (theme == null) {
 | 
			
		||||
            refreshCurrentTheme()
 | 
			
		||||
        }
 | 
			
		||||
        return _theme ?: -1
 | 
			
		||||
        return theme ?: -1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun refreshApiSettings() {
 | 
			
		||||
@@ -478,15 +495,15 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val translationUrl = "https://crwd.in/readerforselfoss"
 | 
			
		||||
        const val TRANSLATION_URL = "https://crwd.in/readerforselfoss"
 | 
			
		||||
 | 
			
		||||
        const val sourceUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
 | 
			
		||||
        const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
 | 
			
		||||
 | 
			
		||||
        const val trackerUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
 | 
			
		||||
        const val BUG_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
 | 
			
		||||
 | 
			
		||||
        const val syncChannelId = "sync-channel-id"
 | 
			
		||||
        const val SYNC_CHANNEL_ID = "sync-channel-id"
 | 
			
		||||
 | 
			
		||||
        const val newItemsChannelId = "new-items-channel-id"
 | 
			
		||||
        const val NEW_ITEMS_CHANNEL = "new-items-channel-id"
 | 
			
		||||
 | 
			
		||||
        const val JUSTIFY = 1
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,12 @@ import kotlinx.datetime.LocalDateTime
 | 
			
		||||
import kotlinx.datetime.TimeZone
 | 
			
		||||
import kotlinx.datetime.toInstant
 | 
			
		||||
 | 
			
		||||
class DateParseException(
 | 
			
		||||
    message: String,
 | 
			
		||||
    e: Throwable? = null,
 | 
			
		||||
) : Throwable(message, e)
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:ThrowsCount")
 | 
			
		||||
fun String.toParsedDate(): Long {
 | 
			
		||||
    // Possible formats are
 | 
			
		||||
    // yyyy-mm-dd hh:mm:ss format
 | 
			
		||||
@@ -17,17 +23,22 @@ fun String.toParsedDate(): Long {
 | 
			
		||||
            if (this.matches(oldVersionFormat)) {
 | 
			
		||||
                this.replace(" ", "T")
 | 
			
		||||
            } else if (this.matches(newVersionFormat)) {
 | 
			
		||||
                newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
 | 
			
		||||
                newVersionFormat
 | 
			
		||||
                    .find(this)
 | 
			
		||||
                    ?.groups
 | 
			
		||||
                    ?.get(1)
 | 
			
		||||
                    ?.value ?: throw DateParseException("Couldn't parse $this")
 | 
			
		||||
            } else {
 | 
			
		||||
                throw Exception("Unrecognized format for $this")
 | 
			
		||||
                throw DateParseException("Unrecognized format for $this")
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            throw Exception("parseDate failed for $this", e)
 | 
			
		||||
            throw DateParseException("parseDate failed for $this", e)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
			
		||||
expect class DateUtils() {
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun parseRelativeDate(dateString: String): String
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
 | 
			
		||||
        this.id.toInt(),
 | 
			
		||||
        this.title,
 | 
			
		||||
        null,
 | 
			
		||||
        this.tags?.split(","),
 | 
			
		||||
        this.tags.split(","),
 | 
			
		||||
        this.spout,
 | 
			
		||||
        this.error,
 | 
			
		||||
        this.icon,
 | 
			
		||||
@@ -74,6 +74,7 @@ fun SelfossModel.Item.toEntity(): ITEM =
 | 
			
		||||
        this.author,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:MagicNumber")
 | 
			
		||||
fun SelfossModel.Tag.getColorHexCode(): String =
 | 
			
		||||
    if (this.color.length == 4) { // #000
 | 
			
		||||
        val char1 = this.color.get(1)
 | 
			
		||||
@@ -82,4 +83,4 @@ fun SelfossModel.Tag.getColorHexCode(): String =
 | 
			
		||||
        "#$char1$char1$char2$char2$char3$char3"
 | 
			
		||||
    } else {
 | 
			
		||||
        this.color
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
enum class ItemType(val position: Int, val type: String) {
 | 
			
		||||
@Suppress("detekt:MagicNumber")
 | 
			
		||||
enum class ItemType(
 | 
			
		||||
    val position: Int,
 | 
			
		||||
    val type: String,
 | 
			
		||||
) {
 | 
			
		||||
    UNREAD(1, "unread"),
 | 
			
		||||
    ALL(2, "all"),
 | 
			
		||||
    STARRED(3, "starred"),
 | 
			
		||||
@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
fun String?.isEmptyOrNullOrNullString(): Boolean = this == null || this == "null" || this.isEmpty()
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:MagicNumber")
 | 
			
		||||
fun String.longHash(): Long {
 | 
			
		||||
    var h = 98764321261L
 | 
			
		||||
    val l = this.length
 | 
			
		||||
 
 | 
			
		||||
@@ -8,27 +8,29 @@ import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
 | 
			
		||||
class DatesTest {
 | 
			
		||||
    private val newVersionDateVariant =     "2022-12-24T17:00:08+00"
 | 
			
		||||
    private val newVersionDate =            "2013-04-07T13:43:00+01:00"
 | 
			
		||||
    private val newVersionDate2 =            "2013-04-07T13:43:00-01:00"
 | 
			
		||||
    private val oldVersionDate =            "2013-05-07 13:46:00"
 | 
			
		||||
    private val oldVersionDateVariant =     "2021-03-21 10:32:00.000000"
 | 
			
		||||
 | 
			
		||||
    private val newVersionDateVariant = "2022-12-24T17:00:08+00"
 | 
			
		||||
    private val newVersionDate = "2013-04-07T13:43:00+01:00"
 | 
			
		||||
    private val newVersionDate2 = "2013-04-07T13:43:00-01:00"
 | 
			
		||||
    private val oldVersionDate = "2013-05-07 13:46:00"
 | 
			
		||||
    private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun new_version_date_should_be_parsed() {
 | 
			
		||||
        val date = newVersionDate.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0)
 | 
			
		||||
                .toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun new_version_date2_should_be_parsed() {
 | 
			
		||||
        val date = newVersionDate2.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
            LocalDateTime(2013, 4, 7, 13, 43, 0, 0)
 | 
			
		||||
                .toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
@@ -38,7 +40,8 @@ class DatesTest {
 | 
			
		||||
    fun old_version_date_should_be_parsed() {
 | 
			
		||||
        val date = oldVersionDate.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
            LocalDateTime(2013, 5, 7, 13, 46, 0, 0)
 | 
			
		||||
                .toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
@@ -48,7 +51,8 @@ class DatesTest {
 | 
			
		||||
    fun old_version_variant_date_should_be_parsed() {
 | 
			
		||||
        val date = oldVersionDateVariant.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
            LocalDateTime(2021, 3, 21, 10, 32, 0, 0)
 | 
			
		||||
                .toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
@@ -58,7 +62,8 @@ class DatesTest {
 | 
			
		||||
    fun new_version_variant_date_should_be_parsed() {
 | 
			
		||||
        val date = newVersionDateVariant.toParsedDate()
 | 
			
		||||
        val expected =
 | 
			
		||||
            LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
            LocalDateTime(2022, 12, 24, 17, 0, 8, 0)
 | 
			
		||||
                .toInstant(TimeZone.currentSystemDefault())
 | 
			
		||||
                .toEpochMilliseconds()
 | 
			
		||||
 | 
			
		||||
        assertEquals(expected, date)
 | 
			
		||||
@@ -4,7 +4,5 @@ import app.cash.sqldelight.db.SqlDriver
 | 
			
		||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
 | 
			
		||||
 | 
			
		||||
actual class DriverFactory {
 | 
			
		||||
    actual fun createDriver(): SqlDriver {
 | 
			
		||||
        return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
 | 
			
		||||
import io.ktor.client.engine.cio.CIOEngineConfig
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:EmptyFunctionBlock")
 | 
			
		||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
 | 
			
		||||
import io.ktor.client.engine.cio.CIOEngineConfig
 | 
			
		||||
 | 
			
		||||
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
			
		||||
actual class DateUtils {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
            TODO("Not yet implemented")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
			
		||||
actual class DateUtils actual constructor() {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
            TODO("Not yet implemented")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,5 @@ import app.cash.sqldelight.db.SqlDriver
 | 
			
		||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
 | 
			
		||||
 | 
			
		||||
actual class DriverFactory {
 | 
			
		||||
    actual fun createDriver(): SqlDriver {
 | 
			
		||||
        return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
 | 
			
		||||
import io.ktor.client.engine.cio.CIOEngineConfig
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:EmptyFunctionBlock")
 | 
			
		||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.rest
 | 
			
		||||
 | 
			
		||||
import io.ktor.client.engine.cio.CIOEngineConfig
 | 
			
		||||
 | 
			
		||||
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
package bou.amine.apps.readerforselfossv2.utils
 | 
			
		||||
 | 
			
		||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
 | 
			
		||||
actual class DateUtils {
 | 
			
		||||
    actual companion object {
 | 
			
		||||
        actual fun parseRelativeDate(dateString: String): String {
 | 
			
		||||
            TODO("Not yet implemented")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user