Compare commits
2 Commits
fix-bottom
...
6b5f6cbbe0
Author | SHA1 | Date | |
---|---|---|---|
6b5f6cbbe0 | |||
5035392aff |
@@ -3,34 +3,25 @@ root = true
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[.editorconfig]
|
||||
insert_final_newline = false
|
||||
ij_kotlin_line_break_after_multiline_when_entry = false
|
||||
[{*.kt,*.kts}]
|
||||
|
||||
[*.{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
|
||||
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
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_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = unset
|
||||
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
|
||||
max_line_length = 140
|
@@ -1,10 +0,0 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
base_path: "../../../"
|
||||
|
||||
files:
|
||||
- source: /androidApp/src/main/res/values/strings.xml
|
||||
translation: /androidApp/src/main/res/values-%android_code%/%original_file_name%
|
||||
translate_attributes: '0'
|
||||
content_segmentation: '0'
|
||||
preserve_hierarchy: true
|
@@ -6,45 +6,40 @@ jobs:
|
||||
BuildAndTestAndCoverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: "Check android app changes"
|
||||
id: check-android-changes
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
androidApp/src/**
|
||||
shared/src/commonMain/**
|
||||
shared/src/androidMain/**
|
||||
shared/src/commonTest/**
|
||||
- name: Fetch tags
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
run: git fetch --tags -p
|
||||
- uses: actions/setup-java@v4
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- uses: gradle/actions/setup-gradle@v3
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
- uses: android-actions/setup-android@v3
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
- name: Configure gradle...
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
|
||||
- name: Build and test
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest
|
||||
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
|
||||
- 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
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
run: |
|
||||
./gradlew :koverHtmlReport
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: steps.check-android-changes.outputs.any_modified == 'true'
|
||||
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
|
@@ -1,4 +1,4 @@
|
||||
name: Realease
|
||||
name: Create tag
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.gitea/workflows/on_called_build.yml
|
||||
uses: ./.gitea/workflows/common_build.yml
|
||||
createTagAndChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
@@ -16,7 +16,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
- name: Config git
|
||||
run: |
|
||||
git config --global user.email aminecmi+giteadrone@pm.me
|
||||
@@ -51,7 +50,7 @@ jobs:
|
||||
followtags: true
|
||||
ssh_key: ${{ secrets.PRIVATE_KEY }}
|
||||
tags: true
|
||||
branch: master
|
||||
branch: release
|
||||
- name: copy file via ssh password
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
@@ -86,6 +85,7 @@ jobs:
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
- name: Configure gradle...
|
||||
@@ -124,4 +124,4 @@ jobs:
|
||||
priority: high
|
||||
convert_markdown: true
|
||||
body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
|
||||
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
|
||||
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
|
@@ -1,11 +1,11 @@
|
||||
name: PR
|
||||
name: Check PR code
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
PR:
|
||||
Lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
@@ -14,77 +14,15 @@ jobs:
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- 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
|
||||
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...
|
||||
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
|
||||
- name: Detecting...
|
||||
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
|
||||
translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: "Check translations changes"
|
||||
id: check-translations-changes
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
files: |
|
||||
androidApp/src/main/res/values/strings.xml
|
||||
- name: upload translation sources
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: './.gitea/workflows/assets/crowdin.yml'
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
create_pull_request: false
|
||||
push_translations: false
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
- name: wait
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true'
|
||||
run: sleep 10s
|
||||
- name: download translations
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
config: './.gitea/workflows/assets/crowdin.yml'
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
create_pull_request: false
|
||||
push_translations: false
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
- name: Check for uncommitted changes
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true'
|
||||
id: check-changes
|
||||
uses: mskri/check-uncommitted-changes-action@v1.0.1
|
||||
- name: Commit Changes
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
|
||||
run: |
|
||||
git config --global user.email aminecmi+giteadrone@pm.me
|
||||
git config --global user.name giteadrone
|
||||
git add ./androidApp/src/main/res/*
|
||||
git commit -m "translation: translation files"
|
||||
- name: Push changes
|
||||
if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
|
||||
uses: appleboy/git-push-action@v1.0.0
|
||||
with:
|
||||
author_name: giteadrone
|
||||
author_email: aminecmi+giteadrone@pm.me
|
||||
remote: ${{ secrets.REMOTE_URL }}
|
||||
ssh_key: ${{ secrets.PRIVATE_KEY }}
|
||||
branch: ${{ github.head_ref || github.ref_name }}
|
||||
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
|
||||
build:
|
||||
needs: Lint
|
||||
uses: ./.gitea/workflows/on_called_build.yml
|
||||
uses: ./.gitea/workflows/common_build.yml
|
||||
|
@@ -1,67 +0,0 @@
|
||||
name: PR test
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
integrationTests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
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'
|
||||
- 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: Change url until I find a better way to do it
|
||||
run: |
|
||||
sed -i "s/const val DEFAULT_URL = \"http:\/\/10\.0\.2\.2\:8888\"/const val DEFAULT_URL = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt
|
||||
- name: Tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 29
|
||||
profile: pixel_2
|
||||
script: |
|
||||
./gradlew androidApp:clearScreenshotsTask || true
|
||||
./gradlew androidApp:createScreenshotDirectory
|
||||
adb logcat -G 16M
|
||||
./gradlew JacocoDebugCodeCoverage || (./gradlew androidApp:fetchScreenshots && adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt)
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshot-espresso
|
||||
path: androidApp/build/reports/androidTests/connected/screenshots
|
||||
retention-days: 2
|
||||
overwrite: true
|
||||
include-hidden-files: true
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
include-hidden-files: true
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-espresso
|
||||
path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage
|
||||
retention-days: 1
|
||||
overwrite: true
|
||||
include-hidden-files: true
|
||||
- name: Clean
|
||||
if: always()
|
||||
run: |
|
||||
docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
@@ -1,4 +1,4 @@
|
||||
name: Master
|
||||
name: Check master code
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -6,4 +6,4 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.gitea/workflows/on_called_build.yml
|
||||
uses: ./.gitea/workflows/common_build.yml
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -323,6 +323,4 @@ fabric.properties
|
||||
crowdin.properties
|
||||
|
||||
.kotlin/
|
||||
build-cache/
|
||||
|
||||
act
|
||||
build-cache/
|
115
CHANGELOG.md
115
CHANGELOG.md
@@ -1,118 +1,3 @@
|
||||
**v125040991
|
||||
|
||||
- fix: Connectivity toast message was causing issues.
|
||||
- Changelog for v125030901
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125030901
|
||||
|
||||
- Merge pull request 'fix-reload' (#195) from fix-reload into master
|
||||
- fix: Infinite scroll needs loading stats.
|
||||
- fix: do not reload items on resume.
|
||||
- Merge pull request 'tests' (#193) from tests into master
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- chore: better handling of coroutine dispatchers.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- chore: comment robolectric tests for now.
|
||||
- fix: Fixed source deletion test.
|
||||
- Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
|
||||
- Refactor star icon handling
|
||||
- Don't restart activity changing alignment
|
||||
- Changelog for v125030711
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125030711
|
||||
|
||||
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
|
||||
- chore: check changes for translations and android.
|
||||
- fix: initial status loading issues.
|
||||
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
|
||||
- chore: new connectivity dep. Closes #84.
|
||||
- Changelog for v125030681
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125030681
|
||||
|
||||
- chore: do not send reports on simulators.
|
||||
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
|
||||
- chore: do not send reports on simulators.
|
||||
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
|
||||
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
|
||||
- chore: we don't need to check if the url is valid in upsert screen.
|
||||
- fix: Url validation was not failing login. Added tests.
|
||||
- chore: crowding ci integration.
|
||||
- Show a confirmation dialog before deleting sources (#185)
|
||||
- Changelog for v125020581
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125020581
|
||||
|
||||
- fix: url can be empty ?
|
||||
- Changelog for v125020471
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125020471
|
||||
|
||||
- chore: no more docker-compose.
|
||||
- bump: gradle plugin.
|
||||
- Merge pull request 'fix: check index exists.' (#183) from fix-index into master
|
||||
- fix: check index exists.
|
||||
- Changelog for v125020411
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125020411
|
||||
|
||||
- Merge pull request 'bump' (#182) from bump into master
|
||||
- chore: non transiant R classes.
|
||||
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||
- bump
|
||||
- fix: One more missing context.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125010241
|
||||
|
||||
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||
- refactor: context fragments issues.
|
||||
- logs: Context issues.
|
||||
- fix: Handle empty url issue, again.
|
||||
- fix: Link not opening.
|
||||
- Changelog for v125010201
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125010201
|
||||
|
||||
- fix: Handle empty url issue.
|
||||
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||
- chore: changing actions in reader fragment.
|
||||
- Changelog for v125010131
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125010131
|
||||
|
||||
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||
- Changelog for v125010111
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125010111
|
||||
|
||||
- Debug trying to fix context issues. (#174)
|
||||
- Changelog for v125010031
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v125010031
|
||||
|
||||
- Merge pull request 'Bump dependencies' (#173) from upgarde into master
|
||||
|
@@ -10,41 +10,30 @@ plugins {
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
id("app.cash.sqldelight") version "2.0.2"
|
||||
jacoco
|
||||
}
|
||||
|
||||
fun Project.execWithOutput(
|
||||
cmd: String,
|
||||
ignore: Boolean = false,
|
||||
): String {
|
||||
val result: String =
|
||||
ByteArrayOutputStream().use { outputStream ->
|
||||
project.exec {
|
||||
commandLine = cmd.split(" ")
|
||||
standardOutput = outputStream
|
||||
isIgnoreExitValue = ignore
|
||||
}
|
||||
outputStream.toString()
|
||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
val result: String = ByteArrayOutputStream().use { outputStream ->
|
||||
project.exec {
|
||||
commandLine = cmd.split(" ")
|
||||
standardOutput = outputStream
|
||||
isIgnoreExitValue = ignore
|
||||
}
|
||||
outputStream.toString()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun gitVersion(): String {
|
||||
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
|
||||
val process =
|
||||
if (maybeTagOfCurrentCommit.isEmpty()) {
|
||||
println("No tag on current commit. Will take the latest one.")
|
||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
||||
} else {
|
||||
println("Tag found on current commit")
|
||||
execWithOutput("git -C ../ describe --contains HEAD")
|
||||
}
|
||||
return process
|
||||
.replace("^0", "")
|
||||
.replace("'", "")
|
||||
.substring(1)
|
||||
.replace("\\.", "")
|
||||
.trim()
|
||||
val process = if (maybeTagOfCurrentCommit.isEmpty()) {
|
||||
println("No tag on current commit. Will take the latest one.")
|
||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
||||
} else {
|
||||
println("Tag found on current commit")
|
||||
execWithOutput("git -C ../ describe --contains HEAD")
|
||||
}
|
||||
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
|
||||
}
|
||||
|
||||
fun versionCodeFromGit(): Int {
|
||||
@@ -65,15 +54,6 @@ fun versionNameFromGit(): String {
|
||||
return gitVersion()
|
||||
}
|
||||
|
||||
val exclusions =
|
||||
listOf(
|
||||
"**/R.class",
|
||||
"**/R\$*.class",
|
||||
"**/BuildConfig.*",
|
||||
"**/Manifest*.*",
|
||||
"**/*Test*.*",
|
||||
)
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
@@ -105,7 +85,7 @@ android {
|
||||
|
||||
// tests
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments["useTestStorageService"] = "true"
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
@@ -119,44 +99,6 @@ android {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
getByName("debug") {
|
||||
isTestCoverageEnabled = true
|
||||
enableAndroidTestCoverage = true
|
||||
installation {
|
||||
installOptions("-g", "-r")
|
||||
}
|
||||
|
||||
val androidTests = "connectedAndroidTest"
|
||||
tasks.register<JacocoReport>("JacocoDebugCodeCoverage") {
|
||||
// Depend on unit tests and Android tests tasks
|
||||
dependsOn(listOf(androidTests))
|
||||
// Set task grouping and description
|
||||
group = "Reporting"
|
||||
description = "Execute UI and unit tests, generate and combine Jacoco coverage report"
|
||||
// Configure reports to generate both XML and HTML formats
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
html.required.set(true)
|
||||
}
|
||||
// Set source directories to the main source directory
|
||||
sourceDirectories.setFrom(layout.projectDirectory.dir("src/main"))
|
||||
// Set class directories to compiled Java and Kotlin classes, excluding specified exclusions
|
||||
classDirectories.setFrom(
|
||||
files(
|
||||
fileTree(layout.buildDirectory.dir("intermediates/javac/")) {
|
||||
exclude(exclusions)
|
||||
},
|
||||
fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) {
|
||||
exclude(exclusions)
|
||||
},
|
||||
),
|
||||
)
|
||||
// Collect execution data from .exec and .ec files generated during test execution
|
||||
executionData.setFrom(
|
||||
files(
|
||||
fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
flavorDimensions.add("build")
|
||||
@@ -169,10 +111,12 @@ android {
|
||||
namespace = "bou.amine.apps.readerforselfossv2.android"
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -197,12 +141,12 @@ dependencies {
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
|
||||
implementation("org.jsoup:jsoup:1.18.3")
|
||||
|
||||
// multidex
|
||||
//multidex
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
|
||||
// About
|
||||
implementation("com.mikepenz:aboutlibraries-core:11.6.3")
|
||||
implementation("com.mikepenz:aboutlibraries:11.6.3")
|
||||
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
|
||||
implementation("com.mikepenz:aboutlibraries:10.5.1")
|
||||
|
||||
// Material-ish things
|
||||
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
||||
@@ -212,47 +156,49 @@ dependencies {
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||
|
||||
// Themes
|
||||
implementation("com.leinardi.android:speed-dial:3.3.0")
|
||||
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
|
||||
|
||||
// Pager
|
||||
implementation("me.relex:circleindicator:2.1.6")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
|
||||
// Dependency Injection
|
||||
//Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di:7.23.1")
|
||||
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
|
||||
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
|
||||
|
||||
// Settings
|
||||
//Settings
|
||||
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
|
||||
|
||||
// Logging
|
||||
//Logging
|
||||
implementation("io.github.aakira:napier:2.7.1")
|
||||
|
||||
// PhotoView
|
||||
//PhotoView
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
||||
|
||||
// Network information
|
||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
||||
|
||||
// SQLDELIGHT
|
||||
implementation("app.cash.sqldelight:android-driver:2.0.2")
|
||||
|
||||
// test
|
||||
//test
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.13.14")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||
androidTestImplementation("androidx.test:runner:1.7.0-alpha01")
|
||||
androidTestImplementation("androidx.test:rules:1.7.0-alpha01")
|
||||
androidTestImplementation("androidx.test:runner:1.6.2")
|
||||
androidTestImplementation("androidx.test:rules:1.6.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
implementation("androidx.test.espresso:espresso-idling-resource:3.6.1")
|
||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1")
|
||||
androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02")
|
||||
androidTestUtil("androidx.test:orchestrator:1.5.1")
|
||||
testImplementation("org.robolectric:robolectric:4.14.1")
|
||||
testImplementation("androidx.test:core-ktx:1.7.0-alpha01")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
|
||||
testImplementation("androidx.test:core-ktx:1.6.1")
|
||||
|
||||
implementation("ch.acra:acra-http:$acraVersion")
|
||||
implementation("ch.acra:acra-toast:$acraVersion")
|
||||
@@ -264,24 +210,16 @@ tasks.withType<Test> {
|
||||
useJUnit()
|
||||
testLogging {
|
||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||
events =
|
||||
setOf(
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR,
|
||||
)
|
||||
events = setOf(
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
|
||||
)
|
||||
showStandardStreams = true
|
||||
}
|
||||
if (this.name == "connectedAndroidTest") {
|
||||
configure<JacocoTaskExtension> {
|
||||
isIncludeNoLocationClasses = true
|
||||
excludes = listOf("jdk.internal.*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
excludeFields = arrayOf("generated")
|
||||
offlineMode = true
|
||||
fetchRemoteLicense = false
|
||||
fetchRemoteFunding = false
|
||||
@@ -289,31 +227,4 @@ aboutLibraries {
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||
}
|
||||
|
||||
val clearScreenshotsTask =
|
||||
tasks.register<Exec>("clearScreenshots") {
|
||||
println("AMINE : clear")
|
||||
commandLine = listOf("adb", "shell", "rm", "-r", "/storage/emulated/0/Pictures/selfoss_tests/screenshots/*")
|
||||
}
|
||||
|
||||
val createScreenshotDirectoryTask =
|
||||
tasks.register<Exec>("createScreenshotDirectory") {
|
||||
println("AMINE : create directory")
|
||||
group = "reporting"
|
||||
commandLine = listOf("adb", "shell", "mkdir", "-p", "/storage/emulated/0/Pictures/selfoss_tests/screenshots")
|
||||
}
|
||||
|
||||
tasks.register<Exec>("fetchScreenshots") {
|
||||
val reportsDirectory = file("$buildDir/reports/androidTests/connected")
|
||||
println("AMINE : fetch")
|
||||
group = "reporting"
|
||||
executable(android.adbExecutable.toString())
|
||||
commandLine = listOf("adb", "pull", "/storage/emulated/0/Pictures/selfoss_tests/screenshots", reportsDirectory.toString())
|
||||
|
||||
finalizedBy(clearScreenshotsTask)
|
||||
|
||||
doFirst {
|
||||
reportsDirectory.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,7 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Environment.DIRECTORY_PICTURES
|
||||
import android.os.Environment.getExternalStoragePublicDirectory
|
||||
import android.util.Log
|
||||
import androidx.annotation.ArrayRes
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.Espresso.onData
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
@@ -14,39 +9,29 @@ import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.base.DefaultFailureHandler
|
||||
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.hamcrest.Matchers.hasToString
|
||||
import org.junit.BeforeClass
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
|
||||
// For now, do not move this as it is modified by the integration tests
|
||||
const val DEFAULT_URL = "http://10.0.2.2:8888"
|
||||
|
||||
fun performLogin(someUrl: String? = null) {
|
||||
Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL}")
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(
|
||||
typeTextIntoFocusedView(
|
||||
if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL,
|
||||
if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888",
|
||||
),
|
||||
)
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
}
|
||||
|
||||
fun loginAndInitHome() {
|
||||
performLogin()
|
||||
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
|
||||
onView(withText("OK")).perform(click())
|
||||
}
|
||||
|
||||
fun changeAndCancelSetting(
|
||||
oldValue: String,
|
||||
newValue: String,
|
||||
@@ -112,12 +97,6 @@ fun testPreferencesFromArray(
|
||||
}
|
||||
}
|
||||
|
||||
fun goToSources() {
|
||||
openMenu()
|
||||
onView(withText(R.string.menu_home_sources))
|
||||
.perform(click())
|
||||
}
|
||||
|
||||
fun testAddSourceWithUrl(
|
||||
url: String,
|
||||
sourceName: String,
|
||||
@@ -125,14 +104,12 @@ fun testAddSourceWithUrl(
|
||||
onView(withId(R.id.fab))
|
||||
.perform(click())
|
||||
onView(withId(R.id.nameInput))
|
||||
.perform(click())
|
||||
.perform(typeTextIntoFocusedView(sourceName))
|
||||
.perform(click()).perform(typeTextIntoFocusedView(sourceName))
|
||||
onView(withId(R.id.sourceUri))
|
||||
.perform(click())
|
||||
.perform(typeTextIntoFocusedView(url))
|
||||
onView(withId(R.id.tags))
|
||||
.perform(click())
|
||||
.perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
|
||||
.perform(click()).perform(typeTextIntoFocusedView("tag1,tag2,tag3"))
|
||||
onView(withId(R.id.spoutsSpinner))
|
||||
.perform(click())
|
||||
onData(hasToString("RSS Feed")).perform(click())
|
||||
@@ -140,93 +117,3 @@ fun testAddSourceWithUrl(
|
||||
.perform(click())
|
||||
onView(withText(sourceName)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun checkHomeLoadingDone() {
|
||||
onView(withId(R.id.swipeRefreshLayout)).inRoot(not(isDialog())).perform(waitForRecyclerViewToStopLoading(300000))
|
||||
}
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
open class WithANRException {
|
||||
companion object {
|
||||
// Running count of the number of Android Not Responding dialogues to prevent endless dismissal.
|
||||
private var anrCount = 0
|
||||
|
||||
// `RootViewWithoutFocusException` class is private, need to match the message (instead of using type matching).
|
||||
private val rootViewWithoutFocusExceptionMsg =
|
||||
java.lang.String.format(
|
||||
Locale.ROOT,
|
||||
"Waited for the root of the view hierarchy to have " +
|
||||
"window focus and not request layout for 10 seconds. If you specified a non " +
|
||||
"default root matcher, it may be picking a root that never takes focus. " +
|
||||
"Root:",
|
||||
)
|
||||
private const val OTHER_EXCEPTION = "System Ul isn't responding"
|
||||
|
||||
private fun handleAnrDialogue() {
|
||||
val device = UiDevice.getInstance(getInstrumentation())
|
||||
// If running the device in English Locale
|
||||
val waitButton = device.findObject(UiSelector().textContains("wait"))
|
||||
if (waitButton.exists()) waitButton.click()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpHandler() {
|
||||
Espresso.setFailureHandler { error, viewMatcher ->
|
||||
|
||||
takeScreenshot()
|
||||
if (error.message!!.contains(OTHER_EXCEPTION)) {
|
||||
handleAnrDialogue()
|
||||
} else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) &&
|
||||
anrCount < 20
|
||||
) {
|
||||
anrCount++
|
||||
handleAnrDialogue()
|
||||
} else { // chain all failures down to the default espresso handler
|
||||
Log.e("AMINE", "AMINE : ${error.message}")
|
||||
println("AMINE : ${error.message}")
|
||||
DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:NestedBlockDepth")
|
||||
fun takeScreenshot() {
|
||||
try {
|
||||
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
|
||||
|
||||
val folder =
|
||||
File(
|
||||
File(
|
||||
getExternalStoragePublicDirectory(DIRECTORY_PICTURES),
|
||||
"selfoss_tests",
|
||||
).absolutePath,
|
||||
"screenshots",
|
||||
)
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs()
|
||||
}
|
||||
|
||||
var out: BufferedOutputStream? = null
|
||||
val size = folder.list().size + 1
|
||||
try {
|
||||
out = BufferedOutputStream(FileOutputStream(folder.path + "/" + size + ".png"))
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
Log.d("Screenshots", "Screenshot taken")
|
||||
} catch (e: IOException) {
|
||||
Log.e("Screenshots", "Could not save the screenshot", e)
|
||||
} finally {
|
||||
if (out != null) {
|
||||
try {
|
||||
out.close()
|
||||
} catch (e: IOException) {
|
||||
Log.e("Screenshots", "Could not save the screenshot", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
Log.e("Screenshots", "Could not take the screenshot", ex)
|
||||
}
|
||||
}
|
||||
|
@@ -8,134 +8,51 @@ import android.widget.RelativeLayout
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.PerformException
|
||||
import androidx.test.espresso.Root
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withChild
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withClassName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withParent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.espresso.util.HumanReadables
|
||||
import androidx.test.espresso.util.TreeIterables
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.any
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
fun withError(
|
||||
@StringRes id: Int,
|
||||
): TypeSafeMatcher<View?> {
|
||||
return object : TypeSafeMatcher<View?>() {
|
||||
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
|
||||
}
|
||||
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?) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitUntilShown(
|
||||
viewText: String,
|
||||
millis: Long,
|
||||
): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View> = isRoot()
|
||||
|
||||
override fun getDescription(): String = "wait for $millis millis, for a specific view with text <$viewText> to be visible."
|
||||
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View,
|
||||
) {
|
||||
uiController.loopMainThreadUntilIdle()
|
||||
val startTime = System.currentTimeMillis()
|
||||
val endTime = startTime + millis
|
||||
val viewMatcher = withText(viewText)
|
||||
|
||||
do {
|
||||
for (child in TreeIterables.breadthFirstViewTraversal(view)) {
|
||||
if (viewMatcher.matches(child) && child.isShown) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uiController.loopMainThreadForAtLeast(100)
|
||||
} while (System.currentTimeMillis() < endTime)
|
||||
|
||||
// timeout happens
|
||||
throw PerformException
|
||||
.Builder()
|
||||
.withActionDescription(this.description)
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(TimeoutException())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
fun isPopupWindow(): Matcher<Root> {
|
||||
return isPlatformPopup()
|
||||
}
|
||||
|
||||
fun waitForRecyclerViewToStopLoading(millis: Long): ViewAction {
|
||||
return object : ViewAction {
|
||||
override fun getConstraints(): Matcher<View> = any(View::class.java)
|
||||
|
||||
override fun getDescription(): String = "wait for $millis millis for the recyclerview to stop loading."
|
||||
|
||||
override fun perform(
|
||||
uiController: UiController,
|
||||
view: View?,
|
||||
) {
|
||||
uiController.loopMainThreadUntilIdle()
|
||||
val startTime = System.currentTimeMillis()
|
||||
val endTime = startTime + millis
|
||||
|
||||
do {
|
||||
// either the empty view is displayed
|
||||
for (child in TreeIterables.breadthFirstViewTraversal(view)) {
|
||||
// found view with required ID
|
||||
if (withId(R.id.emptyText).matches(child) && child.isVisible) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// or the refresh layout is refreshing
|
||||
if (view is SwipeRefreshLayout && !view.isRefreshing) {
|
||||
return
|
||||
}
|
||||
uiController.loopMainThreadForAtLeast(100)
|
||||
} while (System.currentTimeMillis() < endTime)
|
||||
|
||||
// timeout happens
|
||||
throw PerformException
|
||||
.Builder()
|
||||
.withActionDescription(this.description)
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(TimeoutException())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isPopupWindow(): Matcher<Root> = isPlatformPopup()
|
||||
|
||||
fun withDrawable(
|
||||
@DrawableRes id: Int,
|
||||
) = object : TypeSafeMatcher<View>() {
|
||||
@@ -143,7 +60,6 @@ fun withDrawable(
|
||||
description.appendText("ImageView with drawable same as drawable with id $id")
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
override fun matchesSafely(view: View): Boolean {
|
||||
val context = view.context
|
||||
val expectedBitmap = context.getDrawable(id)!!.toBitmap()
|
||||
@@ -157,8 +73,8 @@ fun withDrawable(
|
||||
|
||||
fun hasBottombarItemText(
|
||||
@StringRes id: Int,
|
||||
): Matcher<View>? =
|
||||
allOf(
|
||||
): Matcher<View>? {
|
||||
return allOf(
|
||||
withResourceName("fixed_bottom_navigation_icon"),
|
||||
withParent(
|
||||
allOf(
|
||||
@@ -167,21 +83,23 @@ fun hasBottombarItemText(
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun withSettingsCheckboxWidget(
|
||||
@StringRes id: Int,
|
||||
): Matcher<View>? =
|
||||
allOf(
|
||||
): Matcher<View>? {
|
||||
return allOf(
|
||||
withId(android.R.id.switch_widget),
|
||||
withParent(
|
||||
withSettingsCheckboxFrame(id),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun withSettingsCheckboxFrame(
|
||||
@StringRes id: Int,
|
||||
): Matcher<View>? =
|
||||
allOf(
|
||||
): Matcher<View>? {
|
||||
return allOf(
|
||||
withId(android.R.id.widget_frame),
|
||||
hasSibling(
|
||||
allOf(
|
||||
@@ -192,6 +110,7 @@ fun withSettingsCheckboxFrame(
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun openMenu() {
|
||||
openActionBarOverflowOrOptionsMenu(
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
@@ -15,29 +14,21 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@LargeTest
|
||||
class `2-HomeActivityTest` : WithANRException() {
|
||||
class HomeActivityTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun registerIdlingResource() {
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
checkHomeLoadingDone()
|
||||
fun init() {
|
||||
loginAndInitHome()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -65,7 +56,7 @@ class `2-HomeActivityTest` : WithANRException() {
|
||||
fun testMenuActions() {
|
||||
onView(withId(R.id.action_search)).perform(click())
|
||||
onView(
|
||||
withId(com.google.android.material.R.id.search_src_text),
|
||||
withId(R.id.search_src_text),
|
||||
).check(matches(isFocused()))
|
||||
onView(isRoot()).perform(ViewActions.pressBack())
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
@@ -15,73 +16,58 @@ import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@LargeTest
|
||||
class `1-LoginActivityTest` : WithANRException() {
|
||||
class LoginActivityTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
private fun getActivity(): Activity? {
|
||||
var activity: Activity? = null
|
||||
activityRule.scenario.onActivity {
|
||||
activity = it
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
@Before
|
||||
fun registerIdlingResource() {
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
IdlingRegistry.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
}
|
||||
|
||||
@After
|
||||
fun unregisterIdlingResource() {
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
IdlingRegistry.getInstance()
|
||||
.unregister(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `1-viewIsInitialized`() {
|
||||
fun viewIsInitialized() {
|
||||
onView(withId(R.id.urlView)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.selfSigned))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(isNotChecked()))
|
||||
onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked()))
|
||||
.check(
|
||||
matches(isClickable()),
|
||||
)
|
||||
onView(withId(R.id.withLogin))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(isNotChecked()))
|
||||
.check(
|
||||
onView(withId(R.id.withLogin)).check(matches(isDisplayed()))
|
||||
.check(matches(isNotChecked())).check(
|
||||
matches(isClickable()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `2-urlError`() {
|
||||
fun urlError() {
|
||||
performLogin("10.0.2.2:8888")
|
||||
onView(withId(R.id.urlView)).perform(click())
|
||||
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `3-urlSlashError`() {
|
||||
performLogin("https://google.fr/toto")
|
||||
onView(withId(R.id.urlView)).perform(click())
|
||||
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `4-connectError`() {
|
||||
performLogin("http://10.0.2.2:8889")
|
||||
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `5-multiError`() {
|
||||
fun multiError() {
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
@@ -89,10 +75,8 @@ class `1-LoginActivityTest` : WithANRException() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `6-connect`() {
|
||||
fun connect() {
|
||||
performLogin()
|
||||
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
|
||||
onView(withText("OK")).perform(click())
|
||||
checkHomeLoadingDone()
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
@@ -20,29 +19,22 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
@LargeTest
|
||||
class `4-SettingsActivityGeneralTest` : WithANRException() {
|
||||
class SettingsActivityGeneralTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
loginAndInitHome()
|
||||
openActionBarOverflowOrOptionsMenu(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
)
|
||||
@@ -50,7 +42,6 @@ class `4-SettingsActivityGeneralTest` : WithANRException() {
|
||||
onView(withText(R.string.pref_header_general)).perform(click())
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
@Test
|
||||
fun testGeneral() {
|
||||
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
|
||||
@@ -73,6 +64,19 @@ class `4-SettingsActivityGeneralTest` : WithANRException() {
|
||||
),
|
||||
),
|
||||
)
|
||||
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
allOf(
|
||||
isDisplayed(),
|
||||
not(isChecked()),
|
||||
),
|
||||
),
|
||||
)
|
||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
isEnabled(),
|
||||
),
|
||||
)
|
||||
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
|
||||
matches(
|
||||
@@ -114,7 +118,6 @@ class `4-SettingsActivityGeneralTest` : WithANRException() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:ForbiddenComment")
|
||||
@Test
|
||||
fun testGeneralActionsNumberItems() {
|
||||
onView(withText(R.string.pref_api_items_number_title)).perform(click())
|
||||
@@ -156,6 +159,19 @@ class `4-SettingsActivityGeneralTest` : WithANRException() {
|
||||
|
||||
@Test
|
||||
fun testGeneralActionsCheckboxes() {
|
||||
// article viewer settings
|
||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
isEnabled(),
|
||||
),
|
||||
)
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
|
||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
not(isEnabled()),
|
||||
),
|
||||
)
|
||||
|
||||
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
|
||||
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
|
@@ -1,36 +1,31 @@
|
||||
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.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class `6-SettingsActivityOfflineTest` : WithANRException() {
|
||||
class SettingsActivityOfflineTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
lateinit var context: Context
|
||||
|
||||
@@ -39,18 +34,14 @@ class `6-SettingsActivityOfflineTest` : WithANRException() {
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
context = activity.window.context
|
||||
}
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
loginAndInitHome()
|
||||
openActionBarOverflowOrOptionsMenu(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
)
|
||||
onView(withText(R.string.title_activity_settings)).perform(click())
|
||||
onView(withText(R.string.pref_header_offline)).perform(click())
|
||||
}
|
||||
|
||||
@After
|
||||
fun back() {
|
||||
onView(isRoot()).perform(ViewActions.pressBack())
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
@Test
|
||||
fun testOffline() {
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
|
||||
@@ -116,7 +107,6 @@ class `6-SettingsActivityOfflineTest` : WithANRException() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
@Test
|
||||
fun testOfflineActions() {
|
||||
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
|
@@ -1,34 +1,29 @@
|
||||
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.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isChecked
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class `5-SettingsActivityReaderTest` : WithANRException() {
|
||||
class SettingsActivityReaderTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(SettingsActivity::class.java)
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
lateinit var context: Context
|
||||
|
||||
@@ -37,17 +32,14 @@ class `5-SettingsActivityReaderTest` : WithANRException() {
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
context = activity.window.context
|
||||
}
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
loginAndInitHome()
|
||||
openActionBarOverflowOrOptionsMenu(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
)
|
||||
onView(withText(R.string.title_activity_settings)).perform(click())
|
||||
onView(withText(R.string.pref_header_viewer)).perform(click())
|
||||
}
|
||||
|
||||
@After
|
||||
fun back() {
|
||||
onView(isRoot()).perform(ViewActions.pressBack())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReader() {
|
||||
onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check(
|
@@ -2,17 +2,14 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isSelected
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Before
|
||||
@@ -22,11 +19,9 @@ import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
class `3-SettingsActivityTest` : WithANRException() {
|
||||
class SettingsActivityTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
|
||||
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
lateinit var context: Context
|
||||
|
||||
@Before
|
||||
@@ -34,9 +29,7 @@ class `3-SettingsActivityTest` : WithANRException() {
|
||||
activityRule.scenario.onActivity { activity ->
|
||||
context = activity.window.context
|
||||
}
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
loginAndInitHome()
|
||||
openMenu()
|
||||
onView(withText(R.string.title_activity_settings)).perform(click())
|
||||
}
|
||||
@@ -75,9 +68,6 @@ class `3-SettingsActivityTest` : WithANRException() {
|
||||
changeAndSaveSetting("", "10") {
|
||||
onView(withText(R.string.pref_api_timeout)).perform(click())
|
||||
}
|
||||
changeAndSaveSetting("", "60") {
|
||||
onView(withText(R.string.pref_api_timeout)).perform(click())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +87,6 @@ class `3-SettingsActivityTest` : WithANRException() {
|
||||
@Test
|
||||
fun testAbout() {
|
||||
onView(withText(R.string.action_about)).perform(click())
|
||||
onView(isRoot()).perform(waitUntilShown("ACRA", 30000))
|
||||
onView(withText("ACRA")).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
@@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import androidx.test.espresso.AmbiguousViewMatcherException
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.swipeDown
|
||||
@@ -15,7 +14,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@@ -23,22 +21,19 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class `7-SourcesActivityTest` : WithANRException() {
|
||||
class SourcesActivityTest {
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(HomeActivity::class.java)
|
||||
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
|
||||
|
||||
lateinit var sourceName: String
|
||||
|
||||
@Before
|
||||
fun init() {
|
||||
IdlingRegistry
|
||||
.getInstance()
|
||||
.register(CountingIdlingResourceSingleton.countingIdlingResource)
|
||||
sourceName = UUID.randomUUID().toString().substring(0, 15)
|
||||
|
||||
loginAndInitHome()
|
||||
goToSources()
|
||||
}
|
||||
|
||||
@@ -50,7 +45,6 @@ class `7-SourcesActivityTest` : WithANRException() {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
@Test
|
||||
fun addSourceCheckContent() {
|
||||
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
|
||||
@@ -76,8 +70,12 @@ class `7-SourcesActivityTest` : WithANRException() {
|
||||
fun deleteTheCreatedSource() {
|
||||
onView(withText(sourceName)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.deleteBtn)).perform(click())
|
||||
onView(withText(R.string.confirm_delete_title)).check(matches(isDisplayed()))
|
||||
onView(withId(android.R.id.button1)).perform(click())
|
||||
onView(withText(sourceName)).check(doesNotExist())
|
||||
}
|
||||
|
||||
private fun goToSources() {
|
||||
openMenu()
|
||||
onView(withText(R.string.menu_home_sources))
|
||||
.perform(click())
|
||||
}
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".MyApp"
|
||||
android:allowBackup="false"
|
||||
android:configChanges="uiMode"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/NoBar"
|
||||
tools:replace="android:allowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:label="@string/title_activity_login"></activity>
|
||||
<activity android:name=".HomeActivity"></activity>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".HomeActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".HomeActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SourcesActivity"
|
||||
android:parentActivityName=".HomeActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".HomeActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".UpsertSourceActivity"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".SourcesActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".SourcesActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ReaderActivity"></activity>
|
||||
<activity
|
||||
android:name=".ImageActivity"
|
||||
android:theme="@style/Theme.AppCompat.ImageActivity"></activity>
|
||||
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="2.1" />
|
||||
</application>
|
||||
</manifest>
|
@@ -31,11 +31,11 @@ import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationBar
|
||||
import com.ashokvarma.bottomnavigation.BottomNavigationItem
|
||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||
@@ -49,8 +49,6 @@ import org.kodein.di.instance
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val MIN_WIDTH_CARD_DP = 300
|
||||
|
||||
class HomeActivity :
|
||||
AppCompatActivity(),
|
||||
SearchView.OnQueryTextListener,
|
||||
@@ -104,7 +102,7 @@ class HomeActivity :
|
||||
|
||||
if (appSettingsService.isItemCachingEnabled()) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
repository.tryToCacheItemsAndGetNewOnes()
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
@@ -120,9 +118,12 @@ class HomeActivity :
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
repository.offlineOverride = false
|
||||
lastFetchDone = false
|
||||
items.clear()
|
||||
getElementsAccordingToTab()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
getElementsAccordingToTab()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
|
||||
val swipeDirs =
|
||||
@@ -199,7 +200,6 @@ class HomeActivity :
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
private fun handleBottomBar() {
|
||||
tabNewBadge =
|
||||
TextBadgeItem()
|
||||
@@ -282,11 +282,11 @@ class HomeActivity :
|
||||
|
||||
handleBottomBarActions()
|
||||
|
||||
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||
|
||||
handleRecurringTask()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
repository.handleDBActions()
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
@@ -294,10 +294,10 @@ class HomeActivity :
|
||||
getElementsAccordingToTab()
|
||||
}
|
||||
|
||||
private fun handleGdprDialog(gdprShown: Boolean) {
|
||||
private fun handleGDPRDialog(GDPRShown: Boolean) {
|
||||
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
|
||||
messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
|
||||
if (!gdprShown) {
|
||||
if (!GDPRShown) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
|
||||
alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
|
||||
@@ -314,44 +314,50 @@ class HomeActivity :
|
||||
|
||||
private fun reloadLayoutManager() {
|
||||
val currentManager = binding.recyclerView.layoutManager
|
||||
val layoutManager: RecyclerView.LayoutManager
|
||||
|
||||
fun gridLayoutManager() {
|
||||
val layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
fun staggererdGridLayoutManager() {
|
||||
var layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
// This will only update the layout manager if settings changed
|
||||
when (currentManager) {
|
||||
is StaggeredGridLayoutManager ->
|
||||
if (!appSettingsService.isCardViewEnabled()) {
|
||||
gridLayoutManager()
|
||||
layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
is GridLayoutManager ->
|
||||
if (appSettingsService.isCardViewEnabled()) {
|
||||
staggererdGridLayoutManager()
|
||||
layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
else ->
|
||||
if (currentManager == null) {
|
||||
if (!appSettingsService.isCardViewEnabled()) {
|
||||
gridLayoutManager()
|
||||
layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
} else {
|
||||
staggererdGridLayoutManager()
|
||||
layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,7 +393,6 @@ class HomeActivity :
|
||||
lastFetchDone = false
|
||||
|
||||
elementsShown = ItemType.fromInt(position + 1)
|
||||
items = ArrayList()
|
||||
getElementsAccordingToTab()
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
|
||||
@@ -460,34 +465,25 @@ class HomeActivity :
|
||||
appendResults: Boolean,
|
||||
itemType: ItemType,
|
||||
) {
|
||||
@Suppress("detekt:ComplexCondition")
|
||||
if ((appendResults && items.size > 0) || (!appendResults && items.size == 0)) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.displayedItems = itemType
|
||||
items =
|
||||
if (appendResults) {
|
||||
repository.getOlderItems()
|
||||
} else {
|
||||
repository.getNewerItems()
|
||||
}
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
handleListResult()
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
repository.displayedItems = itemType
|
||||
items =
|
||||
if (appendResults) {
|
||||
repository.getOlderItems()
|
||||
} else {
|
||||
repository.getNewerItems()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
} else {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
handleListResult()
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleListResult(appendResults: Boolean = false) {
|
||||
val oldManager = binding.recyclerView.layoutManager
|
||||
if (appendResults) {
|
||||
val oldManager = binding.recyclerView.layoutManager
|
||||
firstVisible =
|
||||
when (oldManager) {
|
||||
is StaggeredGridLayoutManager ->
|
||||
@@ -500,13 +496,7 @@ class HomeActivity :
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:ComplexCondition")
|
||||
if (recyclerAdapter == null ||
|
||||
(
|
||||
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
|
||||
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
|
||||
)
|
||||
) {
|
||||
if (recyclerAdapter == null) {
|
||||
if (appSettingsService.isCardViewEnabled()) {
|
||||
recyclerAdapter =
|
||||
ItemCardAdapter(
|
||||
@@ -541,10 +531,7 @@ class HomeActivity :
|
||||
}
|
||||
|
||||
private fun reloadBadges() {
|
||||
if (appSettingsService.isInfiniteLoadingEnabled() ||
|
||||
appSettingsService.isDisplayUnreadCountEnabled() ||
|
||||
appSettingsService.isDisplayAllCountEnabled()
|
||||
) {
|
||||
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.reloadBadges()
|
||||
@@ -556,7 +543,7 @@ class HomeActivity :
|
||||
private fun calculateNoOfColumns(): Int {
|
||||
val displayMetrics = resources.displayMetrics
|
||||
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
||||
return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
|
||||
return (dpWidth / 300).toInt()
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(p0: String?): Boolean {
|
||||
@@ -605,11 +592,10 @@ class HomeActivity :
|
||||
.show()
|
||||
}
|
||||
|
||||
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.issue_tracker -> {
|
||||
baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL)
|
||||
baseContext.openUrlInBrowser(AppSettingsService.TRACKER_URL)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -623,26 +609,22 @@ class HomeActivity :
|
||||
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
|
||||
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val updatedRemote = repository.updateRemote()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (updatedRemote) {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_success_response,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_failer_message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
if (updatedRemote) {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_success_response,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_failer_message,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
@@ -653,33 +635,30 @@ class HomeActivity :
|
||||
R.id.readAll -> {
|
||||
if (elementsShown == ItemType.UNREAD) {
|
||||
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val success = repository.markAllAsRead(items)
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (success) {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.all_posts_read,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
tabNewBadge.removeBadge()
|
||||
if (success) {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.all_posts_read,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
tabNewBadge.removeBadge()
|
||||
|
||||
getElementsAccordingToTab()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.all_posts_not_read,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
getElementsAccordingToTab()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.all_posts_not_read,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
handleListResult()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
|
@@ -84,9 +84,7 @@ class ImageActivity : AppCompatActivity() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private inner class ScreenSlidePagerAdapter(
|
||||
fa: FragmentActivity,
|
||||
) : FragmentStateAdapter(fa) {
|
||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||
override fun getItemCount(): Int = allImages.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
|
||||
|
@@ -30,8 +30,6 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
|
||||
|
||||
class LoginActivity :
|
||||
AppCompatActivity(),
|
||||
DIAware {
|
||||
@@ -108,7 +106,7 @@ class LoginActivity :
|
||||
|
||||
private fun goToMain() {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
repository.updateApiInformation()
|
||||
ACRA.errorReporter.putCustomData(
|
||||
"SELFOSS_API_VERSION",
|
||||
@@ -127,12 +125,8 @@ class LoginActivity :
|
||||
binding.urlView.error = getString(R.string.wrong_infos)
|
||||
binding.loginView.error = getString(R.string.wrong_infos)
|
||||
binding.passwordView.error = getString(R.string.wrong_infos)
|
||||
binding.urlView.requestFocus()
|
||||
|
||||
showProgress(false)
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
private fun attemptLogin() {
|
||||
// Reset errors.
|
||||
binding.urlView.error = null
|
||||
@@ -140,23 +134,13 @@ class LoginActivity :
|
||||
binding.passwordView.error = null
|
||||
|
||||
// Store values at the time of the login attempt.
|
||||
val url =
|
||||
binding.urlView.text
|
||||
.toString()
|
||||
.trim()
|
||||
val login =
|
||||
binding.loginView.text
|
||||
.toString()
|
||||
.trim()
|
||||
val password =
|
||||
binding.passwordView.text
|
||||
.toString()
|
||||
.trim()
|
||||
val url = binding.urlView.text.toString().trim()
|
||||
val login = binding.loginView.text.toString().trim()
|
||||
val password = binding.passwordView.text.toString().trim()
|
||||
|
||||
failInvalidUrl(url)
|
||||
failLoginDetails(password, login)
|
||||
|
||||
val cancelUrl = failInvalidUrl(url)
|
||||
if (cancelUrl) return
|
||||
val cancelDetails = failLoginDetails(password, login)
|
||||
if (cancelDetails) return
|
||||
showProgress(true)
|
||||
|
||||
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
|
||||
@@ -164,48 +148,41 @@ class LoginActivity :
|
||||
repository.refreshLoginInformation(url, login, password)
|
||||
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
repository.updateApiInformation()
|
||||
val result = repository.login()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (result) {
|
||||
val errorFetching = repository.checkIfFetchFails()
|
||||
if (!errorFetching) {
|
||||
goToMain()
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (e.message?.startsWith("No transformation found") == true) {
|
||||
Toast
|
||||
.makeText(
|
||||
applicationContext,
|
||||
R.string.application_selfoss_only,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
preferenceError()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
if (e.message?.startsWith("No transformation found") == true) {
|
||||
Toast
|
||||
.makeText(
|
||||
applicationContext,
|
||||
R.string.application_selfoss_only,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
preferenceError()
|
||||
showProgress(false)
|
||||
}
|
||||
} finally {
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
val result = repository.login()
|
||||
if (result) {
|
||||
val errorFetching = repository.checkIfFetchFails()
|
||||
if (!errorFetching) {
|
||||
goToMain()
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
showProgress(false)
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
|
||||
private fun failLoginDetails(
|
||||
password: String,
|
||||
login: String,
|
||||
): Boolean {
|
||||
) {
|
||||
var lastFocusedView: View? = null
|
||||
var cancel = false
|
||||
if (isWithLogin) {
|
||||
@@ -222,17 +199,16 @@ class LoginActivity :
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, lastFocusedView)
|
||||
return cancel
|
||||
}
|
||||
|
||||
private fun failInvalidUrl(url: String): Boolean {
|
||||
private fun failInvalidUrl(url: String) {
|
||||
val focusView = binding.urlView
|
||||
var cancel = false
|
||||
if (url.isBaseUrlInvalid()) {
|
||||
cancel = true
|
||||
binding.urlView.error = getString(R.string.login_url_problem)
|
||||
inValidCount++
|
||||
if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
|
||||
if (inValidCount == 3) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||
@@ -245,7 +221,6 @@ class LoginActivity :
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, focusView)
|
||||
return cancel
|
||||
}
|
||||
|
||||
private fun maybeCancelAndFocusView(
|
||||
@@ -298,7 +273,7 @@ class LoginActivity :
|
||||
return when (item.itemId) {
|
||||
R.id.issue_tracker -> {
|
||||
val browserIntent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL))
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.TRACKER_URL))
|
||||
startActivity(browserIntent)
|
||||
return true
|
||||
}
|
||||
@@ -308,10 +283,9 @@ class LoginActivity :
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutSpecial2("Bug reports")
|
||||
.withAboutSpecial2Description(AppSettingsService.BUG_URL)
|
||||
.withAboutSpecial2Description(AppSettingsService.TRACKER_URL)
|
||||
.withAboutSpecial1("Project Page")
|
||||
.withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
|
||||
.withShowLoadingProgress(false)
|
||||
.start(this)
|
||||
true
|
||||
}
|
||||
|
@@ -10,16 +10,18 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
|
||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
|
||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
|
||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||
import bou.amine.apps.readerforselfossv2.di.networkModule
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
|
||||
import com.github.ln_12.library.ConnectivityStatus
|
||||
import io.github.aakira.napier.DebugAntilog
|
||||
import io.github.aakira.napier.Napier
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
@@ -42,21 +44,26 @@ class MyApp :
|
||||
import(networkModule)
|
||||
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
|
||||
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
|
||||
bind<ConnectivityService>() with singleton { ConnectivityService() }
|
||||
bind<Repository>() with
|
||||
singleton {
|
||||
Repository(
|
||||
instance(),
|
||||
instance(),
|
||||
instance(),
|
||||
isConnectionAvailable,
|
||||
instance(),
|
||||
)
|
||||
}
|
||||
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
|
||||
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
|
||||
}
|
||||
|
||||
private val repository: Repository by instance()
|
||||
private val viewModel: AppViewModel by instance()
|
||||
private val connectivityStatus: ConnectivityStatus by instance()
|
||||
private val driverFactory: DriverFactory by instance()
|
||||
private val connectivityService: ConnectivityService by instance()
|
||||
|
||||
// TODO: handle with the "previous" way
|
||||
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -69,12 +76,13 @@ class MyApp :
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(
|
||||
AppLifeCycleObserver(
|
||||
connectivityService,
|
||||
connectivityStatus,
|
||||
repository,
|
||||
),
|
||||
)
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
connectivityService.networkAvailableProvider.collect { networkAvailable ->
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage =
|
||||
if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
@@ -82,14 +90,13 @@ class MyApp :
|
||||
} else {
|
||||
R.string.network_connectivity_lost
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
Toast
|
||||
.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +108,6 @@ class MyApp :
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
sendReportsInDevMode = false
|
||||
reportFormat = StringFormat.JSON
|
||||
reportContent =
|
||||
listOf(
|
||||
@@ -154,7 +160,7 @@ class MyApp :
|
||||
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val newItemsChannelmChannel =
|
||||
NotificationChannel(
|
||||
AppSettingsService.NEW_ITEMS_CHANNEL,
|
||||
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
|
||||
newItemsChannelname,
|
||||
newItemsChannelimportance,
|
||||
)
|
||||
@@ -181,15 +187,18 @@ class MyApp :
|
||||
}
|
||||
|
||||
class AppLifeCycleObserver(
|
||||
val connectivityService: ConnectivityService,
|
||||
val connectivityStatus: ConnectivityStatus,
|
||||
val repository: Repository,
|
||||
) : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
connectivityService.start()
|
||||
repository.connectionMonitored = true
|
||||
connectivityStatus.start()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
connectivityService.stop()
|
||||
repository.connectionMonitored = false
|
||||
connectivityStatus.stop()
|
||||
super.onPause(owner)
|
||||
}
|
||||
}
|
||||
|
@@ -22,12 +22,10 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class ReaderActivity :
|
||||
AppCompatActivity(),
|
||||
DIAware {
|
||||
class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
private var currentItem: Int = 0
|
||||
|
||||
private var toolbarMenu: Menu? = null
|
||||
private lateinit var toolbarMenu: Menu
|
||||
|
||||
private lateinit var binding: ActivityReaderBinding
|
||||
|
||||
@@ -37,7 +35,22 @@ class ReaderActivity :
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||
if (willAddToFavorite) {
|
||||
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
|
||||
} else {
|
||||
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canFavorite() {
|
||||
showMenuItem(true)
|
||||
}
|
||||
|
||||
private fun canRemoveFromFavorite() {
|
||||
showMenuItem(false)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityReaderBinding.inflate(layoutInflater)
|
||||
@@ -57,21 +70,14 @@ class ReaderActivity :
|
||||
finish()
|
||||
}
|
||||
|
||||
readItem()
|
||||
try {
|
||||
readItem(allItems[currentItem])
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.pager.adapter = ScreenSlidePagerAdapter(this)
|
||||
binding.pager.setCurrentItem(currentItem, false)
|
||||
|
||||
binding.pager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
currentItem = position
|
||||
updateStarIcon()
|
||||
readItem()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -80,30 +86,21 @@ class ReaderActivity :
|
||||
binding.indicator.setViewPager(binding.pager)
|
||||
}
|
||||
|
||||
private fun readItem() {
|
||||
val item = allItems.getOrNull(currentItem)
|
||||
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess() && item != null) {
|
||||
private fun readItem(item: SelfossModel.Item) {
|
||||
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStarIcon() {
|
||||
if (toolbarMenu != null) {
|
||||
val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
|
||||
toolbarMenu!!.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(oldInstanceState: Bundle) {
|
||||
super.onSaveInstanceState(oldInstanceState)
|
||||
oldInstanceState.clear()
|
||||
}
|
||||
|
||||
private inner class ScreenSlidePagerAdapter(
|
||||
fa: FragmentActivity,
|
||||
) : FragmentStateAdapter(fa) {
|
||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
|
||||
FragmentStateAdapter(fa) {
|
||||
override fun getItemCount(): Int = allItems.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
|
||||
@@ -112,8 +109,8 @@ class ReaderActivity :
|
||||
override fun onKeyDown(
|
||||
keyCode: Int,
|
||||
event: KeyEvent?,
|
||||
): Boolean =
|
||||
when (keyCode) {
|
||||
): Boolean {
|
||||
return when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
val currentFragment =
|
||||
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
|
||||
@@ -132,17 +129,17 @@ class ReaderActivity :
|
||||
super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun alignmentMenu() {
|
||||
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
|
||||
if (toolbarMenu != null) {
|
||||
toolbarMenu!!.findItem(R.id.align_left).isVisible = !showJustify
|
||||
toolbarMenu!!.findItem(R.id.align_justify).isVisible = showJustify
|
||||
}
|
||||
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
|
||||
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.reader_menu, menu)
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.reader_menu, menu)
|
||||
toolbarMenu = menu
|
||||
|
||||
alignmentMenu()
|
||||
@@ -150,50 +147,85 @@ class ReaderActivity :
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
menu.removeItem(R.id.star)
|
||||
} else {
|
||||
updateStarIcon()
|
||||
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
|
||||
binding.pager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
readItem(allItems[position])
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
fun afterSave() {
|
||||
allItems[binding.pager.currentItem] =
|
||||
allItems[binding.pager.currentItem].toggleStar()
|
||||
canRemoveFromFavorite()
|
||||
}
|
||||
|
||||
fun afterUnsave() {
|
||||
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
|
||||
canFavorite()
|
||||
}
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressedDispatcher.onBackPressed()
|
||||
R.id.star -> toggleFavorite()
|
||||
R.id.align_left -> switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
|
||||
R.id.align_justify -> switchAlignmentSetting(AppSettingsService.JUSTIFY)
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.star -> {
|
||||
if (allItems[binding.pager.currentItem].starred) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unstarr(allItems[binding.pager.currentItem])
|
||||
}
|
||||
afterUnsave()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.starr(allItems[binding.pager.currentItem])
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.align_left -> {
|
||||
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
|
||||
refreshFragment()
|
||||
}
|
||||
|
||||
R.id.align_justify -> {
|
||||
switchAlignmentSetting(AppSettingsService.JUSTIFY)
|
||||
refreshFragment()
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun toggleFavorite() {
|
||||
val item = allItems.getOrNull(currentItem) ?: return
|
||||
|
||||
val starred = item.starred
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (starred) {
|
||||
repository.unstarr(item)
|
||||
} else {
|
||||
repository.starr(item)
|
||||
}
|
||||
}
|
||||
|
||||
item.toggleStar()
|
||||
updateStarIcon()
|
||||
private fun switchAlignmentSetting(allignment: Int) {
|
||||
appSettingsService.changeAllignment(allignment)
|
||||
alignmentMenu()
|
||||
}
|
||||
|
||||
private fun switchAlignmentSetting(alignment: Int) {
|
||||
appSettingsService.changeAllignment(alignment)
|
||||
alignmentMenu()
|
||||
|
||||
val fragmentManager = supportFragmentManager
|
||||
val fragments = fragmentManager.fragments
|
||||
|
||||
for (fragment in fragments) {
|
||||
if (fragment is ArticleFragment) {
|
||||
fragment.refreshAlignment()
|
||||
}
|
||||
}
|
||||
private fun refreshFragment() {
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
startActivity(intent)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
|
@@ -18,9 +18,7 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class SourcesActivity :
|
||||
AppCompatActivity(),
|
||||
DIAware {
|
||||
class SourcesActivity : AppCompatActivity(), DIAware {
|
||||
private lateinit var binding: ActivitySourcesBinding
|
||||
|
||||
override val di by closestDI()
|
||||
@@ -50,7 +48,6 @@ class SourcesActivity :
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
val mLayoutManager = LinearLayoutManager(this)
|
||||
|
||||
var items: ArrayList<SelfossModel.SourceDetail>
|
||||
@@ -58,28 +55,24 @@ class SourcesActivity :
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = mLayoutManager
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val response = repository.getSourcesDetails()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (response.isNotEmpty()) {
|
||||
items = response
|
||||
val mAdapter =
|
||||
SourcesListAdapter(
|
||||
this@SourcesActivity,
|
||||
items,
|
||||
)
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
mAdapter.notifyDataSetChanged()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.cant_get_sources,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
if (response.isNotEmpty()) {
|
||||
items = response
|
||||
val mAdapter =
|
||||
SourcesListAdapter(
|
||||
this@SourcesActivity,
|
||||
items,
|
||||
)
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
mAdapter.notifyDataSetChanged()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.cant_get_sources,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
|
@@ -9,10 +9,11 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
|
||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -20,9 +21,7 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class UpsertSourceActivity :
|
||||
AppCompatActivity(),
|
||||
DIAware {
|
||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
||||
private var existingSource: SelfossModel.SourceDetail? = null
|
||||
private var mSpoutsValue: String? = null
|
||||
|
||||
@@ -30,6 +29,7 @@ class UpsertSourceActivity :
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -74,10 +74,15 @@ class UpsertSourceActivity :
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
handleSpoutsSpinner()
|
||||
|
||||
val baseUrl = appSettingsService.getBaseUrl()
|
||||
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
|
||||
mustLoginToAddSource()
|
||||
} else {
|
||||
handleSpoutsSpinner()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun handleSpoutsSpinner() {
|
||||
val spoutsKV = HashMap<String, String>()
|
||||
binding.spoutsSpinner.onItemSelectedListener =
|
||||
@@ -100,51 +105,44 @@ class UpsertSourceActivity :
|
||||
}
|
||||
|
||||
fun handleSpoutFailure(networkIssue: Boolean = false) {
|
||||
Toast
|
||||
.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
Toast.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
binding.progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val items = repository.getSpouts()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (items.isNotEmpty()) {
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.formContainer.visibility = View.VISIBLE
|
||||
|
||||
val spinnerArrayAdapter =
|
||||
ArrayAdapter(
|
||||
this@UpsertSourceActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
itemsStrings,
|
||||
)
|
||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spoutsSpinner.adapter = spinnerArrayAdapter
|
||||
|
||||
if (existingSource != null) {
|
||||
initFields(items)
|
||||
}
|
||||
} else {
|
||||
handleSpoutFailure()
|
||||
if (items.isNotEmpty()) {
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.formContainer.visibility = View.VISIBLE
|
||||
|
||||
val spinnerArrayAdapter =
|
||||
ArrayAdapter(
|
||||
this@UpsertSourceActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
itemsStrings,
|
||||
)
|
||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spoutsSpinner.adapter = spinnerArrayAdapter
|
||||
|
||||
if (existingSource != null) {
|
||||
initFields(items)
|
||||
}
|
||||
} else {
|
||||
handleSpoutFailure()
|
||||
}
|
||||
} catch (e: NetworkUnavailableException) {
|
||||
handleSpoutFailure(networkIssue = true)
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +153,13 @@ class UpsertSourceActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun mustLoginToAddSource() {
|
||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
||||
val i = Intent(this, LoginActivity::class.java)
|
||||
startActivity(i)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleSaveSource() {
|
||||
val url = binding.sourceUri.text.toString()
|
||||
|
||||
@@ -165,10 +170,8 @@ class UpsertSourceActivity :
|
||||
sourceDetailsUnavailable -> {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val successfullyAddedSource =
|
||||
if (existingSource != null) {
|
||||
repository.updateSource(
|
||||
@@ -186,21 +189,15 @@ class UpsertSourceActivity :
|
||||
binding.tags.text.toString(),
|
||||
)
|
||||
}
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (successfullyAddedSource) {
|
||||
finish()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
if (successfullyAddedSource) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ import org.kodein.di.instance
|
||||
|
||||
class ItemCardAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
override val items: ArrayList<SelfossModel.Item>,
|
||||
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
|
||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
|
||||
override lateinit var binding: CardItemBinding
|
||||
@@ -118,18 +118,16 @@ class ItemCardAdapter(
|
||||
binding.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
binding.itemImage.visibility = View.VISIBLE
|
||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: CardItemBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ import org.kodein.di.instance
|
||||
|
||||
class ItemListAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
override val items: ArrayList<SelfossModel.Item>,
|
||||
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
|
||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
|
||||
override lateinit var binding: ListItemBinding
|
||||
@@ -65,15 +65,13 @@ class ItemListAdapter(
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
} else {
|
||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ListItemBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root)
|
||||
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,7 +21,7 @@ import org.kodein.di.DIAware
|
||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
|
||||
RecyclerView.Adapter<VH>(),
|
||||
DIAware {
|
||||
abstract var items: ArrayList<SelfossModel.Item>
|
||||
abstract val items: ArrayList<SelfossModel.Item>
|
||||
abstract val repository: Repository
|
||||
abstract val binding: ViewBinding
|
||||
abstract val appSettingsService: AppSettingsService
|
||||
@@ -31,7 +31,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
|
||||
protected val c: Context get() = app.baseContext
|
||||
|
||||
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
|
||||
this.items = items
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
updateHomeItems(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
@@ -6,17 +6,16 @@ import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -30,23 +29,68 @@ import org.kodein.di.instance
|
||||
class SourcesListAdapter(
|
||||
private val app: Activity,
|
||||
private val items: ArrayList<SelfossModel.SourceDetail>,
|
||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
|
||||
DIAware {
|
||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
|
||||
private val c: Context = app.baseContext
|
||||
private lateinit var binding: SourceListItemBinding
|
||||
|
||||
override val di: DI by closestDI(app)
|
||||
private val repository: Repository by instance()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder {
|
||||
val binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.bind(items[position], position)
|
||||
val itm = items[position]
|
||||
|
||||
val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
val (id, title) = items[position]
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val successfullyDeletedSource = repository.deleteSource(id, title)
|
||||
if (successfullyDeletedSource) {
|
||||
items.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.mView.setOnClickListener {
|
||||
val source = items[position]
|
||||
|
||||
repository.setSelectedSource(source)
|
||||
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
|
||||
}
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
|
||||
if (!itm.error.isNullOrBlank()) {
|
||||
binding.errorText.visibility = View.VISIBLE
|
||||
binding.errorText.text = itm.error
|
||||
} else {
|
||||
binding.errorText.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.sourceTitle.text = itm.title.getHtmlDecoded()
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
@@ -55,77 +99,5 @@ class SourcesListAdapter(
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: SourceListItemBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
private val context: Context = app.applicationContext
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
fun bind(
|
||||
source: SelfossModel.SourceDetail,
|
||||
position: Int,
|
||||
) {
|
||||
binding.apply {
|
||||
sourceTitle.text = source.title.getHtmlDecoded()
|
||||
if (source.getIcon(repository.baseUrl).isEmpty()) {
|
||||
itemImage.setBackgroundAndText(source.title.getHtmlDecoded())
|
||||
} else {
|
||||
context.circularDrawable(source.getIcon(repository.baseUrl), itemImage, appSettingsService)
|
||||
}
|
||||
|
||||
errorText.apply {
|
||||
visibility = if (!source.error.isNullOrBlank()) View.VISIBLE else View.GONE
|
||||
text = source.error
|
||||
}
|
||||
|
||||
deleteBtn.setOnClickListener { showDeleteConfirmationDialog(source, position) }
|
||||
|
||||
root.setOnClickListener {
|
||||
repository.setSelectedSource(source)
|
||||
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteConfirmationDialog(
|
||||
source: SelfossModel.SourceDetail,
|
||||
position: Int,
|
||||
) {
|
||||
AlertDialog
|
||||
.Builder(app)
|
||||
.setTitle(app.getString(R.string.confirm_delete_title))
|
||||
.setMessage(app.getString(R.string.confirm_delete_message, source.title))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> deleteSource(source, position) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun deleteSource(
|
||||
source: SelfossModel.SourceDetail,
|
||||
position: Int,
|
||||
) {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val successfullyDeletedSource = repository.deleteSource(source.id, source.title)
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
launch(Dispatchers.Main) {
|
||||
if (successfullyDeletedSource) {
|
||||
items.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, itemCount)
|
||||
} else {
|
||||
Toast
|
||||
.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
}
|
||||
}
|
||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
|
||||
}
|
||||
|
@@ -26,8 +26,6 @@ import org.kodein.di.instance
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
private const val NOTIFICATION_DELAY = 4000L
|
||||
|
||||
class LoadingWorker(
|
||||
val context: Context,
|
||||
params: WorkerParameters,
|
||||
@@ -63,7 +61,7 @@ class LoadingWorker(
|
||||
handleNewItemsNotification(apiItems, notificationManager)
|
||||
}
|
||||
}
|
||||
apiItems.map { it.preloadImages(context, appSettingsService) }
|
||||
apiItems.map { it.preloadImages(context) }
|
||||
}
|
||||
}
|
||||
return Result.success()
|
||||
@@ -95,7 +93,7 @@ class LoadingWorker(
|
||||
NotificationCompat
|
||||
.Builder(
|
||||
applicationContext,
|
||||
AppSettingsService.NEW_ITEMS_CHANNEL,
|
||||
AppSettingsService.NEW_ITEMS_CHANNEL_ID,
|
||||
).setContentTitle(context.getString(R.string.new_items_notification_title))
|
||||
.setContentText(
|
||||
context.getString(
|
||||
@@ -103,16 +101,16 @@ class LoadingWorker(
|
||||
newSize,
|
||||
),
|
||||
).setPriority(PRIORITY_DEFAULT)
|
||||
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL)
|
||||
.setChannelId(AppSettingsService.NEW_ITEMS_CHANNEL_ID)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||
|
||||
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.notify(2, newItemsNotification.build())
|
||||
}
|
||||
}
|
||||
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.cancel(1)
|
||||
}
|
||||
}
|
||||
|
@@ -2,14 +2,18 @@ package bou.amine.apps.readerforselfossv2.android.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.util.TypedValue.DATA_NULL_UNDEFINED
|
||||
import android.view.GestureDetector
|
||||
import android.view.InflateException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -19,6 +23,7 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
@@ -27,27 +32,25 @@ import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toModel
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.addHomeMadeActionItem
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.getColorFromAttr
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||
import bou.amine.apps.readerforselfossv2.model.MercuryModel
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -62,38 +65,28 @@ import java.util.Locale
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
private const val IMAGE_JPG = "image/jpg"
|
||||
private const val IMAGE_PNG = "image/png"
|
||||
private const val IMAGE_WEBP = "image/webp"
|
||||
|
||||
private const val WHITE_COLOR_HEX = 0xFFFFFF
|
||||
|
||||
private const val DEFAULT_FONT_SIZE = 16
|
||||
|
||||
class ArticleFragment :
|
||||
Fragment(),
|
||||
DIAware {
|
||||
private var colorOnSurface: Int = 0
|
||||
private var colorSurface: Int = 0
|
||||
private var fontSize: Int = DEFAULT_FONT_SIZE
|
||||
class ArticleFragment : Fragment(), DIAware {
|
||||
private var fontSize: Int = 16
|
||||
private lateinit var item: SelfossModel.Item
|
||||
private var url: String? = null
|
||||
private lateinit var url: String
|
||||
private lateinit var contentText: String
|
||||
private lateinit var contentSource: String
|
||||
private lateinit var contentImage: String
|
||||
private lateinit var contentTitle: String
|
||||
private lateinit var allImages: ArrayList<String>
|
||||
private lateinit var fab: SpeedDialView
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var textAlignment: String
|
||||
private lateinit var binding: FragmentArticleBinding
|
||||
|
||||
override val di: DI by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
private val connectivityService: ConnectivityService by instance()
|
||||
|
||||
private var typeface: Typeface? = null
|
||||
private var resId: Int = 0
|
||||
private var font = ""
|
||||
private var staticBar = false
|
||||
|
||||
private val mercuryApi: MercuryApi by instance()
|
||||
|
||||
@@ -105,7 +98,6 @@ class ArticleFragment :
|
||||
item = pi.toModel()
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -120,9 +112,6 @@ class ArticleFragment :
|
||||
e.sendSilentlyWithAcra()
|
||||
}
|
||||
|
||||
colorOnSurface = getColorFromAttr(com.google.android.material.R.attr.colorOnSurface)
|
||||
colorSurface = getColorFromAttr(com.google.android.material.R.attr.colorSurface)
|
||||
|
||||
contentText = item.content
|
||||
contentTitle = item.title.getHtmlDecoded()
|
||||
contentImage = item.getThumbnail(repository.baseUrl)
|
||||
@@ -136,11 +125,23 @@ class ArticleFragment :
|
||||
allImages = item.getImages()
|
||||
|
||||
fontSize = appSettingsService.getFontSize()
|
||||
staticBar = appSettingsService.isStaticBarEnabled()
|
||||
font = appSettingsService.getFont()
|
||||
|
||||
refreshAlignment()
|
||||
|
||||
handleFloatingToolbar()
|
||||
fab = binding.fab
|
||||
|
||||
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
|
||||
|
||||
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
|
||||
|
||||
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
|
||||
|
||||
if (staticBar) {
|
||||
fab.hide()
|
||||
floatingToolbar.show()
|
||||
}
|
||||
|
||||
binding.source.text = contentSource
|
||||
if (typeface != null) {
|
||||
@@ -148,20 +149,37 @@ class ArticleFragment :
|
||||
}
|
||||
|
||||
handleContent()
|
||||
|
||||
binding.nestedScrollView.setOnScrollChangeListener(
|
||||
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
if (scrollY > oldScrollY) {
|
||||
floatingToolbar.hide()
|
||||
fab.hide()
|
||||
} else {
|
||||
if (staticBar) {
|
||||
floatingToolbar.show()
|
||||
} else {
|
||||
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (e: InflateException) {
|
||||
e.sendSilentlyWithAcraWithName("webview not available")
|
||||
maybeIfContext {
|
||||
AlertDialog
|
||||
.Builder(it)
|
||||
.setMessage(it.getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(it.getString(R.string.webview_dialog_issue_title))
|
||||
try {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
) { _, _ ->
|
||||
appSettingsService.disableArticleViewer()
|
||||
requireActivity().finish()
|
||||
}.create()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +188,8 @@ class ArticleFragment :
|
||||
|
||||
private fun handleContent() {
|
||||
if (contentText.isEmptyOrNullOrNullString()) {
|
||||
if (connectivityService.isNetworkAvailable() && url.isUrlValid()) {
|
||||
getContentFromMercury(url!!)
|
||||
if (repository.isNetworkAvailable()) {
|
||||
getContentFromMercury()
|
||||
}
|
||||
} else {
|
||||
binding.titleView.text = contentTitle
|
||||
@@ -183,99 +201,82 @@ class ArticleFragment :
|
||||
|
||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) }
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(contentImage)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFloatingToolbar() {
|
||||
fab = binding.speedDial
|
||||
fab.mainFabClosedIconColor = colorOnSurface
|
||||
fab.mainFabOpenedIconColor = colorOnSurface
|
||||
|
||||
maybeIfContext { handleFloatingToolbarActionItems(it) }
|
||||
|
||||
fab.setOnActionSelectedListener { actionItem ->
|
||||
when (actionItem.id) {
|
||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
R.id.unread_action ->
|
||||
if (this@ArticleFragment.item.unread) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = false
|
||||
maybeIfContext {
|
||||
Toast
|
||||
.makeText(
|
||||
it,
|
||||
R.string.marked_as_read,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unmarkAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = true
|
||||
maybeIfContext {
|
||||
Toast
|
||||
.makeText(
|
||||
it,
|
||||
R.string.marked_as_unread,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
false
|
||||
private fun handleFloatingToolbar(): FloatingToolbar {
|
||||
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
|
||||
}
|
||||
floatingToolbar.attachFab(fab)
|
||||
|
||||
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
|
||||
|
||||
floatingToolbar.setClickListener(
|
||||
object : FloatingToolbar.ItemClickListener {
|
||||
override fun onItemClick(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
R.id.unread_action ->
|
||||
try {
|
||||
if (this@ArticleFragment.item.unread) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = false
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.marked_as_read,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unmarkAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = true
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_unread,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MenuItem?) {
|
||||
// We do nothing
|
||||
}
|
||||
},
|
||||
)
|
||||
return floatingToolbar
|
||||
}
|
||||
|
||||
private fun handleFloatingToolbarActionItems(c: Context) {
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.share_action,
|
||||
resources.getDrawable(R.drawable.ic_share_white_24dp),
|
||||
R.string.reader_action_share,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.open_action,
|
||||
resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
|
||||
R.string.reader_action_open,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.unread_action,
|
||||
resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
|
||||
R.string.unmark,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
}
|
||||
|
||||
fun refreshAlignment() {
|
||||
private fun refreshAlignment() {
|
||||
textAlignment =
|
||||
when (appSettingsService.getActiveAllignment()) {
|
||||
1 -> "justify"
|
||||
2 -> "left"
|
||||
else -> "justify"
|
||||
}
|
||||
|
||||
htmlToWebview()
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun getContentFromMercury(url: String) {
|
||||
private fun getContentFromMercury() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
@@ -313,12 +314,17 @@ class ArticleFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeadImage(leadImageUrl: String?) {
|
||||
if (!leadImageUrl.isNullOrEmpty()) {
|
||||
maybeIfContext {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
|
||||
}
|
||||
private fun handleLeadImage(lead_image_url: String?) {
|
||||
if (!lead_image_url.isNullOrEmpty() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(
|
||||
lead_image_url,
|
||||
)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
@@ -331,85 +337,125 @@ class ArticleFragment :
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
url: String,
|
||||
): Boolean =
|
||||
if (url.isUrlValid() &&
|
||||
): Boolean {
|
||||
return if (context != null &&
|
||||
url.isUrlValid() &&
|
||||
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||
) {
|
||||
maybeIfContext { it.openUrlInBrowserAsNewTask(url) }
|
||||
requireContext().openUrlInBrowser(url)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String,
|
||||
): WebResourceResponse? {
|
||||
val (mime: String?, compression: Bitmap.CompressFormat) =
|
||||
if (url
|
||||
.lowercase(Locale.US)
|
||||
.contains(".jpg") ||
|
||||
url.lowercase(Locale.US).contains(".jpeg")
|
||||
) {
|
||||
Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG)
|
||||
} else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG)
|
||||
} else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP)
|
||||
} else {
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
if (url.lowercase(Locale.US).contains(".jpg") ||
|
||||
url.lowercase(Locale.US)
|
||||
.contains(".jpeg")
|
||||
) {
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
|
||||
.get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
|
||||
.get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit()
|
||||
.get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
val image = view.getGlideImageForResource(url, appSettingsService)
|
||||
return WebResourceResponse(
|
||||
mime,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, compression),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
|
||||
private fun htmlToWebview() {
|
||||
maybeIfContext {
|
||||
val context: Context
|
||||
try {
|
||||
context = requireContext()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
return
|
||||
}
|
||||
|
||||
val colorOnSurface = TypedValue()
|
||||
val colorSurface = TypedValue()
|
||||
|
||||
try {
|
||||
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||
val a: TypedArray = it.obtainStyledAttributes(resId, attrs)
|
||||
val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
|
||||
|
||||
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||
""
|
||||
binding.webcontent.visibility = View.VISIBLE
|
||||
|
||||
context.theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
|
||||
|
||||
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
|
||||
}
|
||||
binding.webcontent.visibility = View.VISIBLE
|
||||
|
||||
val colorSurfaceString =
|
||||
String.format(
|
||||
"#%06X",
|
||||
WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
|
||||
0xFFFFFF and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else 0xFFFFFF),
|
||||
)
|
||||
|
||||
val colorOnSurfaceString =
|
||||
String.format(
|
||||
"#%06X",
|
||||
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
|
||||
0xFFFFFF and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
|
||||
)
|
||||
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
handleImageLoading()
|
||||
try {
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
handleImageLoading()
|
||||
|
||||
val gestureDetector =
|
||||
GestureDetector(
|
||||
activity,
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -418,50 +464,49 @@ class ArticleFragment :
|
||||
event,
|
||||
)
|
||||
}
|
||||
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Gesture detector issue ?")
|
||||
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
|
||||
return
|
||||
}
|
||||
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
|
||||
var baseUrl: String? = null
|
||||
try {
|
||||
val itemUrl = URL(url.orEmpty())
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcraWithName("htmlToWebview > ${url.orEmpty()}")
|
||||
}
|
||||
var baseUrl: String? = null
|
||||
try {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
|
||||
}
|
||||
|
||||
val fontName: String =
|
||||
maybeIfContext {
|
||||
val fontName =
|
||||
when (font) {
|
||||
it.getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||
it.getString(R.string.roboto_font_id) -> "Roboto"
|
||||
it.getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
||||
getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||
getString(R.string.roboto_font_id) -> "Roboto"
|
||||
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
||||
else -> ""
|
||||
}
|
||||
}?.toString().orEmpty()
|
||||
|
||||
val fontLinkAndStyle =
|
||||
if (fontName.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||
fontName.replace(
|
||||
" ",
|
||||
"+",
|
||||
)
|
||||
}" rel="stylesheet">
|
||||
val fontLinkAndStyle =
|
||||
if (font.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||
fontName.replace(
|
||||
" ",
|
||||
"+",
|
||||
)
|
||||
}" rel="stylesheet">
|
||||
|<style>
|
||||
| * {
|
||||
| font-family: '$fontName';
|
||||
| }
|
||||
|</style>
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
try {
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
binding.webcontent.loadDataWithBaseURL(
|
||||
baseUrl,
|
||||
"""<html>
|
||||
@@ -478,7 +523,7 @@ class ArticleFragment :
|
||||
| color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int),
|
||||
0xFFFFFF and context.resources.getColor(R.color.colorAccent),
|
||||
)
|
||||
} !important;
|
||||
| }
|
||||
@@ -535,8 +580,10 @@ class ArticleFragment :
|
||||
|
||||
private fun openInBrowserAfterFailing() {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
maybeIfContext {
|
||||
it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
try {
|
||||
requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,8 +600,7 @@ class ArticleFragment :
|
||||
}
|
||||
|
||||
fun performClick(): Boolean {
|
||||
if (allImages != null &&
|
||||
(
|
||||
if (allImages != null && (
|
||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||
)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
@@ -14,15 +15,12 @@ import android.view.ViewGroup
|
||||
import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.ViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
@@ -35,15 +33,10 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
private const val DRAWABLE_SIZE = 30
|
||||
|
||||
class FilterSheetFragment :
|
||||
BottomSheetDialogFragment(),
|
||||
DIAware {
|
||||
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
||||
private lateinit var binding: FilterFragmentBinding
|
||||
override val di: DI by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
private var selectedChip: Chip? = null
|
||||
|
||||
@@ -60,14 +53,12 @@ class FilterSheetFragment :
|
||||
)
|
||||
|
||||
try {
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
handleTagChips()
|
||||
handleSourceChips()
|
||||
handleTagChips(requireContext())
|
||||
handleSourceChips(requireContext())
|
||||
|
||||
binding.progressBar2.visibility = GONE
|
||||
binding.filterView.visibility = VISIBLE
|
||||
CountingIdlingResourceSingleton.decrement()
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
dismiss()
|
||||
@@ -82,24 +73,16 @@ class FilterSheetFragment :
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private suspend fun handleSourceChips() {
|
||||
private suspend fun handleSourceChips(context: Context) {
|
||||
val sourceGroup = binding.sourcesGroup
|
||||
|
||||
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
|
||||
val c: Chip? =
|
||||
maybeIfContext {
|
||||
Chip(it)
|
||||
} as Chip?
|
||||
|
||||
if (c == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val c = Chip(context)
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
|
||||
maybeIfContext {
|
||||
it.imageIntoViewTarget(
|
||||
source.getIcon(repository.baseUrl),
|
||||
Glide.with(context)
|
||||
.load(source.getIcon(repository.baseUrl))
|
||||
.into(
|
||||
object : ViewTarget<Chip?, Drawable?>(c) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
@@ -112,9 +95,7 @@ class FilterSheetFragment :
|
||||
}
|
||||
}
|
||||
},
|
||||
appSettingsService,
|
||||
)
|
||||
}
|
||||
|
||||
c.text = source.title.getHtmlDecoded()
|
||||
|
||||
@@ -150,17 +131,13 @@ class FilterSheetFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTagChips() {
|
||||
private suspend fun handleTagChips(context: Context) {
|
||||
val tagGroup = binding.tagsGroup
|
||||
|
||||
val tags = repository.getTags()
|
||||
|
||||
tags.forEachIndexed { _, tag ->
|
||||
val c: Chip? = maybeIfContext { Chip(it) } as Chip?
|
||||
if (c == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val c = Chip(context)
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
c.text = tag.tag
|
||||
|
||||
@@ -176,8 +153,8 @@ class FilterSheetFragment :
|
||||
}
|
||||
gd.setColor(gdColor)
|
||||
gd.shape = GradientDrawable.RECTANGLE
|
||||
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
|
||||
gd.cornerRadius = DRAWABLE_SIZE.toFloat()
|
||||
gd.setSize(30, 30)
|
||||
gd.cornerRadius = 30F
|
||||
c.chipIcon = gd
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
|
||||
|
@@ -6,21 +6,15 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.instance
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
||||
class ImageFragment :
|
||||
Fragment(),
|
||||
DIAware {
|
||||
override val di: DI by closestDI()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
class ImageFragment : Fragment() {
|
||||
private lateinit var imageUrl: String
|
||||
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
private var _binding: FragmentImageBinding? = null
|
||||
val binding get() = _binding
|
||||
private val binding get() = _binding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -37,7 +31,11 @@ class ImageFragment :
|
||||
val view = binding?.root
|
||||
|
||||
binding!!.photoView.visibility = View.VISIBLE
|
||||
requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService)
|
||||
Glide.with(requireActivity())
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(imageUrl)
|
||||
.into(binding!!.photoView)
|
||||
|
||||
return view
|
||||
}
|
||||
|
@@ -3,21 +3,23 @@ package bou.amine.apps.readerforselfossv2.android.model
|
||||
import android.content.Context
|
||||
import android.webkit.URLUtil
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
||||
fun SelfossModel.Item.preloadImages(
|
||||
context: Context,
|
||||
appSettingsService: AppSettingsService,
|
||||
): Boolean {
|
||||
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
|
||||
val imageUrls = this.getImages()
|
||||
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
|
||||
|
||||
try {
|
||||
for (url in imageUrls) {
|
||||
if (URLUtil.isValidUrl(url)) {
|
||||
context.preloadImage(url, appSettingsService)
|
||||
Glide.with(context).asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(url).submit()
|
||||
}
|
||||
}
|
||||
} catch (e: Error) {
|
||||
|
@@ -26,10 +26,6 @@ import org.kodein.di.android.closestDI
|
||||
|
||||
private const val TITLE_TAG = "settingsActivityTitle"
|
||||
|
||||
const val MAX_ITEMS_NUMBER = 200
|
||||
|
||||
private const val MIN_ITEMS_NUMBER = 1
|
||||
|
||||
class SettingsActivity :
|
||||
AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||
@@ -124,7 +120,6 @@ class SettingsActivity :
|
||||
LibsBuilder()
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withShowLoadingProgress(false)
|
||||
.start(it)
|
||||
}
|
||||
true
|
||||
@@ -148,7 +143,7 @@ class SettingsActivity :
|
||||
InputFilter { source, _, _, dest, _, _ ->
|
||||
try {
|
||||
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) {
|
||||
Toast
|
||||
.makeText(
|
||||
@@ -241,7 +236,7 @@ class SettingsActivity :
|
||||
|
||||
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
openUrl(AppSettingsService.BUG_URL)
|
||||
openUrl(AppSettingsService.TRACKER_URL)
|
||||
true
|
||||
}
|
||||
|
||||
|
@@ -2,60 +2,23 @@ package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
|
||||
fun Context.shareLink(
|
||||
itemUrl: String?,
|
||||
itemUrl: String,
|
||||
itemTitle: String,
|
||||
) {
|
||||
if (itemUrl.isUrlValid()) {
|
||||
val sendIntent = Intent()
|
||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl!!.toStringUriWithHttp())
|
||||
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
|
||||
sendIntent.type = "text/plain"
|
||||
startActivity(
|
||||
Intent
|
||||
.createChooser(
|
||||
sendIntent,
|
||||
getString(R.string.share),
|
||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Fragment.getColorFromAttr(
|
||||
@AttrRes attrColor: Int,
|
||||
resolveRefs: Boolean = true,
|
||||
): Int {
|
||||
val typedValue = TypedValue()
|
||||
maybeIfContextWithLog { this.requireContext().theme.resolveAttribute(attrColor, typedValue, resolveRefs) }
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
fun Fragment.maybeIfContext(fn: (Context) -> Any): Any? {
|
||||
try {
|
||||
return fn(this.requireContext())
|
||||
} catch (e: Exception) {
|
||||
// Do nothing
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.maybeIfContextWithLog(fn: (Context) -> Any): Any? {
|
||||
try {
|
||||
return fn(this.requireContext())
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("Fragment context issue...")
|
||||
return null
|
||||
}
|
||||
val sendIntent = Intent()
|
||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
|
||||
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
|
||||
sendIntent.type = "text/plain"
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
getString(R.string.share),
|
||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
|
@@ -59,5 +59,7 @@ class CircleImageView
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
@@ -15,35 +15,33 @@ import android.widget.Toast
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.ReaderActivity
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
fun Context.openItemUrl(
|
||||
currentItem: Int,
|
||||
linkDecoded: String?,
|
||||
linkDecoded: String,
|
||||
articleViewer: Boolean,
|
||||
app: Activity,
|
||||
) {
|
||||
if (!linkDecoded.isUrlValid()) {
|
||||
Toast
|
||||
.makeText(
|
||||
this,
|
||||
this.getString(R.string.cant_open_invalid_url),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
Toast.makeText(
|
||||
this,
|
||||
this.getString(R.string.cant_open_invalid_url),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
} else {
|
||||
if (articleViewer) {
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
} else {
|
||||
this.openUrlInBrowserAsNewTask(linkDecoded!!)
|
||||
this.openUrlInBrowserAsNewTask(linkDecoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.isUrlValid(): Boolean =
|
||||
!this.isEmptyOrNullOrNullString() && this!!.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
|
||||
fun String.isBaseUrlInvalid(): Boolean {
|
||||
val baseUrl = this.toHttpUrlOrNull()
|
||||
@@ -57,16 +55,14 @@ fun String.isBaseUrlInvalid(): Boolean {
|
||||
}
|
||||
|
||||
fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
|
||||
this.openUrlInBrowserAsNewTask(i.getLinkDecoded())
|
||||
this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
|
||||
}
|
||||
|
||||
fun Context.openUrlInBrowserAsNewTask(url: String?) {
|
||||
if (url.isUrlValid()) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.data = Uri.parse(url)
|
||||
this.mayBeStartActivity(intent)
|
||||
}
|
||||
fun Context.openUrlInBrowserAsNewTask(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.data = Uri.parse(url)
|
||||
this.mayBeStartActivity(intent)
|
||||
}
|
||||
|
||||
fun Context.openUrlInBrowser(url: String) {
|
||||
@@ -75,7 +71,6 @@ fun Context.openUrlInBrowser(url: String) {
|
||||
this.mayBeStartActivity(intent)
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
fun Context.mayBeStartActivity(intent: Intent) {
|
||||
try {
|
||||
this.startActivity(intent)
|
||||
|
@@ -22,5 +22,5 @@ class AcraReportingAdministrator : ReportingAdministrator {
|
||||
context: Context,
|
||||
config: CoreConfiguration,
|
||||
crashReportData: CrashReportData,
|
||||
): Boolean = crashReportData.get("BRAND") != "redroid" && !crashReportData.get("PHONE_MODEL").toString().startsWith("sdk_gphone")
|
||||
): Boolean = crashReportData.get("BRAND") != "redroid"
|
||||
}
|
||||
|
@@ -1,13 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.bottombar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.StringRes
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
|
||||
fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||
this.setText("")
|
||||
@@ -16,25 +9,3 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||
}
|
||||
|
||||
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
|
||||
|
||||
@Suppress("detekt:LongParameterList")
|
||||
fun SpeedDialView.addHomeMadeActionItem(
|
||||
@IdRes actionId: Int,
|
||||
actionIcon: Drawable,
|
||||
@StringRes labelId: Int,
|
||||
colorOnSurface: Int,
|
||||
colorSurface: Int,
|
||||
context: Context,
|
||||
) {
|
||||
this.addActionItem(
|
||||
SpeedDialActionItem
|
||||
.Builder(actionId, actionIcon)
|
||||
.setFabBackgroundColor(context.resources.getColor(R.color.colorAccent))
|
||||
.setFabImageTintColor(colorOnSurface)
|
||||
.setLabel(context.getString(labelId))
|
||||
.setLabelClickable(false)
|
||||
.setLabelBackgroundColor(colorOnSurface)
|
||||
.setLabelColor(colorSurface)
|
||||
.create(),
|
||||
)
|
||||
}
|
||||
|
@@ -2,135 +2,40 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.webkit.WebView
|
||||
import android.widget.ImageView
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.ViewTarget
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
private const val PRELOAD_IMAGE_TIMEOUT = 10000
|
||||
|
||||
@Suppress("detekt:ReturnCount")
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun String.toGlideUrl(appSettingsService: AppSettingsService): Any { // GlideUrl Or String
|
||||
if (this.isEmptyOrNullOrNullString()) {
|
||||
return ""
|
||||
}
|
||||
if (appSettingsService.getBasicUserName().isNotEmpty()) {
|
||||
val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
|
||||
val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))
|
||||
|
||||
return GlideUrl(
|
||||
this,
|
||||
LazyHeaders
|
||||
.Builder()
|
||||
.addHeader("Authorization", "Basic $authBuf")
|
||||
.build(),
|
||||
)
|
||||
} else {
|
||||
return GlideUrl(
|
||||
this,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun WebView.getGlideImageForResource(
|
||||
url: String,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
.asBitmap()
|
||||
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.submit()
|
||||
.get()
|
||||
|
||||
fun Context.preloadImage(
|
||||
url: String,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
.asBitmap()
|
||||
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT))
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.submit()
|
||||
|
||||
fun Context.imageIntoViewTarget(
|
||||
url: String,
|
||||
target: ViewTarget<Chip?, Drawable?>,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.into(target)
|
||||
|
||||
fun Context.bitmapWithCache(
|
||||
url: String,
|
||||
iv: ImageView,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
.asBitmap()
|
||||
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.into(iv)
|
||||
|
||||
fun Context.bitmapCenterCrop(
|
||||
url: String,
|
||||
iv: ImageView,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
) = Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.load(url)
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.bitmapFitCenter(
|
||||
url: String,
|
||||
iv: ImageView,
|
||||
appSettingsService: AppSettingsService,
|
||||
) = Glide
|
||||
.with(this)
|
||||
.asBitmap()
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.circularDrawable(
|
||||
url: String,
|
||||
view: CircleImageView,
|
||||
appSettingsService: AppSettingsService,
|
||||
) {
|
||||
view.textView.text = ""
|
||||
|
||||
Glide
|
||||
.with(this)
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
Glide.with(this)
|
||||
.load(url)
|
||||
.into(view.imageView)
|
||||
}
|
||||
|
||||
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
|
||||
|
||||
fun getBitmapInputStream(
|
||||
bitmap: Bitmap,
|
||||
compressFormat: Bitmap.CompressFormat,
|
||||
): InputStream {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
|
||||
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
|
||||
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
||||
return ByteArrayInputStream(bitmapData)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
lateinit var s: Snackbar
|
||||
@@ -10,13 +11,19 @@ lateinit var s: Snackbar
|
||||
fun isNetworkAccessible(context: Context): Boolean {
|
||||
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 {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
return when {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
val network = connectivityManager.activeNetworkInfo ?: return false
|
||||
return network.isConnectedOrConnecting
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AppViewModel(private val repository: Repository) : ViewModel() {
|
||||
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
|
||||
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
|
||||
private var wasConnected = true
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
repository.isConnectionAvailable.collect { isConnected ->
|
||||
if (repository.connectionMonitored) {
|
||||
if (isConnected && !wasConnected && repository.connectionMonitored) {
|
||||
_networkAvailableProvider.emit(true)
|
||||
wasConnected = true
|
||||
} else if (!isConnected && wasConnected && repository.connectionMonitored) {
|
||||
_networkAvailableProvider.emit(false)
|
||||
wasConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -71,13 +71,35 @@
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.leinardi.android.speeddial.SpeedDialView
|
||||
android:id="@+id/speedDial"
|
||||
android:layout_width="wrap_content"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
|
||||
android:layout_gravity="start|bottom|end"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||
android:id="@+id/floatingToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
app:floatingMenu="@menu/reader_toolbar" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:src="@drawable/ic_add_white_24dp"
|
||||
app:backgroundTint="?attr/colorAccent"
|
||||
app:fabSize="mini"
|
||||
app:rippleColor="?attr/colorAccentDark" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progressBar"
|
||||
@@ -97,5 +119,4 @@
|
||||
android:progressTint="?attr/colorAccent" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
23
androidApp/src/main/res/menu/reader_toolbar.xml
Normal file
23
androidApp/src/main/res/menu/reader_toolbar.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/unread_action"
|
||||
android:icon="@drawable/ic_baseline_white_eye_24dp"
|
||||
android:title="@string/unmark"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_action"
|
||||
android:icon="@drawable/ic_open_in_browser_white_24dp"
|
||||
android:title="@string/reader_action_open"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/share_action"
|
||||
android:icon="@drawable/ic_share_white_24dp"
|
||||
android:title="@string/reader_action_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"No s'han llegit totes les publicacions"</string>
|
||||
<string name="all_posts_read">"S'han llegit totes les publicacions"</string>
|
||||
<string name="undo_string">"Desfés"</string>
|
||||
<string name="addStringNoUrl">"Inicieu la sessió per afegir fonts."</string>
|
||||
<string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string>
|
||||
<string name="cant_create_source">"No es pot crear la font."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Quant a"</string>
|
||||
<string name="marked_as_read">"Element llegit"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Nicht alle Beiträge wurden gelesen"</string>
|
||||
<string name="all_posts_read">"Alle Beiträge wurden gelesen"</string>
|
||||
<string name="undo_string">"Rückgängig"</string>
|
||||
<string name="addStringNoUrl">"Melde dich an um Quellen hinzuzufügen."</string>
|
||||
<string name="cant_get_sources">"Quellen können nicht abgerufen werden."</string>
|
||||
<string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string>
|
||||
<string name="cant_get_spouts_no_network">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Linksbündig</string>
|
||||
<string name="reader_text_align_justify">Blocksatz</string>
|
||||
<string name="settings_reader_font">Schriftgröße im Lesemodus</string>
|
||||
<string name="reader_static_bar_title">Statische untere Leiste im Lesemodus</string>
|
||||
<string name="reader_static_bar_on">Die untere Leiste wird dauerhaft angezeigt</string>
|
||||
<string name="reader_static_bar_off">Die untere Leiste kann über einen schwebenden Button angezeigt werden</string>
|
||||
<string name="remove_source">Quelle entfernen</string>
|
||||
<string name="pref_theme_title">Heller/Dunkler Modus</string>
|
||||
<string name="mode_dark">Dunkler Modus</string>
|
||||
<string name="mode_system">Systemeinstellungen übernehmen</string>
|
||||
<string name="mode_light">Heller Modus</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Über"</string>
|
||||
<string name="marked_as_read">"Artikel gelesen"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"No todas las publicaciones fueron leídas"</string>
|
||||
<string name="all_posts_read">"Todas las publicaciones fueron leídas"</string>
|
||||
<string name="undo_string">"Deshacer"</string>
|
||||
<string name="addStringNoUrl">"Iniciar sesión para añadir fuentes."</string>
|
||||
<string name="cant_get_sources">"No se puede obtener la lista de fuentes."</string>
|
||||
<string name="cant_create_source">"No se puede crear la fuente."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Alinear a la izquierda</string>
|
||||
<string name="reader_text_align_justify">Justificado</string>
|
||||
<string name="settings_reader_font">Modo lectura</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Acerca de"</string>
|
||||
<string name="marked_as_read">"Artículo leído"</string>
|
||||
<string name="marked_as_unread">"Artículo no leído"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Tous les posts n'ont pas été lus"</string>
|
||||
<string name="all_posts_read">"Tous les posts sont lus"</string>
|
||||
<string name="undo_string">"Annuler"</string>
|
||||
<string name="addStringNoUrl">"Identifiez-vous pour ajouter une source."</string>
|
||||
<string name="cant_get_sources">"Impossible de récupérer la liste des sources."</string>
|
||||
<string name="cant_create_source">"Impossible de créer la source."</string>
|
||||
<string name="cant_get_spouts_no_network">"Impossible d'obtenir la liste des spouts en raison d'un problème de réseau."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Aligner à gauche</string>
|
||||
<string name="reader_text_align_justify">Justifier le texte</string>
|
||||
<string name="settings_reader_font">Police du lecteur d\'articles</string>
|
||||
<string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
|
||||
<string name="reader_static_bar_on">La barre sera affichée</string>
|
||||
<string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
|
||||
<string name="remove_source">Supprimer la source</string>
|
||||
<string name="pref_theme_title">Thème Clair/Sombre</string>
|
||||
<string name="mode_dark">Thème sombre</string>
|
||||
<string name="mode_system">Utiliser les paramètres système</string>
|
||||
<string name="mode_light">Thème clair</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"À propos"</string>
|
||||
<string name="marked_as_read">"Marqué comme lu"</string>
|
||||
<string name="marked_as_unread">"Marqué comme non lu"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Non se leron todas as publicacións"</string>
|
||||
<string name="all_posts_read">"Leronse todas as publicacións"</string>
|
||||
<string name="undo_string">"Desfacer"</string>
|
||||
<string name="addStringNoUrl">"Accede pra engadir fontes."</string>
|
||||
<string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
|
||||
<string name="cant_create_source">"Non se pode crear unha fonte."</string>
|
||||
<string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Aliñar á esquerda</string>
|
||||
<string name="reader_text_align_justify">Xustificado</string>
|
||||
<string name="settings_reader_font">Modo lector</string>
|
||||
<string name="reader_static_bar_title">Barra inferior estática na vista de artigos</string>
|
||||
<string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
|
||||
<string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
|
||||
<string name="remove_source">Eliminar fonte</string>
|
||||
<string name="pref_theme_title">Modo Claro/Escuro</string>
|
||||
<string name="mode_dark">Modo escuro</string>
|
||||
<string name="mode_system">Seguir axustes do sistema</string>
|
||||
<string name="mode_light">Modo claro</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Acerca de"</string>
|
||||
<string name="marked_as_read">"Elemento lido"</string>
|
||||
<string name="marked_as_unread">"Elemento non lido"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Semua pos belum dibaca"</string>
|
||||
<string name="all_posts_read">"Semua pos sudah dibaca"</string>
|
||||
<string name="undo_string">"Urung"</string>
|
||||
<string name="addStringNoUrl">"Masuk untuk menambah sumber."</string>
|
||||
<string name="cant_get_sources">"Tidak bisa mendapatkan daftar sumber."</string>
|
||||
<string name="cant_create_source">"Tidak dapat membuat sumber."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Tentang"</string>
|
||||
<string name="marked_as_read">"Membaca item"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"All posts weren't read"</string>
|
||||
<string name="all_posts_read">"Tutti i messaggi sono stati letti"</string>
|
||||
<string name="undo_string">"Annulla"</string>
|
||||
<string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string>
|
||||
<string name="cant_get_sources">"Can't get sources list."</string>
|
||||
<string name="cant_create_source">"Can't create source."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Informazioni"</string>
|
||||
<string name="marked_as_read">"Articolo letto"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"모든 게시물을 읽지 않았습니다."</string>
|
||||
<string name="all_posts_read">"모든 게시물을 읽었습니다."</string>
|
||||
<string name="undo_string">"실행 취소"</string>
|
||||
<string name="addStringNoUrl">"로그인 소스를 추가 해야 합니다."</string>
|
||||
<string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string>
|
||||
<string name="cant_create_source">"소스를 만들 수 없습니다."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"정보"</string>
|
||||
<string name="marked_as_read">"항목 읽기"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -23,7 +23,7 @@
|
||||
<string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
|
||||
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
|
||||
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
|
||||
<string name="undo_string">"Ongedaan maken"</string>
|
||||
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
|
||||
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
|
||||
<string name="cant_create_source">"Kan bron niet creëeren"</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +105,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +131,5 @@
|
||||
<string name="action_about">"Over"</string>
|
||||
<string name="marked_as_read">"Artikel gelezen"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
<string name="undo_string">"Ongedaan maken"</string>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Nenhum post foi lido"</string>
|
||||
<string name="all_posts_read">"Todos os posts foram lidos"</string>
|
||||
<string name="undo_string">"Desfazer"</string>
|
||||
<string name="addStringNoUrl">"Faça login para adicionar fontes."</string>
|
||||
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
|
||||
<string name="cant_create_source">"Não é possível criar fonte."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Sobre"</string>
|
||||
<string name="marked_as_read">"Item lido"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Todas as postagens não foram lidas"</string>
|
||||
<string name="all_posts_read">"Todas as postagens foram lidas"</string>
|
||||
<string name="undo_string">"Desfazer"</string>
|
||||
<string name="addStringNoUrl">"Logar para adicionar fontes."</string>
|
||||
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string>
|
||||
<string name="cant_create_source">"Não é possível criar a fonte."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Sobre"</string>
|
||||
<string name="marked_as_read">"Item lido"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"All posts weren't read"</string>
|
||||
<string name="all_posts_read">"All posts were read"</string>
|
||||
<string name="undo_string">"Undo"</string>
|
||||
<string name="addStringNoUrl">"Log in to add sources."</string>
|
||||
<string name="cant_get_sources">"Can't get sources list."</string>
|
||||
<string name="cant_create_source">"Can't create source."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"මේ ගැන"</string>
|
||||
<string name="marked_as_read">"Item read"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"Tüm mesajlar okunmadı"</string>
|
||||
<string name="all_posts_read">"Tüm mesajlar okundu"</string>
|
||||
<string name="undo_string">"Geri al"</string>
|
||||
<string name="addStringNoUrl">"Kaynakları eklemek için giriş yapın."</string>
|
||||
<string name="cant_get_sources">"Kaynakları listesi alınamıyor."</string>
|
||||
<string name="cant_create_source">"Kaynak oluşturulamıyor."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"Hakkında"</string>
|
||||
<string name="marked_as_read">"Öğeleri oku"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"所有帖子都未读"</string>
|
||||
<string name="all_posts_read">"所有帖子已读"</string>
|
||||
<string name="undo_string">"撤销"</string>
|
||||
<string name="addStringNoUrl">"登录以添加数据源。"</string>
|
||||
<string name="cant_get_sources">"无法获取数据列表。"</string>
|
||||
<string name="cant_create_source">"无法创建源数据。"</string>
|
||||
<string name="cant_get_spouts_no_network">"由于网络问题,无法获取 spouts 列表。"</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">左对齐</string>
|
||||
<string name="reader_text_align_justify">左右对齐</string>
|
||||
<string name="settings_reader_font">阅读器字体</string>
|
||||
<string name="reader_static_bar_title">文章查看器中的静态底部栏</string>
|
||||
<string name="reader_static_bar_on">底部栏将始终显示</string>
|
||||
<string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string>
|
||||
<string name="remove_source">删除源</string>
|
||||
<string name="pref_theme_title">浅色/深色模式</string>
|
||||
<string name="mode_dark">深色模式</string>
|
||||
<string name="mode_system">遵循系统设置</string>
|
||||
<string name="mode_light">浅色模式</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"关于我们"</string>
|
||||
<string name="marked_as_read">"已读"</string>
|
||||
<string name="marked_as_unread">"未读条目"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -24,6 +24,7 @@
|
||||
<string name="all_posts_not_read">"所有帖子都未读"</string>
|
||||
<string name="all_posts_read">"所有帖子已读"</string>
|
||||
<string name="undo_string">"撤销"</string>
|
||||
<string name="addStringNoUrl">"登录以添加数据源。"</string>
|
||||
<string name="cant_get_sources">"无法获取数据列表。"</string>
|
||||
<string name="cant_create_source">"无法创建源数据。"</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -105,7 +106,11 @@
|
||||
<string name="reader_text_align_left">Align left</string>
|
||||
<string name="reader_text_align_justify">Justify</string>
|
||||
<string name="settings_reader_font">Reader font</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -127,6 +132,4 @@
|
||||
<string name="action_about">"关于我们"</string>
|
||||
<string name="marked_as_read">"已读"</string>
|
||||
<string name="marked_as_unread">"未讀項目"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="unread_action" type="id" />
|
||||
<item name="open_action" type="id" />
|
||||
<item name="share_action" type="id" />
|
||||
</resources>
|
@@ -23,6 +23,7 @@
|
||||
<string name="all_posts_not_read">"All posts weren't read"</string>
|
||||
<string name="all_posts_read">"All posts were read"</string>
|
||||
<string name="undo_string">"Undo"</string>
|
||||
<string name="addStringNoUrl">"Log in to add sources."</string>
|
||||
<string name="cant_get_sources">"Can't get sources list."</string>
|
||||
<string name="cant_create_source">"Can't create source."</string>
|
||||
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
|
||||
@@ -107,7 +108,11 @@
|
||||
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
|
||||
<string name="open_sans_font_id" translatable="false">open_sans</string>
|
||||
<string name="roboto_font_id" translatable="false">roboto</string>
|
||||
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
@@ -129,6 +134,4 @@
|
||||
<string name="action_about">"About"</string>
|
||||
<string name="marked_as_read">"Item read"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="confirm_delete_title">Confirm Deletion</string>
|
||||
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
|
||||
</resources>
|
||||
</resources>
|
@@ -30,6 +30,14 @@
|
||||
android:summaryOn="@string/pref_article_viewer_on"
|
||||
android:title="@string/pref_article_viewer_title"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:dependency="prefer_article_viewer"
|
||||
android:key="reader_static_bar"
|
||||
android:summaryOff="@string/reader_static_bar_off"
|
||||
android:summaryOn="@string/reader_static_bar_on"
|
||||
android:title="@string/reader_static_bar_title"
|
||||
app:iconSpaceReserved="false"/>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_general_category_displaying">
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("ktlint")
|
||||
/*
|
||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
|
||||
|
||||
import android.view.Menu
|
||||
@@ -27,4 +25,3 @@ fun Menu.assertVisible(
|
||||
val item = this.findItem(id)
|
||||
assertTrue(item.isVisible)
|
||||
}
|
||||
*/
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("ktlint")
|
||||
/*
|
||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
|
||||
|
||||
import android.widget.Button
|
||||
@@ -59,8 +57,7 @@ class LoginActivityTest {
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
/* @Test
|
||||
/* @Test
|
||||
fun connect() {
|
||||
Robolectric.buildActivity(LoginActivity::class.java).use { controller ->
|
||||
controller.setup() // Moves the Activity to the RESUMED state
|
||||
@@ -75,7 +72,4 @@ class LoginActivityTest {
|
||||
assertEquals(expectedIntent.component, actual.component)
|
||||
}
|
||||
}*/
|
||||
/*
|
||||
|
||||
}
|
||||
*/
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("ktlint")
|
||||
/*
|
||||
package bou.amine.apps.readerforselfossv2.android.tests.robolectric
|
||||
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
@@ -10,4 +8,3 @@ class RobotElectriqueRunner(
|
||||
) : RobolectricTestRunner(testClass) {
|
||||
override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build()
|
||||
}
|
||||
*/
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("detekt:LargeClass")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.tests.repository
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||
@@ -11,7 +9,6 @@ import bou.amine.apps.readerforselfossv2.model.SuccessResponse
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.toView
|
||||
import io.mockk.clearAllMocks
|
||||
@@ -25,6 +22,7 @@ import junit.framework.TestCase.assertFalse
|
||||
import junit.framework.TestCase.assertNotSame
|
||||
import junit.framework.TestCase.assertSame
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
@@ -52,12 +50,16 @@ class RepositoryTest {
|
||||
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
|
||||
private val appSettingsService = mockk<AppSettingsService>()
|
||||
private val api = mockk<SelfossApi>()
|
||||
private val connectivityService = mockk<ConnectivityService>()
|
||||
|
||||
private lateinit var repository: Repository
|
||||
|
||||
private fun initializeRepository(isNetworkAvailable: Boolean = true) {
|
||||
every { connectivityService.isNetworkAvailable() } returns isNetworkAvailable
|
||||
repository = Repository(api, appSettingsService, connectivityService, db)
|
||||
private fun initializeRepository(
|
||||
isConnectionAvailable: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(
|
||||
true,
|
||||
),
|
||||
) {
|
||||
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
|
||||
|
||||
runBlocking {
|
||||
repository.updateApiInformation()
|
||||
@@ -107,7 +109,7 @@ class RepositoryTest {
|
||||
fun instantiate_repository_without_api_version() {
|
||||
every { appSettingsService.getApiVersion() } returns -1
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
|
||||
coVerify(exactly = 0) { api.apiInformation() }
|
||||
coVerify(exactly = 0) { api.stats() }
|
||||
@@ -284,7 +286,7 @@ class RepositoryTest {
|
||||
fun get_newer_items_without_connectivity() {
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
runBlocking {
|
||||
repository.getNewerItems()
|
||||
}
|
||||
@@ -311,7 +313,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
|
||||
runBlocking {
|
||||
repository.getNewerItems()
|
||||
@@ -339,7 +341,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
repository.setSourceFilter(
|
||||
SelfossModel.SourceDetail(
|
||||
1,
|
||||
@@ -454,7 +456,7 @@ class RepositoryTest {
|
||||
|
||||
var success: Boolean
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
runBlocking {
|
||||
success = repository.reloadBadges()
|
||||
}
|
||||
@@ -474,7 +476,7 @@ class RepositoryTest {
|
||||
|
||||
var success: Boolean
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
runBlocking {
|
||||
success = repository.reloadBadges()
|
||||
}
|
||||
@@ -569,7 +571,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -587,7 +589,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -604,7 +606,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -622,7 +624,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -772,7 +774,7 @@ class RepositoryTest {
|
||||
@Test
|
||||
fun get_sources_without_connection() {
|
||||
val (_, sourcesDB) = prepareSources()
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -789,7 +791,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -806,7 +808,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -823,7 +825,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -895,7 +897,7 @@ class RepositoryTest {
|
||||
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
|
||||
SuccessResponse(true)
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response =
|
||||
@@ -952,7 +954,7 @@ class RepositoryTest {
|
||||
fun delete_source_without_connection() {
|
||||
coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.deleteSource(5, "src")
|
||||
@@ -1025,7 +1027,7 @@ class RepositoryTest {
|
||||
data = "undocumented...",
|
||||
)
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.updateRemote()
|
||||
@@ -1067,7 +1069,7 @@ class RepositoryTest {
|
||||
fun login_but_without_connection() {
|
||||
coEvery { api.login() } returns SuccessResponse(success = true)
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.login()
|
||||
@@ -1147,7 +1149,7 @@ class RepositoryTest {
|
||||
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
|
||||
StatusAndData(success = false, data = generateTestApiItem())
|
||||
|
||||
initializeRepository(false)
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
prepareSearch()
|
||||
runBlocking {
|
||||
repository.tryToCacheItemsAndGetNewOnes()
|
||||
|
@@ -44,7 +44,7 @@ class FakeItemParameters {
|
||||
var datetime = "2022-09-09T03:32:01-04:00"
|
||||
val title = "Etica della ricerca sotto i riflettori."
|
||||
val content =
|
||||
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>L’etica della scienza è di certo ambito di cui continuiamo a scoprire nuovi aspetti e risvolti.</p>\n<p>L’ultimo è quello delle intelligenze artificiali capaci di creare opere complesse basate su immagini e parole memorizzate con il rischio di fake news e di contenuti disturbanti.</p>\n<p>Per evitare che ciò accada si sta procedendo filtrando secondo criteri di autocensura i dati da cui l’intelligenza artificiale parte.</p>\n<p>Comincia ad intravedersi un futuro prossimo di competizione fra autori umani ed artificiali nel quale sarà importante, quando i loro prodotti saranno indistinguibili, dichiararne l’origine.</p>\n<p>Come si comprende, si conferma che gli aspetti etici dell’innovazione e della ricerca si diversificato sempre di più.</p>\n<p>La biologia molecolare e la genetica già in passato hanno posto all’attenzione comune aspetti di etica della scienza che hanno indotto a nuove riflessioni circa i limiti delle ricerche.</p>\n<p>L’argomento, sempre attuale, torna sulle prime pagine a seguito della pubblicazione di una ricerca della Università di Cambridge che ha sviluppato una struttura cellulare di un topo con un cuore che batte regolarmente.</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image002-1.png?w=481\" alt=\"\" width=\"697\" height=\"430\" /><img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image003-1.png?w=906\" alt=\"\" /><p>Magdalena Zernicka-Goetz</p>\n<img src=\"https://ilblogdellasci.files.wordpress.com/2022/09/image004.jpg?w=474\" alt=\"\" width=\"622\" height=\"465\" /><p>Gianluca Amadei</p>\n<p>Del gruppo fa parte anche uno scienziato italiano Gianluca Amadei,che dinnanzi alle obiezioni di natura etica sulla realizzazione della vita artificiale si è affrettato a sostenere che non è creare nuove vite il fine primario della ricerca, ma quello di salvare quelle esistenti, di dare contributi essenziali alla medicina citando il caso del fallimento tuttora non interpretato di alcune gravidanze e di superare la sperimentazione animale, così contribuendo positivamente alla soluzione di un altro dilemma etico.</p>\n<p>L’embrione sintetico ha ovviamente come primo traguardo il contributo ai trapianti oggi drammaticamente carenti nell’offerta rispetto alla domanda, con attese fino a 4 anni per i trapianti di cuore ed a 2 anni per quelli di fegato. Il lavoro dovrebbe adesso continuare presso l’Ateneo di Padova per creare nuovi organi e nuovi farmaci.</p>"
|
||||
"<p><strong>Luigi Campanella, già Presidente SCI</strong></p>\n<p>L’etica della scienza è di certo ambito di cui continuiamo</p>"
|
||||
var unread = true
|
||||
var starred = true
|
||||
val thumbnail = null
|
||||
|
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
// trick: for the same plugin versions in all sub-modules
|
||||
id("com.android.application").version("8.8.1").apply(false)
|
||||
id("com.android.library").version("8.8.1").apply(false)
|
||||
//trick: for the same plugin versions in all sub-modules
|
||||
id("com.android.application").version("8.7.3").apply(false)
|
||||
id("com.android.library").version("8.7.3").apply(false)
|
||||
id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
|
||||
kotlin("multiplatform").version("2.1.0").apply(false)
|
||||
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
|
||||
@@ -16,6 +16,7 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.register("clean", Delete::class) {
|
||||
delete(layout.buildDirectory)
|
||||
}
|
||||
@@ -23,4 +24,4 @@ tasks.register("clean", Delete::class) {
|
||||
dependencies {
|
||||
kover(project(":shared"))
|
||||
kover(project(":androidApp"))
|
||||
}
|
||||
}
|
786
detekt.yml
786
detekt.yml
@@ -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.*'
|
@@ -1,4 +0,0 @@
|
||||
**v125010111**
|
||||
|
||||
- Debug trying to fix context issues. (#174)
|
||||
- Changelog for v125010031
|
@@ -1,5 +0,0 @@
|
||||
**v125010131**
|
||||
|
||||
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||
- Changelog for v125010111
|
@@ -1,6 +0,0 @@
|
||||
**v125010201**
|
||||
|
||||
- fix: Handle empty url issue.
|
||||
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||
- chore: changing actions in reader fragment.
|
||||
- Changelog for v125010131
|
@@ -1,8 +0,0 @@
|
||||
**v125010241**
|
||||
|
||||
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||
- refactor: context fragments issues.
|
||||
- logs: Context issues.
|
||||
- fix: Handle empty url issue, again.
|
||||
- fix: Link not opening.
|
||||
- Changelog for v125010201
|
@@ -1,7 +0,0 @@
|
||||
**v125020411**
|
||||
|
||||
- Merge pull request 'bump' (#182) from bump into master
|
||||
- chore: non transiant R classes.
|
||||
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||
- bump
|
||||
- fix: One more missing context.
|
@@ -1,7 +0,0 @@
|
||||
**v125020471**
|
||||
|
||||
- chore: no more docker-compose.
|
||||
- bump: gradle plugin.
|
||||
- Merge pull request 'fix: check index exists.' (#183) from fix-index into master
|
||||
- fix: check index exists.
|
||||
- Changelog for v125020411
|
@@ -1,4 +0,0 @@
|
||||
**v125020581**
|
||||
|
||||
- fix: url can be empty ?
|
||||
- Changelog for v125020471
|
@@ -1,12 +0,0 @@
|
||||
**v125030681**
|
||||
|
||||
- chore: do not send reports on simulators.
|
||||
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
|
||||
- chore: do not send reports on simulators.
|
||||
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
|
||||
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
|
||||
- chore: we don't need to check if the url is valid in upsert screen.
|
||||
- fix: Url validation was not failing login. Added tests.
|
||||
- chore: crowding ci integration.
|
||||
- Show a confirmation dialog before deleting sources (#185)
|
||||
- Changelog for v125020581
|
@@ -1,8 +0,0 @@
|
||||
**v125030711**
|
||||
|
||||
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
|
||||
- chore: check changes for translations and android.
|
||||
- fix: initial status loading issues.
|
||||
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
|
||||
- chore: new connectivity dep. Closes #84.
|
||||
- Changelog for v125030681
|
@@ -1,17 +0,0 @@
|
||||
**v125030901**
|
||||
|
||||
- Merge pull request 'fix-reload' (#195) from fix-reload into master
|
||||
- fix: Infinite scroll needs loading stats.
|
||||
- fix: do not reload items on resume.
|
||||
- Merge pull request 'tests' (#193) from tests into master
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- chore: better handling of coroutine dispatchers.
|
||||
- ci: Instrumentation tests coverage in ci.
|
||||
- chore: comment robolectric tests for now.
|
||||
- fix: Fixed source deletion test.
|
||||
- Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
|
||||
- Refactor star icon handling
|
||||
- Don't restart activity changing alignment
|
||||
- Changelog for v125030711
|
@@ -1,4 +0,0 @@
|
||||
**v125040991**
|
||||
|
||||
- fix: Connectivity toast message was causing issues.
|
||||
- Changelog for v125030901
|
@@ -18,13 +18,12 @@ kotlin.code.style=official
|
||||
#Android
|
||||
android.useAndroidX=true
|
||||
#android.nonTransitiveRClass=true
|
||||
android.enableJetifier=false
|
||||
android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
android.nonTransitiveRClass=false
|
||||
#MPP
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
ignoreGitVersion=false
|
||||
kotlin.native.cacheKind.iosX64=none
|
||||
org.gradle.configureondemand=true
|
||||
kotlin.jvm.target.validation.mode=IGNORE
|
||||
org.gradle.configureondemand=true
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sun Feb 09 14:44:52 CET 2025
|
||||
#Mon Nov 25 22:48:24 CET 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -4,6 +4,7 @@ object SqlDelight {
|
||||
const val runtime = "app.cash.sqldelight:runtime:2.0.2"
|
||||
const val android = "app.cash.sqldelight:android-driver:2.0.2"
|
||||
const val native = "app.cash.sqldelight:native-driver:2.0.2"
|
||||
|
||||
}
|
||||
|
||||
plugins {
|
||||
@@ -40,13 +41,13 @@ kotlin {
|
||||
|
||||
implementation("org.jsoup:jsoup:1.15.4")
|
||||
|
||||
// Dependency Injection
|
||||
//Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di:7.14.0")
|
||||
|
||||
// Settings
|
||||
//Settings
|
||||
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
|
||||
|
||||
// Logging
|
||||
//Logging
|
||||
implementation("io.github.aakira:napier:2.6.1")
|
||||
|
||||
// Sql
|
||||
@@ -54,10 +55,6 @@ kotlin {
|
||||
|
||||
// Sql
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||
|
||||
// Connectivity
|
||||
implementation("dev.jordond.connectivity:connectivity-core:1.2.0")
|
||||
implementation("dev.jordond.connectivity:connectivity-device:1.2.0")
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
@@ -117,4 +114,4 @@ sqldelight {
|
||||
packageName.set("bou.amine.apps.readerforselfossv2.dao")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,13 +4,12 @@ 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(
|
||||
actual class DriverFactory(private val context: Context) {
|
||||
actual fun createDriver(): SqlDriver {
|
||||
return AndroidSqliteDriver(
|
||||
ReaderForSelfossDB.Schema,
|
||||
context,
|
||||
"ReaderForSelfossV2-android.db",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -9,14 +9,12 @@ class NaiveTrustManager : X509TrustManager {
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?,
|
||||
) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?,
|
||||
) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
|
||||
|
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.utils
|
||||
import android.text.format.DateUtils
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
actual class DateUtils {
|
||||
actual companion object {
|
||||
actual fun parseRelativeDate(dateString: String): String {
|
||||
|
@@ -12,14 +12,16 @@ actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(bas
|
||||
|
||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
|
||||
|
||||
val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
|
||||
|
||||
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
|
||||
val allImages = ArrayList<String>()
|
||||
|
||||
for (image in Jsoup.parse(content).getElementsByTag("img")) {
|
||||
val url = image.attr("src")
|
||||
if (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)
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,8 @@
|
||||
@file:Suppress("detekt:LongParameterList")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class MercuryModel {
|
||||
@Suppress("detekt:ConstructorParameterNaming")
|
||||
@Serializable
|
||||
class ParsedContent(
|
||||
val title: String? = null,
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("detekt:LongParameterList")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.model
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
||||
@@ -20,10 +18,6 @@ import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class ModelException(
|
||||
message: String,
|
||||
) : Throwable(message)
|
||||
|
||||
class SelfossModel {
|
||||
@Serializable
|
||||
data class Tag(
|
||||
@@ -127,8 +121,8 @@ class SelfossModel {
|
||||
val tags: List<String>,
|
||||
val author: String? = null,
|
||||
) {
|
||||
fun getLinkDecoded(): String? {
|
||||
var stringUrl: String?
|
||||
fun getLinkDecoded(): String {
|
||||
var stringUrl: String
|
||||
stringUrl =
|
||||
if (link.contains("//news.google.com/news/") && link.contains("&url=")) {
|
||||
link.substringAfter("&url=")
|
||||
@@ -146,7 +140,11 @@ class SelfossModel {
|
||||
stringUrl = "http:$stringUrl"
|
||||
}
|
||||
|
||||
return if (stringUrl.isEmptyOrNullOrNullString()) null else stringUrl
|
||||
if (stringUrl.isEmptyOrNullOrNullString()) {
|
||||
throw Exception("Link $link was translated to $stringUrl, but was empty. Handle this.")
|
||||
}
|
||||
|
||||
return stringUrl
|
||||
}
|
||||
|
||||
fun sourceAuthorAndDate(): String {
|
||||
@@ -172,7 +170,7 @@ class SelfossModel {
|
||||
}
|
||||
}
|
||||
|
||||
// this seems to be super slow.
|
||||
// TODO: this seems to be super slow.
|
||||
object TagsListSerializer : KSerializer<List<String>> {
|
||||
override fun deserialize(decoder: Decoder): List<String> =
|
||||
when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("detekt:TooManyFunctions")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.repository
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.dao.ACTION
|
||||
@@ -13,8 +11,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.model.StatusAndData
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.Enums.ItemType
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.toEntity
|
||||
import bou.amine.apps.readerforselfossv2.utils.toParsedDate
|
||||
@@ -26,44 +23,43 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MAX_ITEMS_NUMBER = 200
|
||||
|
||||
class Repository(
|
||||
private val api: SelfossApi,
|
||||
private val appSettingsService: AppSettingsService,
|
||||
private val connectivityService: ConnectivityService,
|
||||
val isConnectionAvailable: MutableStateFlow<Boolean>,
|
||||
private val db: ReaderForSelfossDB,
|
||||
) {
|
||||
var items = ArrayList<SelfossModel.Item>()
|
||||
var connectionMonitored = false
|
||||
|
||||
var baseUrl = appSettingsService.getBaseUrl()
|
||||
|
||||
var displayedItems = ItemType.UNREAD
|
||||
|
||||
private var _tagFilter = MutableStateFlow<SelfossModel.Tag?>(null)
|
||||
var tagFilter = _tagFilter.asStateFlow()
|
||||
private var _sourceFilter = MutableStateFlow<SelfossModel.Source?>(null)
|
||||
var sourceFilter = _sourceFilter.asStateFlow()
|
||||
private var tagFilterFlow = MutableStateFlow<SelfossModel.Tag?>(null)
|
||||
var tagFilter = tagFilterFlow.asStateFlow()
|
||||
private var sourceFilterFlow = MutableStateFlow<SelfossModel.Source?>(null)
|
||||
var sourceFilter = sourceFilterFlow.asStateFlow()
|
||||
var searchFilter: String? = null
|
||||
|
||||
var offlineOverride = false
|
||||
|
||||
private val _badgeUnread = MutableStateFlow(0)
|
||||
val badgeUnread = _badgeUnread.asStateFlow()
|
||||
private val _badgeAll = MutableStateFlow(0)
|
||||
val badgeAll = _badgeAll.asStateFlow()
|
||||
private val _badgeStarred = MutableStateFlow(0)
|
||||
val badgeStarred = _badgeStarred.asStateFlow()
|
||||
private val badgeUnreadFlow = MutableStateFlow(0)
|
||||
val badgeUnread = badgeUnreadFlow.asStateFlow()
|
||||
private val badgeAllFlow = MutableStateFlow(0)
|
||||
val badgeAll = badgeAllFlow.asStateFlow()
|
||||
private val badgeStarredFlow = MutableStateFlow(0)
|
||||
val badgeStarred = badgeStarredFlow.asStateFlow()
|
||||
|
||||
private var fetchedTags = false
|
||||
private var fetchedSources = false
|
||||
|
||||
private var _readerItems = ArrayList<SelfossModel.Item>()
|
||||
private var _selectedSource: SelfossModel.SourceDetail? = null
|
||||
private var readerItems = ArrayList<SelfossModel.Item>()
|
||||
private var selectedSource: SelfossModel.SourceDetail? = null
|
||||
|
||||
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
||||
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
fetchedItems =
|
||||
api.getItems(
|
||||
displayedItems.type,
|
||||
@@ -102,7 +98,7 @@ class Repository(
|
||||
|
||||
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
|
||||
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
val offset = items.size
|
||||
fetchedItems =
|
||||
api.getItems(
|
||||
@@ -122,7 +118,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
|
||||
return if (connectivityService.isNetworkAvailable()) {
|
||||
return if (isNetworkAvailable()) {
|
||||
val items =
|
||||
api.getItems(
|
||||
itemType.type,
|
||||
@@ -131,7 +127,7 @@ class Repository(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
MAX_ITEMS_NUMBER,
|
||||
200,
|
||||
)
|
||||
return if (items.success && items.data != null) {
|
||||
items.data
|
||||
@@ -143,23 +139,22 @@ class Repository(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:ForbiddenComment")
|
||||
suspend fun reloadBadges(): Boolean {
|
||||
var success = false
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
val response = api.stats()
|
||||
if (response.success && response.data != null) {
|
||||
_badgeUnread.value = response.data.unread ?: 0
|
||||
_badgeAll.value = response.data.total
|
||||
_badgeStarred.value = response.data.starred ?: 0
|
||||
badgeUnreadFlow.value = response.data.unread ?: 0
|
||||
badgeAllFlow.value = response.data.total
|
||||
badgeStarredFlow.value = response.data.starred ?: 0
|
||||
success = true
|
||||
}
|
||||
} else if (appSettingsService.isItemCachingEnabled()) {
|
||||
// TODO: do this differently, because it's not efficient
|
||||
val dbItems = getDBItems()
|
||||
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
|
||||
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
|
||||
_badgeAll.value = dbItems.size
|
||||
badgeUnreadFlow.value = dbItems.filter { item -> item.unread }.size
|
||||
badgeStarredFlow.value = dbItems.filter { item -> item.starred }.size
|
||||
badgeAllFlow.value = dbItems.size
|
||||
success = true
|
||||
}
|
||||
return success
|
||||
@@ -168,7 +163,7 @@ class Repository(
|
||||
suspend fun getTags(): List<SelfossModel.Tag> {
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
return if (connectivityService.isNetworkAvailable() && !fetchedTags) {
|
||||
return if (isNetworkAvailable() && !fetchedTags) {
|
||||
val apiTags = api.tags()
|
||||
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
|
||||
resetDBTagsWithData(apiTags.data)
|
||||
@@ -185,7 +180,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
val spouts = api.spouts()
|
||||
if (spouts.success && spouts.data != null) {
|
||||
spouts.data
|
||||
@@ -201,7 +196,7 @@ class Repository(
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
|
||||
if (shouldFetch && connectivityService.isNetworkAvailable()) {
|
||||
if (shouldFetch && isNetworkAvailable()) {
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
val apiSources = api.sourcesStats()
|
||||
if (apiSources.success && apiSources.data != null) {
|
||||
@@ -223,26 +218,17 @@ class Repository(
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
|
||||
if (shouldFetch && connectivityService.isNetworkAvailable()) {
|
||||
sources = sourceDetails(isDatabaseEnabled)
|
||||
if (shouldFetch && isNetworkAvailable()) {
|
||||
val apiSources = api.sourcesDetailed()
|
||||
if (apiSources.success && apiSources.data != null) {
|
||||
fetchedSources = true
|
||||
sources = apiSources.data
|
||||
if (isDatabaseEnabled) {
|
||||
resetDBSourcesWithData(sources)
|
||||
}
|
||||
}
|
||||
} else if (isDatabaseEnabled) {
|
||||
sources = getDBSources().map { it.toView() } as ArrayList<SelfossModel.SourceDetail>
|
||||
if (sources.isEmpty() && !connectivityService.isNetworkAvailable() && !fetchedSources) {
|
||||
sources = sourceDetails(isDatabaseEnabled)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private suspend fun sourceDetails(isDatabaseEnabled: Boolean): ArrayList<SelfossModel.SourceDetail> {
|
||||
var sources = ArrayList<SelfossModel.SourceDetail>()
|
||||
val apiSources = api.sourcesDetailed()
|
||||
if (apiSources.success && apiSources.data != null) {
|
||||
fetchedSources = true
|
||||
sources = apiSources.data
|
||||
if (isDatabaseEnabled) {
|
||||
resetDBSourcesWithData(sources)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
@@ -257,7 +243,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun markAsReadById(id: Int): Boolean =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
api.markAsRead(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), read = true)
|
||||
@@ -274,7 +260,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun unmarkAsReadById(id: Int): Boolean =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
api.unmarkAsRead(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), unread = true)
|
||||
@@ -291,7 +277,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun starrById(id: Int): Boolean =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
api.starr(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), starred = true)
|
||||
@@ -308,7 +294,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun unstarrById(id: Int): Boolean =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
api.unstarr(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), starred = true)
|
||||
@@ -318,8 +304,7 @@ class Repository(
|
||||
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
|
||||
var success = false
|
||||
|
||||
if (connectivityService.isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess
|
||||
) {
|
||||
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
|
||||
success = true
|
||||
for (item in items) {
|
||||
markAsReadLocally(item)
|
||||
@@ -331,10 +316,10 @@ class Repository(
|
||||
private fun markAsReadLocally(item: SelfossModel.Item) {
|
||||
if (item.unread) {
|
||||
item.unread = false
|
||||
_badgeUnread.value -= 1
|
||||
badgeUnreadFlow.value -= 1
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
updateDBItem(item)
|
||||
}
|
||||
}
|
||||
@@ -342,10 +327,10 @@ class Repository(
|
||||
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
|
||||
if (!item.unread) {
|
||||
item.unread = true
|
||||
_badgeUnread.value += 1
|
||||
badgeUnreadFlow.value += 1
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
updateDBItem(item)
|
||||
}
|
||||
}
|
||||
@@ -353,10 +338,10 @@ class Repository(
|
||||
private fun starrLocally(item: SelfossModel.Item) {
|
||||
if (!item.starred) {
|
||||
item.starred = true
|
||||
_badgeStarred.value += 1
|
||||
badgeStarredFlow.value += 1
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
updateDBItem(item)
|
||||
}
|
||||
}
|
||||
@@ -364,10 +349,10 @@ class Repository(
|
||||
private fun unstarrLocally(item: SelfossModel.Item) {
|
||||
if (item.starred) {
|
||||
item.starred = false
|
||||
_badgeStarred.value -= 1
|
||||
badgeStarredFlow.value -= 1
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
updateDBItem(item)
|
||||
}
|
||||
}
|
||||
@@ -379,8 +364,7 @@ class Repository(
|
||||
tags: String,
|
||||
): Boolean {
|
||||
var response = false
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
fetchedSources = false
|
||||
if (isNetworkAvailable()) {
|
||||
response = api
|
||||
.createSourceForVersion(
|
||||
title,
|
||||
@@ -401,8 +385,7 @@ class Repository(
|
||||
tags: String,
|
||||
): Boolean {
|
||||
var response = false
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
fetchedSources = false
|
||||
if (isNetworkAvailable()) {
|
||||
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
|
||||
}
|
||||
|
||||
@@ -414,14 +397,13 @@ class Repository(
|
||||
title: String,
|
||||
): Boolean {
|
||||
var success = false
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
val response = api.deleteSource(id)
|
||||
success = response.isSuccess
|
||||
fetchedSources = false
|
||||
}
|
||||
|
||||
// We filter on success or if the network isn't available
|
||||
if (success || !connectivityService.isNetworkAvailable()) {
|
||||
if (success || !isNetworkAvailable()) {
|
||||
items = ArrayList(items.filter { it.sourcetitle != title })
|
||||
setReaderItems(items)
|
||||
db.itemsQueries.deleteItemsWhereSource(title)
|
||||
@@ -431,7 +413,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun updateRemote(): Boolean =
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
api.update().data.equals("finished")
|
||||
} else {
|
||||
false
|
||||
@@ -439,7 +421,7 @@ class Repository(
|
||||
|
||||
suspend fun login(): Boolean {
|
||||
var result = false
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
try {
|
||||
val response = api.login()
|
||||
result = response.isSuccess == true
|
||||
@@ -452,7 +434,7 @@ class Repository(
|
||||
|
||||
suspend fun checkIfFetchFails(): Boolean {
|
||||
var fetchFailed = true
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
try {
|
||||
// Trying to fetch one item, and check someone is trying to use the app with
|
||||
// a random rss feed, that would throw a NoTransformationFoundException
|
||||
@@ -466,7 +448,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
try {
|
||||
val response = api.logout()
|
||||
if (!response.isSuccess) {
|
||||
@@ -494,7 +476,7 @@ class Repository(
|
||||
suspend fun updateApiInformation() {
|
||||
val apiMajorVersion = appSettingsService.getApiVersion()
|
||||
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
if (isNetworkAvailable()) {
|
||||
val fetchedInformation = api.apiInformation()
|
||||
if (fetchedInformation.success && fetchedInformation.data != null) {
|
||||
if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
|
||||
@@ -513,6 +495,8 @@ class Repository(
|
||||
}
|
||||
}
|
||||
|
||||
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
|
||||
|
||||
private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList()
|
||||
|
||||
private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id)
|
||||
@@ -575,7 +559,6 @@ class Repository(
|
||||
item.id.toString(),
|
||||
)
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
|
||||
try {
|
||||
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
||||
@@ -631,30 +614,30 @@ class Repository(
|
||||
}
|
||||
|
||||
fun setTagFilter(tag: SelfossModel.Tag?) {
|
||||
_tagFilter.value = tag
|
||||
tagFilterFlow.value = tag
|
||||
}
|
||||
|
||||
fun setSourceFilter(source: SelfossModel.Source?) {
|
||||
_sourceFilter.value = source
|
||||
sourceFilterFlow.value = source
|
||||
}
|
||||
|
||||
fun setReaderItems(readerItems: ArrayList<SelfossModel.Item>) {
|
||||
_readerItems = readerItems
|
||||
this.readerItems = readerItems
|
||||
}
|
||||
|
||||
fun getReaderItems(): ArrayList<SelfossModel.Item> = _readerItems
|
||||
fun getReaderItems(): ArrayList<SelfossModel.Item> = readerItems
|
||||
|
||||
fun migrate(driverFactory: DriverFactory) {
|
||||
ReaderForSelfossDB.Schema.migrate(driverFactory.createDriver(), 0, 1)
|
||||
}
|
||||
|
||||
fun setSelectedSource(source: SelfossModel.SourceDetail) {
|
||||
_selectedSource = source
|
||||
selectedSource = source
|
||||
}
|
||||
|
||||
fun unsetSelectedSource() {
|
||||
_selectedSource = null
|
||||
selectedSource = null
|
||||
}
|
||||
|
||||
fun getSelectedSource(): SelfossModel.SourceDetail? = _selectedSource
|
||||
fun getSelectedSource(): SelfossModel.SourceDetail? = selectedSource
|
||||
}
|
||||
|
@@ -17,8 +17,8 @@ import kotlinx.serialization.json.Json
|
||||
class MercuryApi {
|
||||
var client = createHttpClient()
|
||||
|
||||
private fun createHttpClient(): HttpClient =
|
||||
HttpClient {
|
||||
private fun createHttpClient(): HttpClient {
|
||||
return HttpClient {
|
||||
install(HttpCache)
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
@@ -40,6 +40,7 @@ class MercuryApi {
|
||||
}
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun query(url: String): StatusAndData<MercuryModel.ParsedContent> =
|
||||
bodyOrFailure(
|
||||
|
@@ -33,7 +33,6 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
|
||||
SuccessResponse(false)
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
|
||||
try {
|
||||
return if (r != null && r.status.isSuccess()) {
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("detekt:TooManyFunctions", "detekt:LongParameterList", "detekt:LargeClass")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
@@ -32,14 +30,11 @@ import io.ktor.utils.io.charsets.Charsets
|
||||
import io.ktor.utils.io.core.toByteArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
expect fun setupInsecureHttpEngine(config: CIOEngineConfig)
|
||||
|
||||
private const val VERSION_WHERE_POST_LOGIN_SHOULD_WORK = 5
|
||||
|
||||
class SelfossApi(
|
||||
private val appSettingsService: AppSettingsService,
|
||||
) {
|
||||
@@ -83,7 +78,7 @@ class SelfossApi(
|
||||
}
|
||||
modifyRequest {
|
||||
Napier.i("Will modify", tag = "HttpSend")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
Napier.i("Will login", tag = "HttpSend")
|
||||
login()
|
||||
Napier.i("Did login", tag = "HttpSend")
|
||||
@@ -181,7 +176,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 =
|
||||
if (shouldHaveNewLogout()) {
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@file:Suppress("detekt:TooManyFunctions")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.service
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
|
@@ -1,19 +1,7 @@
|
||||
@file:Suppress("detekt:TooManyFunctions")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.service
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
|
||||
private const val DEFAULT_FONT_SIZE = 16
|
||||
|
||||
private const val DEFAULT_REFRESH_MINUTES = 360L
|
||||
|
||||
private const val MIN_REFRESH_MINUTES = 15L
|
||||
|
||||
private const val DEFAULT_API_TIMEOUT = 60L
|
||||
|
||||
private const val DEFAULT_ITEMS_NUMBER = 20
|
||||
|
||||
class AppSettingsService(
|
||||
acraSenderServiceProcess: Boolean = false,
|
||||
) {
|
||||
@@ -48,11 +36,12 @@ class AppSettingsService(
|
||||
private var notifyNewItems: Boolean? = null
|
||||
private var itemsNumber: Int? = 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 activeAlignment: Int? = null
|
||||
|
||||
private var fontSize: Int? = null
|
||||
private var staticBar: Boolean? = null
|
||||
private var font: String = ""
|
||||
private var theme: Int? = null
|
||||
|
||||
@@ -152,14 +141,13 @@ class AppSettingsService(
|
||||
return itemsNumber!!
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun refreshItemsNumber() {
|
||||
itemsNumber =
|
||||
try {
|
||||
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt()
|
||||
settings.getString(API_ITEMS_NUMBER, "20").toInt()
|
||||
} catch (e: Exception) {
|
||||
settings.remove(API_ITEMS_NUMBER)
|
||||
DEFAULT_ITEMS_NUMBER
|
||||
20
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,24 +158,22 @@ class AppSettingsService(
|
||||
return apiTimeout!!
|
||||
}
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
private fun secToMs(n: Long) = n * 1000
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun refreshApiTimeout() {
|
||||
apiTimeout =
|
||||
secToMs(
|
||||
try {
|
||||
val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString())
|
||||
val settingsTimeout = settings.getString(API_TIMEOUT, "60")
|
||||
if (settingsTimeout.toLong() > 0) {
|
||||
settingsTimeout.toLong()
|
||||
} else {
|
||||
settings.remove(API_TIMEOUT)
|
||||
DEFAULT_API_TIMEOUT
|
||||
60
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
settings.remove(API_TIMEOUT)
|
||||
DEFAULT_API_TIMEOUT
|
||||
60
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -301,14 +287,14 @@ class AppSettingsService(
|
||||
}
|
||||
|
||||
private fun refreshRefreshMinutes() {
|
||||
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong()
|
||||
if (refreshMinutes <= MIN_REFRESH_MINUTES) {
|
||||
refreshMinutes = MIN_REFRESH_MINUTES
|
||||
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
|
||||
if (refreshMinutes <= 15) {
|
||||
refreshMinutes = 15
|
||||
}
|
||||
}
|
||||
|
||||
fun getRefreshMinutes(): Long {
|
||||
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) {
|
||||
if (refreshMinutes != 360L) {
|
||||
refreshRefreshMinutes()
|
||||
}
|
||||
return refreshMinutes
|
||||
@@ -382,7 +368,18 @@ class AppSettingsService(
|
||||
if (fontSize != null) {
|
||||
refreshFontSize()
|
||||
}
|
||||
return fontSize ?: DEFAULT_FONT_SIZE
|
||||
return fontSize ?: 16
|
||||
}
|
||||
|
||||
private fun refreshStaticBarEnabled() {
|
||||
staticBar = settings.getBoolean(READER_STATIC_BAR, false)
|
||||
}
|
||||
|
||||
fun isStaticBarEnabled(): Boolean {
|
||||
if (staticBar != null) {
|
||||
refreshStaticBarEnabled()
|
||||
}
|
||||
return staticBar == true
|
||||
}
|
||||
|
||||
private fun refreshFont() {
|
||||
@@ -437,6 +434,7 @@ class AppSettingsService(
|
||||
refreshActiveAllignment()
|
||||
refreshFontSize()
|
||||
refreshFont()
|
||||
refreshStaticBarEnabled()
|
||||
refreshCurrentTheme()
|
||||
}
|
||||
|
||||
@@ -486,11 +484,11 @@ class AppSettingsService(
|
||||
|
||||
const val SOURCE_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform"
|
||||
|
||||
const val BUG_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
|
||||
const val TRACKER_URL = "https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
|
||||
|
||||
const val SYNC_CHANNEL_ID = "sync-channel-id"
|
||||
|
||||
const val NEW_ITEMS_CHANNEL = "new-items-channel-id"
|
||||
const val NEW_ITEMS_CHANNEL_ID = "new-items-channel-id"
|
||||
|
||||
const val JUSTIFY = 1
|
||||
|
||||
@@ -534,6 +532,8 @@ class AppSettingsService(
|
||||
|
||||
const val READER_FONT = "reader_font"
|
||||
|
||||
const val READER_STATIC_BAR = "reader_static_bar"
|
||||
|
||||
const val READER_FONT_SIZE = "reader_font_size"
|
||||
|
||||
const val TEXT_ALIGN = "text_align"
|
||||
|
@@ -1,46 +0,0 @@
|
||||
package bou.amine.apps.readerforselfossv2.service
|
||||
|
||||
import dev.jordond.connectivity.Connectivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ConnectivityService {
|
||||
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
|
||||
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
|
||||
private var currentStatus = true
|
||||
private lateinit var connectivity: Connectivity
|
||||
|
||||
fun start() {
|
||||
connectivity = Connectivity()
|
||||
connectivity.start()
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
connectivity.statusUpdates.collect { status ->
|
||||
when (status) {
|
||||
is Connectivity.Status.Connected -> {
|
||||
if (!currentStatus) {
|
||||
currentStatus = true
|
||||
_networkAvailableProvider.emit(true)
|
||||
}
|
||||
}
|
||||
|
||||
is Connectivity.Status.Disconnected -> {
|
||||
if (currentStatus) {
|
||||
currentStatus = false
|
||||
_networkAvailableProvider.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isNetworkAvailable(): Boolean = currentStatus
|
||||
|
||||
fun stop() {
|
||||
currentStatus = true
|
||||
connectivity.stop()
|
||||
}
|
||||
}
|
@@ -4,12 +4,6 @@ import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
|
||||
class DateParseException(
|
||||
message: String,
|
||||
e: Throwable? = null,
|
||||
) : Throwable(message, e)
|
||||
|
||||
@Suppress("detekt:ThrowsCount")
|
||||
fun String.toParsedDate(): Long {
|
||||
// Possible formats are
|
||||
// yyyy-mm-dd hh:mm:ss format
|
||||
@@ -23,22 +17,17 @@ fun String.toParsedDate(): Long {
|
||||
if (this.matches(oldVersionFormat)) {
|
||||
this.replace(" ", "T")
|
||||
} else if (this.matches(newVersionFormat)) {
|
||||
newVersionFormat
|
||||
.find(this)
|
||||
?.groups
|
||||
?.get(1)
|
||||
?.value ?: throw DateParseException("Couldn't parse $this")
|
||||
newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this")
|
||||
} else {
|
||||
throw DateParseException("Unrecognized format for $this")
|
||||
throw Exception("Unrecognized format for $this")
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
expect class DateUtils() {
|
||||
companion object {
|
||||
fun parseRelativeDate(dateString: String): String
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user