Compare commits

..

1 Commits

Author SHA1 Message Date
25e336bc5e fix: build.
Some checks failed
Check PR code / Lint (pull_request) Successful in 1m16s
Check PR code / build (pull_request) Failing after 40m21s
Check master code / coverage (pull_request) Failing after 39m9s
2024-12-30 15:42:34 +01:00
89 changed files with 1167 additions and 2344 deletions

View File

@ -1,36 +0,0 @@
root = true
[*]
insert_final_newline = true
[.editorconfig]
insert_final_newline = false
ij_kotlin_line_break_after_multiline_when_entry = false
[*.{kt,kts}]
# Disable wildcard imports entirely
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
end_of_line = lf
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_indent_before_arrow_on_new_line = false
ij_kotlin_line_break_after_multiline_when_entry = true
ij_kotlin_packages_to_use_import_on_demand = unset
indent_size = 4
indent_style = space
ktlint_argument_list_wrapping_ignore_when_parameter_count_greater_or_equal_than = unset
ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 4
ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_code_style = ktlint_official
ktlint_enum_entry_name_casing = upper_or_camel_cases
ktlint_function_naming_ignore_when_annotated_with = unset
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
ktlint_property_naming_constant_naming = screaming_snake_case
max_line_length = 140
[**/build]
ktlint = disabled

View File

@ -3,7 +3,7 @@ on:
workflow_call: workflow_call:
jobs: jobs:
BuildAndTestAndCoverage: BuildAndTest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository code - name: Check out repository code
@ -16,30 +16,9 @@ jobs:
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle - name: Setup Android SDK
- uses: gradle/actions/setup-gradle@v3 uses: android-actions/setup-android@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle... - name: Configure gradle...
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test - name: Build and test
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done run: ./gradlew build --stacktrace
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: coverage
run: |
./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3
with:
name: coverage
path: build/reports/kover/html
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -85,7 +85,6 @@ jobs:
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Configure gradle... - name: Configure gradle...

View File

@ -12,17 +12,15 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
cache: gradle
- name: Install klint - name: Install klint
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.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.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip 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
- name: Linting... - name: Linting...
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
- name: Detecting... - name: Detecting...
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
build: build:
needs: Lint uses: ./.gitea/workflows/common_build.yml
uses: ./.gitea/workflows/common_build.yml

View File

@ -0,0 +1,46 @@
name: Check master code
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch --tags -p
- uses: KengoTODA/actions-setup-docker-compose@v1
with:
version: "2.23.3"
- name: run selfoss
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- uses: gradle/actions/setup-gradle@v3
- uses: android-actions/setup-android@v3
- name: Configure gradle...
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: coverage
run: |
./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3
with:
name: coverage
path: build/reports/kover/html
retention-days: 1
overwrite: true
include-hidden-files: true
- name: Clean
if: always()
run: |
docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -1,39 +1,3 @@
**v125010031
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
- chore: "faster" action.
- fastlane: icon change.
- chore: ignoring a pixel issue.
- test: fixed an ui test issue.
- fix: center the loading thing.
- test: items displaying.
- bump: sqldelight.
- bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
- bump: androix and coroutines.
- bump: ktor. Closes #67.
- Changelog for v124123651
--------------------------------------------------------------------
**v124123651
- Merge pull request 'Bugfixes' (#171) from bugfixes into master
- config: crowdin
- chore: can links be empty ?
- fix: Context issues in article fragment.
- fix: Context issues in fragment sheet.
- fix: build.
- chore: compile issue fix.
- chore: filter some bugs.
- bugfix: catch users using something other than selfoss.
- bugfix: No browser, no link.
- translations
- chore: remove log.
- translation
- Changelog for v124123641
--------------------------------------------------------------------
**v124123641 **v124123641
- Chore: no tests on build. - Chore: no tests on build.

View File

@ -1,7 +1,7 @@
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
val ignoreGitVersion: String by project val ignoreGitVersion: String by project
val acraVersion = "5.12.0" val acraVersion = "5.9.7"
plugins { plugins {
id("com.android.application") id("com.android.application")
@ -9,7 +9,6 @@ plugins {
kotlin("kapt") kotlin("kapt")
id("com.mikepenz.aboutlibraries.plugin") id("com.mikepenz.aboutlibraries.plugin")
id("org.jetbrains.kotlinx.kover") id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2"
} }
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String { fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
@ -66,14 +65,14 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
compileSdk = 35 compileSdk = 34
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
} }
defaultConfig { defaultConfig {
applicationId = "bou.amine.apps.readerforselfossv2.android" applicationId = "bou.amine.apps.readerforselfossv2.android"
minSdk = 25 minSdk = 25
targetSdk = 34 // 35 when edge-to-edge is handled targetSdk = 34
versionCode = versionCodeFromGit() versionCode = versionCodeFromGit()
versionName = versionNameFromGit() versionName = versionNameFromGit()
@ -120,26 +119,28 @@ android {
} }
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
implementation(project(":shared")) implementation(project(":shared"))
implementation("androidx.appcompat:appcompat:1.7.0") implementation("com.google.android.material:material:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.preference:preference-ktx:1.2.1")
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
// Android Support // Android Support
implementation("com.google.android.material:material:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.recyclerview:recyclerview:1.4.0-rc01") implementation("com.google.android.material:material:1.9.0")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0") implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.vectordrawable:vectordrawable:1.2.0") implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.annotation:annotation:1.9.1") implementation("androidx.annotation:annotation:1.7.0")
implementation("androidx.work:work-runtime-ktx:2.10.0") implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.constraintlayout:constraintlayout:2.2.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("org.jsoup:jsoup:1.18.3") implementation("org.jsoup:jsoup:1.15.4")
//multidex //multidex
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
@ -152,31 +153,31 @@ dependencies {
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
// glide // glide
kapt("com.github.bumptech.glide:compiler:4.16.0") kapt("com.github.bumptech.glide:compiler:4.15.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0") implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
// Themes // Themes
implementation("com.github.rubensousa:floatingtoolbar:1.5.1") implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
// Pager // Pager
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0") implementation("androidx.viewpager2:viewpager2:1.1.0-beta02")
//Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.23.1") implementation("org.kodein.di:kodein-di:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x:7.14.0")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.14.0")
//Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0") implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
//Logging //Logging
implementation("io.github.aakira:napier:2.7.1") implementation("io.github.aakira:napier:2.6.1")
//PhotoView //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
@ -184,13 +185,13 @@ dependencies {
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0") implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT // SQLDELIGHT
implementation("app.cash.sqldelight:android-driver:2.0.2") implementation("com.squareup.sqldelight:android-driver:1.5.4")
//test //test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.14") testImplementation("io.mockk:mockk:1.12.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
androidTestImplementation("androidx.test:runner:1.6.2") androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1") androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import androidx.annotation.ArrayRes import androidx.annotation.ArrayRes
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
@ -15,77 +14,70 @@ import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.hasToString
fun performLogin(someUrl: String? = null) { fun performLogin(someUrl: String? = null) {
onView(withId(R.id.urlView)).perform(click()).perform( onView(withId(R.id.urlView)).perform(click()).perform(
typeTextIntoFocusedView( typeTextIntoFocusedView(
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888", if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888"
), )
) )
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())
Thread.sleep(10000)
} }
fun loginAndInitHome() { fun loginAndInitHome() {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
onView(withText("OK")).perform(click()) onView(withText("OK")).perform(click())
} }
fun changeAndCancelSetting( fun changeAndCancelSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText(newValue)) ).perform(replaceText(newValue))
onView( onView(
withId(android.R.id.button2), withId(android.R.id.button2)
).perform(click()) ).perform(click())
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).check(matches(withText(oldValue))) ).check(matches(withText(oldValue)))
onView( onView(
withText(newValue), withText(newValue)
).check(doesNotExist()) ).check(doesNotExist())
onView( onView(
withId(android.R.id.button2), withId(android.R.id.button2)
).perform(click()) ).perform(click())
} }
fun changeAndSaveSetting( fun changeAndSaveSetting(oldValue: String, newValue: String, openSettingItem: () -> Unit) {
oldValue: String,
newValue: String,
openSettingItem: () -> Unit,
) {
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText(newValue)) ).perform(replaceText(newValue))
onView( onView(
withId(android.R.id.button1), withId(android.R.id.button1)
).perform(click()) ).perform(click())
openSettingItem() openSettingItem()
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).check(matches(withText(newValue))) ).check(matches(withText(newValue)))
if (oldValue.isNotEmpty()) { if (oldValue.isNotEmpty()) {
onView( onView(
withText(oldValue), withText(oldValue)
).check(doesNotExist()) ).check(doesNotExist())
} }
onView( onView(
withId(android.R.id.button2), withId(android.R.id.button2)
).perform(click()) ).perform(click())
} }
fun testPreferencesFromArray( fun testPreferencesFromArray(
context: Context, context: Context,
@ArrayRes arrayRes: Int, @ArrayRes arrayRes: Int,
openSettingItem: () -> Unit, openSettingItem: () -> Unit
) { ) {
openSettingItem() openSettingItem()
context.resources.getStringArray(arrayRes).forEach { res -> context.resources.getStringArray(arrayRes).forEach { res ->
@ -95,27 +87,4 @@ fun testPreferencesFromArray(
openSettingItem() openSettingItem()
onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked()))) onView(withText(res)).check(matches(allOf(isDisplayed(), isChecked())))
} }
} }
fun testAddSourceWithUrl(
url: String,
sourceName: String,
) {
onView(withId(R.id.fab))
.perform(click())
onView(withId(R.id.nameInput))
.perform(click())
.perform(typeTextIntoFocusedView(sourceName))
onView(withId(R.id.sourceUri))
.perform(click())
.perform(typeTextIntoFocusedView(url))
onView(withId(R.id.tags))
.perform(click())
.perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
onView(withId(R.id.spoutsSpinner))
.perform(click())
onData(hasToString("RSS Feed")).perform(click())
onView(withId(R.id.saveBtn))
.perform(click())
onView(withText(sourceName)).check(matches(isDisplayed()))
}

View File

@ -1,6 +1,9 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -23,6 +26,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class HomeActivityTest { class HomeActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -35,15 +39,17 @@ class HomeActivityTest {
fun testMenu() { fun testMenu() {
onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( onView(withId(R.id.action_search)).check(matches(isDisplayed())).check(
matches( matches(
isClickable(), isClickable()
), )
) )
onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check( onView(withId(R.id.action_filter)).check(matches(isDisplayed())).check(
matches( matches(
isClickable(), isClickable()
), )
)
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
) )
openMenu()
onView(withText(R.string.readAll)).check(matches(isDisplayed())) onView(withText(R.string.readAll)).check(matches(isDisplayed()))
onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed())) onView(withText(R.string.menu_home_sources)).check(matches(isDisplayed()))
onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed())) onView(withText(R.string.title_activity_settings)).check(matches(isDisplayed()))
@ -56,47 +62,59 @@ 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(R.id.search_src_text)
).check(matches(isFocused())) ).check(matches(isFocused()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
onView(withId(R.id.action_filter)).perform(click()) onView(withId(R.id.action_filter)).perform(click())
onView( onView(
withText(R.string.filter_item_sources), withText(R.string.filter_item_sources)
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView( onView(
withText(R.string.filter_item_tags), withText(R.string.filter_item_tags)
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView( onView(
withId(R.id.floatingActionButton2), withId(R.id.floatingActionButton2)
).check(matches(isDisplayed())) ).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.readAll)).perform(click()) onView(withText(R.string.readAll)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.menu_home_sources)).perform(click()) onView(withText(R.string.menu_home_sources)).perform(click())
onView(withId(R.id.fab)).check(matches(isDisplayed())) onView(withId(R.id.fab)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.menu_home_refresh)).perform(click()) onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed())) onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
/*onView(withText(R.string.issue_tracker_link)).perform(click()) /*onView(withText(R.string.issue_tracker_link)).perform(click())
onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed())) onView(withText(R.string.markall_dialog_message)).check(matches(isDisplayed()))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
openMenu()*/ openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)*/
onView(withText(R.string.action_disconnect)).perform(click()) onView(withText(R.string.action_disconnect)).perform(click())
onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed())) onView(withText(R.string.confirm_disconnect_title)).check(matches(isDisplayed()))
@ -106,13 +124,14 @@ class HomeActivityTest {
fun testEmptyView() { fun testEmptyView() {
onView(withId(R.id.emptyText)).check(matches(isDisplayed())) onView(withId(R.id.emptyText)).check(matches(isDisplayed()))
onView( onView(
hasBottombarItemText(R.string.tab_new), hasBottombarItemText(R.string.tab_new)
).check(matches(isDisplayed())).check(matches(isSelected())) ).check(matches(isDisplayed())).check(matches(isSelected()))
onView( onView(
hasBottombarItemText(R.string.tab_read), hasBottombarItemText(R.string.tab_read)
).check(matches(isDisplayed())).check(matches(not(isSelected()))) ).check(matches(isDisplayed())).check(matches(not(isSelected())))
onView( onView(
hasBottombarItemText(R.string.tab_favs), hasBottombarItemText(R.string.tab_favs)
).check(matches(isDisplayed())).check(matches(not(isSelected()))) ).check(matches(isDisplayed())).check(matches(not(isSelected())))
} }
}
}

View File

@ -1,5 +1,6 @@
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
@ -22,37 +23,40 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class LoginActivityTest { 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 IdlingRegistry.getInstance()
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource) .register(CountingIdlingResourceSingleton.countingIdlingResource)
} }
@After @After
fun unregisterIdlingResource() { fun unregisterIdlingResource() {
IdlingRegistry IdlingRegistry.getInstance()
.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)) onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
.check(matches(isDisplayed()))
.check(matches(isNotChecked()))
.check( .check(
matches(isClickable()), matches(isClickable())
) )
onView(withId(R.id.withLogin)) onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
.check(matches(isDisplayed())) .check(matches(isNotChecked())).check(
.check(matches(isNotChecked())) matches(isClickable())
.check(
matches(isClickable()),
) )
} }
@ -76,4 +80,4 @@ class LoginActivityTest {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
} }
} }

View File

@ -6,7 +6,6 @@ import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isChecked
@ -26,9 +25,11 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityGeneralTest { class SettingsActivityGeneralTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,90 +37,80 @@ class SettingsActivityGeneralTest {
fun init() { fun init() {
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext()
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
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()))
onView( onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title), withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title)
).check( ).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed())) onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check( onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), isChecked()
isChecked(), )
), )
),
) )
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
isEnabled(), 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(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check( onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check( onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches( matches(
not(isEnabled()), not(isEnabled())
), )
) )
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check( onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), isChecked()
isChecked(), )
), )
),
) )
onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check( onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
} }
@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())
@ -127,25 +118,25 @@ class SettingsActivityGeneralTest {
// Value check // Value check
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText("AVC")) ).perform(replaceText("AVC"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText("-1")) ).perform(replaceText("-1"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText("300")) ).perform(replaceText("300"))
.check(matches(withText(""))) .check(matches(withText("")))
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(typeTextIntoFocusedView("300")) ).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30"))) .check(matches(withText("30")))
onView( onView(
withId(android.R.id.edit), withId(android.R.id.edit)
).perform(replaceText("10")) ).perform(replaceText("10"))
.check(matches(withText("10"))) .check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
@ -164,18 +155,18 @@ class SettingsActivityGeneralTest {
// article viewer settings // article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check( onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches( matches(
not(isEnabled()), 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()))
} }
} }

View File

@ -21,9 +21,11 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityOfflineTest { class SettingsActivityOfflineTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -36,79 +38,72 @@ class SettingsActivityOfflineTest {
} }
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext()
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
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(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_items_caching)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
allOf(isNotEnabled(), isDisplayed()), allOf(isNotEnabled(), isDisplayed())
), )
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isNotEnabled(), isNotEnabled()
), )
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_notify_new_items)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(isChecked())
not(isChecked()), )
), )
),
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isNotEnabled(), isNotEnabled()
), )
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), isChecked()
isChecked(), )
), )
),
) )
} }
@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()))
@ -116,50 +111,50 @@ class SettingsActivityOfflineTest {
onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed())) onView(withText(R.string.pref_switch_items_caching_on)).check(matches(isDisplayed()))
onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_items_caching)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
onView(withText(R.string.pref_periodic_refresh_minutes_title)).check( onView(withText(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
isNotEnabled(), isNotEnabled()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isNotEnabled(), isNotEnabled()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isNotEnabled(), isNotEnabled()
), )
) )
onView(withText(R.string.pref_switch_periodic_refresh_off)).check( onView(withText(R.string.pref_switch_periodic_refresh_off)).check(
matches( matches(
isDisplayed(), isDisplayed()
), )
) )
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).perform(click())
onView(withText(R.string.pref_switch_periodic_refresh_on)).check( onView(withText(R.string.pref_switch_periodic_refresh_on)).check(
matches( matches(
isDisplayed(), isDisplayed()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check( onView(withSettingsCheckboxFrame(R.string.pref_periodic_refresh_minutes_title)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_refresh_when_charging)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).check(
matches( matches(
isEnabled(), isEnabled()
), )
) )
changeAndCancelSetting("360", "123") { changeAndCancelSetting("360", "123") {
onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click()) onView(withText(R.string.pref_periodic_refresh_minutes_title)).perform(click())
@ -171,4 +166,4 @@ class SettingsActivityOfflineTest {
onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click()) onView(withSettingsCheckboxFrame(R.string.pref_switch_notify_new_items)).perform(click())
onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_update_sources)).perform(click())
} }
} }

View File

@ -19,9 +19,11 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityReaderTest { class SettingsActivityReaderTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -34,7 +36,7 @@ class SettingsActivityReaderTest {
} }
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext()
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_viewer)).perform(click()) onView(withText(R.string.pref_header_viewer)).perform(click())
@ -45,12 +47,11 @@ class SettingsActivityReaderTest {
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check( onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(), not(
not( isChecked()
isChecked(), )
), )
), )
),
) )
onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed())) onView(withText(R.string.pref_content_reader_font_size)).check(matches(isDisplayed()))
onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed())) onView(withText(R.string.settings_reader_font)).check(matches(isDisplayed()))
@ -60,14 +61,14 @@ class SettingsActivityReaderTest {
fun testReaderActions() { fun testReaderActions() {
onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check( onView(withText(R.string.pref_switch_actions_pager_scroll_off)).check(
matches( matches(
isDisplayed(), isDisplayed()
), )
) )
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click()) onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).perform(click())
onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check( onView(withText(R.string.pref_switch_actions_pager_scroll_on)).check(
matches( matches(
isDisplayed(), isDisplayed()
), )
) )
onView(withText(R.string.pref_content_reader_font_size)).perform(click()) onView(withText(R.string.pref_content_reader_font_size)).perform(click())
@ -82,4 +83,4 @@ class SettingsActivityReaderTest {
onView(withText(R.string.settings_reader_font)).perform(click()) onView(withText(R.string.settings_reader_font)).perform(click())
} }
} }
} }

View File

@ -1,7 +1,9 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@ -20,6 +22,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityTest { class SettingsActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
lateinit var context: Context lateinit var context: Context
@ -30,12 +33,16 @@ class SettingsActivityTest {
context = activity.window.context context = activity.window.context
} }
loginAndInitHome() loginAndInitHome()
openMenu() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
} }
@Test @Test
fun testAllSettings() { fun testAllSettings() {
onView(withText(R.string.pref_header_general)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_general)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_viewer)).check(matches(isDisplayed()))
onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed())) onView(withText(R.string.pref_header_offline)).check(matches(isDisplayed()))
@ -45,13 +52,14 @@ class SettingsActivityTest {
matches( matches(
allOf( allOf(
isDisplayed(), isDisplayed(),
not(isSelected()), not(isSelected())
), )
), )
) )
onView(withText(R.string.action_about)).check(matches(isDisplayed())) onView(withText(R.string.action_about)).check(matches(isDisplayed()))
} }
@Test @Test
fun testThemes() { fun testThemes() {
testPreferencesFromArray(context, R.array.ModeTitles) { testPreferencesFromArray(context, R.array.ModeTitles) {
@ -59,6 +67,7 @@ class SettingsActivityTest {
} }
} }
@Test @Test
fun testExperimentail() { fun testExperimentail() {
onView(withText(R.string.pref_header_experimental)).perform(click()) onView(withText(R.string.pref_header_experimental)).perform(click())
@ -70,11 +79,13 @@ class SettingsActivityTest {
} }
} }
@Test @Test
fun testBugReports() { fun testBugReports() {
onView(withText(R.string.pref_switch_disable_acra)).perform(click()) onView(withText(R.string.pref_switch_disable_acra)).perform(click())
} }
@Test @Test
fun testLinks() { fun testLinks() {
onView(withText(R.string.pref_header_links)).perform(click()) onView(withText(R.string.pref_header_links)).perform(click())
@ -84,9 +95,10 @@ class SettingsActivityTest {
onView(withText(R.string.translation)).check(matches(isDisplayed())) onView(withText(R.string.translation)).check(matches(isDisplayed()))
} }
@Test @Test
fun testAbout() { fun testAbout() {
onView(withText(R.string.action_about)).perform(click()) onView(withText(R.string.action_about)).perform(click())
onView(withText("ACRA")).check(matches(isDisplayed())) onView(withText("ACRA")).check(matches(isDisplayed()))
} }
} }

View File

@ -1,20 +1,19 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.AmbiguousViewMatcherException import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.scrollCompletelyTo
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -24,6 +23,7 @@ import java.util.UUID
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SourcesActivityTest { class SourcesActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -34,49 +34,32 @@ class SourcesActivityTest {
sourceName = UUID.randomUUID().toString().substring(0, 15) sourceName = UUID.randomUUID().toString().substring(0, 15)
loginAndInitHome() loginAndInitHome()
goToSources() openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>()
)
onView(withText(R.string.menu_home_sources))
.perform(click())
} }
@Test @Test
fun addSource() { fun addSource() {
testAddSourceWithUrl( onView(withId(R.id.fab))
"https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10",
sourceName,
)
}
@Suppress("detekt:SwallowedException")
@Test
fun addSourceCheckContent() {
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
onView(isRoot()).perform(ViewActions.pressBack())
openMenu()
onView(withText(R.string.menu_home_refresh)).perform(click())
onView(withText(R.string.refresh_dialog_message)).check(matches(isDisplayed()))
onView(
withId(android.R.id.button1),
).perform(click())
Thread.sleep(10000)
onView(withId(R.id.swipeRefreshLayout)).perform(swipeDown())
Thread.sleep(2000)
try {
onView(withId(R.id.sourceTitleAndDate)).check(matches(isDisplayed()))
} catch (e: AmbiguousViewMatcherException) {
assert(true)
}
goToSources()
}
@After
fun deleteTheCreatedSource() {
onView(withText(sourceName)).check(matches(isDisplayed()))
onView(withId(R.id.deleteBtn)).perform(click())
onView(withText(sourceName)).check(doesNotExist())
}
private fun goToSources() {
openMenu()
onView(withText(R.string.menu_home_sources))
.perform(click()) .perform(click())
onView(withId(R.id.nameInput))
.perform(click()).perform(typeTextIntoFocusedView(sourceName))
onView(withId(R.id.sourceUri))
.perform(click())
.perform(typeTextIntoFocusedView("https://lorem-rss.herokuapp.com/feed?unit=year&interval=1&length=10"))
onView(withId(R.id.tags))
.perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
onView(withId(R.id.spoutsSpinner))
.perform(click())
onView(withText("RSS Feed"))
.perform(scrollCompletelyTo())
.perform(click())
onView(withId(R.id.saveBtn))
.perform(click())
onView(withText(sourceName)).check(matches(isDisplayed()))
} }
}
}

View File

@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
@ -8,8 +7,6 @@ import android.widget.RelativeLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Root import androidx.test.espresso.Root
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.hasSibling
@ -25,35 +22,38 @@ import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher import org.hamcrest.TypeSafeMatcher
fun withError(
@StringRes id: Int, fun withError(@StringRes id: Int): 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 && (view !is EditText || view.error == null)) { if (view == 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 as EditText).error.toString() == context.getString(id) return view.error.toString() == context.getString(id)
} }
override fun describeTo(description: Description?) { override fun describeTo(description: Description?) {
// Nothing
} }
} }
} }
fun isPopupWindow(): Matcher<Root> = isPlatformPopup() fun isPopupWindow(): Matcher<Root> {
return isPlatformPopup()
}
fun withDrawable( fun withDrawable(@DrawableRes id: Int) = object : TypeSafeMatcher<View>() {
@DrawableRes id: Int,
) = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) { override fun describeTo(description: Description) {
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()
@ -65,46 +65,37 @@ fun withDrawable(
} }
} }
fun hasBottombarItemText( fun hasBottombarItemText(@StringRes id: Int): Matcher<View>? {
@StringRes id: Int, return allOf(
): Matcher<View>? =
allOf(
withResourceName("fixed_bottom_navigation_icon"), withResourceName("fixed_bottom_navigation_icon"),
withParent( withParent(
allOf( allOf(
withResourceName("fixed_bottom_navigation_icon_container"), withResourceName("fixed_bottom_navigation_icon_container"),
hasSibling(withText(id)), hasSibling(withText(id))
), )
), )
) )
}
fun withSettingsCheckboxWidget( fun withSettingsCheckboxWidget(@StringRes id: Int): Matcher<View>? {
@StringRes id: Int, return allOf(
): Matcher<View>? =
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): Matcher<View>? {
@StringRes id: Int, return allOf(
): Matcher<View>? =
allOf(
withId(android.R.id.widget_frame), withId(android.R.id.widget_frame),
hasSibling( hasSibling(
allOf( allOf(
withClassName(Matchers.equalTo(RelativeLayout::class.java.name)), withClassName(Matchers.equalTo(RelativeLayout::class.java.name)),
withChild( withChild(
withText(id), withText(id)
), )
), )
), )
) )
}
fun openMenu() {
openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext<Context>(),
)
}

View File

@ -49,12 +49,7 @@ 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 : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
class HomeActivity :
AppCompatActivity(),
SearchView.OnQueryTextListener,
DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD private var elementsShown: ItemType = ItemType.UNREAD
@ -176,12 +171,11 @@ class HomeActivity :
getElementsAccordingToTab() getElementsAccordingToTab()
} }
} else { } else {
Toast Toast.makeText(
.makeText( this@HomeActivity,
this@HomeActivity, "Found null when swiping at positon $position.",
"Found null when swiping at positon $position.", Toast.LENGTH_LONG,
Toast.LENGTH_LONG, ).show()
).show()
} }
} }
} }
@ -202,23 +196,19 @@ class HomeActivity :
} }
} }
@Suppress("detekt:LongMethod")
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge = tabNewBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false) .setHideOnSelect(false).hide(false)
.hide(false)
tabArchiveBadge = tabArchiveBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false) .setHideOnSelect(false).hide(false)
.hide(false)
tabStarredBadge = tabStarredBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false) .setHideOnSelect(false).hide(false)
.hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch { lifecycleScope.launch {
@ -246,12 +236,14 @@ class HomeActivity :
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new), getString(R.string.tab_new),
).setBadgeItem(tabNewBadge) )
.setBadgeItem(tabNewBadge)
val tabArchive = val tabArchive =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp, R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read), getString(R.string.tab_read),
).setBadgeItem(tabArchiveBadge) )
.setBadgeItem(tabArchiveBadge)
val tabStarred = val tabStarred =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp, R.drawable.ic_tab_favorite_black_24dp,
@ -285,7 +277,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()
@ -297,10 +289,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))
@ -433,17 +425,17 @@ class HomeActivity :
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener) binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
} }
private fun getLastVisibleItem(): Int = private fun getLastVisibleItem(): Int {
when (val manager = binding.recyclerView.layoutManager) { return when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
manager manager.findLastCompletelyVisibleItemPositions(
.findLastCompletelyVisibleItemPositions( null,
null, ).last()
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
}
private fun mayBeEmpty() = private fun mayBeEmpty() =
if (items.isEmpty()) { if (items.isEmpty()) {
@ -546,7 +538,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 / MIN_WIDTH_CARD_DP).toInt() return (dpWidth / 300).toInt()
} }
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
@ -585,8 +577,7 @@ class HomeActivity :
messageRes: Int, messageRes: Int,
doFn: () -> Unit, doFn: () -> Unit,
) { ) {
AlertDialog AlertDialog.Builder(this@HomeActivity)
.Builder(this@HomeActivity)
.setMessage(messageRes) .setMessage(messageRes)
.setTitle(titleRes) .setTitle(titleRes)
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() } .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
@ -595,11 +586,10 @@ 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.BUG_URL) baseContext.openUrlInBrowser(AppSettingsService.trackerUrl)
return true return true
} }
@ -616,19 +606,18 @@ class HomeActivity :
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
Toast Toast.makeText(
.makeText( this@HomeActivity,
this@HomeActivity, R.string.refresh_success_response,
R.string.refresh_success_response, Toast.LENGTH_LONG,
Toast.LENGTH_LONG, )
).show() .show()
} else { } else {
Toast Toast.makeText(
.makeText( this@HomeActivity,
this@HomeActivity, R.string.refresh_failer_message,
R.string.refresh_failer_message, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -644,26 +633,25 @@ class HomeActivity :
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
Toast Toast.makeText(
.makeText( this@HomeActivity,
this@HomeActivity, R.string.all_posts_read,
R.string.all_posts_read, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
tabNewBadge.removeBadge() tabNewBadge.removeBadge()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast Toast.makeText(
.makeText( this@HomeActivity,
this@HomeActivity, R.string.all_posts_not_read,
R.string.all_posts_not_read, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
handleListResult() handleListResult()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@ -673,7 +661,7 @@ class HomeActivity :
R.id.action_disconnect -> { R.id.action_disconnect -> {
needsConfirmation( needsConfirmation(
R.string.confirm_disconnect_title, R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description, R.string.confirm_disconnect_description
) { ) {
runBlocking { runBlocking {
repository.logout() repository.logout()
@ -714,8 +702,7 @@ class HomeActivity :
private fun handleRecurringTask() { private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) { if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = val myConstraints =
Constraints Constraints.Builder()
.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled()) .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
@ -724,19 +711,19 @@ class HomeActivity :
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>( PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(), appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES, TimeUnit.MINUTES
).setConstraints(myConstraints) )
.setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager WorkManager.getInstance(
.getInstance( baseContext,
baseContext, ).enqueueUniquePeriodicWork(
).enqueueUniquePeriodicWork( "selfoss-loading",
"selfoss-loading", ExistingPeriodicWorkPolicy.KEEP,
ExistingPeriodicWorkPolicy.KEEP, backgroundWork
backgroundWork, )
)
} }
} }
} }

View File

@ -84,9 +84,7 @@ class ImageActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private inner class ScreenSlidePagerAdapter( private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
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])

View File

@ -30,11 +30,7 @@ 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 : AppCompatActivity(), DIAware {
class LoginActivity :
AppCompatActivity(),
DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithLogin = false private var isWithLogin = false
@ -112,7 +108,7 @@ class LoginActivity :
repository.updateApiInformation() repository.updateApiInformation()
ACRA.errorReporter.putCustomData( ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION", "SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString(), appSettingsService.getApiVersion().toString()
) )
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -136,18 +132,9 @@ 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 = val url = binding.urlView.text.toString().trim()
binding.urlView.text val login = binding.loginView.text.toString().trim()
.toString() val password = binding.passwordView.text.toString().trim()
.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)
@ -164,12 +151,11 @@ class LoginActivity :
repository.updateApiInformation() repository.updateApiInformation()
} catch (e: Exception) { } catch (e: Exception) {
if (e.message?.startsWith("No transformation found") == true) { if (e.message?.startsWith("No transformation found") == true) {
Toast Toast.makeText(
.makeText( applicationContext,
applicationContext, R.string.application_selfoss_only,
R.string.application_selfoss_only, Toast.LENGTH_LONG,
Toast.LENGTH_LONG, ).show()
).show()
preferenceError() preferenceError()
showProgress(false) showProgress(false)
} }
@ -219,7 +205,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 == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) { if (inValidCount == 3) {
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))
@ -284,7 +270,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.BUG_URL)) Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
startActivity(browserIntent) startActivity(browserIntent)
return true return true
} }
@ -294,9 +280,9 @@ class LoginActivity :
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports") .withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.BUG_URL) .withAboutSpecial2Description(AppSettingsService.trackerUrl)
.withAboutSpecial1("Project Page") .withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL) .withAboutSpecial1Description(AppSettingsService.sourceUrl)
.start(this) .start(this)
true true
} }
@ -304,4 +290,4 @@ class LoginActivity :
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
} }

View File

@ -9,11 +9,11 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.di.networkModule
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.github.ln_12.library.ConnectivityStatus import com.github.ln_12.library.ConnectivityStatus
@ -36,23 +36,21 @@ import org.kodein.di.bind
import org.kodein.di.instance import org.kodein.di.instance
import org.kodein.di.singleton import org.kodein.di.singleton
class MyApp : class MyApp : MultiDexApplication(), DIAware {
MultiDexApplication(),
DIAware {
override val di by DI.lazy { override val di by DI.lazy {
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) } bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess() || TestingHelper().isUnitTest()) }
import(networkModule) import(networkModule)
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) } bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) } bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
bind<Repository>() with bind<Repository>() with
singleton { singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
isConnectionAvailable, isConnectionAvailable,
instance(), instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) } bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) } bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
@ -62,7 +60,6 @@ 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)
@ -92,12 +89,11 @@ class MyApp :
R.string.network_connectivity_lost R.string.network_connectivity_lost
} }
Toast Toast.makeText(
.makeText( applicationContext,
applicationContext, toastMessage,
toastMessage, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
} }
} }
@ -155,13 +151,13 @@ class MyApp :
val name = getString(R.string.notification_channel_sync) val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(AppSettingsService.SYNC_CHANNEL_ID, name, importance) val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync) val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = val newItemsChannelmChannel =
NotificationChannel( NotificationChannel(
AppSettingsService.NEW_ITEMS_CHANNEL, AppSettingsService.newItemsChannelId,
newItemsChannelname, newItemsChannelname,
newItemsChannelimportance, newItemsChannelimportance,
) )
@ -203,4 +199,4 @@ class MyApp :
super.onPause(owner) super.onPause(owner)
} }
} }
} }

View File

@ -22,9 +22,7 @@ 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 : class ReaderActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
@ -53,7 +51,6 @@ class ReaderActivity :
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)
@ -102,9 +99,8 @@ class ReaderActivity :
oldInstanceState.clear() oldInstanceState.clear()
} }
private inner class ScreenSlidePagerAdapter( private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
fa: FragmentActivity, FragmentStateAdapter(fa) {
) : 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])
@ -113,26 +109,25 @@ class ReaderActivity :
override fun onKeyDown( override fun onKeyDown(
keyCode: Int, keyCode: Int,
event: KeyEvent?, event: KeyEvent?,
): Boolean = ): Boolean {
when (keyCode) { return 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
currentFragment.volumeButtonScrollDown() currentFragment.scrollDown()
true true
} }
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
val currentFragment = val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.volumeButtonScrollUp() currentFragment.scrollUp()
true true
} }
else -> { else -> {
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
@ -192,7 +187,6 @@ class ReaderActivity :
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@ -206,12 +200,10 @@ class ReaderActivity :
afterSave() afterSave()
} }
} }
R.id.align_left -> { R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT) switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment() refreshFragment()
} }
R.id.align_justify -> { R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY) switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment() refreshFragment()

View File

@ -18,9 +18,7 @@ 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 : class SourcesActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private lateinit var binding: ActivitySourcesBinding private lateinit var binding: ActivitySourcesBinding
override val di by closestDI() override val di by closestDI()
@ -70,12 +68,11 @@ class SourcesActivity :
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
} else { } else {
Toast Toast.makeText(
.makeText( this@SourcesActivity,
this@SourcesActivity, R.string.cant_get_sources,
R.string.cant_get_sources, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -84,4 +81,4 @@ class SourcesActivity :
startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java)) startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
} }
} }
} }

View File

@ -21,9 +21,7 @@ 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 : class UpsertSourceActivity : AppCompatActivity(), DIAware {
AppCompatActivity(),
DIAware {
private var existingSource: SelfossModel.SourceDetail? = null private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
@ -85,7 +83,6 @@ class UpsertSourceActivity :
} }
} }
@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 =
@ -108,12 +105,11 @@ class UpsertSourceActivity :
} }
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast Toast.makeText(
.makeText( this@UpsertSourceActivity,
this@UpsertSourceActivity, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
@ -174,7 +170,6 @@ class UpsertSourceActivity :
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 =
@ -197,12 +192,11 @@ class UpsertSourceActivity :
if (successfullyAddedSource) { if (successfullyAddedSource) {
finish() finish()
} else { } else {
Toast Toast.makeText(
.makeText( this@UpsertSourceActivity,
this@UpsertSourceActivity, R.string.cant_create_source,
R.string.cant_create_source, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
} }
} }

View File

@ -49,10 +49,7 @@ class ItemCardAdapter(
return ViewHolder(binding) return ViewHolder(binding)
} }
private fun handleClickListeners( private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
holderBinding: CardItemBinding,
position: Int,
) {
holderBinding.favButton.setOnClickListener { holderBinding.favButton.setOnClickListener {
val item = items[position] val item = items[position]
if (item.starred) { if (item.starred) {
@ -99,13 +96,12 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = binding.sourceTitleAndDate.text = try {
try { itm.sourceAuthorAndDate()
itm.sourceAuthorAndDate() } catch (e: Exception) {
} catch (e: Exception) { e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date") itm.sourceAuthorOnly()
itm.sourceAuthorOnly() }
}
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@ -129,7 +125,5 @@ class ItemCardAdapter(
} }
} }
inner class ViewHolder( inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
val binding: CardItemBinding, }
) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -53,13 +53,12 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = binding.sourceTitleAndDate.text = try {
try { itm.sourceAuthorAndDate()
itm.sourceAuthorAndDate() } catch (e: Exception) {
} catch (e: Exception) { e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date") itm.sourceAuthorOnly()
itm.sourceAuthorOnly() }
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
@ -73,7 +72,5 @@ class ItemListAdapter(
} }
} }
inner class ViewHolder( inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
val binding: ListItemBinding, }
) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -18,9 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
RecyclerView.Adapter<VH>(),
DIAware {
abstract val items: ArrayList<SelfossModel.Item> abstract val items: ArrayList<SelfossModel.Item>
abstract val repository: Repository abstract val repository: Repository
abstract val binding: ViewBinding abstract val binding: ViewBinding
@ -47,7 +45,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_read, R.string.marked_as_read,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG,
).setAction(R.string.undo_string) { )
.setAction(R.string.undo_string) {
unreadItemAtIndex(item, position, false) unreadItemAtIndex(item, position, false)
} }
@ -67,7 +66,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
R.string.marked_as_unread, R.string.marked_as_unread,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG,
).setAction(R.string.undo_string) { )
.setAction(R.string.undo_string) {
readItemAtIndex(item, position, false) readItemAtIndex(item, position, false)
} }
@ -77,10 +77,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
s.show() s.show()
} }
protected fun handleLinkOpening( protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
holderBinding: ViewBinding,
position: Int,
) {
holderBinding.root.setOnClickListener { holderBinding.root.setOnClickListener {
repository.setReaderItems(items) repository.setReaderItems(items)
c.openItemUrl( c.openItemUrl(

View File

@ -29,8 +29,7 @@ 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>(), ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private lateinit var binding: SourceListItemBinding private lateinit var binding: SourceListItemBinding
@ -62,12 +61,11 @@ class SourcesListAdapter(
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount) notifyItemRangeChanged(position, itemCount)
} else { } else {
Toast Toast.makeText(
.makeText( app,
app, R.string.can_delete_source,
R.string.can_delete_source, Toast.LENGTH_SHORT,
Toast.LENGTH_SHORT, ).show()
).show()
} }
} }
} }
@ -101,7 +99,5 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder( inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
val mView: ConstraintLayout,
) : RecyclerView.ViewHolder(mView)
} }

View File

@ -23,15 +23,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.instance import org.kodein.di.instance
import java.util.Timer import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
private const val NOTIFICATION_DELAY = 4000L class LoadingWorker(val context: Context, params: WorkerParameters) :
Worker(context, params),
class LoadingWorker(
val context: Context,
params: WorkerParameters,
) : Worker(context, params),
DIAware { DIAware {
override val di by lazy { (applicationContext as MyApp).di } override val di by lazy { (applicationContext as MyApp).di }
private val repository: Repository by instance() private val repository: Repository by instance()
@ -44,13 +40,12 @@ class LoadingWorker(
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = val notification =
NotificationCompat NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
.Builder(applicationContext, AppSettingsService.SYNC_CHANNEL_ID)
.setContentTitle(context.getString(R.string.loading_notification_title)) .setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text)) .setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true) .setOngoing(true)
.setPriority(PRIORITY_LOW) .setPriority(PRIORITY_LOW)
.setChannelId(AppSettingsService.SYNC_CHANNEL_ID) .setChannelId(AppSettingsService.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp) .setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build()) notificationManager.notify(1, notification.build())
@ -92,27 +87,28 @@ class LoadingWorker(
PendingIntent.getActivity(context, 0, intent, pflags) PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification = val newItemsNotification =
NotificationCompat NotificationCompat.Builder(
.Builder( applicationContext,
applicationContext, AppSettingsService.newItemsChannelId,
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(
R.string.new_items_notification_text, R.string.new_items_notification_text,
newSize, newSize,
), ),
).setPriority(PRIORITY_DEFAULT) )
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL) .setPriority(PRIORITY_DEFAULT)
.setChannelId(AppSettingsService.newItemsChannelId)
.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(NOTIFICATION_DELAY) { Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build()) notificationManager.notify(2, newItemsNotification.build())
} }
} }
Timer("", false).schedule(NOTIFICATION_DELAY) { Timer("", false).schedule(4000) {
notificationManager.cancel(1) notificationManager.cancel(1)
} }
} }

View File

@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
@ -9,7 +8,6 @@ import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
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
@ -54,7 +52,6 @@ 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
import org.acra.ktx.sendSilentlyWithAcra
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
@ -66,14 +63,8 @@ import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg" private const val IMAGE_JPG = "image/jpg"
private const val WHITE_COLOR_HEX = 0xFFFFFF class ArticleFragment : Fragment(), DIAware {
private var fontSize: Int = 16
private const val DEFAULT_FONT_SIZE = 16
class ArticleFragment :
Fragment(),
DIAware {
private var fontSize: Int = DEFAULT_FONT_SIZE
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
@ -104,7 +95,6 @@ class ArticleFragment :
item = pi.toModel() item = pi.toModel()
} }
@Suppress("detekt:LongMethod")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -113,22 +103,16 @@ class ArticleFragment :
try { try {
binding = FragmentArticleBinding.inflate(inflater, container, false) binding = FragmentArticleBinding.inflate(inflater, container, false)
try { url = item.getLinkDecoded()
url = item.getLinkDecoded()
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = contentSource = try {
try { item.sourceAuthorAndDate()
item.sourceAuthorAndDate() } catch (e: Exception) {
} catch (e: Exception) { e.sendSilentlyWithAcraWithName("Article Fragment parse date")
e.sendSilentlyWithAcraWithName("Article Fragment parse date") item.sourceAuthorOnly()
item.sourceAuthorOnly() }
}
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
@ -173,9 +157,8 @@ class ArticleFragment :
) )
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
try { if (context != null) {
AlertDialog AlertDialog.Builder(requireContext())
.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
@ -183,10 +166,9 @@ class ArticleFragment :
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
}.create() }
.create()
.show() .show()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@ -236,32 +218,28 @@ class ArticleFragment :
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item) R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> R.id.unread_action ->
try { if (context != null) {
if (this@ArticleFragment.item.unread) { if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item) repository.markAsRead(this@ArticleFragment.item)
} }
this@ArticleFragment.item.unread = false this@ArticleFragment.item.unread = false
Toast Toast.makeText(
.makeText( context,
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 Toast.makeText(
.makeText( context,
context, R.string.marked_as_unread,
R.string.marked_as_unread, Toast.LENGTH_LONG,
Toast.LENGTH_LONG, ).show()
).show()
} }
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
else -> Unit else -> Unit
@ -285,7 +263,6 @@ class ArticleFragment :
} }
} }
@Suppress("detekt:SwallowedException")
private fun getContentFromMercury() { private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@ -324,15 +301,16 @@ class ArticleFragment :
} }
} }
private fun handleLeadImage(leadImageUrl: String?) { private fun handleLeadImage(lead_image_url: String?) {
if (!leadImageUrl.isNullOrEmpty() && context != null) { if (!lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
Glide Glide
.with(requireContext()) .with(requireContext())
.asBitmap() .asBitmap()
.load( .load(
leadImageUrl, lead_image_url,
).apply(RequestOptions.fitCenterTransform()) )
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView) .into(binding.imageView)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
@ -346,133 +324,84 @@ class ArticleFragment :
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
url: String, url: String,
): Boolean = ): Boolean {
if (context != null && return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
requireContext().openUrlInBrowser(url) requireContext().openUrlInBrowser(url)
true true
} else { } else {
false false
} }
}
@Suppress("detekt:LongMethod", "detekt:SwallowedException")
@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 glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
var glideResource: WebResourceResponse? = null
if (url.lowercase(Locale.US).contains(".jpg") || if (url.lowercase(Locale.US).contains(".jpg") ||
url url.lowercase(Locale.US)
.lowercase(Locale.US)
.contains(".jpeg") .contains(".jpeg")
) { ) {
try { try {
val image = val image =
Glide Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.with(view)
.asBitmap()
.apply(glideOptions)
.load(url)
.submit()
.get() .get()
glideResource = return WebResourceResponse(
WebResourceResponse( IMAGE_JPG,
IMAGE_JPG, "UTF-8",
"UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG), )
)
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
} }
} else if (url.lowercase(Locale.US).contains(".png")) { } else if (url.lowercase(Locale.US).contains(".png")) {
try { try {
val image = val image =
Glide Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.with(view)
.asBitmap()
.apply(glideOptions)
.load(url)
.submit()
.get() .get()
glideResource = return WebResourceResponse(
WebResourceResponse( IMAGE_JPG,
IMAGE_JPG, "UTF-8",
"UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
getBitmapInputStream(image, Bitmap.CompressFormat.PNG), )
)
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
} }
} else if (url.lowercase(Locale.US).contains(".webp")) { } else if (url.lowercase(Locale.US).contains(".webp")) {
try { try {
val image = val image =
Glide Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
.with(view)
.asBitmap()
.apply(glideOptions)
.load(url)
.submit()
.get() .get()
glideResource = return WebResourceResponse(
WebResourceResponse( IMAGE_JPG,
IMAGE_JPG, "UTF-8",
"UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP), )
)
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
// Do nothing // Do nothing
} }
} }
return glideResource ?: super.shouldInterceptRequest(view, url) return super.shouldInterceptRequest(view, url)
} }
} }
} }
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
private fun htmlToWebview() { private fun htmlToWebview() {
val context: Context if (context != null) {
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 = requireContext().obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0) binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE binding.webcontent.visibility = View.VISIBLE
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) val colorOnSurface = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) val colorSurface = TypedValue()
} catch (e: IllegalStateException) { requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
}
val colorSurfaceString =
String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else WHITE_COLOR_HEX),
)
val colorOnSurfaceString =
String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
)
try {
binding.webcontent.settings.useWideViewPort = true binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false binding.webcontent.settings.javaScriptEnabled = false
@ -483,25 +412,19 @@ class ArticleFragment :
GestureDetector( GestureDetector(
activity, activity,
object : GestureDetector.SimpleOnGestureListener() { object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean = performClick() override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick()
}
}, },
) )
binding.webcontent.setOnTouchListener { _, event -> binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
gestureDetector.onTouchEvent(
event,
)
}
binding.webcontent.settings.layoutAlgorithm = binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
return
}
try {
var baseUrl: String? = null var baseUrl: String? = null
try { try {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
@ -551,12 +474,12 @@ class ArticleFragment :
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
WHITE_COLOR_HEX and context.resources.getColor(R.color.colorAccent), 0xFFFFFF and resources.getColor(R.color.colorAccent),
) )
} !important; } !important;
| } | }
| *:not(a) { | *:not(a) {
| color: $colorOnSurfaceString; | color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
| } | }
| * { | * {
| font-size: ${fontSize}px; | font-size: ${fontSize}px;
@ -564,11 +487,26 @@ class ArticleFragment :
| word-break: break-word; | word-break: break-word;
| overflow:hidden; | overflow:hidden;
| line-height: 1.5em; | line-height: 1.5em;
| background-color: $colorSurfaceString; | background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| } | }
| body, html { | body, html {
| background-color: $colorSurfaceString !important; | background-color: ${
| border-color: $colorSurfaceString !important; String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| border-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| padding: 0 !important; | padding: 0 !important;
| margin: 0 !important; | margin: 0 !important;
| } | }
@ -578,7 +516,12 @@ class ArticleFragment :
| pre, code { | pre, code {
| white-space: pre-wrap; | white-space: pre-wrap;
| width:100%; | width:100%;
| background-color: $colorSurfaceString; | background-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| } | }
| </style> | </style>
| $fontLinkAndStyle | $fontLinkAndStyle
@ -591,27 +534,25 @@ class ArticleFragment :
"utf-8", "utf-8",
null, null,
) )
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is still null ?")
} }
} }
fun volumeButtonScrollDown() { fun scrollDown() {
val height = binding.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, height / 2) binding.nestedScrollView.smoothScrollBy(0, height / 2)
} }
fun volumeButtonScrollUp() { fun scrollUp() {
val height = binding.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, -height / 2) binding.nestedScrollView.smoothScrollBy(0, -height / 2)
} }
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
try { if (context != null) {
requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item) requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
} catch (e: IllegalStateException) { } else {
e.sendSilentlyWithAcraWithName("Context required is null") Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
} }
} }
@ -628,11 +569,10 @@ class ArticleFragment :
} }
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 )
)
) { ) {
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra) val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
@ -644,4 +584,4 @@ class ArticleFragment :
} }
return false return false
} }
} }

View File

@ -33,11 +33,7 @@ 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
private const val DRAWABLE_SIZE = 30 class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
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()
@ -56,17 +52,19 @@ class FilterSheetFragment :
false, false,
) )
try { val context: Context? = context
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
handleTagChips(requireContext()) handleTagChips(context)
handleSourceChips(requireContext()) handleSourceChips(context)
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
} }
} catch (e: IllegalStateException) {
dismiss()
e.sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} }
binding.floatingActionButton2.setOnClickListener { binding.floatingActionButton2.setOnClickListener {
@ -84,8 +82,7 @@ class FilterSheetFragment :
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
Glide Glide.with(context)
.with(context)
.load(source.getIcon(repository.baseUrl)) .load(source.getIcon(repository.baseUrl))
.into( .into(
object : ViewTarget<Chip?, Drawable?>(c) { object : ViewTarget<Chip?, Drawable?>(c) {
@ -158,8 +155,8 @@ class FilterSheetFragment :
} }
gd.setColor(gdColor) gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE gd.shape = GradientDrawable.RECTANGLE
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE) gd.setSize(30, 30)
gd.cornerRadius = DRAWABLE_SIZE.toFloat() gd.cornerRadius = 30F
c.chipIcon = gd c.chipIcon = gd
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable") e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
@ -195,4 +192,4 @@ class FilterSheetFragment :
companion object { companion object {
const val TAG = "FilterModalBottomSheet" const val TAG = "FilterModalBottomSheet"
} }
} }

View File

@ -14,7 +14,7 @@ class ImageFragment : Fragment() {
private lateinit var imageUrl: String private lateinit var imageUrl: String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null private var _binding: FragmentImageBinding? = null
val binding get() = _binding private val binding get() = _binding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -31,8 +31,7 @@ class ImageFragment : Fragment() {
val view = binding?.root val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE binding!!.photoView.visibility = View.VISIBLE
Glide Glide.with(requireActivity())
.with(requireActivity())
.asBitmap() .asBitmap()
.apply(glideOptions) .apply(glideOptions)
.load(imageUrl) .load(imageUrl)

View File

@ -9,22 +9,17 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
private const val PRELOAD_IMAGE_TIMEOUT = 10000
fun SelfossModel.Item.preloadImages(context: Context): Boolean { fun SelfossModel.Item.preloadImages(context: Context): Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT) 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 Glide.with(context).asBitmap()
.with(context)
.asBitmap()
.apply(glideOptions) .apply(glideOptions)
.load(url) .load(url).submit()
.submit()
} }
} }
} catch (e: Error) { } catch (e: Error) {
@ -45,4 +40,4 @@ fun String.toTextDrawableString(): String {
} }
} }
return textDrawable.toString() return textDrawable.toString()
} }

View File

@ -17,19 +17,12 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBin
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.openUrlInBrowser import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.API_ITEMS_NUMBER
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.CURRENT_THEME
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.READER_FONT_SIZE
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI 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,
@ -68,14 +61,15 @@ class SettingsActivity :
outState.putCharSequence(TITLE_TAG, title) outState.putCharSequence(TITLE_TAG, title)
} }
override fun onSupportNavigateUp(): Boolean = override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) { return if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings) supportActionBar?.title = getText(R.string.title_activity_settings)
false false
} else { } else {
super.onBackPressed() super.onBackPressed()
true true
} }
}
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
@ -84,17 +78,15 @@ class SettingsActivity :
// Instantiate the new Fragment // Instantiate the new Fragment
val args = pref.extras val args = pref.extras
val fragment = val fragment =
supportFragmentManager.fragmentFactory supportFragmentManager.fragmentFactory.instantiate(
.instantiate( classLoader,
classLoader, pref.fragment.toString(),
pref.fragment.toString(), ).apply {
).apply { arguments = args
arguments = args setTargetFragment(caller, 0)
setTargetFragment(caller, 0) }
}
// Replace the existing Fragment with the new Fragment // Replace the existing Fragment with the new Fragment
supportFragmentManager supportFragmentManager.beginTransaction()
.beginTransaction()
.replace(R.id.settings, fragment) .replace(R.id.settings, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
@ -110,10 +102,10 @@ class SettingsActivity :
) { ) {
setPreferencesFromResource(R.xml.pref_main, rootKey) setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener = preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue -> Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt(), newValue.toString().toInt()
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ ) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
@ -139,7 +131,7 @@ class SettingsActivity :
setPreferencesFromResource(R.xml.pref_general, rootKey) setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = val editTextPreference =
preferenceManager.findPreference<EditTextPreference>(API_ITEMS_NUMBER) preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
editTextPreference?.setOnBindEditTextListener { editText -> editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = editText.filters =
@ -147,14 +139,13 @@ 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 MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null if (input in 1..200) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
Toast Toast.makeText(
.makeText( activity,
activity, R.string.items_number_should_be_number,
R.string.items_number_should_be_number, Toast.LENGTH_LONG
Toast.LENGTH_LONG, ).show()
).show()
} }
"" ""
}, },
@ -170,7 +161,7 @@ class SettingsActivity :
) { ) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey) setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>(READER_FONT_SIZE) val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
fontSize?.setOnBindEditTextListener { editText -> fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { editText.addTextChangedListener {
@ -227,6 +218,23 @@ class SettingsActivity :
} }
} }
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt()
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
}
}
class LinksPreferenceFragment : PreferenceFragmentCompat() { class LinksPreferenceFragment : PreferenceFragmentCompat() {
private fun openUrl(url: String) { private fun openUrl(url: String) {
context?.openUrlInBrowser(url) context?.openUrlInBrowser(url)
@ -240,19 +248,19 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.BUG_URL) openUrl(AppSettingsService.trackerUrl)
true true
} }
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.SOURCE_URL) openUrl(AppSettingsService.sourceUrl)
false false
} }
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(AppSettingsService.TRANSLATION_URL) openUrl(AppSettingsService.translationUrl)
false false
} }
} }
@ -266,4 +274,4 @@ class SettingsActivity :
setPreferencesFromResource(R.xml.pref_experimental, rootKey) setPreferencesFromResource(R.xml.pref_experimental, rootKey)
} }
} }
} }

View File

@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.testing
import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.idling.CountingIdlingResource
object CountingIdlingResourceSingleton { object CountingIdlingResourceSingleton {
private const val RESOURCE = "GLOBAL" private const val RESOURCE = "GLOBAL"
@JvmField @JvmField
@ -17,4 +18,4 @@ object CountingIdlingResourceSingleton {
countingIdlingResource.decrement() countingIdlingResource.decrement()
} }
} }
} }

View File

@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android.testing
import android.os.Build import android.os.Build
class TestingHelper { class TestingHelper {
fun isUnitTest(): Boolean { fun isUnitTest(): Boolean {
var device = Build.DEVICE var device = Build.DEVICE
@ -15,4 +16,4 @@ class TestingHelper {
} }
return device == "robolectric" && product == "robolectric" return device == "robolectric" && product == "robolectric"
} }
} }

View File

@ -16,10 +16,9 @@ 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 Intent.createChooser(
.createChooser( sendIntent,
sendIntent, getString(R.string.share),
getString(R.string.share), ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
) )
} }

View File

@ -59,5 +59,7 @@ class CircleImageView
textView.text = text.toTextDrawableString() textView.text = text.toTextDrawableString()
} }
private fun colorFromIdentifier(key: String): Int = colorScheme[abs(key.hashCode()) % colorScheme.size] private fun colorFromIdentifier(key: String): Int {
return colorScheme[abs(key.hashCode()) % colorScheme.size]
}
} }

View File

@ -18,6 +18,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
@ -25,12 +26,11 @@ fun Context.openItemUrl(
app: Activity, app: Activity,
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
Toast Toast.makeText(
.makeText( this,
this, this.getString(R.string.cant_open_invalid_url),
this.getString(R.string.cant_open_invalid_url), Toast.LENGTH_LONG,
Toast.LENGTH_LONG, ).show()
).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
@ -42,7 +42,8 @@ fun Context.openItemUrl(
} }
} }
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches() fun String.isUrlValid(): Boolean =
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlInvalid(): Boolean { fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull() val baseUrl = this.toHttpUrlOrNull()
@ -60,6 +61,7 @@ fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
} }
fun Context.openUrlInBrowserAsNewTask(url: String) { fun Context.openUrlInBrowserAsNewTask(url: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(url) intent.data = Uri.parse(url)
@ -72,13 +74,13 @@ 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)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
} }
} }
class LinkOnTouchListener : View.OnTouchListener { class LinkOnTouchListener : View.OnTouchListener {
@ -120,4 +122,4 @@ class LinkOnTouchListener : View.OnTouchListener {
} }
return ret return ret
} }
} }

View File

@ -6,4 +6,4 @@ import org.acra.ktx.sendSilentlyWithAcra
fun Throwable.sendSilentlyWithAcraWithName(name: String) { fun Throwable.sendSilentlyWithAcraWithName(name: String) {
ACRA.errorReporter.putCustomData("error_source", name) ACRA.errorReporter.putCustomData("error_source", name)
this.sendSilentlyWithAcra() this.sendSilentlyWithAcra()
} }

View File

@ -8,19 +8,22 @@ import org.acra.config.CoreConfiguration
import org.acra.config.ReportingAdministrator import org.acra.config.ReportingAdministrator
import org.acra.data.CrashReportData import org.acra.data.CrashReportData
@AutoService(ReportingAdministrator::class) @AutoService(ReportingAdministrator::class)
class AcraReportingAdministrator : ReportingAdministrator { class AcraReportingAdministrator : ReportingAdministrator {
override fun shouldStartCollecting( override fun shouldStartCollecting(
context: Context, context: Context,
config: CoreConfiguration, config: CoreConfiguration,
reportBuilder: ReportBuilder, reportBuilder: ReportBuilder
): Boolean = ): Boolean {
reportBuilder.exception !is DeadSystemException && return reportBuilder.exception !is DeadSystemException
(reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException") }
override fun shouldSendReport( override fun shouldSendReport(
context: Context, context: Context,
config: CoreConfiguration, config: CoreConfiguration,
crashReportData: CrashReportData, crashReportData: CrashReportData
): Boolean = crashReportData.get("BRAND") != "redroid" ): Boolean {
} return crashReportData.get("BRAND") != "redroid"
}
}

View File

@ -13,8 +13,7 @@ import java.io.InputStream
fun Context.bitmapCenterCrop( fun Context.bitmapCenterCrop(
url: String, url: String,
iv: ImageView, iv: ImageView,
) = Glide ) = Glide.with(this)
.with(this)
.asBitmap() .asBitmap()
.load(url) .load(url)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
@ -26,20 +25,17 @@ fun Context.circularDrawable(
) { ) {
view.textView.text = "" view.textView.text = ""
Glide Glide.with(this)
.with(this)
.load(url) .load(url)
.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, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream) bitmap.compress(compressFormat, 80, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
return ByteArrayInputStream(bitmapData) return ByteArrayInputStream(bitmapData)
} }

View File

@ -3,6 +3,7 @@ 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
@ -10,13 +11,19 @@ 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
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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
} }
} }

View File

@ -7,9 +7,7 @@ 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( class AppViewModel(private val repository: Repository) : ViewModel() {
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
@ -21,10 +19,11 @@ class AppViewModel(
if (isConnected && !wasConnected && repository.connectionMonitored) { if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true) _networkAvailableProvider.emit(true)
wasConnected = true wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) { } else if (!isConnected && wasConnected && repository.connectionMonitored)
_networkAvailableProvider.emit(false) {
wasConnected = false _networkAvailableProvider.emit(false)
} wasConnected = false
}
} }
} }
} }

View File

@ -23,7 +23,6 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin"> android:padding="@dimen/activity_horizontal_margin">
<!-- Login progress --> <!-- Login progress -->
@ -38,7 +37,7 @@
<LinearLayout <LinearLayout
android:id="@+id/loginForm" android:id="@+id/loginForm"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<EditText <EditText
@ -101,4 +100,4 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="0"
android:entries="@array/ModeTitles"
android:entryValues="@array/ModeValues"
android:key="currentMode"
app:iconSpaceReserved="false"
android:title="@string/pref_theme_title"
app:useSimpleSummaryProvider="false" />
</PreferenceScreen>

View File

@ -11,8 +11,10 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.Robolectric import org.robolectric.Robolectric
@RunWith(RobotElectriqueRunner::class)
@RunWith(RobotElectriqueRunnerclass::class)
class LoginActivityTest { class LoginActivityTest {
@Test @Test
fun login_shouldDisplay() { fun login_shouldDisplay() {
Robolectric.buildActivity(LoginActivity::class.java).use { controller -> Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
@ -72,4 +74,4 @@ class LoginActivityTest {
assertEquals(expectedIntent.component, actual.component) assertEquals(expectedIntent.component, actual.component)
} }
}*/ }*/
} }

View File

@ -3,8 +3,10 @@ package bou.amine.apps.readerforselfossv2.android.tests.robolectric
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
class RobotElectriqueRunner( class RobotElectriqueRunnerclass(testClass: Class<*>?) :
testClass: Class<*>?, RobolectricTestRunner(testClass) {
) : RobolectricTestRunner(testClass) {
override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build() override fun buildGlobalConfig(): Config {
} return Config.Builder().setSdk(25, 30, 33).build()
}
}

View File

@ -11,17 +11,13 @@ fun dialogMessage(): String {
return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString() return latestDialog.findViewById<TextView>(android.R.id.message)?.text.toString()
} }
fun Menu.assertClickable( fun Menu.assertClickable(@IdRes id: Int) {
@IdRes id: Int,
) {
this.assertVisible(id) this.assertVisible(id)
val item = this.findItem(id) val item = this.findItem(id)
assertTrue(item.isEnabled) assertTrue(item.isEnabled)
} }
fun Menu.assertVisible( fun Menu.assertVisible(@IdRes id: Int) {
@IdRes id: Int,
) {
val item = this.findItem(id) val item = this.findItem(id)
assertTrue(item.isVisible) assertTrue(item.isVisible)
} }

View File

@ -1,5 +1,3 @@
@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
@ -44,14 +42,14 @@ private const val FEED_URL = "https://test.com/feed"
private const val TAGS = "Test, New" private const val TAGS = "Test, New"
private const val NUMBER_ARTICLES = 100
private const val NUMBER_UNREAD = 50
private const val NUMBER_STARRED = 20
class RepositoryTest { 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 val NUMBER_ARTICLES = 100
private val NUMBER_UNREAD = 50
private val NUMBER_STARRED = 20
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository( private fun initializeRepository(
@ -77,20 +75,19 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = data = SelfossModel.ApiInformation(
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(false, true), SelfossModel.ApiConfiguration(false, true)
), ),
) )
coEvery { api.stats() } returns coEvery { api.stats() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED), data = SelfossModel.Stats(NUMBER_ARTICLES, NUMBER_UNREAD, NUMBER_STARRED),
) )
every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit every { db.itemsQueries.deleteItemsWhereSource(any()) } returns Unit
every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems() every { db.itemsQueries.items().executeAsList() } returns generateTestDBItems()
@ -120,7 +117,7 @@ class RepositoryTest {
fun get_api_4_date_with_api_1_version_stored() { fun get_api_4_date_with_api_1_version_stored() {
every { appSettingsService.getApiVersion() } returns 1 every { appSettingsService.getApiVersion() } returns 1
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
every { appSettingsService.updateApiVersion(any()) } returns Unit every { appSettingsService.updateApiVersion(any()) } returns Unit
initializeRepository() initializeRepository()
@ -136,15 +133,14 @@ class RepositoryTest {
fun get_public_access() { fun get_public_access() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = data = SelfossModel.ApiInformation(
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, true), SelfossModel.ApiConfiguration(true, true)
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -157,15 +153,14 @@ class RepositoryTest {
fun get_public_access_username_not_empty() { fun get_public_access_username_not_empty() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = data = SelfossModel.ApiInformation(
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, true), SelfossModel.ApiConfiguration(true, true)
), ),
) )
every { appSettingsService.getUserName() } returns "username" every { appSettingsService.getUserName() } returns "username"
initializeRepository() initializeRepository()
@ -178,15 +173,14 @@ class RepositoryTest {
fun get_public_access_no_auth() { fun get_public_access_no_auth() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = data = SelfossModel.ApiInformation(
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(true, false), SelfossModel.ApiConfiguration(true, false)
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -199,15 +193,14 @@ class RepositoryTest {
fun get_public_access_disabled() { fun get_public_access_disabled() {
every { appSettingsService.updatePublicAccess(any()) } returns Unit every { appSettingsService.updatePublicAccess(any()) } returns Unit
coEvery { api.apiInformation() } returns coEvery { api.apiInformation() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = data = SelfossModel.ApiInformation(
SelfossModel.ApiInformation(
"2.19-ba1e8e3", "2.19-ba1e8e3",
"4.0.0", "4.0.0",
SelfossModel.ApiConfiguration(false, true), SelfossModel.ApiConfiguration(false, true)
), ),
) )
every { appSettingsService.getUserName() } returns "" every { appSettingsService.getUserName() } returns ""
initializeRepository() initializeRepository()
@ -223,10 +216,10 @@ class RepositoryTest {
val itemParameters = FakeItemParameters() val itemParameters = FakeItemParameters()
itemParameters.datetime = "2021-04-23 11:45:32" itemParameters.datetime = "2021-04-23 11:45:32"
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = generateTestApiItem(itemParameters), data = generateTestApiItem(itemParameters),
) )
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -239,7 +232,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_items() { fun get_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
runBlocking { runBlocking {
@ -254,7 +247,7 @@ class RepositoryTest {
@Test @Test
fun get_all_newer_items() { fun get_all_newer_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.ALL repository.displayedItems = ItemType.ALL
@ -270,7 +263,7 @@ class RepositoryTest {
@Test @Test
fun get_newer_starred_items() { fun get_newer_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -309,8 +302,8 @@ class RepositoryTest {
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -337,8 +330,8 @@ class RepositoryTest {
coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems( coEvery { db.itemsQueries.items().executeAsList() } returns generateTestDBItems(
itemParameter1, itemParameter1,
) + ) +
generateTestDBItems(itemParameter2) + generateTestDBItems(itemParameter2) +
generateTestDBItems(itemParameter3) generateTestDBItems(itemParameter3)
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
@ -367,7 +360,7 @@ class RepositoryTest {
@Test @Test
fun get_older_items() { fun get_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -383,7 +376,7 @@ class RepositoryTest {
@Test @Test
fun get_all_older_items() { fun get_all_older_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.items = ArrayList(generateTestApiItem()) repository.items = ArrayList(generateTestApiItem())
@ -400,7 +393,7 @@ class RepositoryTest {
@Test @Test
fun get_older_starred_items() { fun get_older_starred_items() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = true, data = generateTestApiItem()) StatusAndData(success = true, data = generateTestApiItem())
initializeRepository() initializeRepository()
repository.displayedItems = ItemType.STARRED repository.displayedItems = ItemType.STARRED
@ -840,7 +833,7 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -868,7 +861,7 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -896,7 +889,7 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -969,10 +962,10 @@ class RepositoryTest {
@Test @Test
fun update_remote() { fun update_remote() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "finished", data = "finished",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -987,10 +980,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_but_response_fails() { fun update_remote_but_response_fails() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = false, success = false,
data = "unallowed access", data = "unallowed access",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1005,10 +998,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_with_unallowed_access() { fun update_remote_with_unallowed_access() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "unallowed access", data = "unallowed access",
) )
initializeRepository() initializeRepository()
var response: Boolean var response: Boolean
@ -1023,10 +1016,10 @@ class RepositoryTest {
@Test @Test
fun update_remote_without_connection() { fun update_remote_without_connection() {
coEvery { api.update() } returns coEvery { api.update() } returns
StatusAndData( StatusAndData(
success = true, success = true,
data = "undocumented...", data = "undocumented...",
) )
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
@ -1116,11 +1109,11 @@ class RepositoryTest {
any(), any(),
) )
} returnsMany } returnsMany
listOf( listOf(
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter2)), StatusAndData(success = true, data = generateTestApiItem(itemParameter2)),
StatusAndData(success = true, data = generateTestApiItem(itemParameter1)), StatusAndData(success = true, data = generateTestApiItem(itemParameter1)),
) )
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1134,7 +1127,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_but_response_fails() { fun cache_items_but_response_fails() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository() initializeRepository()
prepareSearch() prepareSearch()
@ -1148,7 +1141,7 @@ class RepositoryTest {
@Test @Test
fun cache_items_without_connection() { fun cache_items_without_connection() {
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
StatusAndData(success = false, data = generateTestApiItem()) StatusAndData(success = false, data = generateTestApiItem())
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
prepareSearch() prepareSearch()
@ -1175,4 +1168,4 @@ class RepositoryTest {
) )
repository.searchFilter = "search" repository.searchFilter = "search"
} }
} }

View File

@ -3,8 +3,8 @@ package bou.amine.apps.readerforselfossv2.tests.repository
import bou.amine.apps.readerforselfossv2.dao.ITEM import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> = fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<ITEM> {
listOf( return listOf(
ITEM( ITEM(
id = item.id, id = item.id,
datetime = item.datetime, datetime = item.datetime,
@ -20,9 +20,10 @@ fun generateTestDBItems(item: FakeItemParameters = FakeItemParameters()): List<I
author = item.author, author = item.author,
), ),
) )
}
fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> = fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<SelfossModel.Item> {
listOf( return listOf(
SelfossModel.Item( SelfossModel.Item(
id = item.id.toInt(), id = item.id.toInt(),
datetime = item.datetime, datetime = item.datetime,
@ -38,6 +39,7 @@ fun generateTestApiItem(item: FakeItemParameters = FakeItemParameters()): List<S
author = item.author, author = item.author,
), ),
) )
}
class FakeItemParameters { class FakeItemParameters {
var id = "20" var id = "20"
@ -54,4 +56,4 @@ class FakeItemParameters {
var sourcetitle = "La Chimica e la Società" var sourcetitle = "La Chimica e la Società"
var tags = "Chimica, Testing" var tags = "Chimica, Testing"
var author = "Someone important" var author = "Someone important"
} }

View File

@ -1,3 +1,10 @@
buildscript {
dependencies {
// SqlDelight
classpath("com.squareup.sqldelight:gradle-plugin:1.5.5")
}
}
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.7.3").apply(false)

View File

@ -3,4 +3,11 @@ files:
translation: /androidApp/src/main/res/values-%android_code%/%original_file_name% translation: /androidApp/src/main/res/values-%android_code%/%original_file_name%
translate_attributes: '0' translate_attributes: '0'
content_segmentation: '0' content_segmentation: '0'
translation_replace": {
"/master/app": "/androidApp",
"/master/androidApp": "/androidApp"
}
"ignore": [
"/gl/**"
]
preserve_hierarchy: true preserve_hierarchy: true

View File

@ -1,786 +0,0 @@
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.*'

View File

@ -1,16 +0,0 @@
**v124123651**
- Merge pull request 'Bugfixes' (#171) from bugfixes into master
- config: crowdin
- chore: can links be empty ?
- fix: Context issues in article fragment.
- fix: Context issues in fragment sheet.
- fix: build.
- chore: compile issue fix.
- chore: filter some bugs.
- bugfix: catch users using something other than selfoss.
- bugfix: No browser, no link.
- translations
- chore: remove log.
- translation
- Changelog for v124123641

View File

@ -1,14 +0,0 @@
**v125010031**
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
- chore: "faster" action.
- fastlane: icon change.
- chore: ignoring a pixel issue.
- test: fixed an ui test issue.
- fix: center the loading thing.
- test: items displaying.
- bump: sqldelight.
- bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
- bump: androix and coroutines.
- bump: ktor. Closes #67.
- Changelog for v124123651

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 294 KiB

View File

@ -1,18 +1,18 @@
val ktorVersion = "3.0.3" val ktorVersion = "2.3.2"
object SqlDelight { object SqlDelight {
const val runtime = "app.cash.sqldelight:runtime:2.0.2" const val runtime = "com.squareup.sqldelight:runtime:1.5.4"
const val android = "app.cash.sqldelight:android-driver:2.0.2" const val android = "com.squareup.sqldelight:android-driver:1.5.4"
const val native = "app.cash.sqldelight:native-driver:2.0.2" const val native = "com.squareup.sqldelight:native-driver:1.5.4"
} }
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("com.android.library") id("com.android.library")
id("com.squareup.sqldelight")
kotlin("plugin.serialization") version "1.9.0" kotlin("plugin.serialization") version "1.9.0"
id("org.jetbrains.kotlinx.kover") id("org.jetbrains.kotlinx.kover")
id("app.cash.sqldelight") version "2.0.2"
} }
kotlin { kotlin {
@ -37,7 +37,7 @@ kotlin {
implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion") implementation("io.ktor:ktor-client-auth:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.15.4")
@ -66,6 +66,7 @@ kotlin {
val androidMain by getting { val androidMain by getting {
dependencies { dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("io.ktor:ktor-client-okhttp:2.2.4")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
// Sql // Sql
@ -85,6 +86,7 @@ kotlin {
dependencies { dependencies {
implementation(SqlDelight.native) implementation(SqlDelight.native)
implementation("io.ktor:ktor-client-ios:2.1.1")
} }
} }
val iosX64Test by getting val iosX64Test by getting
@ -108,10 +110,11 @@ android {
namespace = "bou.amine.apps.readerforselfossv2" namespace = "bou.amine.apps.readerforselfossv2"
} }
sqldelight { sqldelight {
databases { database("ReaderForSelfossDB") {
create("ReaderForSelfossDB") { packageName = "bou.amine.apps.readerforselfossv2.dao"
packageName.set("bou.amine.apps.readerforselfossv2.dao") sourceFolders = listOf("sqldelight")
}
} }
} }

View File

@ -0,0 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(ReaderForSelfossDB.Schema, context, "ReaderForSelfossV2-android.db")
}
}

View File

@ -1,16 +0,0 @@
package bou.amine.apps.readerforselfossv2.dao
import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
actual class DriverFactory(
private val context: Context,
) {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(
ReaderForSelfossDB.Schema,
context,
"ReaderForSelfossV2-android.db",
)
}

View File

@ -8,20 +8,16 @@ class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted( override fun checkClientTrusted(
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()
} }
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) { actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager() config.https.trustManager = NaiveTrustManager()
} }

View File

@ -1,9 +1,9 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import kotlinx.datetime.Clock import io.github.aakira.napier.Napier
import kotlinx.datetime.*
@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 {

View File

@ -4,36 +4,46 @@ import android.net.Uri
import android.text.Html import android.text.Html
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.util.Locale import java.util.*
actual fun String.getHtmlDecoded(): String = Html.fromHtml(this).toString() actual fun String.getHtmlDecoded(): String {
return Html.fromHtml(this).toString()
}
actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon) actual fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail) actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return 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 (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) { if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp")
) {
allImages.add(url) allImages.add(url)
} }
} }
return allImages return allImages
} }
actual fun SelfossModel.Source.getIcon(baseUrl: String): String = constructUrl(baseUrl, "favicons", icon) actual fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
actual fun constructUrl( actual fun constructUrl(
baseUrl: String, baseUrl: String,
path: String, path: String,
file: String?, file: String?,
): String = ): String {
if (file == null || file == "null" || file.isEmpty()) { return if (file == null || file == "null" || file.isEmpty()) {
"" ""
} else { } else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon() val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
@ -41,3 +51,4 @@ actual fun constructUrl(
baseUriBuilder.toString() baseUriBuilder.toString()
} }
}

View File

@ -1,4 +1,4 @@
package bou.amine.apps.readerforselfossv2.di package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.rest.SelfossApi

View File

@ -1,6 +1,6 @@
package bou.amine.apps.readerforselfossv2.dao package bou.amine.apps.readerforselfossv2.dao
import app.cash.sqldelight.db.SqlDriver import com.squareup.sqldelight.db.SqlDriver
expect class DriverFactory { expect class DriverFactory {
fun createDriver(): SqlDriver fun createDriver(): SqlDriver

View File

@ -1,16 +1,13 @@
@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,
val content: String? = null, val content: String? = null,
val lead_image_url: String? = null, val lead_image_url: String? = null, // NOSONAR
val url: String? = null, val url: String? = null,
val error: Boolean? = null, val error: Boolean? = null,
val message: String? = null, val message: String? = null,

View File

@ -3,20 +3,19 @@ package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
class SuccessResponse( class SuccessResponse(val success: Boolean) {
val success: Boolean,
) {
val isSuccess: Boolean val isSuccess: Boolean
get() = success get() = success
} }
class StatusAndData<T>( class StatusAndData<T>(val success: Boolean, val data: T? = null) {
val success: Boolean,
val data: T? = null,
) {
companion object { companion object {
fun <T> succes(d: T): StatusAndData<T> = StatusAndData(true, d) fun <T> succes(d: T): StatusAndData<T> {
return StatusAndData(true, d)
}
fun <T> error(): StatusAndData<T> = StatusAndData(false) fun <T> error(): StatusAndData<T> {
return StatusAndData(false)
}
} }
} }

View File

@ -1,10 +1,7 @@
@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
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveKind
@ -13,16 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeCollection import kotlinx.serialization.encoding.encodeCollection
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.*
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
class ModelException(
message: String,
) : Throwable(message)
class SelfossModel { class SelfossModel {
@Serializable @Serializable
@ -146,10 +134,6 @@ class SelfossModel {
stringUrl = "http:$stringUrl" stringUrl = "http:$stringUrl"
} }
if (stringUrl.isEmptyOrNullOrNullString()) {
throw ModelException("Link $link was translated to $stringUrl, but was empty. Handle this.")
}
return stringUrl return stringUrl
} }
@ -176,13 +160,14 @@ class SelfossModel {
} }
} }
// this seems to be super slow. // TODO: 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())) { return when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") } is JsonArray -> json.toList().map { it.toString().replace("^\"|\"$".toRegex(), "") }
else -> json.toString().split(",") else -> json.toString().split(",")
} }
}
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING) get() = PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING)
@ -191,10 +176,7 @@ class SelfossModel {
encoder: Encoder, encoder: Encoder,
value: List<String>, value: List<String>,
) { ) {
encoder.encodeCollection( encoder.encodeCollection(PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING), value.size) { this.toString() }
PrimitiveSerialDescriptor("tags", PrimitiveKind.STRING),
value.size,
) { this.toString() }
} }
} }
@ -209,11 +191,7 @@ class SelfossModel {
} }
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = get() = PrimitiveSerialDescriptor("BooleanOrIntForSomeSelfossVersions", PrimitiveKind.BOOLEAN)
PrimitiveSerialDescriptor(
"BooleanOrIntForSomeSelfossVersions",
PrimitiveKind.BOOLEAN,
)
override fun serialize( override fun serialize(
encoder: Encoder, encoder: Encoder,

View File

@ -1,23 +1,12 @@
@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.*
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ITEM
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.dao.SOURCE
import bou.amine.apps.readerforselfossv2.dao.TAG
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.model.SelfossModel 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.ItemType import bou.amine.apps.readerforselfossv2.utils.*
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
import bou.amine.apps.readerforselfossv2.utils.toView
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -25,8 +14,6 @@ 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,
@ -131,7 +118,7 @@ class Repository(
null, null,
null, null,
null, null,
MAX_ITEMS_NUMBER, 200,
) )
return if (items.success && items.data != null) { return if (items.success && items.data != null) {
items.data items.data
@ -143,7 +130,6 @@ class Repository(
} }
} }
@Suppress("detekt:ForbiddenComment")
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
var success = false var success = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
@ -184,8 +170,8 @@ class Repository(
} }
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout> = suspend fun getSpouts(): Map<String, SelfossModel.Spout> {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
val spouts = api.spouts() val spouts = api.spouts()
if (spouts.success && spouts.data != null) { if (spouts.success && spouts.data != null) {
spouts.data spouts.data
@ -195,6 +181,7 @@ class Repository(
} else { } else {
throw NetworkUnavailableException() throw NetworkUnavailableException()
} }
}
suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> { suspend fun getSourcesDetailsOrStats(): ArrayList<SelfossModel.Source> {
var sources = ArrayList<SelfossModel.Source>() var sources = ArrayList<SelfossModel.Source>()
@ -247,13 +234,14 @@ class Repository(
return success return success
} }
private suspend fun markAsReadById(id: Int): Boolean = private suspend fun markAsReadById(id: Int): Boolean {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.markAsRead(id.toString()).isSuccess api.markAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), read = true) insertDBAction(id.toString(), read = true)
true true
} }
}
suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean { suspend fun unmarkAsRead(item: SelfossModel.Item): Boolean {
val success = unmarkAsReadById(item.id) val success = unmarkAsReadById(item.id)
@ -264,13 +252,14 @@ class Repository(
return success return success
} }
private suspend fun unmarkAsReadById(id: Int): Boolean = private suspend fun unmarkAsReadById(id: Int): Boolean {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.unmarkAsRead(id.toString()).isSuccess api.unmarkAsRead(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), unread = true) insertDBAction(id.toString(), unread = true)
true true
} }
}
suspend fun starr(item: SelfossModel.Item): Boolean { suspend fun starr(item: SelfossModel.Item): Boolean {
val success = starrById(item.id) val success = starrById(item.id)
@ -281,13 +270,14 @@ class Repository(
return success return success
} }
private suspend fun starrById(id: Int): Boolean = private suspend fun starrById(id: Int): Boolean {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.starr(id.toString()).isSuccess api.starr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
}
suspend fun unstarr(item: SelfossModel.Item): Boolean { suspend fun unstarr(item: SelfossModel.Item): Boolean {
val success = unstarrById(item.id) val success = unstarrById(item.id)
@ -298,13 +288,14 @@ class Repository(
return success return success
} }
private suspend fun unstarrById(id: Int): Boolean = private suspend fun unstarrById(id: Int): Boolean {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.unstarr(id.toString()).isSuccess api.unstarr(id.toString()).isSuccess
} else { } else {
insertDBAction(id.toString(), starred = true) insertDBAction(id.toString(), starred = true)
true true
} }
}
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
@ -370,13 +361,12 @@ class Repository(
): Boolean { ): Boolean {
var response = false var response = false
if (isNetworkAvailable()) { if (isNetworkAvailable()) {
response = api response = api.createSourceForVersion(
.createSourceForVersion( title,
title, url,
url, spout,
spout, tags,
tags, ).isSuccess == true
).isSuccess == true
} }
return response return response
@ -417,12 +407,13 @@ class Repository(
return success return success
} }
suspend fun updateRemote(): Boolean = suspend fun updateRemote(): Boolean {
if (isNetworkAvailable()) { return if (isNetworkAvailable()) {
api.update().data.equals("finished") api.update().data.equals("finished")
} else { } else {
false false
} }
}
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
@ -431,7 +422,7 @@ class Repository(
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e("login failed", cause, tag = "Repository.login") Napier.e("login failed", cause, tag = "RepositoryImpl.login")
} }
} }
return result return result
@ -445,7 +436,7 @@ class Repository(
// a random rss feed, that would throw a NoTransformationFoundException // a random rss feed, that would throw a NoTransformationFoundException
fetchFailed = !api.getItemsWithoutCatch().success fetchFailed = !api.getItemsWithoutCatch().success
} catch (e: Throwable) { } catch (e: Throwable) {
Napier.e("checkIfFetchFails failed", e, tag = "Repository.shouldBeSelfossInstance") Napier.e("checkIfFetchFails failed", e, tag = "RepositoryImpl.shouldBeSelfossInstance")
} }
} }
@ -457,10 +448,10 @@ class Repository(
try { try {
val response = api.logout() val response = api.logout()
if (!response.isSuccess) { if (!response.isSuccess) {
Napier.e("Couldn't logout.", tag = "Repository.logout") Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
} }
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e("logout failed", cause, tag = "Repository.logout") Napier.e("logout failed", cause, tag = "RepositoryImpl.logout")
} }
appSettingsService.clearAll() appSettingsService.clearAll()
} else { } else {
@ -564,7 +555,6 @@ 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)
@ -588,19 +578,16 @@ class Repository(
markAsReadById(action.articleid.toInt()), markAsReadById(action.articleid.toInt()),
action, action,
) )
action.unread -> action.unread ->
doAndReportOnFail( doAndReportOnFail(
unmarkAsReadById(action.articleid.toInt()), unmarkAsReadById(action.articleid.toInt()),
action, action,
) )
action.starred -> action.starred ->
doAndReportOnFail( doAndReportOnFail(
starrById(action.articleid.toInt()), starrById(action.articleid.toInt()),
action, action,
) )
action.unstarred -> action.unstarred ->
doAndReportOnFail( doAndReportOnFail(
unstarrById(action.articleid.toInt()), unstarrById(action.articleid.toInt()),
@ -631,7 +618,9 @@ class Repository(
_readerItems = readerItems _readerItems = readerItems
} }
fun getReaderItems(): ArrayList<SelfossModel.Item> = _readerItems fun getReaderItems(): ArrayList<SelfossModel.Item> {
return _readerItems
}
fun migrate(driverFactory: DriverFactory) { fun migrate(driverFactory: DriverFactory) {
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1) ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
@ -645,5 +634,7 @@ class Repository(
_selectedSource = null _selectedSource = null
} }
fun getSelectedSource(): SelfossModel.SourceDetail? = _selectedSource fun getSelectedSource(): SelfossModel.SourceDetail? {
return _selectedSource
}
} }

View File

@ -1,26 +1,22 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.MercuryModel import bou.amine.apps.readerforselfossv2.model.*
import bou.amine.apps.readerforselfossv2.model.StatusAndData
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient import io.ktor.client.*
import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.*
import io.ktor.client.plugins.logging.Logger import io.ktor.client.request.*
import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.*
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class MercuryApi { class MercuryApi() {
var client = createHttpClient() var client = createHttpClient()
private fun createHttpClient(): HttpClient = private fun createHttpClient(): HttpClient {
HttpClient { return HttpClient {
install(HttpCache)
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache)
json( json(
Json { Json {
prettyPrint = true prettyPrint = true
@ -40,6 +36,7 @@ class MercuryApi {
} }
expectSuccess = false expectSuccess = false
} }
}
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> = suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
bodyOrFailure( bodyOrFailure(

View File

@ -3,28 +3,23 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient import io.ktor.client.*
import io.ktor.client.call.body import io.ktor.client.call.*
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.*
import io.ktor.client.request.delete import io.ktor.client.request.forms.*
import io.ktor.client.request.forms.submitForm import io.ktor.client.statement.*
import io.ktor.client.request.get import io.ktor.http.*
import io.ktor.client.request.post
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.isSuccess
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse = suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse {
if (r != null && r.status === HttpStatusCode.NotFound) { return if (r != null && r.status === HttpStatusCode.NotFound) {
SuccessResponse(true) SuccessResponse(true)
} else { } else {
maybeResponse(r) maybeResponse(r)
} }
}
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse = suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
if (r != null && r.status.isSuccess()) { return if (r != null && r.status.isSuccess()) {
r.body() r.body()
} else { } else {
if (r != null) { if (r != null) {
@ -32,8 +27,8 @@ 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()) {
@ -103,4 +98,4 @@ suspend fun HttpClient.tryToSubmitForm(
url(url) url(url)
block() block()
} }
} }

View File

@ -1,5 +1,3 @@
@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,24 +33,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json 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(private val appSettingsService: AppSettingsService) {
class SelfossApi(
private val appSettingsService: AppSettingsService,
) {
var client = createHttpClient() var client = createHttpClient()
fun createHttpClient() = fun createHttpClient() =
HttpClient(CIO) { HttpClient(CIO) {
if (appSettingsService.getSelfSigned()) { if (appSettingsService.getSelfSigned()) {
engine { engine {
setupInsecureHttpEngine(this) setupInsecureHTTPEngine(this)
} }
} }
install(HttpCache)
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache)
json( json(
Json { Json {
prettyPrint = true prettyPrint = true
@ -111,14 +105,12 @@ class SelfossApi(
private fun hasLoginInfo() = private fun hasLoginInfo() =
appSettingsService.getUserName().isNotEmpty() && appSettingsService.getUserName().isNotEmpty() &&
appSettingsService appSettingsService.getPassword()
.getPassword()
.isNotEmpty() .isNotEmpty()
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
if (appSettingsService.getUserName().isNotEmpty() && if (appSettingsService.getUserName().isNotEmpty() &&
appSettingsService appSettingsService.getPassword()
.getPassword()
.isNotEmpty() .isNotEmpty()
) { ) {
if (shouldHavePostLogin()) { if (shouldHavePostLogin()) {
@ -135,11 +127,7 @@ class SelfossApi(
client.tryToGet(url("/login")) { client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -160,11 +148,7 @@ class SelfossApi(
client.tryToPost(url("/login")) { client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -180,7 +164,7 @@ class SelfossApi(
}, },
) )
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= VERSION_WHERE_POST_LOGIN_SHOULD_WORK // We are missing 4.1.0 private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse = suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) { if (shouldHaveNewLogout()) {
@ -192,11 +176,7 @@ class SelfossApi(
private suspend fun maybeLogoutIfAvailable() = private suspend fun maybeLogoutIfAvailable() =
responseOrSuccessIf404( responseOrSuccessIf404(
client.tryToGet(url("/logout")) { client.tryToGet(url("/logout")) {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -215,11 +195,7 @@ class SelfossApi(
private suspend fun doLogout() = private suspend fun doLogout() =
maybeResponse( maybeResponse(
client.tryToDelete(url("/api/session/current")) { client.tryToDelete(url("/api/session/current")) {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -257,11 +233,7 @@ class SelfossApi(
parameter("updatedsince", updatedSince) parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset) parameter("offset", offset)
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -286,11 +258,7 @@ class SelfossApi(
} }
parameter("type", "all") parameter("type", "all")
parameter("items", 1) parameter("items", 1)
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -313,11 +281,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -340,11 +304,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -367,11 +327,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -394,11 +350,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -421,11 +373,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -448,11 +396,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -471,11 +415,7 @@ class SelfossApi(
suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> = suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
bodyOrFailure( bodyOrFailure(
client.tryToGet(url("/api/about")) { client.tryToGet(url("/api/about")) {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -498,11 +438,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -525,11 +461,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -552,11 +484,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -579,11 +507,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -612,11 +536,7 @@ class SelfossApi(
ids.map { append("ids[]", it) } ids.map { append("ids[]", it) }
}, },
block = { block = {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -668,11 +588,7 @@ class SelfossApi(
append(tagsParamName, tags) append(tagsParamName, tags)
}, },
block = { block = {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -725,11 +641,7 @@ class SelfossApi(
append(tagsParamName, tags) append(tagsParamName, tags)
}, },
block = { block = {
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,
@ -752,11 +664,7 @@ class SelfossApi(
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers { headers {
append( append(
HttpHeaders.Authorization, HttpHeaders.Authorization,

View File

@ -1,5 +1,3 @@
@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
@ -123,4 +121,4 @@ class ACRASettings : Settings {
longs.remove(key) longs.remove(key)
strings.remove(key) strings.remove(key)
} }
} }

View File

@ -1,22 +1,8 @@
@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 class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
private const val DEFAULT_REFRESH_MINUTES = 360L
private const val MIN_REFRESH_MINUTES = 15L
private const val DEFAULT_API_TIMEOUT = 60L
private const val DEFAULT_ITEMS_NUMBER = 20
class AppSettingsService(
acraSenderServiceProcess: Boolean = false,
) {
val settings: Settings = val settings: Settings =
if (acraSenderServiceProcess) { if (acraSenderServiceProcess) {
ACRASettings() ACRASettings()
@ -25,37 +11,37 @@ class AppSettingsService(
} }
// Api related // Api related
private var apiVersion: Int = -1 private var _apiVersion: Int = -1
private var publicAccess: Boolean? = null private var _publicAccess: Boolean? = null
private var selfSigned: Boolean? = null private var _selfSigned: Boolean? = null
private var baseUrl: String = "" private var _baseUrl: String = ""
private var userName: String = "" private var _userName: String = ""
private var basicUserName: String = "" private var _basicUserName: String = ""
private var password: String = "" private var _password: String = ""
private var basicPassword: String = "" private var _basicPassword: String = ""
// User settings related // User settings related
private var itemsCaching: Boolean? = null private var _itemsCaching: Boolean? = null
private var articleViewer: Boolean? = null private var _articleViewer: Boolean? = null
private var shouldBeCardView: Boolean? = null private var _shouldBeCardView: Boolean? = null
private var displayUnreadCount: Boolean? = null private var _displayUnreadCount: Boolean? = null
private var displayAllCount: Boolean? = null private var _displayAllCount: Boolean? = null
private var fullHeightCards: Boolean? = null private var _fullHeightCards: Boolean? = null
private var updateSources: Boolean? = null private var _updateSources: Boolean? = null
private var periodicRefresh: Boolean? = null private var _periodicRefresh: Boolean? = null
private var refreshWhenChargingOnly: Boolean? = null private var _refreshWhenChargingOnly: Boolean? = null
private var infiniteLoading: Boolean? = null private var _infiniteLoading: Boolean? = null
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 = DEFAULT_REFRESH_MINUTES private var _refreshMinutes: Long = 360
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 _staticBar: Boolean? = null
private var font: String = "" private var _font: String = ""
private var theme: Int? = null private var _theme: Int? = null
init { init {
refreshApiSettings() refreshApiSettings()
@ -63,11 +49,11 @@ class AppSettingsService(
} }
fun getApiVersion(): Int { fun getApiVersion(): Int {
if (apiVersion == -1) { if (_apiVersion == -1) {
refreshApiVersion() refreshApiVersion()
return apiVersion return _apiVersion
} }
return apiVersion return _apiVersion
} }
fun updateApiVersion(apiMajorVersion: Int) { fun updateApiVersion(apiMajorVersion: Int) {
@ -76,14 +62,14 @@ class AppSettingsService(
} }
private fun refreshApiVersion() { private fun refreshApiVersion() {
apiVersion = settings.getInt(API_VERSION_MAJOR, -1) _apiVersion = settings.getInt(API_VERSION_MAJOR, -1)
} }
fun getPublicAccess(): Boolean { fun getPublicAccess(): Boolean {
if (publicAccess == null) { if (_publicAccess == null) {
refreshPublicAccess() refreshPublicAccess()
} }
return publicAccess!! return _publicAccess!!
} }
fun updatePublicAccess(publicAccess: Boolean) { fun updatePublicAccess(publicAccess: Boolean) {
@ -92,14 +78,14 @@ class AppSettingsService(
} }
private fun refreshPublicAccess() { private fun refreshPublicAccess() {
publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false) _publicAccess = settings.getBoolean(API_PUBLIC_ACCESS, false)
} }
fun getSelfSigned(): Boolean { fun getSelfSigned(): Boolean {
if (selfSigned == null) { if (_selfSigned == null) {
refreshSelfSigned() refreshSelfSigned()
} }
return selfSigned!! return _selfSigned!!
} }
fun updateSelfSigned(selfSigned: Boolean) { fun updateSelfSigned(selfSigned: Boolean) {
@ -108,315 +94,312 @@ class AppSettingsService(
} }
private fun refreshSelfSigned() { private fun refreshSelfSigned() {
selfSigned = settings.getBoolean(API_SELF_SIGNED, false) _selfSigned = settings.getBoolean(API_SELF_SIGNED, false)
} }
fun getBaseUrl(): String { fun getBaseUrl(): String {
if (baseUrl.isEmpty()) { if (_baseUrl.isEmpty()) {
refreshBaseUrl() refreshBaseUrl()
} }
return baseUrl return _baseUrl
} }
fun getUserName(): String { fun getUserName(): String {
if (userName.isEmpty()) { if (_userName.isEmpty()) {
refreshUsername() refreshUsername()
} }
return userName return _userName
} }
fun getPassword(): String { fun getPassword(): String {
if (password.isEmpty()) { if (_password.isEmpty()) {
refreshPassword() refreshPassword()
} }
return password return _password
} }
fun getBasicUserName(): String { fun getBasicUserName(): String {
if (basicUserName.isEmpty()) { if (_basicUserName.isEmpty()) {
refreshBasicUsername() refreshBasicUsername()
} }
return basicUserName return _basicUserName
} }
fun getBasicPassword(): String { fun getBasicPassword(): String {
if (basicPassword.isEmpty()) { if (_basicPassword.isEmpty()) {
refreshBasicPassword() refreshBasicPassword()
} }
return basicPassword return _basicPassword
} }
fun getItemsNumber(): Int { fun getItemsNumber(): Int {
if (itemsNumber == null) { if (_itemsNumber == null) {
refreshItemsNumber() refreshItemsNumber()
} }
return itemsNumber!! return _itemsNumber!!
} }
@Suppress("detekt:SwallowedException")
private fun refreshItemsNumber() { private fun refreshItemsNumber() {
itemsNumber = _itemsNumber =
try { try {
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt() settings.getString(API_ITEMS_NUMBER, "20").toInt()
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_ITEMS_NUMBER) settings.remove(API_ITEMS_NUMBER)
DEFAULT_ITEMS_NUMBER 20
} }
} }
fun getApiTimeout(): Long { fun getApiTimeout(): Long {
if (apiTimeout == null) { if (_apiTimeout == null) {
refreshApiTimeout() refreshApiTimeout()
} }
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, DEFAULT_API_TIMEOUT.toString()) val settingsTimeout = settings.getString(API_TIMEOUT, "60")
if (settingsTimeout.toLong() > 0) { if (settingsTimeout.toLong() > 0) {
settingsTimeout.toLong() settingsTimeout.toLong()
} else { } else {
settings.remove(API_TIMEOUT) settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT 60
} }
} catch (e: Exception) { } catch (e: Exception) {
settings.remove(API_TIMEOUT) settings.remove(API_TIMEOUT)
DEFAULT_API_TIMEOUT 60
}, },
) )
} }
private fun refreshBaseUrl() { private fun refreshBaseUrl() {
baseUrl = settings.getString(BASE_URL, "") _baseUrl = settings.getString(BASE_URL, "")
} }
private fun refreshUsername() { private fun refreshUsername() {
userName = settings.getString(LOGIN, "") _userName = settings.getString(LOGIN, "")
} }
private fun refreshPassword() { private fun refreshPassword() {
password = settings.getString(PASSWORD, "") _password = settings.getString(PASSWORD, "")
} }
private fun refreshBasicUsername() { private fun refreshBasicUsername() {
basicUserName = settings.getString(BASIC_LOGIN, "") _basicUserName = settings.getString(BASIC_LOGIN, "")
} }
private fun refreshBasicPassword() { private fun refreshBasicPassword() {
basicPassword = settings.getString(BASIC_PASSWORD, "") _basicPassword = settings.getString(BASIC_PASSWORD, "")
} }
private fun refreshArticleViewerEnabled() { private fun refreshArticleViewerEnabled() {
articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true) _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
} }
fun isArticleViewerEnabled(): Boolean { fun isArticleViewerEnabled(): Boolean {
if (articleViewer != null) { if (_articleViewer != null) {
refreshArticleViewerEnabled() refreshArticleViewerEnabled()
} }
return articleViewer == true return _articleViewer == true
} }
private fun refreshShouldBeCardViewEnabled() { private fun refreshShouldBeCardViewEnabled() {
shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false) _shouldBeCardView = settings.getBoolean(CARD_VIEW_ACTIVE, false)
} }
fun isCardViewEnabled(): Boolean { fun isCardViewEnabled(): Boolean {
if (shouldBeCardView != null) { if (_shouldBeCardView != null) {
refreshShouldBeCardViewEnabled() refreshShouldBeCardViewEnabled()
} }
return shouldBeCardView == true return _shouldBeCardView == true
} }
private fun refreshDisplayUnreadCountEnabled() { private fun refreshDisplayUnreadCountEnabled() {
displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true) _displayUnreadCount = settings.getBoolean(DISPLAY_UNREAD_COUNT, true)
} }
fun isDisplayUnreadCountEnabled(): Boolean { fun isDisplayUnreadCountEnabled(): Boolean {
if (displayUnreadCount != null) { if (_displayUnreadCount != null) {
refreshDisplayUnreadCountEnabled() refreshDisplayUnreadCountEnabled()
} }
return displayUnreadCount == true return _displayUnreadCount == true
} }
private fun refreshDisplayAllCountEnabled() { private fun refreshDisplayAllCountEnabled() {
displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false) _displayAllCount = settings.getBoolean(DISPLAY_OTHER_COUNT, false)
} }
fun isDisplayAllCountEnabled(): Boolean { fun isDisplayAllCountEnabled(): Boolean {
if (displayAllCount != null) { if (_displayAllCount != null) {
refreshDisplayAllCountEnabled() refreshDisplayAllCountEnabled()
} }
return displayAllCount == true return _displayAllCount == true
} }
private fun refreshFullHeightCardsEnabled() { private fun refreshFullHeightCardsEnabled() {
fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false) _fullHeightCards = settings.getBoolean(FULL_HEIGHT_CARDS, false)
} }
fun isFullHeightCardsEnabled(): Boolean { fun isFullHeightCardsEnabled(): Boolean {
if (fullHeightCards != null) { if (_fullHeightCards != null) {
refreshFullHeightCardsEnabled() refreshFullHeightCardsEnabled()
} }
return fullHeightCards == true return _fullHeightCards == true
} }
private fun refreshUpdateSourcesEnabled() { private fun refreshUpdateSourcesEnabled() {
updateSources = settings.getBoolean(UPDATE_SOURCES, true) _updateSources = settings.getBoolean(UPDATE_SOURCES, true)
} }
fun isUpdateSourcesEnabled(): Boolean { fun isUpdateSourcesEnabled(): Boolean {
if (updateSources != null) { if (_updateSources != null) {
refreshUpdateSourcesEnabled() refreshUpdateSourcesEnabled()
} }
return updateSources == true return _updateSources == true
} }
private fun refreshPeriodicRefreshEnabled() { private fun refreshPeriodicRefreshEnabled() {
periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false) _periodicRefresh = settings.getBoolean(PERIODIC_REFRESH, false)
} }
fun isPeriodicRefreshEnabled(): Boolean { fun isPeriodicRefreshEnabled(): Boolean {
if (periodicRefresh != null) { if (_periodicRefresh != null) {
refreshPeriodicRefreshEnabled() refreshPeriodicRefreshEnabled()
} }
return periodicRefresh == true return _periodicRefresh == true
} }
private fun refreshRefreshWhenChargingOnlyEnabled() { private fun refreshRefreshWhenChargingOnlyEnabled() {
refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false) _refreshWhenChargingOnly = settings.getBoolean(REFRESH_WHEN_CHARGING, false)
} }
fun isRefreshWhenChargingOnlyEnabled(): Boolean { fun isRefreshWhenChargingOnlyEnabled(): Boolean {
if (refreshWhenChargingOnly != null) { if (_refreshWhenChargingOnly != null) {
refreshRefreshWhenChargingOnlyEnabled() refreshRefreshWhenChargingOnlyEnabled()
} }
return refreshWhenChargingOnly == true return _refreshWhenChargingOnly == true
} }
private fun refreshRefreshMinutes() { private fun refreshRefreshMinutes() {
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong() _refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
if (refreshMinutes <= MIN_REFRESH_MINUTES) { if (_refreshMinutes <= 15) {
refreshMinutes = MIN_REFRESH_MINUTES _refreshMinutes = 15
} }
} }
fun getRefreshMinutes(): Long { fun getRefreshMinutes(): Long {
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) { if (_refreshMinutes != 360L) {
refreshRefreshMinutes() refreshRefreshMinutes()
} }
return refreshMinutes return _refreshMinutes
} }
private fun refreshInfiniteLoadingEnabled() { private fun refreshInfiniteLoadingEnabled() {
infiniteLoading = settings.getBoolean(INFINITE_LOADING, false) _infiniteLoading = settings.getBoolean(INFINITE_LOADING, false)
} }
fun isInfiniteLoadingEnabled(): Boolean { fun isInfiniteLoadingEnabled(): Boolean {
if (infiniteLoading != null) { if (_infiniteLoading != null) {
refreshInfiniteLoadingEnabled() refreshInfiniteLoadingEnabled()
} }
return infiniteLoading == true return _infiniteLoading == true
} }
private fun refreshItemCachingEnabled() { private fun refreshItemCachingEnabled() {
itemsCaching = settings.getBoolean(ITEMS_CACHING, false) _itemsCaching = settings.getBoolean(ITEMS_CACHING, false)
} }
fun isItemCachingEnabled(): Boolean { fun isItemCachingEnabled(): Boolean {
if (itemsCaching != null) { if (_itemsCaching != null) {
refreshItemCachingEnabled() refreshItemCachingEnabled()
} }
return itemsCaching == true return _itemsCaching == true
} }
private fun refreshNotifyNewItemsEnabled() { private fun refreshNotifyNewItemsEnabled() {
notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false) _notifyNewItems = settings.getBoolean(NOTIFY_NEW_ITEMS, false)
} }
fun isNotifyNewItemsEnabled(): Boolean { fun isNotifyNewItemsEnabled(): Boolean {
if (notifyNewItems != null) { if (_notifyNewItems != null) {
refreshNotifyNewItemsEnabled() refreshNotifyNewItemsEnabled()
} }
return notifyNewItems == true return _notifyNewItems == true
} }
private fun refreshMarkOnScrollEnabled() { private fun refreshMarkOnScrollEnabled() {
markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false) _markOnScroll = settings.getBoolean(MARK_ON_SCROLL, false)
} }
fun isMarkOnScrollEnabled(): Boolean { fun isMarkOnScrollEnabled(): Boolean {
if (markOnScroll != null) { if (_markOnScroll != null) {
refreshMarkOnScrollEnabled() refreshMarkOnScrollEnabled()
} }
return markOnScroll == true return _markOnScroll == true
} }
private fun refreshActiveAllignment() { private fun refreshActiveAllignment() {
activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY) _activeAlignment = settings.getInt(TEXT_ALIGN, JUSTIFY)
} }
fun getActiveAllignment(): Int { fun getActiveAllignment(): Int {
if (activeAlignment != null) { if (_activeAlignment != null) {
refreshActiveAllignment() refreshActiveAllignment()
} }
return activeAlignment ?: JUSTIFY return _activeAlignment ?: JUSTIFY
} }
fun changeAllignment(allignment: Int) { fun changeAllignment(allignment: Int) {
settings.putInt(TEXT_ALIGN, allignment) settings.putInt(TEXT_ALIGN, allignment)
activeAlignment = allignment _activeAlignment = allignment
} }
private fun refreshFontSize() { private fun refreshFontSize() {
fontSize = settings.getString(READER_FONT_SIZE, "16").toInt() _fontSize = settings.getString(READER_FONT_SIZE, "16").toInt()
} }
fun getFontSize(): Int { fun getFontSize(): Int {
if (fontSize != null) { if (_fontSize != null) {
refreshFontSize() refreshFontSize()
} }
return fontSize ?: DEFAULT_FONT_SIZE return _fontSize ?: 16
} }
private fun refreshStaticBarEnabled() { private fun refreshStaticBarEnabled() {
staticBar = settings.getBoolean(READER_STATIC_BAR, false) _staticBar = settings.getBoolean(READER_STATIC_BAR, false)
} }
fun isStaticBarEnabled(): Boolean { fun isStaticBarEnabled(): Boolean {
if (staticBar != null) { if (_staticBar != null) {
refreshStaticBarEnabled() refreshStaticBarEnabled()
} }
return staticBar == true return _staticBar == true
} }
private fun refreshFont() { private fun refreshFont() {
font = settings.getString(READER_FONT, "") _font = settings.getString(READER_FONT, "")
} }
fun getFont(): String { fun getFont(): String {
if (font.isEmpty()) { if (_font.isEmpty()) {
refreshFont() refreshFont()
} }
return font return _font
} }
private fun refreshCurrentTheme() { private fun refreshCurrentTheme() {
theme = settings.getString(CURRENT_THEME, "-1").toInt() _theme = settings.getString(CURRENT_THEME, "-1").toInt()
} }
fun getCurrentTheme(): Int { fun getCurrentTheme(): Int {
if (theme == null) { if (_theme == null) {
refreshCurrentTheme() refreshCurrentTheme()
} }
return theme ?: -1 return _theme ?: -1
} }
fun refreshApiSettings() { fun refreshApiSettings() {
@ -495,15 +478,15 @@ class AppSettingsService(
} }
companion object { companion object {
const val TRANSLATION_URL = "https://crwd.in/readerforselfoss" const val translationUrl = "https://crwd.in/readerforselfoss"
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform" const val sourceUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val BUG_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues" const val trackerUrl = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val SYNC_CHANNEL_ID = "sync-channel-id" const val syncChannelId = "sync-channel-id"
const val NEW_ITEMS_CHANNEL = "new-items-channel-id" const val newItemsChannelId = "new-items-channel-id"
const val JUSTIFY = 1 const val JUSTIFY = 1

View File

@ -4,12 +4,6 @@ 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
@ -23,22 +17,17 @@ 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 newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
.find(this)
?.groups
?.get(1)
?.value ?: throw DateParseException("Couldn't parse $this")
} else { } else {
throw DateParseException("Unrecognized format for $this") throw Exception("Unrecognized format for $this")
} }
} catch (e: Exception) { } catch (e: Exception) {
throw DateParseException("parseDate failed for $this", e) throw Exception("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

View File

@ -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,7 +74,6 @@ 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)
@ -83,4 +82,4 @@ fun SelfossModel.Tag.getColorHexCode(): String =
"#$char1$char1$char2$char2$char3$char3" "#$char1$char1$char2$char2$char3$char3"
} else { } else {
this.color this.color
} }

View File

@ -1,10 +1,6 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
@Suppress("detekt:MagicNumber") enum class ItemType(val position: Int, val type: String) {
enum class ItemType(
val position: Int,
val type: String,
) {
UNREAD(1, "unread"), UNREAD(1, "unread"),
ALL(2, "all"), ALL(2, "all"),
STARRED(3, "starred"), STARRED(3, "starred"),

View File

@ -2,7 +2,6 @@ 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

View File

@ -1,5 +1,3 @@
import kotlin.Boolean;
CREATE TABLE `ACTION` ( CREATE TABLE `ACTION` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`articleid` TEXT NOT NULL, `articleid` TEXT NOT NULL,

View File

@ -1,5 +1,3 @@
import kotlin.Boolean;
CREATE TABLE ITEM ( CREATE TABLE ITEM (
`id` TEXT NOT NULL, `id` TEXT NOT NULL,
`datetime` TEXT NOT NULL, `datetime` TEXT NOT NULL,

View File

@ -8,29 +8,27 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DatesTest { class DatesTest {
private val newVersionDateVariant = "2022-12-24T17:00:08+00" private val newVersionDateVariant = "2022-12-24T17:00:08+00"
private val newVersionDate = "2013-04-07T13:43:00+01:00" private val newVersionDate = "2013-04-07T13:43:00+01:00"
private val newVersionDate2 = "2013-04-07T13:43:00-01:00" private val newVersionDate2 = "2013-04-07T13:43:00-01:00"
private val oldVersionDate = "2013-05-07 13:46:00" private val oldVersionDate = "2013-05-07 13:46:00"
private val oldVersionDateVariant = "2021-03-21 10:32:00.000000" private val oldVersionDateVariant = "2021-03-21 10:32:00.000000"
@Test @Test
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) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
} }
@Test @Test
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) LocalDateTime(2013, 4, 7, 13, 43, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@ -40,8 +38,7 @@ 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) LocalDateTime(2013, 5, 7, 13, 46, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@ -51,8 +48,7 @@ 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) LocalDateTime(2021, 3, 21, 10, 32, 0, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)
@ -62,8 +58,7 @@ 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) LocalDateTime(2022, 12, 24, 17, 0, 8, 0).toInstant(TimeZone.currentSystemDefault())
.toInstant(TimeZone.currentSystemDefault())
.toEpochMilliseconds() .toEpochMilliseconds()
assertEquals(expected, date) assertEquals(expected, date)

View File

@ -1,8 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao package bou.amine.apps.readerforselfossv2.dao
import app.cash.sqldelight.db.SqlDriver import com.squareup.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
actual class DriverFactory { actual class DriverFactory {
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
} }

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
@Suppress("detekt:EmptyFunctionBlock")
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
}

View File

@ -0,0 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}

View File

@ -1,10 +1,9 @@
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 {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@ -1,10 +1,9 @@
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 {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }

View File

@ -1,8 +1,10 @@
package bou.amine.apps.readerforselfossv2.dao package bou.amine.apps.readerforselfossv2.dao
import app.cash.sqldelight.db.SqlDriver import com.squareup.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.native.NativeSqliteDriver import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
actual class DriverFactory { actual class DriverFactory {
actual fun createDriver(): SqlDriver = NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db") actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(ReaderForSelfossDB.Schema, "ReaderForSelfossV2-IOS.db")
}
} }

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
@Suppress("detekt:EmptyFunctionBlock")
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
}

View File

@ -0,0 +1,6 @@
package bou.amine.apps.readerforselfossv2.rest
import io.ktor.client.engine.cio.CIOEngineConfig
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) {
}

View File

@ -1,10 +1,9 @@
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 {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
} }