Compare commits
27 Commits
6b5f6cbbe0
...
v125020471
Author | SHA1 | Date | |
---|---|---|---|
61c7bb20cc | |||
6a0f5baf0a | |||
39f9505c00 | |||
6a6d447456 | |||
0bb4fe6aed | |||
7df4c3368c | |||
c69635b5ae | |||
3a829df70e | |||
7a0202689f | |||
b20f6888f5 | |||
6b96eb358d | |||
dfc1bf9fa3 | |||
b173664ff0 | |||
bc20a421ae | |||
794500355a | |||
44f9dd53d3 | |||
717d6b664c | |||
e23289a3dc | |||
2f5ebe2420 | |||
1893904135 | |||
a4cb28ba81 | |||
ae3cada1c7 | |||
309500276f | |||
ce255b23cd | |||
3b3a575dae | |||
7bcf4574b4 | |||
c79ab5e92b |
@@ -3,25 +3,34 @@ root = true
|
|||||||
[*]
|
[*]
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[{*.kt,*.kts}]
|
[.editorconfig]
|
||||||
|
insert_final_newline = false
|
||||||
|
ij_kotlin_line_break_after_multiline_when_entry = false
|
||||||
|
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
# Disable wildcard imports entirely
|
# Disable wildcard imports entirely
|
||||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||||
|
|
||||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
ij_kotlin_allow_trailing_comma = true
|
ij_kotlin_allow_trailing_comma = true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
|
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
|
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = space
|
indent_style = space
|
||||||
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
|
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_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
|
||||||
ktlint_code_style = ktlint_official
|
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_body_expression_wrapping = multiline
|
||||||
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
|
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
|
||||||
ktlint_ignore_back_ticked_identifier = false
|
ktlint_ignore_back_ticked_identifier = false
|
||||||
|
ktlint_property_naming_constant_naming = screaming_snake_case
|
||||||
max_line_length = 140
|
max_line_length = 140
|
||||||
|
|
||||||
|
[**/build]
|
||||||
|
ktlint = disabled
|
@@ -26,9 +26,10 @@ jobs:
|
|||||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||||
with:
|
with:
|
||||||
version: "2.23.3"
|
version: "2.23.3"
|
||||||
- name: run selfoss
|
# TESTS ARE RUN LOCALLY
|
||||||
run: |
|
# - name: run selfoss
|
||||||
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
|
# run: |
|
||||||
|
# docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
|
||||||
- name: coverage
|
- name: coverage
|
||||||
run: |
|
run: |
|
||||||
./gradlew :koverHtmlReport
|
./gradlew :koverHtmlReport
|
||||||
@@ -39,7 +40,8 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
overwrite: true
|
overwrite: true
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
- name: Clean
|
# TESTS ARE RUN LOCALLY
|
||||||
if: always()
|
# - name: Clean
|
||||||
run: |
|
# if: always()
|
||||||
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
# run: |
|
||||||
|
# docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
||||||
|
@@ -16,6 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: master
|
||||||
- name: Config git
|
- name: Config git
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email aminecmi+giteadrone@pm.me
|
git config --global user.email aminecmi+giteadrone@pm.me
|
||||||
@@ -50,7 +51,7 @@ jobs:
|
|||||||
followtags: true
|
followtags: true
|
||||||
ssh_key: ${{ secrets.PRIVATE_KEY }}
|
ssh_key: ${{ secrets.PRIVATE_KEY }}
|
||||||
tags: true
|
tags: true
|
||||||
branch: release
|
branch: master
|
||||||
- name: copy file via ssh password
|
- name: copy file via ssh password
|
||||||
uses: appleboy/scp-action@v0.1.7
|
uses: appleboy/scp-action@v0.1.7
|
||||||
with:
|
with:
|
||||||
|
@@ -16,13 +16,13 @@ jobs:
|
|||||||
java-version: '17'
|
java-version: '17'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Install klint
|
- 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
|
- 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...
|
- name: Linting...
|
||||||
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
|
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
|
||||||
- name: Detecting...
|
- 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:
|
build:
|
||||||
needs: Lint
|
needs: Lint
|
||||||
uses: ./.gitea/workflows/common_build.yml
|
uses: ./.gitea/workflows/common_build.yml
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -324,3 +324,5 @@ crowdin.properties
|
|||||||
|
|
||||||
.kotlin/
|
.kotlin/
|
||||||
build-cache/
|
build-cache/
|
||||||
|
|
||||||
|
act
|
||||||
|
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,3 +1,48 @@
|
|||||||
|
**v125020411
|
||||||
|
|
||||||
|
- Merge pull request 'bump' (#182) from bump into master
|
||||||
|
- chore: non transiant R classes.
|
||||||
|
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||||
|
- bump
|
||||||
|
- fix: One more missing context.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010241
|
||||||
|
|
||||||
|
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||||
|
- refactor: context fragments issues.
|
||||||
|
- logs: Context issues.
|
||||||
|
- fix: Handle empty url issue, again.
|
||||||
|
- fix: Link not opening.
|
||||||
|
- Changelog for v125010201
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010201
|
||||||
|
|
||||||
|
- fix: Handle empty url issue.
|
||||||
|
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||||
|
- chore: changing actions in reader fragment.
|
||||||
|
- Changelog for v125010131
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010131
|
||||||
|
|
||||||
|
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||||
|
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||||
|
- Changelog for v125010111
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
|
**v125010111
|
||||||
|
|
||||||
|
- Debug trying to fix context issues. (#174)
|
||||||
|
- Changelog for v125010031
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
|
||||||
**v125010031
|
**v125010031
|
||||||
|
|
||||||
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
|
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
|
||||||
|
@@ -156,7 +156,7 @@ dependencies {
|
|||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||||
|
|
||||||
// Themes
|
// Themes
|
||||||
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
|
implementation("com.leinardi.android:speed-dial:3.3.0")
|
||||||
|
|
||||||
// Pager
|
// Pager
|
||||||
implementation("me.relex:circleindicator:2.1.6")
|
implementation("me.relex:circleindicator:2.1.6")
|
||||||
|
@@ -104,12 +104,14 @@ fun testAddSourceWithUrl(
|
|||||||
onView(withId(R.id.fab))
|
onView(withId(R.id.fab))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
onView(withId(R.id.nameInput))
|
onView(withId(R.id.nameInput))
|
||||||
.perform(click()).perform(typeTextIntoFocusedView(sourceName))
|
.perform(click())
|
||||||
|
.perform(typeTextIntoFocusedView(sourceName))
|
||||||
onView(withId(R.id.sourceUri))
|
onView(withId(R.id.sourceUri))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
.perform(typeTextIntoFocusedView(url))
|
.perform(typeTextIntoFocusedView(url))
|
||||||
onView(withId(R.id.tags))
|
onView(withId(R.id.tags))
|
||||||
.perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
|
.perform(click())
|
||||||
|
.perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
|
||||||
onView(withId(R.id.spoutsSpinner))
|
onView(withId(R.id.spoutsSpinner))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
onData(hasToString("RSS Feed")).perform(click())
|
onData(hasToString("RSS Feed")).perform(click())
|
||||||
|
@@ -30,28 +30,21 @@ fun withError(
|
|||||||
): TypeSafeMatcher<View?> {
|
): TypeSafeMatcher<View?> {
|
||||||
return object : TypeSafeMatcher<View?>() {
|
return object : TypeSafeMatcher<View?>() {
|
||||||
override fun matchesSafely(view: View?): Boolean {
|
override fun matchesSafely(view: View?): Boolean {
|
||||||
if (view == null) {
|
if (view != null && (view !is EditText || view.error == null)) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
val context = view.context
|
|
||||||
if (view !is EditText) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (view.error == null) {
|
|
||||||
return false
|
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?) {
|
override fun describeTo(description: Description?) {
|
||||||
|
// Nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPopupWindow(): Matcher<Root> {
|
fun isPopupWindow(): Matcher<Root> = isPlatformPopup()
|
||||||
return isPlatformPopup()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withDrawable(
|
fun withDrawable(
|
||||||
@DrawableRes id: Int,
|
@DrawableRes id: Int,
|
||||||
@@ -60,6 +53,7 @@ fun withDrawable(
|
|||||||
description.appendText("ImageView with drawable same as drawable with id $id")
|
description.appendText("ImageView with drawable same as drawable with id $id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
override fun matchesSafely(view: View): Boolean {
|
override fun matchesSafely(view: View): Boolean {
|
||||||
val context = view.context
|
val context = view.context
|
||||||
val expectedBitmap = context.getDrawable(id)!!.toBitmap()
|
val expectedBitmap = context.getDrawable(id)!!.toBitmap()
|
||||||
@@ -73,8 +67,8 @@ fun withDrawable(
|
|||||||
|
|
||||||
fun hasBottombarItemText(
|
fun hasBottombarItemText(
|
||||||
@StringRes id: Int,
|
@StringRes id: Int,
|
||||||
): Matcher<View>? {
|
): Matcher<View>? =
|
||||||
return allOf(
|
allOf(
|
||||||
withResourceName("fixed_bottom_navigation_icon"),
|
withResourceName("fixed_bottom_navigation_icon"),
|
||||||
withParent(
|
withParent(
|
||||||
allOf(
|
allOf(
|
||||||
@@ -83,23 +77,21 @@ fun hasBottombarItemText(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun withSettingsCheckboxWidget(
|
fun withSettingsCheckboxWidget(
|
||||||
@StringRes id: Int,
|
@StringRes id: Int,
|
||||||
): Matcher<View>? {
|
): Matcher<View>? =
|
||||||
return allOf(
|
allOf(
|
||||||
withId(android.R.id.switch_widget),
|
withId(android.R.id.switch_widget),
|
||||||
withParent(
|
withParent(
|
||||||
withSettingsCheckboxFrame(id),
|
withSettingsCheckboxFrame(id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun withSettingsCheckboxFrame(
|
fun withSettingsCheckboxFrame(
|
||||||
@StringRes id: Int,
|
@StringRes id: Int,
|
||||||
): Matcher<View>? {
|
): Matcher<View>? =
|
||||||
return allOf(
|
allOf(
|
||||||
withId(android.R.id.widget_frame),
|
withId(android.R.id.widget_frame),
|
||||||
hasSibling(
|
hasSibling(
|
||||||
allOf(
|
allOf(
|
||||||
@@ -110,7 +102,6 @@ fun withSettingsCheckboxFrame(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun openMenu() {
|
fun openMenu() {
|
||||||
openActionBarOverflowOrOptionsMenu(
|
openActionBarOverflowOrOptionsMenu(
|
||||||
|
@@ -56,7 +56,7 @@ class HomeActivityTest {
|
|||||||
fun testMenuActions() {
|
fun testMenuActions() {
|
||||||
onView(withId(R.id.action_search)).perform(click())
|
onView(withId(R.id.action_search)).perform(click())
|
||||||
onView(
|
onView(
|
||||||
withId(R.id.search_src_text),
|
withId(com.google.android.material.R.id.search_src_text),
|
||||||
).check(matches(isFocused()))
|
).check(matches(isFocused()))
|
||||||
onView(isRoot()).perform(ViewActions.pressBack())
|
onView(isRoot()).perform(ViewActions.pressBack())
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.android
|
package bou.amine.apps.readerforselfossv2.android
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.IdlingRegistry
|
import androidx.test.espresso.IdlingRegistry
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
@@ -26,35 +25,33 @@ class LoginActivityTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||||
|
|
||||||
private fun getActivity(): Activity? {
|
|
||||||
var activity: Activity? = null
|
|
||||||
activityRule.scenario.onActivity {
|
|
||||||
activity = it
|
|
||||||
}
|
|
||||||
return activity
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun registerIdlingResource() {
|
fun registerIdlingResource() {
|
||||||
IdlingRegistry.getInstance()
|
IdlingRegistry
|
||||||
|
.getInstance()
|
||||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun unregisterIdlingResource() {
|
fun unregisterIdlingResource() {
|
||||||
IdlingRegistry.getInstance()
|
IdlingRegistry
|
||||||
|
.getInstance()
|
||||||
.unregister(CountingIdlingResourceSingleton.countingIdlingResource)
|
.unregister(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun viewIsInitialized() {
|
fun viewIsInitialized() {
|
||||||
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
|
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(
|
.check(
|
||||||
matches(isClickable()),
|
matches(isClickable()),
|
||||||
)
|
)
|
||||||
onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
|
onView(withId(R.id.withLogin))
|
||||||
.check(matches(isNotChecked())).check(
|
.check(matches(isDisplayed()))
|
||||||
|
.check(matches(isNotChecked()))
|
||||||
|
.check(
|
||||||
matches(isClickable()),
|
matches(isClickable()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -42,6 +42,7 @@ class SettingsActivityGeneralTest {
|
|||||||
onView(withText(R.string.pref_header_general)).perform(click())
|
onView(withText(R.string.pref_header_general)).perform(click())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
@Test
|
@Test
|
||||||
fun testGeneral() {
|
fun testGeneral() {
|
||||||
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
|
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
|
||||||
@@ -64,19 +65,6 @@ class SettingsActivityGeneralTest {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
|
|
||||||
matches(
|
|
||||||
allOf(
|
|
||||||
isDisplayed(),
|
|
||||||
not(isChecked()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
|
||||||
matches(
|
|
||||||
isEnabled(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
|
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
|
||||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
|
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
|
||||||
matches(
|
matches(
|
||||||
@@ -118,6 +106,7 @@ class SettingsActivityGeneralTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:ForbiddenComment")
|
||||||
@Test
|
@Test
|
||||||
fun testGeneralActionsNumberItems() {
|
fun testGeneralActionsNumberItems() {
|
||||||
onView(withText(R.string.pref_api_items_number_title)).perform(click())
|
onView(withText(R.string.pref_api_items_number_title)).perform(click())
|
||||||
@@ -159,19 +148,6 @@ class SettingsActivityGeneralTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGeneralActionsCheckboxes() {
|
fun testGeneralActionsCheckboxes() {
|
||||||
// article viewer settings
|
|
||||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
|
||||||
matches(
|
|
||||||
isEnabled(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
|
|
||||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
|
||||||
matches(
|
|
||||||
not(isEnabled()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(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(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
|
||||||
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
|
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
|
||||||
|
@@ -42,6 +42,7 @@ class SettingsActivityOfflineTest {
|
|||||||
onView(withText(R.string.pref_header_offline)).perform(click())
|
onView(withText(R.string.pref_header_offline)).perform(click())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
@Test
|
@Test
|
||||||
fun testOffline() {
|
fun testOffline() {
|
||||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
|
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
|
||||||
@@ -107,6 +108,7 @@ class SettingsActivityOfflineTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
@Test
|
@Test
|
||||||
fun testOfflineActions() {
|
fun testOfflineActions() {
|
||||||
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
|
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
|
||||||
|
@@ -45,6 +45,7 @@ class SourcesActivityTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
@Test
|
@Test
|
||||||
fun addSourceCheckContent() {
|
fun addSourceCheckContent() {
|
||||||
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
|
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
|
||||||
|
@@ -31,11 +31,11 @@ import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
|
|||||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
|
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
|
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar
|
import com.ashokvarma.bottomnavigation.BottomNavigationBar
|
||||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem
|
import com.ashokvarma.bottomnavigation.BottomNavigationItem
|
||||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||||
@@ -49,6 +49,8 @@ import org.kodein.di.instance
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val MIN_WIDTH_CARD_DP = 300
|
||||||
|
|
||||||
class HomeActivity :
|
class HomeActivity :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
SearchView.OnQueryTextListener,
|
SearchView.OnQueryTextListener,
|
||||||
@@ -200,6 +202,7 @@ class HomeActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
private fun handleBottomBar() {
|
private fun handleBottomBar() {
|
||||||
tabNewBadge =
|
tabNewBadge =
|
||||||
TextBadgeItem()
|
TextBadgeItem()
|
||||||
@@ -282,7 +285,7 @@ class HomeActivity :
|
|||||||
|
|
||||||
handleBottomBarActions()
|
handleBottomBarActions()
|
||||||
|
|
||||||
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||||
|
|
||||||
handleRecurringTask()
|
handleRecurringTask()
|
||||||
CountingIdlingResourceSingleton.increment()
|
CountingIdlingResourceSingleton.increment()
|
||||||
@@ -294,10 +297,10 @@ class HomeActivity :
|
|||||||
getElementsAccordingToTab()
|
getElementsAccordingToTab()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGDPRDialog(GDPRShown: Boolean) {
|
private fun handleGdprDialog(gdprShown: Boolean) {
|
||||||
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
||||||
if (!GDPRShown) {
|
if (!gdprShown) {
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
||||||
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
||||||
@@ -314,50 +317,44 @@ class HomeActivity :
|
|||||||
|
|
||||||
private fun reloadLayoutManager() {
|
private fun reloadLayoutManager() {
|
||||||
val currentManager = binding.recyclerView.layoutManager
|
val currentManager = binding.recyclerView.layoutManager
|
||||||
val layoutManager: RecyclerView.LayoutManager
|
|
||||||
|
|
||||||
// This will only update the layout manager if settings changed
|
fun gridLayoutManager() {
|
||||||
|
val layoutManager =
|
||||||
|
GridLayoutManager(
|
||||||
|
this,
|
||||||
|
calculateNoOfColumns(),
|
||||||
|
)
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
}
|
||||||
|
|
||||||
|
fun staggererdGridLayoutManager() {
|
||||||
|
var layoutManager =
|
||||||
|
StaggeredGridLayoutManager(
|
||||||
|
calculateNoOfColumns(),
|
||||||
|
StaggeredGridLayoutManager.VERTICAL,
|
||||||
|
)
|
||||||
|
layoutManager.gapStrategy =
|
||||||
|
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
}
|
||||||
|
|
||||||
when (currentManager) {
|
when (currentManager) {
|
||||||
is StaggeredGridLayoutManager ->
|
is StaggeredGridLayoutManager ->
|
||||||
if (!appSettingsService.isCardViewEnabled()) {
|
if (!appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager =
|
gridLayoutManager()
|
||||||
GridLayoutManager(
|
|
||||||
this,
|
|
||||||
calculateNoOfColumns(),
|
|
||||||
)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is GridLayoutManager ->
|
is GridLayoutManager ->
|
||||||
if (appSettingsService.isCardViewEnabled()) {
|
if (appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager =
|
staggererdGridLayoutManager()
|
||||||
StaggeredGridLayoutManager(
|
|
||||||
calculateNoOfColumns(),
|
|
||||||
StaggeredGridLayoutManager.VERTICAL,
|
|
||||||
)
|
|
||||||
layoutManager.gapStrategy =
|
|
||||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else ->
|
else ->
|
||||||
if (currentManager == null) {
|
if (currentManager == null) {
|
||||||
if (!appSettingsService.isCardViewEnabled()) {
|
if (!appSettingsService.isCardViewEnabled()) {
|
||||||
layoutManager =
|
gridLayoutManager()
|
||||||
GridLayoutManager(
|
|
||||||
this,
|
|
||||||
calculateNoOfColumns(),
|
|
||||||
)
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
} else {
|
} else {
|
||||||
layoutManager =
|
staggererdGridLayoutManager()
|
||||||
StaggeredGridLayoutManager(
|
|
||||||
calculateNoOfColumns(),
|
|
||||||
StaggeredGridLayoutManager.VERTICAL,
|
|
||||||
)
|
|
||||||
layoutManager.gapStrategy =
|
|
||||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
|
||||||
binding.recyclerView.layoutManager = layoutManager
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,8 +479,8 @@ class HomeActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleListResult(appendResults: Boolean = false) {
|
private fun handleListResult(appendResults: Boolean = false) {
|
||||||
|
val oldManager = binding.recyclerView.layoutManager
|
||||||
if (appendResults) {
|
if (appendResults) {
|
||||||
val oldManager = binding.recyclerView.layoutManager
|
|
||||||
firstVisible =
|
firstVisible =
|
||||||
when (oldManager) {
|
when (oldManager) {
|
||||||
is StaggeredGridLayoutManager ->
|
is StaggeredGridLayoutManager ->
|
||||||
@@ -496,7 +493,13 @@ class HomeActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recyclerAdapter == null) {
|
@Suppress("detekt:ComplexCondition")
|
||||||
|
if (recyclerAdapter == null ||
|
||||||
|
(
|
||||||
|
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
|
||||||
|
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
|
||||||
|
)
|
||||||
|
) {
|
||||||
if (appSettingsService.isCardViewEnabled()) {
|
if (appSettingsService.isCardViewEnabled()) {
|
||||||
recyclerAdapter =
|
recyclerAdapter =
|
||||||
ItemCardAdapter(
|
ItemCardAdapter(
|
||||||
@@ -543,7 +546,7 @@ class HomeActivity :
|
|||||||
private fun calculateNoOfColumns(): Int {
|
private fun calculateNoOfColumns(): Int {
|
||||||
val displayMetrics = resources.displayMetrics
|
val displayMetrics = resources.displayMetrics
|
||||||
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
||||||
return (dpWidth / 300).toInt()
|
return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(p0: String?): Boolean {
|
override fun onQueryTextChange(p0: String?): Boolean {
|
||||||
@@ -592,10 +595,11 @@ class HomeActivity :
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.issue_tracker -> {
|
R.id.issue_tracker -> {
|
||||||
baseContext.openUrlInBrowser(AppSettingsService.TRACKER_URL)
|
baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -84,7 +84,9 @@ class ImageActivity : AppCompatActivity() {
|
|||||||
return super.onOptionsItemSelected(item)
|
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 getItemCount(): Int = allImages.size
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
|
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
|
||||||
|
@@ -30,6 +30,8 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
|
||||||
|
|
||||||
class LoginActivity :
|
class LoginActivity :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
DIAware {
|
DIAware {
|
||||||
@@ -134,9 +136,18 @@ class LoginActivity :
|
|||||||
binding.passwordView.error = null
|
binding.passwordView.error = null
|
||||||
|
|
||||||
// Store values at the time of the login attempt.
|
// Store values at the time of the login attempt.
|
||||||
val url = binding.urlView.text.toString().trim()
|
val url =
|
||||||
val login = binding.loginView.text.toString().trim()
|
binding.urlView.text
|
||||||
val password = binding.passwordView.text.toString().trim()
|
.toString()
|
||||||
|
.trim()
|
||||||
|
val login =
|
||||||
|
binding.loginView.text
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
val password =
|
||||||
|
binding.passwordView.text
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
|
||||||
failInvalidUrl(url)
|
failInvalidUrl(url)
|
||||||
failLoginDetails(password, login)
|
failLoginDetails(password, login)
|
||||||
@@ -208,7 +219,7 @@ class LoginActivity :
|
|||||||
cancel = true
|
cancel = true
|
||||||
binding.urlView.error = getString(R.string.login_url_problem)
|
binding.urlView.error = getString(R.string.login_url_problem)
|
||||||
inValidCount++
|
inValidCount++
|
||||||
if (inValidCount == 3) {
|
if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
|
||||||
val alertDialog = AlertDialog.Builder(this).create()
|
val alertDialog = AlertDialog.Builder(this).create()
|
||||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||||
@@ -273,7 +284,7 @@ class LoginActivity :
|
|||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.issue_tracker -> {
|
R.id.issue_tracker -> {
|
||||||
val browserIntent =
|
val browserIntent =
|
||||||
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.TRACKER_URL))
|
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL))
|
||||||
startActivity(browserIntent)
|
startActivity(browserIntent)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -283,7 +294,7 @@ class LoginActivity :
|
|||||||
.withAboutIconShown(true)
|
.withAboutIconShown(true)
|
||||||
.withAboutVersionShown(true)
|
.withAboutVersionShown(true)
|
||||||
.withAboutSpecial2("Bug reports")
|
.withAboutSpecial2("Bug reports")
|
||||||
.withAboutSpecial2Description(AppSettingsService.TRACKER_URL)
|
.withAboutSpecial2Description(AppSettingsService.BUG_URL)
|
||||||
.withAboutSpecial1("Project Page")
|
.withAboutSpecial1("Project Page")
|
||||||
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
|
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
|
||||||
.start(this)
|
.start(this)
|
||||||
|
@@ -62,6 +62,7 @@ class MyApp :
|
|||||||
private val connectivityStatus: ConnectivityStatus by instance()
|
private val connectivityStatus: ConnectivityStatus by instance()
|
||||||
private val driverFactory: DriverFactory by instance()
|
private val driverFactory: DriverFactory by instance()
|
||||||
|
|
||||||
|
@Suppress("detekt:ForbiddenComment")
|
||||||
// TODO: handle with the "previous" way
|
// TODO: handle with the "previous" way
|
||||||
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ class MyApp :
|
|||||||
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
|
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
|
||||||
val newItemsChannelmChannel =
|
val newItemsChannelmChannel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
|
AppSettingsService.NEW_ITEMS_CHANNEL,
|
||||||
newItemsChannelname,
|
newItemsChannelname,
|
||||||
newItemsChannelimportance,
|
newItemsChannelimportance,
|
||||||
)
|
)
|
||||||
|
@@ -22,7 +22,9 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
class ReaderActivity : AppCompatActivity(), DIAware {
|
class ReaderActivity :
|
||||||
|
AppCompatActivity(),
|
||||||
|
DIAware {
|
||||||
private var currentItem: Int = 0
|
private var currentItem: Int = 0
|
||||||
|
|
||||||
private lateinit var toolbarMenu: Menu
|
private lateinit var toolbarMenu: Menu
|
||||||
@@ -51,6 +53,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
showMenuItem(false)
|
showMenuItem(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityReaderBinding.inflate(layoutInflater)
|
binding = ActivityReaderBinding.inflate(layoutInflater)
|
||||||
@@ -99,8 +102,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
oldInstanceState.clear()
|
oldInstanceState.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
|
private inner class ScreenSlidePagerAdapter(
|
||||||
FragmentStateAdapter(fa) {
|
fa: FragmentActivity,
|
||||||
|
) : FragmentStateAdapter(fa) {
|
||||||
override fun getItemCount(): Int = allItems.size
|
override fun getItemCount(): Int = allItems.size
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
|
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
|
||||||
@@ -109,8 +113,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
override fun onKeyDown(
|
override fun onKeyDown(
|
||||||
keyCode: Int,
|
keyCode: Int,
|
||||||
event: KeyEvent?,
|
event: KeyEvent?,
|
||||||
): Boolean {
|
): Boolean =
|
||||||
return when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
val currentFragment =
|
val currentFragment =
|
||||||
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
|
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
|
||||||
@@ -129,7 +133,6 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
super.onKeyDown(keyCode, event)
|
super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun alignmentMenu() {
|
private fun alignmentMenu() {
|
||||||
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
|
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
|
||||||
@@ -158,12 +161,14 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
|||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
super.onPageSelected(position)
|
super.onPageSelected(position)
|
||||||
|
|
||||||
if (allItems[position].starred) {
|
if (!allItems.isNullOrEmpty() && allItems.size >= position) {
|
||||||
canRemoveFromFavorite()
|
if (allItems[position].starred) {
|
||||||
} else {
|
canRemoveFromFavorite()
|
||||||
canFavorite()
|
} else {
|
||||||
|
canFavorite()
|
||||||
|
}
|
||||||
|
readItem(allItems[position])
|
||||||
}
|
}
|
||||||
readItem(allItems[position])
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@@ -18,7 +18,9 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
class SourcesActivity : AppCompatActivity(), DIAware {
|
class SourcesActivity :
|
||||||
|
AppCompatActivity(),
|
||||||
|
DIAware {
|
||||||
private lateinit var binding: ActivitySourcesBinding
|
private lateinit var binding: ActivitySourcesBinding
|
||||||
|
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
@@ -68,11 +70,12 @@ class SourcesActivity : AppCompatActivity(), DIAware {
|
|||||||
binding.recyclerView.adapter = mAdapter
|
binding.recyclerView.adapter = mAdapter
|
||||||
mAdapter.notifyDataSetChanged()
|
mAdapter.notifyDataSetChanged()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast
|
||||||
this@SourcesActivity,
|
.makeText(
|
||||||
R.string.cant_get_sources,
|
this@SourcesActivity,
|
||||||
Toast.LENGTH_SHORT,
|
R.string.cant_get_sources,
|
||||||
).show()
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
CountingIdlingResourceSingleton.decrement()
|
CountingIdlingResourceSingleton.decrement()
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,9 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.closestDI
|
import org.kodein.di.android.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
class UpsertSourceActivity :
|
||||||
|
AppCompatActivity(),
|
||||||
|
DIAware {
|
||||||
private var existingSource: SelfossModel.SourceDetail? = null
|
private var existingSource: SelfossModel.SourceDetail? = null
|
||||||
private var mSpoutsValue: String? = null
|
private var mSpoutsValue: String? = null
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
private fun handleSpoutsSpinner() {
|
private fun handleSpoutsSpinner() {
|
||||||
val spoutsKV = HashMap<String, String>()
|
val spoutsKV = HashMap<String, String>()
|
||||||
binding.spoutsSpinner.onItemSelectedListener =
|
binding.spoutsSpinner.onItemSelectedListener =
|
||||||
@@ -105,11 +108,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun handleSpoutFailure(networkIssue: Boolean = false) {
|
fun handleSpoutFailure(networkIssue: Boolean = false) {
|
||||||
Toast.makeText(
|
Toast
|
||||||
this@UpsertSourceActivity,
|
.makeText(
|
||||||
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
this@UpsertSourceActivity,
|
||||||
Toast.LENGTH_SHORT,
|
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
||||||
).show()
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
binding.progress.visibility = View.GONE
|
binding.progress.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +174,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
|||||||
sourceDetailsUnavailable -> {
|
sourceDetailsUnavailable -> {
|
||||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
val successfullyAddedSource =
|
val successfullyAddedSource =
|
||||||
@@ -192,11 +197,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
|||||||
if (successfullyAddedSource) {
|
if (successfullyAddedSource) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast
|
||||||
this@UpsertSourceActivity,
|
.makeText(
|
||||||
R.string.cant_create_source,
|
this@UpsertSourceActivity,
|
||||||
Toast.LENGTH_SHORT,
|
R.string.cant_create_source,
|
||||||
).show()
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -118,16 +118,18 @@ class ItemCardAdapter(
|
|||||||
binding.itemImage.setImageDrawable(null)
|
binding.itemImage.setImageDrawable(null)
|
||||||
} else {
|
} else {
|
||||||
binding.itemImage.visibility = View.VISIBLE
|
binding.itemImage.visibility = View.VISIBLE
|
||||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||||
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||||
} else {
|
} else {
|
||||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
|
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class ViewHolder(
|
||||||
|
val binding: CardItemBinding,
|
||||||
|
) : RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
@@ -65,13 +65,15 @@ class ItemListAdapter(
|
|||||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||||
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||||
} else {
|
} else {
|
||||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
inner class ViewHolder(
|
||||||
|
val binding: ListItemBinding,
|
||||||
|
) : RecyclerView.ViewHolder(binding.root)
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
|||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@@ -16,6 +16,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi
|
|||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -29,12 +30,14 @@ import org.kodein.di.instance
|
|||||||
class SourcesListAdapter(
|
class SourcesListAdapter(
|
||||||
private val app: Activity,
|
private val app: Activity,
|
||||||
private val items: ArrayList<SelfossModel.SourceDetail>,
|
private val items: ArrayList<SelfossModel.SourceDetail>,
|
||||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
|
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
|
||||||
|
DIAware {
|
||||||
private val c: Context = app.baseContext
|
private val c: Context = app.baseContext
|
||||||
private lateinit var binding: SourceListItemBinding
|
private lateinit var binding: SourceListItemBinding
|
||||||
|
|
||||||
override val di: DI by closestDI(app)
|
override val di: DI by closestDI(app)
|
||||||
private val repository: Repository by instance()
|
private val repository: Repository by instance()
|
||||||
|
private val appSettingsService: AppSettingsService by instance()
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
@@ -61,11 +64,12 @@ class SourcesListAdapter(
|
|||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, itemCount)
|
notifyItemRangeChanged(position, itemCount)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast
|
||||||
app,
|
.makeText(
|
||||||
R.string.can_delete_source,
|
app,
|
||||||
Toast.LENGTH_SHORT,
|
R.string.can_delete_source,
|
||||||
).show()
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +84,7 @@ class SourcesListAdapter(
|
|||||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||||
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
|
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
|
||||||
} else {
|
} else {
|
||||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!itm.error.isNullOrBlank()) {
|
if (!itm.error.isNullOrBlank()) {
|
||||||
@@ -99,5 +103,7 @@ class SourcesListAdapter(
|
|||||||
|
|
||||||
override fun getItemCount(): Int = items.size
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
|
inner class ViewHolder(
|
||||||
|
val mView: ConstraintLayout,
|
||||||
|
) : RecyclerView.ViewHolder(mView)
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,8 @@ import org.kodein.di.instance
|
|||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
private const val NOTIFICATION_DELAY = 4000L
|
||||||
|
|
||||||
class LoadingWorker(
|
class LoadingWorker(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
params: WorkerParameters,
|
params: WorkerParameters,
|
||||||
@@ -61,7 +63,7 @@ class LoadingWorker(
|
|||||||
handleNewItemsNotification(apiItems, notificationManager)
|
handleNewItemsNotification(apiItems, notificationManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apiItems.map { it.preloadImages(context) }
|
apiItems.map { it.preloadImages(context, appSettingsService) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
@@ -93,7 +95,7 @@ class LoadingWorker(
|
|||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(
|
.Builder(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
|
AppSettingsService.NEW_ITEMS_CHANNEL,
|
||||||
).setContentTitle(context.getString(R.string.new_items_notification_title))
|
).setContentTitle(context.getString(R.string.new_items_notification_title))
|
||||||
.setContentText(
|
.setContentText(
|
||||||
context.getString(
|
context.getString(
|
||||||
@@ -101,16 +103,16 @@ class LoadingWorker(
|
|||||||
newSize,
|
newSize,
|
||||||
),
|
),
|
||||||
).setPriority(PRIORITY_DEFAULT)
|
).setPriority(PRIORITY_DEFAULT)
|
||||||
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL_ID)
|
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||||
|
|
||||||
Timer("", false).schedule(4000) {
|
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||||
notificationManager.notify(2, newItemsNotification.build())
|
notificationManager.notify(2, newItemsNotification.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timer("", false).schedule(4000) {
|
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||||
notificationManager.cancel(1)
|
notificationManager.cancel(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,18 +2,14 @@ package bou.amine.apps.readerforselfossv2.android.fragments
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.content.res.TypedArray
|
import android.content.res.TypedArray
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
|
||||||
import android.util.TypedValue.DATA_NULL_UNDEFINED
|
import android.util.TypedValue.DATA_NULL_UNDEFINED
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.InflateException
|
import android.view.InflateException
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -23,7 +19,6 @@ import android.webkit.WebView
|
|||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
||||||
import bou.amine.apps.readerforselfossv2.android.R
|
import bou.amine.apps.readerforselfossv2.android.R
|
||||||
@@ -32,10 +27,15 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
|
|||||||
import bou.amine.apps.readerforselfossv2.android.model.toModel
|
import bou.amine.apps.readerforselfossv2.android.model.toModel
|
||||||
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
|
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.addHomeMadeActionItem
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.getColorFromAttr
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
|
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
|
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
|
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||||
import bou.amine.apps.readerforselfossv2.model.MercuryModel
|
import bou.amine.apps.readerforselfossv2.model.MercuryModel
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
@@ -46,11 +46,7 @@ import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
|||||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
||||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||||
import com.bumptech.glide.Glide
|
import com.leinardi.android.speeddial.SpeedDialView
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -65,9 +61,19 @@ import java.util.Locale
|
|||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
private const val IMAGE_JPG = "image/jpg"
|
private const val IMAGE_JPG = "image/jpg"
|
||||||
|
private const val IMAGE_PNG = "image/png"
|
||||||
|
private const val IMAGE_WEBP = "image/webp"
|
||||||
|
|
||||||
class ArticleFragment : Fragment(), DIAware {
|
private const val WHITE_COLOR_HEX = 0xFFFFFF
|
||||||
private var fontSize: Int = 16
|
|
||||||
|
private const val DEFAULT_FONT_SIZE = 16
|
||||||
|
|
||||||
|
class ArticleFragment :
|
||||||
|
Fragment(),
|
||||||
|
DIAware {
|
||||||
|
private var colorOnSurface: Int = 0
|
||||||
|
private var colorSurface: Int = 0
|
||||||
|
private var fontSize: Int = DEFAULT_FONT_SIZE
|
||||||
private lateinit var item: SelfossModel.Item
|
private lateinit var item: SelfossModel.Item
|
||||||
private lateinit var url: String
|
private lateinit var url: String
|
||||||
private lateinit var contentText: String
|
private lateinit var contentText: String
|
||||||
@@ -75,7 +81,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
private lateinit var contentImage: String
|
private lateinit var contentImage: String
|
||||||
private lateinit var contentTitle: String
|
private lateinit var contentTitle: String
|
||||||
private lateinit var allImages: ArrayList<String>
|
private lateinit var allImages: ArrayList<String>
|
||||||
private lateinit var fab: FloatingActionButton
|
private lateinit var fab: SpeedDialView
|
||||||
private lateinit var textAlignment: String
|
private lateinit var textAlignment: String
|
||||||
private lateinit var binding: FragmentArticleBinding
|
private lateinit var binding: FragmentArticleBinding
|
||||||
|
|
||||||
@@ -86,7 +92,6 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
private var typeface: Typeface? = null
|
private var typeface: Typeface? = null
|
||||||
private var resId: Int = 0
|
private var resId: Int = 0
|
||||||
private var font = ""
|
private var font = ""
|
||||||
private var staticBar = false
|
|
||||||
|
|
||||||
private val mercuryApi: MercuryApi by instance()
|
private val mercuryApi: MercuryApi by instance()
|
||||||
|
|
||||||
@@ -98,6 +103,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
item = pi.toModel()
|
item = pi.toModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod")
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -112,6 +118,9 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
e.sendSilentlyWithAcra()
|
e.sendSilentlyWithAcra()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
colorOnSurface = getColorFromAttr(com.google.android.material.R.attr.colorOnSurface)
|
||||||
|
colorSurface = getColorFromAttr(com.google.android.material.R.attr.colorSurface)
|
||||||
|
|
||||||
contentText = item.content
|
contentText = item.content
|
||||||
contentTitle = item.title.getHtmlDecoded()
|
contentTitle = item.title.getHtmlDecoded()
|
||||||
contentImage = item.getThumbnail(repository.baseUrl)
|
contentImage = item.getThumbnail(repository.baseUrl)
|
||||||
@@ -125,23 +134,11 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
allImages = item.getImages()
|
allImages = item.getImages()
|
||||||
|
|
||||||
fontSize = appSettingsService.getFontSize()
|
fontSize = appSettingsService.getFontSize()
|
||||||
staticBar = appSettingsService.isStaticBarEnabled()
|
|
||||||
font = appSettingsService.getFont()
|
font = appSettingsService.getFont()
|
||||||
|
|
||||||
refreshAlignment()
|
refreshAlignment()
|
||||||
|
|
||||||
fab = binding.fab
|
handleFloatingToolbar()
|
||||||
|
|
||||||
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
|
|
||||||
|
|
||||||
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
|
|
||||||
|
|
||||||
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
|
|
||||||
|
|
||||||
if (staticBar) {
|
|
||||||
fab.hide()
|
|
||||||
floatingToolbar.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.source.text = contentSource
|
binding.source.text = contentSource
|
||||||
if (typeface != null) {
|
if (typeface != null) {
|
||||||
@@ -149,37 +146,20 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleContent()
|
handleContent()
|
||||||
|
|
||||||
binding.nestedScrollView.setOnScrollChangeListener(
|
|
||||||
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
|
||||||
if (scrollY > oldScrollY) {
|
|
||||||
floatingToolbar.hide()
|
|
||||||
fab.hide()
|
|
||||||
} else {
|
|
||||||
if (staticBar) {
|
|
||||||
floatingToolbar.show()
|
|
||||||
} else {
|
|
||||||
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (e: InflateException) {
|
} catch (e: InflateException) {
|
||||||
e.sendSilentlyWithAcraWithName("webview not available")
|
e.sendSilentlyWithAcraWithName("webview not available")
|
||||||
try {
|
maybeIfContext {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog
|
||||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
.Builder(it)
|
||||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
.setMessage(it.getString(R.string.webview_dialog_issue_message))
|
||||||
|
.setTitle(it.getString(R.string.webview_dialog_issue_title))
|
||||||
.setPositiveButton(
|
.setPositiveButton(
|
||||||
android.R.string.ok,
|
android.R.string.ok,
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
appSettingsService.disableArticleViewer()
|
appSettingsService.disableArticleViewer()
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
}
|
}.create()
|
||||||
.create()
|
|
||||||
.show()
|
.show()
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,70 +181,84 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
|
|
||||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||||
binding.imageView.visibility = View.VISIBLE
|
binding.imageView.visibility = View.VISIBLE
|
||||||
Glide
|
maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) }
|
||||||
.with(requireContext())
|
|
||||||
.asBitmap()
|
|
||||||
.load(contentImage)
|
|
||||||
.apply(RequestOptions.fitCenterTransform())
|
|
||||||
.into(binding.imageView)
|
|
||||||
} else {
|
} else {
|
||||||
binding.imageView.visibility = View.GONE
|
binding.imageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFloatingToolbar(): FloatingToolbar {
|
private fun handleFloatingToolbar() {
|
||||||
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
fab = binding.speedDial
|
||||||
if (appSettingsService.getPublicAccess()) {
|
fab.mainFabClosedIconColor = colorOnSurface
|
||||||
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
|
fab.mainFabOpenedIconColor = colorOnSurface
|
||||||
}
|
|
||||||
floatingToolbar.attachFab(fab)
|
|
||||||
|
|
||||||
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
|
maybeIfContext { handleFloatingToolbarActionItems(it) }
|
||||||
|
|
||||||
floatingToolbar.setClickListener(
|
fab.setOnActionSelectedListener { actionItem ->
|
||||||
object : FloatingToolbar.ItemClickListener {
|
when (actionItem.id) {
|
||||||
override fun onItemClick(item: MenuItem) {
|
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||||
when (item.itemId) {
|
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
R.id.unread_action ->
|
||||||
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
if (this@ArticleFragment.item.unread) {
|
||||||
R.id.unread_action ->
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
repository.markAsRead(this@ArticleFragment.item)
|
||||||
if (this@ArticleFragment.item.unread) {
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
this@ArticleFragment.item.unread = false
|
||||||
repository.markAsRead(this@ArticleFragment.item)
|
maybeIfContext {
|
||||||
}
|
Toast
|
||||||
this@ArticleFragment.item.unread = false
|
.makeText(
|
||||||
Toast.makeText(
|
it,
|
||||||
requireContext(),
|
R.string.marked_as_read,
|
||||||
R.string.marked_as_read,
|
Toast.LENGTH_LONG,
|
||||||
Toast.LENGTH_LONG,
|
).show()
|
||||||
).show()
|
}
|
||||||
} else {
|
} else {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
repository.unmarkAsRead(this@ArticleFragment.item)
|
repository.unmarkAsRead(this@ArticleFragment.item)
|
||||||
}
|
}
|
||||||
this@ArticleFragment.item.unread = true
|
this@ArticleFragment.item.unread = true
|
||||||
Toast.makeText(
|
maybeIfContext {
|
||||||
context,
|
Toast
|
||||||
R.string.marked_as_unread,
|
.makeText(
|
||||||
Toast.LENGTH_LONG,
|
it,
|
||||||
).show()
|
R.string.marked_as_unread,
|
||||||
}
|
Toast.LENGTH_LONG,
|
||||||
} catch (e: IllegalStateException) {
|
).show()
|
||||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: MenuItem?) {
|
else -> Unit
|
||||||
// We do nothing
|
}
|
||||||
}
|
false
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFloatingToolbarActionItems(c: Context) {
|
||||||
|
fab.addHomeMadeActionItem(
|
||||||
|
R.id.share_action,
|
||||||
|
resources.getDrawable(R.drawable.ic_share_white_24dp),
|
||||||
|
R.string.reader_action_share,
|
||||||
|
colorOnSurface,
|
||||||
|
colorSurface,
|
||||||
|
c,
|
||||||
|
)
|
||||||
|
fab.addHomeMadeActionItem(
|
||||||
|
R.id.open_action,
|
||||||
|
resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
|
||||||
|
R.string.reader_action_open,
|
||||||
|
colorOnSurface,
|
||||||
|
colorSurface,
|
||||||
|
c,
|
||||||
|
)
|
||||||
|
fab.addHomeMadeActionItem(
|
||||||
|
R.id.unread_action,
|
||||||
|
resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
|
||||||
|
R.string.unmark,
|
||||||
|
colorOnSurface,
|
||||||
|
colorSurface,
|
||||||
|
c,
|
||||||
)
|
)
|
||||||
return floatingToolbar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAlignment() {
|
private fun refreshAlignment() {
|
||||||
@@ -276,6 +270,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
private fun getContentFromMercury() {
|
private fun getContentFromMercury() {
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
|
||||||
@@ -314,17 +309,12 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLeadImage(lead_image_url: String?) {
|
private fun handleLeadImage(leadImageUrl: String?) {
|
||||||
if (!lead_image_url.isNullOrEmpty() && context != null) {
|
if (!leadImageUrl.isNullOrEmpty()) {
|
||||||
binding.imageView.visibility = View.VISIBLE
|
maybeIfContext {
|
||||||
Glide
|
binding.imageView.visibility = View.VISIBLE
|
||||||
.with(requireContext())
|
it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
|
||||||
.asBitmap()
|
}
|
||||||
.load(
|
|
||||||
lead_image_url,
|
|
||||||
)
|
|
||||||
.apply(RequestOptions.fitCenterTransform())
|
|
||||||
.into(binding.imageView)
|
|
||||||
} else {
|
} else {
|
||||||
binding.imageView.visibility = View.GONE
|
binding.imageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@@ -337,125 +327,85 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
override fun shouldOverrideUrlLoading(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
url: String,
|
url: String,
|
||||||
): Boolean {
|
): Boolean =
|
||||||
return if (context != null &&
|
if (url.isUrlValid() &&
|
||||||
url.isUrlValid() &&
|
|
||||||
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||||
) {
|
) {
|
||||||
requireContext().openUrlInBrowser(url)
|
maybeIfContext { it.openUrlInBrowserAsNewTask(url) }
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun shouldInterceptRequest(
|
override fun shouldInterceptRequest(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
url: String,
|
url: String,
|
||||||
): WebResourceResponse? {
|
): WebResourceResponse? {
|
||||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
val (mime: String?, compression: Bitmap.CompressFormat) =
|
||||||
if (url.lowercase(Locale.US).contains(".jpg") ||
|
if (url
|
||||||
url.lowercase(Locale.US)
|
.lowercase(Locale.US)
|
||||||
.contains(".jpeg")
|
.contains(".jpg") ||
|
||||||
) {
|
url.lowercase(Locale.US).contains(".jpeg")
|
||||||
try {
|
) {
|
||||||
val image =
|
Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG)
|
||||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
|
} else if (url.lowercase(Locale.US).contains(".png")) {
|
||||||
.get()
|
Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG)
|
||||||
return WebResourceResponse(
|
} else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||||
IMAGE_JPG,
|
Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP)
|
||||||
"UTF-8",
|
} else {
|
||||||
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
|
return super.shouldInterceptRequest(view, url)
|
||||||
)
|
|
||||||
} 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()
|
|
||||||
.get()
|
|
||||||
return 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()
|
|
||||||
.get()
|
|
||||||
return WebResourceResponse(
|
|
||||||
IMAGE_JPG,
|
|
||||||
"UTF-8",
|
|
||||||
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
|
|
||||||
)
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.shouldInterceptRequest(view, url)
|
try {
|
||||||
|
val image = view.getGlideImageForResource(url, appSettingsService)
|
||||||
|
return WebResourceResponse(
|
||||||
|
mime,
|
||||||
|
"UTF-8",
|
||||||
|
getBitmapInputStream(image, compression),
|
||||||
|
)
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
return super.shouldInterceptRequest(view, url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
|
||||||
private fun htmlToWebview() {
|
private fun htmlToWebview() {
|
||||||
val context: Context
|
maybeIfContext {
|
||||||
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 attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||||
val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
|
val a: TypedArray = it.obtainStyledAttributes(resId, attrs)
|
||||||
|
|
||||||
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||||
binding.webcontent.visibility = View.VISIBLE
|
""
|
||||||
|
|
||||||
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, 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")
|
|
||||||
}
|
}
|
||||||
|
binding.webcontent.visibility = View.VISIBLE
|
||||||
|
|
||||||
val colorSurfaceString =
|
val colorSurfaceString =
|
||||||
String.format(
|
String.format(
|
||||||
"#%06X",
|
"#%06X",
|
||||||
0xFFFFFF and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else 0xFFFFFF),
|
WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
|
||||||
)
|
)
|
||||||
|
|
||||||
val colorOnSurfaceString =
|
val colorOnSurfaceString =
|
||||||
String.format(
|
String.format(
|
||||||
"#%06X",
|
"#%06X",
|
||||||
0xFFFFFF and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
|
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
binding.webcontent.settings.useWideViewPort = true
|
||||||
|
binding.webcontent.settings.loadWithOverviewMode = true
|
||||||
|
binding.webcontent.settings.javaScriptEnabled = false
|
||||||
|
|
||||||
|
handleImageLoading()
|
||||||
try {
|
try {
|
||||||
binding.webcontent.settings.useWideViewPort = true
|
|
||||||
binding.webcontent.settings.loadWithOverviewMode = true
|
|
||||||
binding.webcontent.settings.javaScriptEnabled = false
|
|
||||||
|
|
||||||
handleImageLoading()
|
|
||||||
|
|
||||||
val gestureDetector =
|
val gestureDetector =
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
activity,
|
activity,
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
|
||||||
return performClick()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -464,49 +414,50 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.webcontent.settings.layoutAlgorithm =
|
|
||||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
|
e.sendSilentlyWithAcraWithName("Gesture detector issue ?")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
binding.webcontent.settings.layoutAlgorithm =
|
||||||
var baseUrl: String? = null
|
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||||
try {
|
|
||||||
val itemUrl = URL(url)
|
|
||||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
|
||||||
} catch (e: MalformedURLException) {
|
|
||||||
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
|
|
||||||
}
|
|
||||||
|
|
||||||
val fontName =
|
var baseUrl: String? = null
|
||||||
|
try {
|
||||||
|
val itemUrl = URL(url)
|
||||||
|
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontName: String =
|
||||||
|
maybeIfContext {
|
||||||
when (font) {
|
when (font) {
|
||||||
getString(R.string.open_sans_font_id) -> "Open Sans"
|
it.getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||||
getString(R.string.roboto_font_id) -> "Roboto"
|
it.getString(R.string.roboto_font_id) -> "Roboto"
|
||||||
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
it.getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
|
}?.toString().orEmpty()
|
||||||
|
|
||||||
val fontLinkAndStyle =
|
val fontLinkAndStyle =
|
||||||
if (font.isNotEmpty()) {
|
if (fontName.isNotEmpty()) {
|
||||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||||
fontName.replace(
|
fontName.replace(
|
||||||
" ",
|
" ",
|
||||||
"+",
|
"+",
|
||||||
)
|
)
|
||||||
}" rel="stylesheet">
|
}" rel="stylesheet">
|
||||||
|<style>
|
|<style>
|
||||||
| * {
|
| * {
|
||||||
| font-family: '$fontName';
|
| font-family: '$fontName';
|
||||||
| }
|
| }
|
||||||
|</style>
|
|</style>
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
binding.webcontent.loadDataWithBaseURL(
|
binding.webcontent.loadDataWithBaseURL(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
"""<html>
|
"""<html>
|
||||||
@@ -523,7 +474,7 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
| color: ${
|
| color: ${
|
||||||
String.format(
|
String.format(
|
||||||
"#%06X",
|
"#%06X",
|
||||||
0xFFFFFF and context.resources.getColor(R.color.colorAccent),
|
WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int),
|
||||||
)
|
)
|
||||||
} !important;
|
} !important;
|
||||||
| }
|
| }
|
||||||
@@ -580,10 +531,8 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
|
|
||||||
private fun openInBrowserAfterFailing() {
|
private fun openInBrowserAfterFailing() {
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
try {
|
maybeIfContext {
|
||||||
requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +549,8 @@ class ArticleFragment : Fragment(), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun performClick(): Boolean {
|
fun performClick(): Boolean {
|
||||||
if (allImages != null && (
|
if (allImages != null &&
|
||||||
|
(
|
||||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.android.fragments
|
package bou.amine.apps.readerforselfossv2.android.fragments
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
@@ -16,11 +15,13 @@ import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
|||||||
import bou.amine.apps.readerforselfossv2.android.R
|
import bou.amine.apps.readerforselfossv2.android.R
|
||||||
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
|
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
|
||||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||||
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
|
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.ViewTarget
|
import com.bumptech.glide.request.target.ViewTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
@@ -33,10 +34,15 @@ import org.kodein.di.DIAware
|
|||||||
import org.kodein.di.android.x.closestDI
|
import org.kodein.di.android.x.closestDI
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
private const val DRAWABLE_SIZE = 30
|
||||||
|
|
||||||
|
class FilterSheetFragment :
|
||||||
|
BottomSheetDialogFragment(),
|
||||||
|
DIAware {
|
||||||
private lateinit var binding: FilterFragmentBinding
|
private lateinit var binding: FilterFragmentBinding
|
||||||
override val di: DI by closestDI()
|
override val di: DI by closestDI()
|
||||||
private val repository: Repository by instance()
|
private val repository: Repository by instance()
|
||||||
|
private val appSettingsService: AppSettingsService by instance()
|
||||||
|
|
||||||
private var selectedChip: Chip? = null
|
private var selectedChip: Chip? = null
|
||||||
|
|
||||||
@@ -54,8 +60,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
handleTagChips(requireContext())
|
handleTagChips()
|
||||||
handleSourceChips(requireContext())
|
handleSourceChips()
|
||||||
|
|
||||||
binding.progressBar2.visibility = GONE
|
binding.progressBar2.visibility = GONE
|
||||||
binding.filterView.visibility = VISIBLE
|
binding.filterView.visibility = VISIBLE
|
||||||
@@ -73,16 +79,24 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleSourceChips(context: Context) {
|
private suspend fun handleSourceChips() {
|
||||||
val sourceGroup = binding.sourcesGroup
|
val sourceGroup = binding.sourcesGroup
|
||||||
|
|
||||||
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
|
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
|
||||||
val c = Chip(context)
|
val c: Chip? =
|
||||||
|
maybeIfContext {
|
||||||
|
Chip(it)
|
||||||
|
} as Chip?
|
||||||
|
|
||||||
|
if (c == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ellipsize = TextUtils.TruncateAt.END
|
c.ellipsize = TextUtils.TruncateAt.END
|
||||||
|
|
||||||
Glide.with(context)
|
maybeIfContext {
|
||||||
.load(source.getIcon(repository.baseUrl))
|
it.imageIntoViewTarget(
|
||||||
.into(
|
source.getIcon(repository.baseUrl),
|
||||||
object : ViewTarget<Chip?, Drawable?>(c) {
|
object : ViewTarget<Chip?, Drawable?>(c) {
|
||||||
override fun onResourceReady(
|
override fun onResourceReady(
|
||||||
resource: Drawable,
|
resource: Drawable,
|
||||||
@@ -95,7 +109,9 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
appSettingsService,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
c.text = source.title.getHtmlDecoded()
|
c.text = source.title.getHtmlDecoded()
|
||||||
|
|
||||||
@@ -131,13 +147,17 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleTagChips(context: Context) {
|
private suspend fun handleTagChips() {
|
||||||
val tagGroup = binding.tagsGroup
|
val tagGroup = binding.tagsGroup
|
||||||
|
|
||||||
val tags = repository.getTags()
|
val tags = repository.getTags()
|
||||||
|
|
||||||
tags.forEachIndexed { _, tag ->
|
tags.forEachIndexed { _, tag ->
|
||||||
val c = Chip(context)
|
val c: Chip? = maybeIfContext { Chip(it) } as Chip?
|
||||||
|
if (c == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ellipsize = TextUtils.TruncateAt.END
|
c.ellipsize = TextUtils.TruncateAt.END
|
||||||
c.text = tag.tag
|
c.text = tag.tag
|
||||||
|
|
||||||
@@ -153,8 +173,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
|||||||
}
|
}
|
||||||
gd.setColor(gdColor)
|
gd.setColor(gdColor)
|
||||||
gd.shape = GradientDrawable.RECTANGLE
|
gd.shape = GradientDrawable.RECTANGLE
|
||||||
gd.setSize(30, 30)
|
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
|
||||||
gd.cornerRadius = 30F
|
gd.cornerRadius = DRAWABLE_SIZE.toFloat()
|
||||||
c.chipIcon = gd
|
c.chipIcon = gd
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
|
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
|
||||||
|
@@ -6,15 +6,21 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
|
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
|
||||||
import com.bumptech.glide.Glide
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.x.closestDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
class ImageFragment : Fragment() {
|
class ImageFragment :
|
||||||
|
Fragment(),
|
||||||
|
DIAware {
|
||||||
|
override val di: DI by closestDI()
|
||||||
|
private val appSettingsService: AppSettingsService by instance()
|
||||||
private lateinit var imageUrl: String
|
private lateinit var imageUrl: String
|
||||||
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
|
||||||
private var _binding: FragmentImageBinding? = null
|
private var _binding: FragmentImageBinding? = null
|
||||||
private val binding get() = _binding
|
val binding get() = _binding
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -31,11 +37,7 @@ class ImageFragment : Fragment() {
|
|||||||
val view = binding?.root
|
val view = binding?.root
|
||||||
|
|
||||||
binding!!.photoView.visibility = View.VISIBLE
|
binding!!.photoView.visibility = View.VISIBLE
|
||||||
Glide.with(requireActivity())
|
requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService)
|
||||||
.asBitmap()
|
|
||||||
.apply(glideOptions)
|
|
||||||
.load(imageUrl)
|
|
||||||
.into(binding!!.photoView)
|
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@@ -3,23 +3,21 @@ package bou.amine.apps.readerforselfossv2.android.model
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
|
|
||||||
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
|
fun SelfossModel.Item.preloadImages(
|
||||||
|
context: Context,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
): Boolean {
|
||||||
val imageUrls = this.getImages()
|
val imageUrls = this.getImages()
|
||||||
|
|
||||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (url in imageUrls) {
|
for (url in imageUrls) {
|
||||||
if (URLUtil.isValidUrl(url)) {
|
if (URLUtil.isValidUrl(url)) {
|
||||||
Glide.with(context).asBitmap()
|
context.preloadImage(url, appSettingsService)
|
||||||
.apply(glideOptions)
|
|
||||||
.load(url).submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
|
@@ -26,6 +26,10 @@ import org.kodein.di.android.closestDI
|
|||||||
|
|
||||||
private const val TITLE_TAG = "settingsActivityTitle"
|
private const val TITLE_TAG = "settingsActivityTitle"
|
||||||
|
|
||||||
|
const val MAX_ITEMS_NUMBER = 200
|
||||||
|
|
||||||
|
private const val MIN_ITEMS_NUMBER = 1
|
||||||
|
|
||||||
class SettingsActivity :
|
class SettingsActivity :
|
||||||
AppCompatActivity(),
|
AppCompatActivity(),
|
||||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||||
@@ -143,7 +147,7 @@ class SettingsActivity :
|
|||||||
InputFilter { source, _, _, dest, _, _ ->
|
InputFilter { source, _, _, dest, _, _ ->
|
||||||
try {
|
try {
|
||||||
val input: Int = (dest.toString() + source.toString()).toInt()
|
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) {
|
} catch (nfe: NumberFormatException) {
|
||||||
Toast
|
Toast
|
||||||
.makeText(
|
.makeText(
|
||||||
@@ -236,7 +240,7 @@ class SettingsActivity :
|
|||||||
|
|
||||||
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
|
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
openUrl(AppSettingsService.TRACKER_URL)
|
openUrl(AppSettingsService.BUG_URL)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,12 @@ package bou.amine.apps.readerforselfossv2.android.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.TypedValue
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import bou.amine.apps.readerforselfossv2.android.R
|
import bou.amine.apps.readerforselfossv2.android.R
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||||
|
|
||||||
fun Context.shareLink(
|
fun Context.shareLink(
|
||||||
@@ -16,9 +21,39 @@ fun Context.shareLink(
|
|||||||
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
|
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent.createChooser(
|
Intent
|
||||||
sendIntent,
|
.createChooser(
|
||||||
getString(R.string.share),
|
sendIntent,
|
||||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
getString(R.string.share),
|
||||||
|
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
fun Fragment.getColorFromAttr(
|
||||||
|
@AttrRes attrColor: Int,
|
||||||
|
resolveRefs: Boolean = true,
|
||||||
|
): Int {
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
maybeIfContextWithLog { this.requireContext().theme.resolveAttribute(attrColor, typedValue, resolveRefs) }
|
||||||
|
return typedValue.data
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
|
fun Fragment.maybeIfContext(fn: (Context) -> Any): Any? {
|
||||||
|
try {
|
||||||
|
return fn(this.requireContext())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Do nothing
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.maybeIfContextWithLog(fn: (Context) -> Any): Any? {
|
||||||
|
try {
|
||||||
|
return fn(this.requireContext())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.sendSilentlyWithAcraWithName("Fragment context issue...")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -59,7 +59,5 @@ class CircleImageView
|
|||||||
textView.text = text.toTextDrawableString()
|
textView.text = text.toTextDrawableString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun colorFromIdentifier(key: String): Int {
|
private fun colorFromIdentifier(key: String): Int = colorScheme[abs(key.hashCode()) % colorScheme.size]
|
||||||
return colorScheme[abs(key.hashCode()) % colorScheme.size]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -25,11 +25,12 @@ fun Context.openItemUrl(
|
|||||||
app: Activity,
|
app: Activity,
|
||||||
) {
|
) {
|
||||||
if (!linkDecoded.isUrlValid()) {
|
if (!linkDecoded.isUrlValid()) {
|
||||||
Toast.makeText(
|
Toast
|
||||||
this,
|
.makeText(
|
||||||
this.getString(R.string.cant_open_invalid_url),
|
this,
|
||||||
Toast.LENGTH_LONG,
|
this.getString(R.string.cant_open_invalid_url),
|
||||||
).show()
|
Toast.LENGTH_LONG,
|
||||||
|
).show()
|
||||||
} else {
|
} else {
|
||||||
if (articleViewer) {
|
if (articleViewer) {
|
||||||
val intent = Intent(this, ReaderActivity::class.java)
|
val intent = Intent(this, ReaderActivity::class.java)
|
||||||
@@ -71,6 +72,7 @@ fun Context.openUrlInBrowser(url: String) {
|
|||||||
this.mayBeStartActivity(intent)
|
this.mayBeStartActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
fun Context.mayBeStartActivity(intent: Intent) {
|
fun Context.mayBeStartActivity(intent: Intent) {
|
||||||
try {
|
try {
|
||||||
this.startActivity(intent)
|
this.startActivity(intent)
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.android.utils.bottombar
|
package bou.amine.apps.readerforselfossv2.android.utils.bottombar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import bou.amine.apps.readerforselfossv2.android.R
|
||||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||||
|
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||||
|
import com.leinardi.android.speeddial.SpeedDialView
|
||||||
|
|
||||||
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||||
this.setText("")
|
this.setText("")
|
||||||
@@ -9,3 +16,25 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
|
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
|
||||||
|
|
||||||
|
@Suppress("detekt:LongParameterList")
|
||||||
|
fun SpeedDialView.addHomeMadeActionItem(
|
||||||
|
@IdRes actionId: Int,
|
||||||
|
actionIcon: Drawable,
|
||||||
|
@StringRes labelId: Int,
|
||||||
|
colorOnSurface: Int,
|
||||||
|
colorSurface: Int,
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
this.addActionItem(
|
||||||
|
SpeedDialActionItem
|
||||||
|
.Builder(actionId, actionIcon)
|
||||||
|
.setFabBackgroundColor(context.resources.getColor(R.color.colorAccent))
|
||||||
|
.setFabImageTintColor(colorOnSurface)
|
||||||
|
.setLabel(context.getString(labelId))
|
||||||
|
.setLabelClickable(false)
|
||||||
|
.setLabelBackgroundColor(colorOnSurface)
|
||||||
|
.setLabelColor(colorSurface)
|
||||||
|
.create(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -2,40 +2,135 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||||
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
|
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.bumptech.glide.request.target.ViewTarget
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
private const val PRELOAD_IMAGE_TIMEOUT = 10000
|
||||||
|
|
||||||
|
@Suppress("detekt:ReturnCount")
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun String.toGlideUrl(appSettingsService: AppSettingsService): Any { // GlideUrl Or String
|
||||||
|
if (this.isEmptyOrNullOrNullString()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (appSettingsService.getBasicUserName().isNotEmpty()) {
|
||||||
|
val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
|
||||||
|
val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
|
return GlideUrl(
|
||||||
|
this,
|
||||||
|
LazyHeaders
|
||||||
|
.Builder()
|
||||||
|
.addHeader("Authorization", "Basic $authBuf")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return GlideUrl(
|
||||||
|
this,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun WebView.getGlideImageForResource(
|
||||||
|
url: String,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
|
.submit()
|
||||||
|
.get()
|
||||||
|
|
||||||
|
fun Context.preloadImage(
|
||||||
|
url: String,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT))
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
|
.submit()
|
||||||
|
|
||||||
|
fun Context.imageIntoViewTarget(
|
||||||
|
url: String,
|
||||||
|
target: ViewTarget<Chip?, Drawable?>,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
|
.into(target)
|
||||||
|
|
||||||
|
fun Context.bitmapWithCache(
|
||||||
|
url: String,
|
||||||
|
iv: ImageView,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
|
.into(iv)
|
||||||
|
|
||||||
fun Context.bitmapCenterCrop(
|
fun Context.bitmapCenterCrop(
|
||||||
url: String,
|
url: String,
|
||||||
iv: ImageView,
|
iv: ImageView,
|
||||||
) = Glide.with(this)
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(url)
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
.apply(RequestOptions.centerCropTransform())
|
.apply(RequestOptions.centerCropTransform())
|
||||||
.into(iv)
|
.into(iv)
|
||||||
|
|
||||||
|
fun Context.bitmapFitCenter(
|
||||||
|
url: String,
|
||||||
|
iv: ImageView,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
|
) = Glide
|
||||||
|
.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
|
.apply(RequestOptions.fitCenterTransform())
|
||||||
|
.into(iv)
|
||||||
|
|
||||||
fun Context.circularDrawable(
|
fun Context.circularDrawable(
|
||||||
url: String,
|
url: String,
|
||||||
view: CircleImageView,
|
view: CircleImageView,
|
||||||
|
appSettingsService: AppSettingsService,
|
||||||
) {
|
) {
|
||||||
view.textView.text = ""
|
view.textView.text = ""
|
||||||
|
|
||||||
Glide.with(this)
|
Glide
|
||||||
.load(url)
|
.with(this)
|
||||||
|
.load(url.toGlideUrl(appSettingsService))
|
||||||
.into(view.imageView)
|
.into(view.imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
|
||||||
|
|
||||||
fun getBitmapInputStream(
|
fun getBitmapInputStream(
|
||||||
bitmap: Bitmap,
|
bitmap: Bitmap,
|
||||||
compressFormat: Bitmap.CompressFormat,
|
compressFormat: Bitmap.CompressFormat,
|
||||||
): InputStream {
|
): InputStream {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
|
bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
|
||||||
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
||||||
return ByteArrayInputStream(bitmapData)
|
return ByteArrayInputStream(bitmapData)
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Build
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
lateinit var s: Snackbar
|
lateinit var s: Snackbar
|
||||||
@@ -11,19 +10,13 @@ lateinit var s: Snackbar
|
|||||||
fun isNetworkAccessible(context: Context): Boolean {
|
fun isNetworkAccessible(context: Context): Boolean {
|
||||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
|
||||||
val network = connectivityManager.activeNetwork ?: return false
|
|
||||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val network = connectivityManager.activeNetworkInfo ?: return false
|
|
||||||
return network.isConnectedOrConnecting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AppViewModel(private val repository: Repository) : ViewModel() {
|
class AppViewModel(
|
||||||
|
private val repository: Repository,
|
||||||
|
) : ViewModel() {
|
||||||
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
|
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
|
||||||
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
|
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
|
||||||
private var wasConnected = true
|
private var wasConnected = true
|
||||||
|
@@ -71,35 +71,13 @@
|
|||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<FrameLayout
|
<com.leinardi.android.speeddial.SpeedDialView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/speedDial"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start|bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
|
||||||
app:layout_constraintStart_toStartOf="parent">
|
|
||||||
|
|
||||||
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
|
||||||
android:id="@+id/floatingToolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
app:floatingMenu="@menu/reader_toolbar" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end|bottom"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:src="@drawable/ic_add_white_24dp"
|
|
||||||
app:backgroundTint="?attr/colorAccent"
|
|
||||||
app:fabSize="mini"
|
|
||||||
app:rippleColor="?attr/colorAccentDark" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
@@ -119,4 +97,5 @@
|
|||||||
android:progressTint="?attr/colorAccent" />
|
android:progressTint="?attr/colorAccent" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/unread_action"
|
|
||||||
android:icon="@drawable/ic_baseline_white_eye_24dp"
|
|
||||||
android:title="@string/unmark"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/open_action"
|
|
||||||
android:icon="@drawable/ic_open_in_browser_white_24dp"
|
|
||||||
android:title="@string/reader_action_open"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/share_action"
|
|
||||||
android:icon="@drawable/ic_share_white_24dp"
|
|
||||||
android:title="@string/reader_action_share"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
</menu>
|
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Linksbündig</string>
|
<string name="reader_text_align_left">Linksbündig</string>
|
||||||
<string name="reader_text_align_justify">Blocksatz</string>
|
<string name="reader_text_align_justify">Blocksatz</string>
|
||||||
<string name="settings_reader_font">Schriftgröße im Lesemodus</string>
|
<string name="settings_reader_font">Schriftgröße im Lesemodus</string>
|
||||||
<string name="reader_static_bar_title">Statische untere Leiste im Lesemodus</string>
|
|
||||||
<string name="reader_static_bar_on">Die untere Leiste wird dauerhaft angezeigt</string>
|
|
||||||
<string name="reader_static_bar_off">Die untere Leiste kann über einen schwebenden Button angezeigt werden</string>
|
|
||||||
<string name="remove_source">Quelle entfernen</string>
|
<string name="remove_source">Quelle entfernen</string>
|
||||||
<string name="pref_theme_title">Heller/Dunkler Modus</string>
|
<string name="pref_theme_title">Heller/Dunkler Modus</string>
|
||||||
<string name="mode_dark">Dunkler Modus</string>
|
<string name="mode_dark">Dunkler Modus</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Alinear a la izquierda</string>
|
<string name="reader_text_align_left">Alinear a la izquierda</string>
|
||||||
<string name="reader_text_align_justify">Justificado</string>
|
<string name="reader_text_align_justify">Justificado</string>
|
||||||
<string name="settings_reader_font">Modo lectura</string>
|
<string name="settings_reader_font">Modo lectura</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Aligner à gauche</string>
|
<string name="reader_text_align_left">Aligner à gauche</string>
|
||||||
<string name="reader_text_align_justify">Justifier le texte</string>
|
<string name="reader_text_align_justify">Justifier le texte</string>
|
||||||
<string name="settings_reader_font">Police du lecteur d\'articles</string>
|
<string name="settings_reader_font">Police du lecteur d\'articles</string>
|
||||||
<string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
|
|
||||||
<string name="reader_static_bar_on">La barre sera affichée</string>
|
|
||||||
<string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
|
|
||||||
<string name="remove_source">Supprimer la source</string>
|
<string name="remove_source">Supprimer la source</string>
|
||||||
<string name="pref_theme_title">Thème Clair/Sombre</string>
|
<string name="pref_theme_title">Thème Clair/Sombre</string>
|
||||||
<string name="mode_dark">Thème sombre</string>
|
<string name="mode_dark">Thème sombre</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Aliñar á esquerda</string>
|
<string name="reader_text_align_left">Aliñar á esquerda</string>
|
||||||
<string name="reader_text_align_justify">Xustificado</string>
|
<string name="reader_text_align_justify">Xustificado</string>
|
||||||
<string name="settings_reader_font">Modo lector</string>
|
<string name="settings_reader_font">Modo lector</string>
|
||||||
<string name="reader_static_bar_title">Barra inferior estática na vista de artigos</string>
|
|
||||||
<string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
|
|
||||||
<string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
|
|
||||||
<string name="remove_source">Eliminar fonte</string>
|
<string name="remove_source">Eliminar fonte</string>
|
||||||
<string name="pref_theme_title">Modo Claro/Escuro</string>
|
<string name="pref_theme_title">Modo Claro/Escuro</string>
|
||||||
<string name="mode_dark">Modo escuro</string>
|
<string name="mode_dark">Modo escuro</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -23,6 +23,7 @@
|
|||||||
<string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
|
<string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
|
||||||
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
|
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
|
||||||
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
|
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
|
||||||
|
<string name="undo_string">"Ongedaan maken"</string>
|
||||||
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
|
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
|
||||||
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
|
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
|
||||||
<string name="cant_create_source">"Kan bron niet creëeren"</string>
|
<string name="cant_create_source">"Kan bron niet creëeren"</string>
|
||||||
@@ -105,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
@@ -131,5 +129,4 @@
|
|||||||
<string name="action_about">"Over"</string>
|
<string name="action_about">"Over"</string>
|
||||||
<string name="marked_as_read">"Artikel gelezen"</string>
|
<string name="marked_as_read">"Artikel gelezen"</string>
|
||||||
<string name="marked_as_unread">"Item unread"</string>
|
<string name="marked_as_unread">"Item unread"</string>
|
||||||
<string name="undo_string">"Ongedaan maken"</string>
|
|
||||||
</resources>
|
</resources>
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">左对齐</string>
|
<string name="reader_text_align_left">左对齐</string>
|
||||||
<string name="reader_text_align_justify">左右对齐</string>
|
<string name="reader_text_align_justify">左右对齐</string>
|
||||||
<string name="settings_reader_font">阅读器字体</string>
|
<string name="settings_reader_font">阅读器字体</string>
|
||||||
<string name="reader_static_bar_title">文章查看器中的静态底部栏</string>
|
|
||||||
<string name="reader_static_bar_on">底部栏将始终显示</string>
|
|
||||||
<string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string>
|
|
||||||
<string name="remove_source">删除源</string>
|
<string name="remove_source">删除源</string>
|
||||||
<string name="pref_theme_title">浅色/深色模式</string>
|
<string name="pref_theme_title">浅色/深色模式</string>
|
||||||
<string name="mode_dark">深色模式</string>
|
<string name="mode_dark">深色模式</string>
|
||||||
|
@@ -106,9 +106,6 @@
|
|||||||
<string name="reader_text_align_left">Align left</string>
|
<string name="reader_text_align_left">Align left</string>
|
||||||
<string name="reader_text_align_justify">Justify</string>
|
<string name="reader_text_align_justify">Justify</string>
|
||||||
<string name="settings_reader_font">Reader font</string>
|
<string name="settings_reader_font">Reader font</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
6
androidApp/src/main/res/values/ids.xml
Normal file
6
androidApp/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="unread_action" type="id" />
|
||||||
|
<item name="open_action" type="id" />
|
||||||
|
<item name="share_action" type="id" />
|
||||||
|
</resources>
|
@@ -108,9 +108,6 @@
|
|||||||
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
|
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
|
||||||
<string name="open_sans_font_id" translatable="false">open_sans</string>
|
<string name="open_sans_font_id" translatable="false">open_sans</string>
|
||||||
<string name="roboto_font_id" translatable="false">roboto</string>
|
<string name="roboto_font_id" translatable="false">roboto</string>
|
||||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
|
||||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
|
||||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
|
||||||
<string name="remove_source">Remove source</string>
|
<string name="remove_source">Remove source</string>
|
||||||
<string name="pref_theme_title">Light/Dark mode</string>
|
<string name="pref_theme_title">Light/Dark mode</string>
|
||||||
<string name="mode_dark">Dark mode</string>
|
<string name="mode_dark">Dark mode</string>
|
||||||
|
@@ -30,14 +30,6 @@
|
|||||||
android:summaryOn="@string/pref_article_viewer_on"
|
android:summaryOn="@string/pref_article_viewer_on"
|
||||||
android:title="@string/pref_article_viewer_title"
|
android:title="@string/pref_article_viewer_title"
|
||||||
app:iconSpaceReserved="false"/>
|
app:iconSpaceReserved="false"/>
|
||||||
<SwitchPreference
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:dependency="prefer_article_viewer"
|
|
||||||
android:key="reader_static_bar"
|
|
||||||
android:summaryOff="@string/reader_static_bar_off"
|
|
||||||
android:summaryOn="@string/reader_static_bar_on"
|
|
||||||
android:title="@string/reader_static_bar_title"
|
|
||||||
app:iconSpaceReserved="false"/>
|
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/pref_general_category_displaying">
|
android:title="@string/pref_general_category_displaying">
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("detekt:LargeClass")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.tests.repository
|
package bou.amine.apps.readerforselfossv2.tests.repository
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||||
@@ -50,7 +52,6 @@ class RepositoryTest {
|
|||||||
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
|
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
|
||||||
private val appSettingsService = mockk<AppSettingsService>()
|
private val appSettingsService = mockk<AppSettingsService>()
|
||||||
private val api = mockk<SelfossApi>()
|
private val api = mockk<SelfossApi>()
|
||||||
|
|
||||||
private lateinit var repository: Repository
|
private lateinit var repository: Repository
|
||||||
|
|
||||||
private fun initializeRepository(
|
private fun initializeRepository(
|
||||||
|
@@ -44,7 +44,7 @@ class FakeItemParameters {
|
|||||||
var datetime = "2022-09-09T03:32:01-04:00"
|
var datetime = "2022-09-09T03:32:01-04:00"
|
||||||
val title = "Etica della ricerca sotto i riflettori."
|
val title = "Etica della ricerca sotto i riflettori."
|
||||||
val content =
|
val content =
|
||||||
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>L’etica della scienza è di certo ambito di cui continuiamo</p>"
|
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>L’etica della scienza è di certo ambito di cui continuiamo a scoprire nuovi aspetti e risvolti.</p>\n<p>L’ultimo è quello delle intelligenze artificiali capaci di creare opere complesse basate su immagini e parole memorizzate con il rischio di fake news e di contenuti disturbanti.</p>\n<p>Per evitare che ciò accada si sta procedendo filtrando secondo criteri di autocensura i dati da cui l’intelligenza artificiale parte.</p>\n<p>Comincia ad intravedersi un futuro prossimo di competizione fra autori umani ed artificiali nel quale sarà importante, quando i loro prodotti saranno indistinguibili, dichiararne l’origine.</p>\n<p>Come si comprende, si conferma che gli aspetti etici dell’innovazione e della ricerca si diversificato sempre di più.</p>\n<p>La biologia molecolare e la genetica già in passato hanno posto all’attenzione comune aspetti di etica della scienza che hanno indotto a nuove riflessioni circa i limiti delle ricerche.</p>\n<p>L’argomento, sempre attuale, torna sulle prime pagine a seguito della pubblicazione di una ricerca della Università di Cambridge che ha sviluppato una struttura cellulare di un topo con un cuore che batte regolarmente.</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image002-1.png?w=481\" alt=\"\" width=\"697\" height=\"430\" /><img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image003-1.png?w=906\" alt=\"\" /><p>Magdalena Zernicka-Goetz</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image004.jpg?w=474\" alt=\"\" width=\"622\" height=\"465\" /><p>Gianluca Amadei</p>\n<p>Del gruppo fa parte anche uno scienziato italiano Gianluca Amadei,che dinnanzi alle obiezioni di natura etica sulla realizzazione della vita artificiale si è affrettato a sostenere che non è creare nuove vite il fine primario della ricerca, ma quello di salvare quelle esistenti, di dare contributi essenziali alla medicina citando il caso del fallimento tuttora non interpretato di alcune gravidanze e di superare la sperimentazione animale, così contribuendo positivamente alla soluzione di un altro dilemma etico.</p>\n<p>L’embrione sintetico ha ovviamente come primo traguardo il contributo ai trapianti oggi drammaticamente carenti nell’offerta rispetto alla domanda, con attese fino a 4 anni per i trapianti di cuore ed a 2 anni per quelli di fegato. Il lavoro dovrebbe adesso continuare presso l’Ateneo di Padova per creare nuovi organi e nuovi farmaci.</p>"
|
||||||
var unread = true
|
var unread = true
|
||||||
var starred = true
|
var starred = true
|
||||||
val thumbnail = null
|
val thumbnail = null
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
//trick: for the same plugin versions in all sub-modules
|
// trick: for the same plugin versions in all sub-modules
|
||||||
id("com.android.application").version("8.7.3").apply(false)
|
id("com.android.application").version("8.8.1").apply(false)
|
||||||
id("com.android.library").version("8.7.3").apply(false)
|
id("com.android.library").version("8.8.1").apply(false)
|
||||||
id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
|
id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
|
||||||
kotlin("multiplatform").version("2.1.0").apply(false)
|
kotlin("multiplatform").version("2.1.0").apply(false)
|
||||||
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
|
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
|
||||||
@@ -16,7 +16,6 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register("clean", Delete::class) {
|
tasks.register("clean", Delete::class) {
|
||||||
delete(layout.buildDirectory)
|
delete(layout.buildDirectory)
|
||||||
}
|
}
|
||||||
|
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.*'
|
@@ -0,0 +1,4 @@
|
|||||||
|
**v125010111**
|
||||||
|
|
||||||
|
- Debug trying to fix context issues. (#174)
|
||||||
|
- Changelog for v125010031
|
@@ -0,0 +1,5 @@
|
|||||||
|
**v125010131**
|
||||||
|
|
||||||
|
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||||
|
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||||
|
- Changelog for v125010111
|
@@ -0,0 +1,6 @@
|
|||||||
|
**v125010201**
|
||||||
|
|
||||||
|
- fix: Handle empty url issue.
|
||||||
|
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||||
|
- chore: changing actions in reader fragment.
|
||||||
|
- Changelog for v125010131
|
@@ -0,0 +1,8 @@
|
|||||||
|
**v125010241**
|
||||||
|
|
||||||
|
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||||
|
- refactor: context fragments issues.
|
||||||
|
- logs: Context issues.
|
||||||
|
- fix: Handle empty url issue, again.
|
||||||
|
- fix: Link not opening.
|
||||||
|
- Changelog for v125010201
|
@@ -0,0 +1,7 @@
|
|||||||
|
**v125020411**
|
||||||
|
|
||||||
|
- Merge pull request 'bump' (#182) from bump into master
|
||||||
|
- chore: non transiant R classes.
|
||||||
|
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||||
|
- bump
|
||||||
|
- fix: One more missing context.
|
@@ -18,8 +18,8 @@ kotlin.code.style=official
|
|||||||
#Android
|
#Android
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
#android.nonTransitiveRClass=true
|
#android.nonTransitiveRClass=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=false
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=true
|
||||||
#MPP
|
#MPP
|
||||||
kotlin.mpp.enableCInteropCommonization=true
|
kotlin.mpp.enableCInteropCommonization=true
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Mon Nov 25 22:48:24 CET 2024
|
#Sun Feb 09 14:44:52 CET 2025
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -4,12 +4,13 @@ import android.content.Context
|
|||||||
import app.cash.sqldelight.db.SqlDriver
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||||
|
|
||||||
actual class DriverFactory(private val context: Context) {
|
actual class DriverFactory(
|
||||||
actual fun createDriver(): SqlDriver {
|
private val context: Context,
|
||||||
return AndroidSqliteDriver(
|
) {
|
||||||
|
actual fun createDriver(): SqlDriver =
|
||||||
|
AndroidSqliteDriver(
|
||||||
ReaderForSelfossDB.Schema,
|
ReaderForSelfossDB.Schema,
|
||||||
context,
|
context,
|
||||||
"ReaderForSelfossV2-android.db",
|
"ReaderForSelfossV2-android.db",
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -9,12 +9,14 @@ class NaiveTrustManager : X509TrustManager {
|
|||||||
chain: Array<out X509Certificate>?,
|
chain: Array<out X509Certificate>?,
|
||||||
authType: String?,
|
authType: String?,
|
||||||
) {
|
) {
|
||||||
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun checkServerTrusted(
|
override fun checkServerTrusted(
|
||||||
chain: Array<out X509Certificate>?,
|
chain: Array<out X509Certificate>?,
|
||||||
authType: String?,
|
authType: String?,
|
||||||
) {
|
) {
|
||||||
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
|
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
|
||||||
|
@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.utils
|
|||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
|
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||||
actual class DateUtils {
|
actual class DateUtils {
|
||||||
actual companion object {
|
actual companion object {
|
||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
||||||
|
@@ -12,16 +12,14 @@ actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(bas
|
|||||||
|
|
||||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = 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> {
|
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
|
||||||
val allImages = ArrayList<String>()
|
val allImages = ArrayList<String>()
|
||||||
|
|
||||||
for (image in Jsoup.parse(content).getElementsByTag("img")) {
|
for (image in Jsoup.parse(content).getElementsByTag("img")) {
|
||||||
val url = image.attr("src")
|
val url = image.attr("src")
|
||||||
if (url.lowercase(Locale.US).contains(".jpg") ||
|
if (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) {
|
||||||
url.lowercase(Locale.US).contains(".jpeg") ||
|
|
||||||
url.lowercase(Locale.US).contains(".png") ||
|
|
||||||
url.lowercase(Locale.US).contains(".webp")
|
|
||||||
) {
|
|
||||||
allImages.add(url)
|
allImages.add(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
|
@file:Suppress("detekt:LongParameterList")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.model
|
package bou.amine.apps.readerforselfossv2.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
class MercuryModel {
|
class MercuryModel {
|
||||||
|
@Suppress("detekt:ConstructorParameterNaming")
|
||||||
@Serializable
|
@Serializable
|
||||||
class ParsedContent(
|
class ParsedContent(
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("detekt:LongParameterList")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.model
|
package bou.amine.apps.readerforselfossv2.model
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
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.int
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
class ModelException(
|
||||||
|
message: String,
|
||||||
|
) : Throwable(message)
|
||||||
|
|
||||||
class SelfossModel {
|
class SelfossModel {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Tag(
|
data class Tag(
|
||||||
@@ -141,7 +147,7 @@ class SelfossModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stringUrl.isEmptyOrNullOrNullString()) {
|
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
|
return stringUrl
|
||||||
@@ -170,7 +176,7 @@ class SelfossModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this seems to be super slow.
|
// this seems to be super slow.
|
||||||
object TagsListSerializer : KSerializer<List<String>> {
|
object TagsListSerializer : KSerializer<List<String>> {
|
||||||
override fun deserialize(decoder: Decoder): List<String> =
|
override fun deserialize(decoder: Decoder): List<String> =
|
||||||
when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
|
when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("detekt:TooManyFunctions")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.repository
|
package bou.amine.apps.readerforselfossv2.repository
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.dao.ACTION
|
import bou.amine.apps.readerforselfossv2.dao.ACTION
|
||||||
@@ -11,7 +13,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
|||||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
|
import bou.amine.apps.readerforselfossv2.model.StatusAndData
|
||||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
|
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
|
||||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||||
import bou.amine.apps.readerforselfossv2.utils.toEntity
|
import bou.amine.apps.readerforselfossv2.utils.toEntity
|
||||||
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
|
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
|
||||||
@@ -23,6 +25,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val MAX_ITEMS_NUMBER = 200
|
||||||
|
|
||||||
class Repository(
|
class Repository(
|
||||||
private val api: SelfossApi,
|
private val api: SelfossApi,
|
||||||
private val appSettingsService: AppSettingsService,
|
private val appSettingsService: AppSettingsService,
|
||||||
@@ -36,26 +40,26 @@ class Repository(
|
|||||||
|
|
||||||
var displayedItems = ItemType.UNREAD
|
var displayedItems = ItemType.UNREAD
|
||||||
|
|
||||||
private var tagFilterFlow = MutableStateFlow<SelfossModel.Tag?>(null)
|
private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null)
|
||||||
var tagFilter = tagFilterFlow.asStateFlow()
|
var tagFilter = _tagFilter.asStateFlow()
|
||||||
private var sourceFilterFlow = MutableStateFlow<SelfossModel.Source?>(null)
|
private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null)
|
||||||
var sourceFilter = sourceFilterFlow.asStateFlow()
|
var sourceFilter = _sourceFilter.asStateFlow()
|
||||||
var searchFilter: String? = null
|
var searchFilter: String? = null
|
||||||
|
|
||||||
var offlineOverride = false
|
var offlineOverride = false
|
||||||
|
|
||||||
private val badgeUnreadFlow = MutableStateFlow(0)
|
private val _badgeUnread = MutableStateFlow(0)
|
||||||
val badgeUnread = badgeUnreadFlow.asStateFlow()
|
val badgeUnread = _badgeUnread.asStateFlow()
|
||||||
private val badgeAllFlow = MutableStateFlow(0)
|
private val _badgeAll = MutableStateFlow(0)
|
||||||
val badgeAll = badgeAllFlow.asStateFlow()
|
val badgeAll = _badgeAll.asStateFlow()
|
||||||
private val badgeStarredFlow = MutableStateFlow(0)
|
private val _badgeStarred = MutableStateFlow(0)
|
||||||
val badgeStarred = badgeStarredFlow.asStateFlow()
|
val badgeStarred = _badgeStarred.asStateFlow()
|
||||||
|
|
||||||
private var fetchedTags = false
|
private var fetchedTags = false
|
||||||
private var fetchedSources = false
|
private var fetchedSources = false
|
||||||
|
|
||||||
private var readerItems = ArrayList<SelfossModel.Item>()
|
private var _readerItems = ArrayList<SelfossModel.Item>()
|
||||||
private var selectedSource: SelfossModel.SourceDetail? = null
|
private var _selectedSource: SelfossModel.SourceDetail? = null
|
||||||
|
|
||||||
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
||||||
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
||||||
@@ -127,7 +131,7 @@ class Repository(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
200,
|
MAX_ITEMS_NUMBER,
|
||||||
)
|
)
|
||||||
return if (items.success && items.data != null) {
|
return if (items.success && items.data != null) {
|
||||||
items.data
|
items.data
|
||||||
@@ -139,22 +143,23 @@ class Repository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:ForbiddenComment")
|
||||||
suspend fun reloadBadges(): Boolean {
|
suspend fun reloadBadges(): Boolean {
|
||||||
var success = false
|
var success = false
|
||||||
if (isNetworkAvailable()) {
|
if (isNetworkAvailable()) {
|
||||||
val response = api.stats()
|
val response = api.stats()
|
||||||
if (response.success && response.data != null) {
|
if (response.success && response.data != null) {
|
||||||
badgeUnreadFlow.value = response.data.unread ?: 0
|
_badgeUnread.value = response.data.unread ?: 0
|
||||||
badgeAllFlow.value = response.data.total
|
_badgeAll.value = response.data.total
|
||||||
badgeStarredFlow.value = response.data.starred ?: 0
|
_badgeStarred.value = response.data.starred ?: 0
|
||||||
success = true
|
success = true
|
||||||
}
|
}
|
||||||
} else if (appSettingsService.isItemCachingEnabled()) {
|
} else if (appSettingsService.isItemCachingEnabled()) {
|
||||||
// TODO: do this differently, because it's not efficient
|
// TODO: do this differently, because it's not efficient
|
||||||
val dbItems = getDBItems()
|
val dbItems = getDBItems()
|
||||||
badgeUnreadFlow.value = dbItems.filter { item -> item.unread }.size
|
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
|
||||||
badgeStarredFlow.value = dbItems.filter { item -> item.starred }.size
|
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
|
||||||
badgeAllFlow.value = dbItems.size
|
_badgeAll.value = dbItems.size
|
||||||
success = true
|
success = true
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -316,7 +321,7 @@ class Repository(
|
|||||||
private fun markAsReadLocally(item: SelfossModel.Item) {
|
private fun markAsReadLocally(item: SelfossModel.Item) {
|
||||||
if (item.unread) {
|
if (item.unread) {
|
||||||
item.unread = false
|
item.unread = false
|
||||||
badgeUnreadFlow.value -= 1
|
_badgeUnread.value -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@@ -327,7 +332,7 @@ class Repository(
|
|||||||
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
|
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
|
||||||
if (!item.unread) {
|
if (!item.unread) {
|
||||||
item.unread = true
|
item.unread = true
|
||||||
badgeUnreadFlow.value += 1
|
_badgeUnread.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@@ -338,7 +343,7 @@ class Repository(
|
|||||||
private fun starrLocally(item: SelfossModel.Item) {
|
private fun starrLocally(item: SelfossModel.Item) {
|
||||||
if (!item.starred) {
|
if (!item.starred) {
|
||||||
item.starred = true
|
item.starred = true
|
||||||
badgeStarredFlow.value += 1
|
_badgeStarred.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@@ -349,7 +354,7 @@ class Repository(
|
|||||||
private fun unstarrLocally(item: SelfossModel.Item) {
|
private fun unstarrLocally(item: SelfossModel.Item) {
|
||||||
if (item.starred) {
|
if (item.starred) {
|
||||||
item.starred = false
|
item.starred = false
|
||||||
badgeStarredFlow.value -= 1
|
_badgeStarred.value -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
@@ -559,6 +564,7 @@ class Repository(
|
|||||||
item.id.toString(),
|
item.id.toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
|
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
|
||||||
try {
|
try {
|
||||||
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
||||||
@@ -614,30 +620,30 @@ class Repository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setTagFilter(tag: SelfossModel.Tag?) {
|
fun setTagFilter(tag: SelfossModel.Tag?) {
|
||||||
tagFilterFlow.value = tag
|
_tagFilter.value = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSourceFilter(source: SelfossModel.Source?) {
|
fun setSourceFilter(source: SelfossModel.Source?) {
|
||||||
sourceFilterFlow.value = source
|
_sourceFilter.value = source
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
|
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
|
||||||
this.readerItems = readerItems
|
_readerItems = readerItems
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getReaderItems(): ArrayList<SelfossModel.Item> = readerItems
|
fun getReaderItems(): ArrayList<SelfossModel.Item> = _readerItems
|
||||||
|
|
||||||
fun migrate(driverFactory: DriverFactory) {
|
fun migrate(driverFactory: DriverFactory) {
|
||||||
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
|
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedSource(source: SelfossModel.SourceDetail) {
|
fun setSelectedSource(source: SelfossModel.SourceDetail) {
|
||||||
selectedSource = source
|
_selectedSource = source
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsetSelectedSource() {
|
fun unsetSelectedSource() {
|
||||||
selectedSource = null
|
_selectedSource = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSelectedSource(): SelfossModel.SourceDetail? = selectedSource
|
fun getSelectedSource(): SelfossModel.SourceDetail? = _selectedSource
|
||||||
}
|
}
|
||||||
|
@@ -17,8 +17,8 @@ import kotlinx.serialization.json.Json
|
|||||||
class MercuryApi {
|
class MercuryApi {
|
||||||
var client = createHttpClient()
|
var client = createHttpClient()
|
||||||
|
|
||||||
private fun createHttpClient(): HttpClient {
|
private fun createHttpClient(): HttpClient =
|
||||||
return HttpClient {
|
HttpClient {
|
||||||
install(HttpCache)
|
install(HttpCache)
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(
|
json(
|
||||||
@@ -40,7 +40,6 @@ class MercuryApi {
|
|||||||
}
|
}
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
|
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
|
||||||
bodyOrFailure(
|
bodyOrFailure(
|
||||||
|
@@ -33,6 +33,7 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
|
|||||||
SuccessResponse(false)
|
SuccessResponse(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
|
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
|
||||||
try {
|
try {
|
||||||
return if (r != null && r.status.isSuccess()) {
|
return if (r != null && r.status.isSuccess()) {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("detekt:TooManyFunctions", "detekt:LongParameterList", "detekt:LargeClass")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.rest
|
package bou.amine.apps.readerforselfossv2.rest
|
||||||
|
|
||||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||||
@@ -35,6 +37,8 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
|
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
|
||||||
|
|
||||||
|
private const val VERSION_WHERE_POST_LOGIN_SHOULD_WORK = 5
|
||||||
|
|
||||||
class SelfossApi(
|
class SelfossApi(
|
||||||
private val appSettingsService: AppSettingsService,
|
private val appSettingsService: AppSettingsService,
|
||||||
) {
|
) {
|
||||||
@@ -176,7 +180,7 @@ class SelfossApi(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
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 =
|
suspend fun logout(): SuccessResponse =
|
||||||
if (shouldHaveNewLogout()) {
|
if (shouldHaveNewLogout()) {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("detekt:TooManyFunctions")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.service
|
package bou.amine.apps.readerforselfossv2.service
|
||||||
|
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
@@ -1,7 +1,19 @@
|
|||||||
|
@file:Suppress("detekt:TooManyFunctions")
|
||||||
|
|
||||||
package bou.amine.apps.readerforselfossv2.service
|
package bou.amine.apps.readerforselfossv2.service
|
||||||
|
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
|
||||||
|
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(
|
class AppSettingsService(
|
||||||
acraSenderServiceProcess: Boolean = false,
|
acraSenderServiceProcess: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -36,12 +48,11 @@ class AppSettingsService(
|
|||||||
private var notifyNewItems: Boolean? = null
|
private var notifyNewItems: Boolean? = null
|
||||||
private var itemsNumber: Int? = null
|
private var itemsNumber: Int? = null
|
||||||
private var apiTimeout: Long? = null
|
private var apiTimeout: Long? = null
|
||||||
private var refreshMinutes: Long = 360
|
private var refreshMinutes: Long = DEFAULT_REFRESH_MINUTES
|
||||||
private var markOnScroll: Boolean? = null
|
private var markOnScroll: Boolean? = null
|
||||||
private var activeAlignment: Int? = null
|
private var activeAlignment: Int? = null
|
||||||
|
|
||||||
private var fontSize: Int? = null
|
private var fontSize: Int? = null
|
||||||
private var staticBar: Boolean? = null
|
|
||||||
private var font: String = ""
|
private var font: String = ""
|
||||||
private var theme: Int? = null
|
private var theme: Int? = null
|
||||||
|
|
||||||
@@ -141,13 +152,14 @@ class AppSettingsService(
|
|||||||
return itemsNumber!!
|
return itemsNumber!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
private fun refreshItemsNumber() {
|
private fun refreshItemsNumber() {
|
||||||
itemsNumber =
|
itemsNumber =
|
||||||
try {
|
try {
|
||||||
settings.getString(API_ITEMS_NUMBER, "20").toInt()
|
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
settings.remove(API_ITEMS_NUMBER)
|
settings.remove(API_ITEMS_NUMBER)
|
||||||
20
|
DEFAULT_ITEMS_NUMBER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,22 +170,24 @@ class AppSettingsService(
|
|||||||
return apiTimeout!!
|
return apiTimeout!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:MagicNumber")
|
||||||
private fun secToMs(n: Long) = n * 1000
|
private fun secToMs(n: Long) = n * 1000
|
||||||
|
|
||||||
|
@Suppress("detekt:SwallowedException")
|
||||||
private fun refreshApiTimeout() {
|
private fun refreshApiTimeout() {
|
||||||
apiTimeout =
|
apiTimeout =
|
||||||
secToMs(
|
secToMs(
|
||||||
try {
|
try {
|
||||||
val settingsTimeout = settings.getString(API_TIMEOUT, "60")
|
val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString())
|
||||||
if (settingsTimeout.toLong() > 0) {
|
if (settingsTimeout.toLong() > 0) {
|
||||||
settingsTimeout.toLong()
|
settingsTimeout.toLong()
|
||||||
} else {
|
} else {
|
||||||
settings.remove(API_TIMEOUT)
|
settings.remove(API_TIMEOUT)
|
||||||
60
|
DEFAULT_API_TIMEOUT
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
settings.remove(API_TIMEOUT)
|
settings.remove(API_TIMEOUT)
|
||||||
60
|
DEFAULT_API_TIMEOUT
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -287,14 +301,14 @@ class AppSettingsService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshRefreshMinutes() {
|
private fun refreshRefreshMinutes() {
|
||||||
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
|
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong()
|
||||||
if (refreshMinutes <= 15) {
|
if (refreshMinutes <= MIN_REFRESH_MINUTES) {
|
||||||
refreshMinutes = 15
|
refreshMinutes = MIN_REFRESH_MINUTES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRefreshMinutes(): Long {
|
fun getRefreshMinutes(): Long {
|
||||||
if (refreshMinutes != 360L) {
|
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) {
|
||||||
refreshRefreshMinutes()
|
refreshRefreshMinutes()
|
||||||
}
|
}
|
||||||
return refreshMinutes
|
return refreshMinutes
|
||||||
@@ -368,18 +382,7 @@ class AppSettingsService(
|
|||||||
if (fontSize != null) {
|
if (fontSize != null) {
|
||||||
refreshFontSize()
|
refreshFontSize()
|
||||||
}
|
}
|
||||||
return fontSize ?: 16
|
return fontSize ?: DEFAULT_FONT_SIZE
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshStaticBarEnabled() {
|
|
||||||
staticBar = settings.getBoolean(READER_STATIC_BAR, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isStaticBarEnabled(): Boolean {
|
|
||||||
if (staticBar != null) {
|
|
||||||
refreshStaticBarEnabled()
|
|
||||||
}
|
|
||||||
return staticBar == true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshFont() {
|
private fun refreshFont() {
|
||||||
@@ -434,7 +437,6 @@ class AppSettingsService(
|
|||||||
refreshActiveAllignment()
|
refreshActiveAllignment()
|
||||||
refreshFontSize()
|
refreshFontSize()
|
||||||
refreshFont()
|
refreshFont()
|
||||||
refreshStaticBarEnabled()
|
|
||||||
refreshCurrentTheme()
|
refreshCurrentTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,11 +486,11 @@ class AppSettingsService(
|
|||||||
|
|
||||||
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
|
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
|
||||||
|
|
||||||
const val TRACKER_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
|
const val BUG_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
|
||||||
|
|
||||||
const val SYNC_CHANNEL_ID = "sync-channel-id"
|
const val SYNC_CHANNEL_ID = "sync-channel-id"
|
||||||
|
|
||||||
const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
|
const val NEW_ITEMS_CHANNEL = "new-items-channel-id"
|
||||||
|
|
||||||
const val JUSTIFY = 1
|
const val JUSTIFY = 1
|
||||||
|
|
||||||
@@ -532,8 +534,6 @@ class AppSettingsService(
|
|||||||
|
|
||||||
const val READER_FONT = "reader_font"
|
const val READER_FONT = "reader_font"
|
||||||
|
|
||||||
const val READER_STATIC_BAR = "reader_static_bar"
|
|
||||||
|
|
||||||
const val READER_FONT_SIZE = "reader_font_size"
|
const val READER_FONT_SIZE = "reader_font_size"
|
||||||
|
|
||||||
const val TEXT_ALIGN = "text_align"
|
const val TEXT_ALIGN = "text_align"
|
||||||
|
@@ -4,6 +4,12 @@ import kotlinx.datetime.LocalDateTime
|
|||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
|
|
||||||
|
class DateParseException(
|
||||||
|
message: String,
|
||||||
|
e: Throwable? = null,
|
||||||
|
) : Throwable(message, e)
|
||||||
|
|
||||||
|
@Suppress("detekt:ThrowsCount")
|
||||||
fun String.toParsedDate(): Long {
|
fun String.toParsedDate(): Long {
|
||||||
// Possible formats are
|
// Possible formats are
|
||||||
// yyyy-mm-dd hh:mm:ss format
|
// yyyy-mm-dd hh:mm:ss format
|
||||||
@@ -17,17 +23,22 @@ fun String.toParsedDate(): Long {
|
|||||||
if (this.matches(oldVersionFormat)) {
|
if (this.matches(oldVersionFormat)) {
|
||||||
this.replace(" ", "T")
|
this.replace(" ", "T")
|
||||||
} else if (this.matches(newVersionFormat)) {
|
} 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 {
|
} else {
|
||||||
throw Exception("Unrecognized format for $this")
|
throw DateParseException("Unrecognized format for $this")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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()
|
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||||
expect class DateUtils() {
|
expect class DateUtils() {
|
||||||
companion object {
|
companion object {
|
||||||
fun parseRelativeDate(dateString: String): String
|
fun parseRelativeDate(dateString: String): String
|
||||||
|
@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
|
|||||||
this.id.toInt(),
|
this.id.toInt(),
|
||||||
this.title,
|
this.title,
|
||||||
null,
|
null,
|
||||||
this.tags?.split(","),
|
this.tags.split(","),
|
||||||
this.spout,
|
this.spout,
|
||||||
this.error,
|
this.error,
|
||||||
this.icon,
|
this.icon,
|
||||||
@@ -74,6 +74,7 @@ fun SelfossModel.Item.toEntity(): ITEM =
|
|||||||
this.author,
|
this.author,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("detekt:MagicNumber")
|
||||||
fun SelfossModel.Tag.getColorHexCode(): String =
|
fun SelfossModel.Tag.getColorHexCode(): String =
|
||||||
if (this.color.length == 4) { // #000
|
if (this.color.length == 4) { // #000
|
||||||
val char1 = this.color.get(1)
|
val char1 = this.color.get(1)
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
|
||||||
|
|
||||||
object Enums {
|
|
||||||
enum class ItemType(
|
|
||||||
val position: Int,
|
|
||||||
val type: String,
|
|
||||||
) {
|
|
||||||
UNREAD(1, "unread"),
|
|
||||||
ALL(2, "all"),
|
|
||||||
STARRED(3, "starred"),
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromInt(value: Int) = values().first { it.position == value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,16 @@
|
|||||||
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
|
@Suppress("detekt:MagicNumber")
|
||||||
|
enum class ItemType(
|
||||||
|
val position: Int,
|
||||||
|
val type: String,
|
||||||
|
) {
|
||||||
|
UNREAD(1, "unread"),
|
||||||
|
ALL(2, "all"),
|
||||||
|
STARRED(3, "starred"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int) = values().first { it.position == value }
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.utils
|
|||||||
|
|
||||||
fun String?.isEmptyOrNullOrNullString(): Boolean = this == null || this == "null" || this.isEmpty()
|
fun String?.isEmptyOrNullOrNullString(): Boolean = this == null || this == "null" || this.isEmpty()
|
||||||
|
|
||||||
|
@Suppress("detekt:MagicNumber")
|
||||||
fun String.longHash(): Long {
|
fun String.longHash(): Long {
|
||||||
var h = 98764321261L
|
var h = 98764321261L
|
||||||
val l = this.length
|
val l = this.length
|
||||||
|
@@ -18,7 +18,8 @@ class DatesTest {
|
|||||||
fun new_version_date_should_be_parsed() {
|
fun new_version_date_should_be_parsed() {
|
||||||
val date = newVersionDate.toParsedDate()
|
val date = newVersionDate.toParsedDate()
|
||||||
val expected =
|
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()
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
assertEquals(expected, date)
|
assertEquals(expected, date)
|
||||||
@@ -28,7 +29,8 @@ class DatesTest {
|
|||||||
fun new_version_date2_should_be_parsed() {
|
fun new_version_date2_should_be_parsed() {
|
||||||
val date = newVersionDate2.toParsedDate()
|
val date = newVersionDate2.toParsedDate()
|
||||||
val expected =
|
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()
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
assertEquals(expected, date)
|
assertEquals(expected, date)
|
||||||
@@ -38,7 +40,8 @@ class DatesTest {
|
|||||||
fun old_version_date_should_be_parsed() {
|
fun old_version_date_should_be_parsed() {
|
||||||
val date = oldVersionDate.toParsedDate()
|
val date = oldVersionDate.toParsedDate()
|
||||||
val expected =
|
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()
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
assertEquals(expected, date)
|
assertEquals(expected, date)
|
||||||
@@ -48,7 +51,8 @@ class DatesTest {
|
|||||||
fun old_version_variant_date_should_be_parsed() {
|
fun old_version_variant_date_should_be_parsed() {
|
||||||
val date = oldVersionDateVariant.toParsedDate()
|
val date = oldVersionDateVariant.toParsedDate()
|
||||||
val expected =
|
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()
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
assertEquals(expected, date)
|
assertEquals(expected, date)
|
||||||
@@ -58,7 +62,8 @@ class DatesTest {
|
|||||||
fun new_version_variant_date_should_be_parsed() {
|
fun new_version_variant_date_should_be_parsed() {
|
||||||
val date = newVersionDateVariant.toParsedDate()
|
val date = newVersionDateVariant.toParsedDate()
|
||||||
val expected =
|
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()
|
.toEpochMilliseconds()
|
||||||
|
|
||||||
assertEquals(expected, date)
|
assertEquals(expected, date)
|
@@ -4,7 +4,5 @@ import app.cash.sqldelight.db.SqlDriver
|
|||||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||||
|
|
||||||
actual class DriverFactory {
|
actual class DriverFactory {
|
||||||
actual fun createDriver(): SqlDriver {
|
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
|
||||||
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,5 +2,6 @@ package bou.amine.apps.readerforselfossv2.rest
|
|||||||
|
|
||||||
import io.ktor.client.engine.cio.CIOEngineConfig
|
import io.ktor.client.engine.cio.CIOEngineConfig
|
||||||
|
|
||||||
|
@Suppress("detekt:EmptyFunctionBlock")
|
||||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
|
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||||
actual class DateUtils {
|
actual class DateUtils {
|
||||||
actual companion object {
|
actual companion object {
|
||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
|
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||||
actual class DateUtils actual constructor() {
|
actual class DateUtils actual constructor() {
|
||||||
actual companion object {
|
actual companion object {
|
||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
@@ -4,7 +4,5 @@ import app.cash.sqldelight.db.SqlDriver
|
|||||||
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||||
|
|
||||||
actual class DriverFactory {
|
actual class DriverFactory {
|
||||||
actual fun createDriver(): SqlDriver {
|
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
|
||||||
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,5 +2,6 @@ package bou.amine.apps.readerforselfossv2.rest
|
|||||||
|
|
||||||
import io.ktor.client.engine.cio.CIOEngineConfig
|
import io.ktor.client.engine.cio.CIOEngineConfig
|
||||||
|
|
||||||
|
@Suppress("detekt:EmptyFunctionBlock")
|
||||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package bou.amine.apps.readerforselfossv2.utils
|
package bou.amine.apps.readerforselfossv2.utils
|
||||||
|
|
||||||
|
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||||
actual class DateUtils {
|
actual class DateUtils {
|
||||||
actual companion object {
|
actual companion object {
|
||||||
actual fun parseRelativeDate(dateString: String): String {
|
actual fun parseRelativeDate(dateString: String): String {
|
||||||
|
Reference in New Issue
Block a user