Compare commits
45 Commits
4fbebf2954
...
v125030711
Author | SHA1 | Date | |
---|---|---|---|
62354ec70a | |||
18a17251ac | |||
5e91724ee2 | |||
212d259a33 | |||
3bf60f1146 | |||
ef13e300f0 | |||
f170d1157d | |||
af4752f0f0 | |||
f0fa1a17b6 | |||
bb84d1541c | |||
c9227b2c1c | |||
6eaad0c7c5 | |||
a1c98aa7d0 | |||
d5ec118679 | |||
a1c0241a58 | |||
f38936f9b4 | |||
a90ccec707 | |||
2564b19726 | |||
61c7bb20cc | |||
6a0f5baf0a | |||
39f9505c00 | |||
6a6d447456 | |||
0bb4fe6aed | |||
7df4c3368c | |||
c69635b5ae | |||
3a829df70e | |||
7a0202689f | |||
b20f6888f5 | |||
6b96eb358d | |||
dfc1bf9fa3 | |||
b173664ff0 | |||
bc20a421ae | |||
794500355a | |||
44f9dd53d3 | |||
717d6b664c | |||
e23289a3dc | |||
2f5ebe2420 | |||
1893904135 | |||
a4cb28ba81 | |||
ae3cada1c7 | |||
309500276f | |||
ce255b23cd | |||
3b3a575dae | |||
7bcf4574b4 | |||
c79ab5e92b |
10
.gitea/workflows/assets/crowdin.yml
Normal file
10
.gitea/workflows/assets/crowdin.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
@@ -10,36 +10,52 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: "Check android app changes"
|
||||
id: check-android-changes
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
androidApp/src/**
|
||||
- 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 # 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
|
||||
# TESTS ARE RUN LOCALLY
|
||||
# - 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
|
||||
# TESTS ARE RUN LOCALLY
|
||||
# - name: Clean
|
||||
# if: always()
|
||||
# run: |
|
||||
# docker compose -f .gitea/workflows/assets/docker-compose.yml stop
|
||||
|
@@ -16,6 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
- name: Config git
|
||||
run: |
|
||||
git config --global user.email aminecmi+giteadrone@pm.me
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
followtags: true
|
||||
ssh_key: ${{ secrets.PRIVATE_KEY }}
|
||||
tags: true
|
||||
branch: release
|
||||
branch: master
|
||||
- name: copy file via ssh password
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
@@ -124,4 +125,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
|
||||
|
@@ -3,6 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- chore-crowdin-ci
|
||||
|
||||
jobs:
|
||||
Lint:
|
||||
@@ -16,13 +17,75 @@ jobs:
|
||||
java-version: '17'
|
||||
cache: gradle
|
||||
- name: Install klint
|
||||
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
|
||||
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
|
||||
- name: Install detekt
|
||||
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
|
||||
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip
|
||||
- name: Linting...
|
||||
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
|
||||
- name: Detecting...
|
||||
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
|
||||
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
|
||||
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@v45
|
||||
with:
|
||||
files: |
|
||||
androidApp/src/main/res/values/strings.xml
|
||||
- name: upload translation sources
|
||||
if: steps.check-api-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-api-changes.outputs.any_modified == 'true'
|
||||
run: sleep 10s
|
||||
- name: download translations
|
||||
if: steps.check-api-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-api-changes.outputs.any_modified == 'true'
|
||||
id: check-changes
|
||||
uses: mskri/check-uncommitted-changes-action@v1.0.1
|
||||
- name: Commit Changes
|
||||
if: steps.check-api-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-api-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 }}
|
||||
build:
|
||||
needs: Lint
|
||||
uses: ./.gitea/workflows/common_build.yml
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -323,4 +323,6 @@ fabric.properties
|
||||
crowdin.properties
|
||||
|
||||
.kotlin/
|
||||
build-cache/
|
||||
build-cache/
|
||||
|
||||
act
|
||||
|
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,3 +1,80 @@
|
||||
**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
|
||||
|
@@ -12,28 +12,38 @@ plugins {
|
||||
id("app.cash.sqldelight") version "2.0.2"
|
||||
}
|
||||
|
||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
val result: String = ByteArrayOutputStream().use { outputStream ->
|
||||
project.exec {
|
||||
commandLine = cmd.split(" ")
|
||||
standardOutput = outputStream
|
||||
isIgnoreExitValue = ignore
|
||||
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()
|
||||
}
|
||||
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 {
|
||||
@@ -116,7 +126,6 @@ android {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -141,7 +150,7 @@ 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
|
||||
@@ -156,37 +165,34 @@ dependencies {
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||
|
||||
// Themes
|
||||
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
|
||||
implementation("com.leinardi.android:speed-dial:3.3.0")
|
||||
|
||||
// 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")
|
||||
@@ -210,11 +216,12 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -227,4 +234,4 @@ aboutLibraries {
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||
}
|
||||
}
|
||||
|
@@ -30,21 +30,16 @@ fun withError(
|
||||
): TypeSafeMatcher<View?> {
|
||||
return object : TypeSafeMatcher<View?>() {
|
||||
override fun matchesSafely(view: View?): Boolean {
|
||||
if (view == null) {
|
||||
return false
|
||||
}
|
||||
val context = view.context
|
||||
if (view !is EditText) {
|
||||
return false
|
||||
}
|
||||
if (view.error == null) {
|
||||
if (view != null && (view !is EditText || view.error == null)) {
|
||||
return false
|
||||
}
|
||||
val context = view!!.context
|
||||
|
||||
return view.error.toString() == context.getString(id)
|
||||
return (view as EditText).error.toString() == context.getString(id)
|
||||
}
|
||||
|
||||
override fun describeTo(description: Description?) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +53,7 @@ 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()
|
||||
|
@@ -56,7 +56,7 @@ class HomeActivityTest {
|
||||
fun testMenuActions() {
|
||||
onView(withId(R.id.action_search)).perform(click())
|
||||
onView(
|
||||
withId(R.id.search_src_text),
|
||||
withId(com.google.android.material.R.id.search_src_text),
|
||||
).check(matches(isFocused()))
|
||||
onView(isRoot()).perform(ViewActions.pressBack())
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
@@ -26,14 +25,6 @@ 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
|
||||
@@ -69,9 +60,23 @@ class LoginActivityTest {
|
||||
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 connectError() {
|
||||
performLogin("http://10.0.2.2:8889")
|
||||
onView(withId(R.id.urlView)).perform(click())
|
||||
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 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 multiError() {
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
@@ -42,6 +42,7 @@ class SettingsActivityGeneralTest {
|
||||
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()))
|
||||
@@ -64,19 +65,6 @@ class SettingsActivityGeneralTest {
|
||||
),
|
||||
),
|
||||
)
|
||||
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
allOf(
|
||||
isDisplayed(),
|
||||
not(isChecked()),
|
||||
),
|
||||
),
|
||||
)
|
||||
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
|
||||
matches(
|
||||
isEnabled(),
|
||||
),
|
||||
)
|
||||
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
|
||||
matches(
|
||||
@@ -118,6 +106,7 @@ class SettingsActivityGeneralTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:ForbiddenComment")
|
||||
@Test
|
||||
fun testGeneralActionsNumberItems() {
|
||||
onView(withText(R.string.pref_api_items_number_title)).perform(click())
|
||||
@@ -159,19 +148,6 @@ class SettingsActivityGeneralTest {
|
||||
|
||||
@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()))
|
||||
|
@@ -42,6 +42,7 @@ class SettingsActivityOfflineTest {
|
||||
onView(withText(R.string.pref_header_offline)).perform(click())
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
@Test
|
||||
fun testOffline() {
|
||||
onView(withSettingsCheckboxWidget(R.string.pref_switch_periodic_refresh)).check(
|
||||
@@ -107,6 +108,7 @@ class SettingsActivityOfflineTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
@Test
|
||||
fun testOfflineActions() {
|
||||
onView(withText(R.string.pref_switch_items_caching_off)).check(matches(isDisplayed()))
|
||||
|
@@ -45,6 +45,7 @@ class SourcesActivityTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
@Test
|
||||
fun addSourceCheckContent() {
|
||||
testAddSourceWithUrl("https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en", sourceName)
|
||||
|
@@ -31,7 +31,7 @@ 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.openUrlInBrowser
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
@@ -49,6 +49,8 @@ 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,
|
||||
@@ -200,6 +202,7 @@ class HomeActivity :
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
private fun handleBottomBar() {
|
||||
tabNewBadge =
|
||||
TextBadgeItem()
|
||||
@@ -282,7 +285,7 @@ class HomeActivity :
|
||||
|
||||
handleBottomBarActions()
|
||||
|
||||
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||
handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
|
||||
|
||||
handleRecurringTask()
|
||||
CountingIdlingResourceSingleton.increment()
|
||||
@@ -294,10 +297,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,50 +317,44 @@ class HomeActivity :
|
||||
|
||||
private fun reloadLayoutManager() {
|
||||
val currentManager = binding.recyclerView.layoutManager
|
||||
val layoutManager: RecyclerView.LayoutManager
|
||||
|
||||
// This will only update the layout manager if settings changed
|
||||
fun gridLayoutManager() {
|
||||
val layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
fun staggererdGridLayoutManager() {
|
||||
var layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
when (currentManager) {
|
||||
is StaggeredGridLayoutManager ->
|
||||
if (!appSettingsService.isCardViewEnabled()) {
|
||||
layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
gridLayoutManager()
|
||||
}
|
||||
|
||||
is GridLayoutManager ->
|
||||
if (appSettingsService.isCardViewEnabled()) {
|
||||
layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
staggererdGridLayoutManager()
|
||||
}
|
||||
|
||||
else ->
|
||||
if (currentManager == null) {
|
||||
if (!appSettingsService.isCardViewEnabled()) {
|
||||
layoutManager =
|
||||
GridLayoutManager(
|
||||
this,
|
||||
calculateNoOfColumns(),
|
||||
)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
gridLayoutManager()
|
||||
} else {
|
||||
layoutManager =
|
||||
StaggeredGridLayoutManager(
|
||||
calculateNoOfColumns(),
|
||||
StaggeredGridLayoutManager.VERTICAL,
|
||||
)
|
||||
layoutManager.gapStrategy =
|
||||
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
staggererdGridLayoutManager()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,8 +479,8 @@ class HomeActivity :
|
||||
}
|
||||
|
||||
private fun handleListResult(appendResults: Boolean = false) {
|
||||
val oldManager = binding.recyclerView.layoutManager
|
||||
if (appendResults) {
|
||||
val oldManager = binding.recyclerView.layoutManager
|
||||
firstVisible =
|
||||
when (oldManager) {
|
||||
is StaggeredGridLayoutManager ->
|
||||
@@ -496,7 +493,13 @@ class HomeActivity :
|
||||
}
|
||||
}
|
||||
|
||||
if (recyclerAdapter == null) {
|
||||
@Suppress("detekt:ComplexCondition")
|
||||
if (recyclerAdapter == null ||
|
||||
(
|
||||
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
|
||||
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
|
||||
)
|
||||
) {
|
||||
if (appSettingsService.isCardViewEnabled()) {
|
||||
recyclerAdapter =
|
||||
ItemCardAdapter(
|
||||
@@ -543,7 +546,7 @@ class HomeActivity :
|
||||
private fun calculateNoOfColumns(): Int {
|
||||
val displayMetrics = resources.displayMetrics
|
||||
val dpWidth = displayMetrics.widthPixels / displayMetrics.density
|
||||
return (dpWidth / 300).toInt()
|
||||
return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(p0: String?): Boolean {
|
||||
@@ -592,10 +595,11 @@ class HomeActivity :
|
||||
.show()
|
||||
}
|
||||
|
||||
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.issue_tracker -> {
|
||||
baseContext.openUrlInBrowser(AppSettingsService.BUG_URL)
|
||||
baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -30,6 +30,8 @@ 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 {
|
||||
@@ -147,9 +149,10 @@ class LoginActivity :
|
||||
.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)
|
||||
@@ -191,7 +194,7 @@ class LoginActivity :
|
||||
private fun failLoginDetails(
|
||||
password: String,
|
||||
login: String,
|
||||
) {
|
||||
): Boolean {
|
||||
var lastFocusedView: View? = null
|
||||
var cancel = false
|
||||
if (isWithLogin) {
|
||||
@@ -208,16 +211,17 @@ class LoginActivity :
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, lastFocusedView)
|
||||
return cancel
|
||||
}
|
||||
|
||||
private fun failInvalidUrl(url: String) {
|
||||
private fun failInvalidUrl(url: String): Boolean {
|
||||
val focusView = binding.urlView
|
||||
var cancel = false
|
||||
if (url.isBaseUrlInvalid()) {
|
||||
cancel = true
|
||||
binding.urlView.error = getString(R.string.login_url_problem)
|
||||
inValidCount++
|
||||
if (inValidCount == 3) {
|
||||
if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
alertDialog.setTitle(getString(R.string.warning_wrong_url))
|
||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||
@@ -230,6 +234,7 @@ class LoginActivity :
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, focusView)
|
||||
return cancel
|
||||
}
|
||||
|
||||
private fun maybeCancelAndFocusView(
|
||||
|
@@ -10,18 +10,16 @@ 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 com.github.ln_12.library.ConnectivityStatus
|
||||
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
|
||||
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
|
||||
@@ -44,26 +42,21 @@ 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(),
|
||||
isConnectionAvailable,
|
||||
instance(),
|
||||
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()
|
||||
|
||||
// TODO: handle with the "previous" way
|
||||
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
private val connectivityService: ConnectivityService by instance()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -76,13 +69,12 @@ class MyApp :
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(
|
||||
AppLifeCycleObserver(
|
||||
connectivityStatus,
|
||||
repository,
|
||||
connectivityService,
|
||||
),
|
||||
)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
connectivityService.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage =
|
||||
if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
@@ -108,6 +100,7 @@ class MyApp :
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
sendReportsInDevMode = false
|
||||
reportFormat = StringFormat.JSON
|
||||
reportContent =
|
||||
listOf(
|
||||
@@ -187,18 +180,15 @@ class MyApp :
|
||||
}
|
||||
|
||||
class AppLifeCycleObserver(
|
||||
val connectivityStatus: ConnectivityStatus,
|
||||
val repository: Repository,
|
||||
val connectivityService: ConnectivityService,
|
||||
) : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
repository.connectionMonitored = true
|
||||
connectivityStatus.start()
|
||||
connectivityService.start()
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
repository.connectionMonitored = false
|
||||
connectivityStatus.stop()
|
||||
connectivityService.stop()
|
||||
super.onPause(owner)
|
||||
}
|
||||
}
|
||||
|
@@ -53,6 +53,7 @@ class ReaderActivity :
|
||||
showMenuItem(false)
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityReaderBinding.inflate(layoutInflater)
|
||||
@@ -160,12 +161,14 @@ class ReaderActivity :
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
if (!allItems.isNullOrEmpty() && allItems.size >= position) {
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
readItem(allItems[position])
|
||||
}
|
||||
readItem(allItems[position])
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@@ -9,11 +9,9 @@ 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.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
|
||||
@@ -31,7 +29,6 @@ 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)
|
||||
@@ -76,15 +73,10 @@ class UpsertSourceActivity :
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val baseUrl = appSettingsService.getBaseUrl()
|
||||
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
|
||||
mustLoginToAddSource()
|
||||
} else {
|
||||
handleSpoutsSpinner()
|
||||
}
|
||||
handleSpoutsSpinner()
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun handleSpoutsSpinner() {
|
||||
val spoutsKV = HashMap<String, String>()
|
||||
binding.spoutsSpinner.onItemSelectedListener =
|
||||
@@ -156,13 +148,6 @@ 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()
|
||||
|
||||
@@ -173,6 +158,7 @@ class UpsertSourceActivity :
|
||||
sourceDetailsUnavailable -> {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val successfullyAddedSource =
|
||||
|
@@ -118,13 +118,13 @@ class ItemCardAdapter(
|
||||
binding.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
binding.itemImage.visibility = View.VISIBLE
|
||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
}
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -65,10 +65,10 @@ class ItemListAdapter(
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
}
|
||||
} else {
|
||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,8 @@ 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.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
|
||||
@@ -16,6 +15,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi
|
||||
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
|
||||
@@ -31,68 +31,21 @@ class SourcesListAdapter(
|
||||
private val items: ArrayList<SelfossModel.SourceDetail>,
|
||||
) : 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 {
|
||||
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding.root)
|
||||
val binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
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()
|
||||
holder.bind(items[position], position)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
@@ -102,6 +55,72 @@ class SourcesListAdapter(
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
inner class ViewHolder(
|
||||
val mView: ConstraintLayout,
|
||||
) : RecyclerView.ViewHolder(mView)
|
||||
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,
|
||||
) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val successfullyDeletedSource = repository.deleteSource(source.id, source.title)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,8 @@ 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,
|
||||
@@ -61,7 +63,7 @@ class LoadingWorker(
|
||||
handleNewItemsNotification(apiItems, notificationManager)
|
||||
}
|
||||
}
|
||||
apiItems.map { it.preloadImages(context) }
|
||||
apiItems.map { it.preloadImages(context, appSettingsService) }
|
||||
}
|
||||
}
|
||||
return Result.success()
|
||||
@@ -106,11 +108,11 @@ class LoadingWorker(
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||
|
||||
Timer("", false).schedule(4000) {
|
||||
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||
notificationManager.notify(2, newItemsNotification.build())
|
||||
}
|
||||
}
|
||||
Timer("", false).schedule(4000) {
|
||||
Timer("", false).schedule(NOTIFICATION_DELAY) {
|
||||
notificationManager.cancel(1)
|
||||
}
|
||||
}
|
||||
|
@@ -2,18 +2,14 @@ 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
|
||||
@@ -23,7 +19,6 @@ 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
|
||||
@@ -32,25 +27,27 @@ 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.openUrlInBrowser
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask
|
||||
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.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 com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -65,30 +62,38 @@ 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 fontSize: Int = 16
|
||||
private var colorOnSurface: Int = 0
|
||||
private var colorSurface: Int = 0
|
||||
private var fontSize: Int = DEFAULT_FONT_SIZE
|
||||
private lateinit var item: SelfossModel.Item
|
||||
private lateinit var url: String
|
||||
private var url: String? = null
|
||||
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: FloatingActionButton
|
||||
private lateinit var fab: SpeedDialView
|
||||
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()
|
||||
|
||||
@@ -100,6 +105,7 @@ class ArticleFragment :
|
||||
item = pi.toModel()
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod")
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -114,6 +120,9 @@ 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)
|
||||
@@ -127,23 +136,11 @@ class ArticleFragment :
|
||||
allImages = item.getImages()
|
||||
|
||||
fontSize = appSettingsService.getFontSize()
|
||||
staticBar = appSettingsService.isStaticBarEnabled()
|
||||
font = appSettingsService.getFont()
|
||||
|
||||
refreshAlignment()
|
||||
|
||||
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()
|
||||
}
|
||||
handleFloatingToolbar()
|
||||
|
||||
binding.source.text = contentSource
|
||||
if (typeface != null) {
|
||||
@@ -151,28 +148,13 @@ 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")
|
||||
try {
|
||||
maybeIfContext {
|
||||
AlertDialog
|
||||
.Builder(requireContext())
|
||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||
.Builder(it)
|
||||
.setMessage(it.getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(it.getString(R.string.webview_dialog_issue_title))
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
) { _, _ ->
|
||||
@@ -180,8 +162,6 @@ class ArticleFragment :
|
||||
requireActivity().finish()
|
||||
}.create()
|
||||
.show()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +170,8 @@ class ArticleFragment :
|
||||
|
||||
private fun handleContent() {
|
||||
if (contentText.isEmptyOrNullOrNullString()) {
|
||||
if (repository.isNetworkAvailable()) {
|
||||
getContentFromMercury()
|
||||
if (connectivityService.isNetworkAvailable() && url.isUrlValid()) {
|
||||
getContentFromMercury(url!!)
|
||||
}
|
||||
} else {
|
||||
binding.titleView.text = contentTitle
|
||||
@@ -203,72 +183,84 @@ class ArticleFragment :
|
||||
|
||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(contentImage)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) }
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFloatingToolbar(): FloatingToolbar {
|
||||
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
|
||||
}
|
||||
floatingToolbar.attachFab(fab)
|
||||
private fun handleFloatingToolbar() {
|
||||
fab = binding.speedDial
|
||||
fab.mainFabClosedIconColor = colorOnSurface
|
||||
fab.mainFabOpenedIconColor = colorOnSurface
|
||||
|
||||
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
|
||||
maybeIfContext { handleFloatingToolbarActionItems(it) }
|
||||
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MenuItem?) {
|
||||
// We do nothing
|
||||
}
|
||||
},
|
||||
else -> Unit
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFloatingToolbarActionItems(c: Context) {
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.share_action,
|
||||
resources.getDrawable(R.drawable.ic_share_white_24dp),
|
||||
R.string.reader_action_share,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.open_action,
|
||||
resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
|
||||
R.string.reader_action_open,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
fab.addHomeMadeActionItem(
|
||||
R.id.unread_action,
|
||||
resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
|
||||
R.string.unmark,
|
||||
colorOnSurface,
|
||||
colorSurface,
|
||||
c,
|
||||
)
|
||||
return floatingToolbar
|
||||
}
|
||||
|
||||
private fun refreshAlignment() {
|
||||
@@ -280,7 +272,8 @@ class ArticleFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentFromMercury() {
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun getContentFromMercury(url: String) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
@@ -318,16 +311,12 @@ class ArticleFragment :
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private fun handleLeadImage(leadImageUrl: String?) {
|
||||
if (!leadImageUrl.isNullOrEmpty()) {
|
||||
maybeIfContext {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
|
||||
}
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
@@ -341,132 +330,79 @@ class ArticleFragment :
|
||||
view: WebView?,
|
||||
url: String,
|
||||
): Boolean =
|
||||
if (context != null &&
|
||||
url.isUrlValid() &&
|
||||
if (url.isUrlValid() &&
|
||||
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||
) {
|
||||
requireContext().openUrlInBrowser(url)
|
||||
maybeIfContext { it.openUrlInBrowserAsNewTask(url) }
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String,
|
||||
): WebResourceResponse? {
|
||||
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
|
||||
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)
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
try {
|
||||
val image =
|
||||
Glide
|
||||
.with(view)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(url)
|
||||
.submit()
|
||||
.get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
try {
|
||||
val image =
|
||||
Glide
|
||||
.with(view)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(url)
|
||||
.submit()
|
||||
.get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
try {
|
||||
val image = view.getGlideImageForResource(url, appSettingsService)
|
||||
return WebResourceResponse(
|
||||
mime,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, compression),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
|
||||
private fun htmlToWebview() {
|
||||
val context: Context
|
||||
try {
|
||||
context = requireContext()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
return
|
||||
}
|
||||
|
||||
val colorOnSurface = TypedValue()
|
||||
val colorSurface = TypedValue()
|
||||
|
||||
try {
|
||||
maybeIfContext {
|
||||
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||
val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
|
||||
val a: TypedArray = it.obtainStyledAttributes(resId, attrs)
|
||||
|
||||
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||
binding.webcontent.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",
|
||||
0xFFFFFF and (if (colorSurface.data != DATA_NULL_UNDEFINED) colorSurface.data else 0xFFFFFF),
|
||||
WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
|
||||
)
|
||||
|
||||
val colorOnSurfaceString =
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and (if (colorOnSurface.data != DATA_NULL_UNDEFINED) colorOnSurface.data else 0),
|
||||
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
|
||||
)
|
||||
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
handleImageLoading()
|
||||
try {
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
handleImageLoading()
|
||||
|
||||
val gestureDetector =
|
||||
GestureDetector(
|
||||
activity,
|
||||
@@ -480,49 +416,50 @@ class ArticleFragment :
|
||||
event,
|
||||
)
|
||||
}
|
||||
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
|
||||
e.sendSilentlyWithAcraWithName("Gesture detector issue ?")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
var baseUrl: String? = null
|
||||
try {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
|
||||
}
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
|
||||
val fontName =
|
||||
var baseUrl: String? = null
|
||||
try {
|
||||
val itemUrl = URL(url.orEmpty())
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcraWithName("htmlToWebview > ${url.orEmpty()}")
|
||||
}
|
||||
|
||||
val fontName: String =
|
||||
maybeIfContext {
|
||||
when (font) {
|
||||
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"
|
||||
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"
|
||||
else -> ""
|
||||
}
|
||||
}?.toString().orEmpty()
|
||||
|
||||
val fontLinkAndStyle =
|
||||
if (font.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||
fontName.replace(
|
||||
" ",
|
||||
"+",
|
||||
)
|
||||
}" rel="stylesheet">
|
||||
val fontLinkAndStyle =
|
||||
if (fontName.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||
fontName.replace(
|
||||
" ",
|
||||
"+",
|
||||
)
|
||||
}" rel="stylesheet">
|
||||
|<style>
|
||||
| * {
|
||||
| font-family: '$fontName';
|
||||
| }
|
||||
|</style>
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
try {
|
||||
binding.webcontent.loadDataWithBaseURL(
|
||||
baseUrl,
|
||||
"""<html>
|
||||
@@ -539,7 +476,7 @@ class ArticleFragment :
|
||||
| color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and context.resources.getColor(R.color.colorAccent),
|
||||
WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int),
|
||||
)
|
||||
} !important;
|
||||
| }
|
||||
@@ -596,10 +533,8 @@ class ArticleFragment :
|
||||
|
||||
private fun openInBrowserAfterFailing() {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
try {
|
||||
requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
} catch (e: IllegalStateException) {
|
||||
e.sendSilentlyWithAcraWithName("Context required is null")
|
||||
maybeIfContext {
|
||||
it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -16,11 +15,13 @@ 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.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
|
||||
@@ -33,12 +34,15 @@ 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 {
|
||||
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
|
||||
|
||||
@@ -56,8 +60,8 @@ class FilterSheetFragment :
|
||||
|
||||
try {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
handleTagChips(requireContext())
|
||||
handleSourceChips(requireContext())
|
||||
handleTagChips()
|
||||
handleSourceChips()
|
||||
|
||||
binding.progressBar2.visibility = GONE
|
||||
binding.filterView.visibility = VISIBLE
|
||||
@@ -75,17 +79,24 @@ class FilterSheetFragment :
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private suspend fun handleSourceChips(context: Context) {
|
||||
private suspend fun handleSourceChips() {
|
||||
val sourceGroup = binding.sourcesGroup
|
||||
|
||||
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
|
||||
val c = Chip(context)
|
||||
val c: Chip? =
|
||||
maybeIfContext {
|
||||
Chip(it)
|
||||
} as Chip?
|
||||
|
||||
if (c == null) {
|
||||
return
|
||||
}
|
||||
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
|
||||
Glide
|
||||
.with(context)
|
||||
.load(source.getIcon(repository.baseUrl))
|
||||
.into(
|
||||
maybeIfContext {
|
||||
it.imageIntoViewTarget(
|
||||
source.getIcon(repository.baseUrl),
|
||||
object : ViewTarget<Chip?, Drawable?>(c) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
@@ -98,7 +109,9 @@ class FilterSheetFragment :
|
||||
}
|
||||
}
|
||||
},
|
||||
appSettingsService,
|
||||
)
|
||||
}
|
||||
|
||||
c.text = source.title.getHtmlDecoded()
|
||||
|
||||
@@ -134,13 +147,17 @@ class FilterSheetFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTagChips(context: Context) {
|
||||
private suspend fun handleTagChips() {
|
||||
val tagGroup = binding.tagsGroup
|
||||
|
||||
val tags = repository.getTags()
|
||||
|
||||
tags.forEachIndexed { _, tag ->
|
||||
val c = Chip(context)
|
||||
val c: Chip? = maybeIfContext { Chip(it) } as Chip?
|
||||
if (c == null) {
|
||||
return
|
||||
}
|
||||
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
c.text = tag.tag
|
||||
|
||||
@@ -156,8 +173,8 @@ class FilterSheetFragment :
|
||||
}
|
||||
gd.setColor(gdColor)
|
||||
gd.shape = GradientDrawable.RECTANGLE
|
||||
gd.setSize(30, 30)
|
||||
gd.cornerRadius = 30F
|
||||
gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
|
||||
gd.cornerRadius = DRAWABLE_SIZE.toFloat()
|
||||
c.chipIcon = gd
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
|
||||
|
@@ -6,13 +6,19 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
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
|
||||
|
||||
class ImageFragment : Fragment() {
|
||||
class ImageFragment :
|
||||
Fragment(),
|
||||
DIAware {
|
||||
override val di: DI by closestDI()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
private lateinit var imageUrl: String
|
||||
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
private var _binding: FragmentImageBinding? = null
|
||||
val binding get() = _binding
|
||||
|
||||
@@ -31,12 +37,7 @@ class ImageFragment : Fragment() {
|
||||
val view = binding?.root
|
||||
|
||||
binding!!.photoView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireActivity())
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(imageUrl)
|
||||
.into(binding!!.photoView)
|
||||
requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService)
|
||||
|
||||
return view
|
||||
}
|
||||
|
@@ -3,26 +3,21 @@ 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): Boolean {
|
||||
fun SelfossModel.Item.preloadImages(
|
||||
context: Context,
|
||||
appSettingsService: AppSettingsService,
|
||||
): Boolean {
|
||||
val imageUrls = this.getImages()
|
||||
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
|
||||
|
||||
try {
|
||||
for (url in imageUrls) {
|
||||
if (URLUtil.isValidUrl(url)) {
|
||||
Glide
|
||||
.with(context)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(url)
|
||||
.submit()
|
||||
context.preloadImage(url, appSettingsService)
|
||||
}
|
||||
}
|
||||
} catch (e: Error) {
|
||||
|
@@ -26,6 +26,10 @@ 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,
|
||||
@@ -143,7 +147,7 @@ class SettingsActivity :
|
||||
InputFilter { source, _, _, dest, _, _ ->
|
||||
try {
|
||||
val input: Int = (dest.toString() + source.toString()).toInt()
|
||||
if (input in 1..200) return@InputFilter null
|
||||
if (input in MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
Toast
|
||||
.makeText(
|
||||
|
@@ -2,24 +2,60 @@ 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,
|
||||
) {
|
||||
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),
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -15,12 +15,12 @@ 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.toStringUriWithHttp
|
||||
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
fun Context.openItemUrl(
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
linkDecoded: String?,
|
||||
articleViewer: Boolean,
|
||||
app: Activity,
|
||||
) {
|
||||
@@ -37,12 +37,13 @@ fun Context.openItemUrl(
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
} else {
|
||||
this.openUrlInBrowserAsNewTask(linkDecoded)
|
||||
this.openUrlInBrowserAsNewTask(linkDecoded!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
fun String?.isUrlValid(): Boolean =
|
||||
!this.isEmptyOrNullOrNullString() && this!!.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
|
||||
fun String.isBaseUrlInvalid(): Boolean {
|
||||
val baseUrl = this.toHttpUrlOrNull()
|
||||
@@ -56,14 +57,16 @@ fun String.isBaseUrlInvalid(): Boolean {
|
||||
}
|
||||
|
||||
fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
|
||||
this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
|
||||
this.openUrlInBrowserAsNewTask(i.getLinkDecoded())
|
||||
}
|
||||
|
||||
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.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.openUrlInBrowser(url: String) {
|
||||
@@ -72,6 +75,7 @@ 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"
|
||||
): Boolean = crashReportData.get("BRAND") != "redroid" && !crashReportData.get("PHONE_MODEL").toString().startsWith("sdk_gphone")
|
||||
}
|
||||
|
@@ -1,6 +1,13 @@
|
||||
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("")
|
||||
@@ -9,3 +16,25 @@ 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,42 +2,135 @@ 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)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.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)
|
||||
.load(url.toGlideUrl(appSettingsService))
|
||||
.into(view.imageView)
|
||||
}
|
||||
|
||||
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
|
||||
|
||||
fun getBitmapInputStream(
|
||||
bitmap: Bitmap,
|
||||
compressFormat: Bitmap.CompressFormat,
|
||||
): InputStream {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
|
||||
bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
|
||||
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
||||
return ByteArrayInputStream(bitmapData)
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
lateinit var s: Snackbar
|
||||
@@ -11,19 +10,13 @@ lateinit var s: Snackbar
|
||||
fun isNetworkAccessible(context: Context): Boolean {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val network = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
|
||||
|
||||
return when {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
val network = connectivityManager.activeNetworkInfo ?: return false
|
||||
return network.isConnectedOrConnecting
|
||||
return when {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@@ -1,32 +0,0 @@
|
||||
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,35 +71,13 @@
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
<com.leinardi.android.speeddial.SpeedDialView
|
||||
android:id="@+id/speedDial"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
android:layout_gravity="bottom|end"
|
||||
app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progressBar"
|
||||
@@ -119,4 +97,5 @@
|
||||
android:progressTint="?attr/colorAccent" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/unread_action"
|
||||
android:icon="@drawable/ic_baseline_white_eye_24dp"
|
||||
android:title="@string/unmark"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_action"
|
||||
android:icon="@drawable/ic_open_in_browser_white_24dp"
|
||||
android:title="@string/reader_action_open"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/share_action"
|
||||
android:icon="@drawable/ic_share_white_24dp"
|
||||
android:title="@string/reader_action_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Quant a"</string>
|
||||
<string name="marked_as_read">"Element llegit"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Über"</string>
|
||||
<string name="marked_as_read">"Artikel gelesen"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<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>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<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>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Acerca de"</string>
|
||||
<string name="marked_as_read">"Elemento lido"</string>
|
||||
<string name="marked_as_unread">"Elemento non lido"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Tentang"</string>
|
||||
<string name="marked_as_read">"Membaca item"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Informazioni"</string>
|
||||
<string name="marked_as_read">"Articolo letto"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"정보"</string>
|
||||
<string name="marked_as_read">"항목 읽기"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -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="addStringNoUrl">"Login om bronnen toe te voegen"</string>
|
||||
<string name="undo_string">"Ongedaan maken"</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,11 +105,7 @@
|
||||
<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>
|
||||
@@ -131,5 +127,6 @@
|
||||
<string name="action_about">"Over"</string>
|
||||
<string name="marked_as_read">"Artikel gelezen"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
<string name="undo_string">"Ongedaan maken"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Sobre"</string>
|
||||
<string name="marked_as_read">"Item lido"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Sobre"</string>
|
||||
<string name="marked_as_read">"Item lido"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"මේ ගැන"</string>
|
||||
<string name="marked_as_read">"Item read"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"Hakkında"</string>
|
||||
<string name="marked_as_read">"Öğeleri oku"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"关于我们"</string>
|
||||
<string name="marked_as_read">"已读"</string>
|
||||
<string name="marked_as_unread">"未读条目"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -24,7 +24,6 @@
|
||||
<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>
|
||||
@@ -106,11 +105,7 @@
|
||||
<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>
|
||||
@@ -132,4 +127,6 @@
|
||||
<string name="action_about">"关于我们"</string>
|
||||
<string name="marked_as_read">"已读"</string>
|
||||
<string name="marked_as_unread">"未讀項目"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
6
androidApp/src/main/res/values/ids.xml
Normal file
6
androidApp/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="unread_action" type="id" />
|
||||
<item name="open_action" type="id" />
|
||||
<item name="share_action" type="id" />
|
||||
</resources>
|
@@ -23,7 +23,6 @@
|
||||
<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>
|
||||
@@ -108,11 +107,7 @@
|
||||
<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>
|
||||
@@ -134,4 +129,6 @@
|
||||
<string name="action_about">"About"</string>
|
||||
<string name="marked_as_read">"Item read"</string>
|
||||
<string name="marked_as_unread">"Item unread"</string>
|
||||
</resources>
|
||||
<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>
|
||||
|
@@ -30,14 +30,6 @@
|
||||
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,3 +1,5 @@
|
||||
@file:Suppress("detekt:LargeClass")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.tests.repository
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||
@@ -9,6 +11,7 @@ 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
|
||||
@@ -22,7 +25,6 @@ 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
|
||||
@@ -42,23 +44,20 @@ private const val FEED_URL = "https://test.com/feed"
|
||||
|
||||
private const val TAGS = "Test, New"
|
||||
|
||||
private val NUMBER_ARTICLES = 100
|
||||
private val NUMBER_UNREAD = 50
|
||||
private val NUMBER_STARRED = 20
|
||||
private const val NUMBER_ARTICLES = 100
|
||||
private const val NUMBER_UNREAD = 50
|
||||
private const val NUMBER_STARRED = 20
|
||||
|
||||
class RepositoryTest {
|
||||
private val db = mockk<ReaderForSelfossDB>(relaxed = true)
|
||||
private val appSettingsService = mockk<AppSettingsService>()
|
||||
private val api = mockk<SelfossApi>()
|
||||
private val connectivityService = mockk<ConnectivityService>()
|
||||
private lateinit var repository: Repository
|
||||
|
||||
private fun initializeRepository(
|
||||
isConnectionAvailable: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(
|
||||
true,
|
||||
),
|
||||
) {
|
||||
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
|
||||
private fun initializeRepository(isNetworkAvailable: Boolean = true) {
|
||||
every { connectivityService.isNetworkAvailable() } returns isNetworkAvailable
|
||||
repository = Repository(api, appSettingsService, connectivityService, db)
|
||||
|
||||
runBlocking {
|
||||
repository.updateApiInformation()
|
||||
@@ -108,7 +107,7 @@ class RepositoryTest {
|
||||
fun instantiate_repository_without_api_version() {
|
||||
every { appSettingsService.getApiVersion() } returns -1
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
|
||||
coVerify(exactly = 0) { api.apiInformation() }
|
||||
coVerify(exactly = 0) { api.stats() }
|
||||
@@ -285,7 +284,7 @@ class RepositoryTest {
|
||||
fun get_newer_items_without_connectivity() {
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
runBlocking {
|
||||
repository.getNewerItems()
|
||||
}
|
||||
@@ -312,7 +311,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
|
||||
runBlocking {
|
||||
repository.getNewerItems()
|
||||
@@ -340,7 +339,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
repository.setSourceFilter(
|
||||
SelfossModel.SourceDetail(
|
||||
1,
|
||||
@@ -455,7 +454,7 @@ class RepositoryTest {
|
||||
|
||||
var success: Boolean
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
runBlocking {
|
||||
success = repository.reloadBadges()
|
||||
}
|
||||
@@ -475,7 +474,7 @@ class RepositoryTest {
|
||||
|
||||
var success: Boolean
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
runBlocking {
|
||||
success = repository.reloadBadges()
|
||||
}
|
||||
@@ -570,7 +569,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -588,7 +587,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -605,7 +604,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -623,7 +622,7 @@ class RepositoryTest {
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testTags: List<SelfossModel.Tag>
|
||||
runBlocking {
|
||||
testTags = repository.getTags()
|
||||
@@ -773,7 +772,7 @@ class RepositoryTest {
|
||||
@Test
|
||||
fun get_sources_without_connection() {
|
||||
val (_, sourcesDB) = prepareSources()
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -790,7 +789,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns true
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -807,7 +806,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns true
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -824,7 +823,7 @@ class RepositoryTest {
|
||||
|
||||
every { appSettingsService.isItemCachingEnabled() } returns false
|
||||
every { appSettingsService.isUpdateSourcesEnabled() } returns false
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var testSources: List<SelfossModel.Source>
|
||||
runBlocking {
|
||||
testSources = repository.getSourcesDetails()
|
||||
@@ -896,7 +895,7 @@ class RepositoryTest {
|
||||
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
|
||||
SuccessResponse(true)
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response =
|
||||
@@ -953,7 +952,7 @@ class RepositoryTest {
|
||||
fun delete_source_without_connection() {
|
||||
coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.deleteSource(5, "src")
|
||||
@@ -1026,7 +1025,7 @@ class RepositoryTest {
|
||||
data = "undocumented...",
|
||||
)
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.updateRemote()
|
||||
@@ -1068,7 +1067,7 @@ class RepositoryTest {
|
||||
fun login_but_without_connection() {
|
||||
coEvery { api.login() } returns SuccessResponse(success = true)
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
var response: Boolean
|
||||
runBlocking {
|
||||
response = repository.login()
|
||||
@@ -1148,7 +1147,7 @@ class RepositoryTest {
|
||||
coEvery { api.getItems(any(), any(), any(), any(), any(), any(), any()) } returns
|
||||
StatusAndData(success = false, data = generateTestApiItem())
|
||||
|
||||
initializeRepository(MutableStateFlow(false))
|
||||
initializeRepository(false)
|
||||
prepareSearch()
|
||||
runBlocking {
|
||||
repository.tryToCacheItemsAndGetNewOnes()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
//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)
|
||||
// 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)
|
||||
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,7 +16,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.register("clean", Delete::class) {
|
||||
delete(layout.buildDirectory)
|
||||
}
|
||||
@@ -24,4 +23,4 @@ tasks.register("clean", Delete::class) {
|
||||
dependencies {
|
||||
kover(project(":shared"))
|
||||
kover(project(":androidApp"))
|
||||
}
|
||||
}
|
||||
|
786
detekt.yml
Normal file
786
detekt.yml
Normal file
@@ -0,0 +1,786 @@
|
||||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: false
|
||||
weights:
|
||||
# complexity: 2
|
||||
# LongParameterList: 1
|
||||
# style: 1
|
||||
# comments: 1
|
||||
|
||||
config:
|
||||
validation: true
|
||||
warningsAsErrors: false
|
||||
checkExhaustiveness: false
|
||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||
excludes: ''
|
||||
|
||||
processors:
|
||||
active: true
|
||||
exclude:
|
||||
- 'DetektProgressListener'
|
||||
# - 'KtFileCountProcessor'
|
||||
# - 'PackageCountProcessor'
|
||||
# - 'ClassCountProcessor'
|
||||
# - 'FunctionCountProcessor'
|
||||
# - 'PropertyCountProcessor'
|
||||
# - 'ProjectComplexityProcessor'
|
||||
# - 'ProjectCognitiveComplexityProcessor'
|
||||
# - 'ProjectLLOCProcessor'
|
||||
# - 'ProjectCLOCProcessor'
|
||||
# - 'ProjectLOCProcessor'
|
||||
# - 'ProjectSLOCProcessor'
|
||||
# - 'LicenseHeaderLoaderExtension'
|
||||
|
||||
console-reports:
|
||||
active: true
|
||||
exclude:
|
||||
- 'ProjectStatisticsReport'
|
||||
- 'ComplexityReport'
|
||||
- 'NotificationReport'
|
||||
- 'FindingsReport'
|
||||
- 'FileBasedFindingsReport'
|
||||
# - 'LiteFindingsReport'
|
||||
|
||||
output-reports:
|
||||
active: true
|
||||
exclude:
|
||||
# - 'TxtOutputReport'
|
||||
# - 'XmlOutputReport'
|
||||
# - 'HtmlOutputReport'
|
||||
# - 'MdOutputReport'
|
||||
# - 'SarifOutputReport'
|
||||
|
||||
comments:
|
||||
active: true
|
||||
AbsentOrWrongFileLicense:
|
||||
active: false
|
||||
licenseTemplateFile: 'license.template'
|
||||
licenseTemplateIsRegex: false
|
||||
CommentOverPrivateFunction:
|
||||
active: false
|
||||
CommentOverPrivateProperty:
|
||||
active: false
|
||||
DeprecatedBlockTag:
|
||||
active: false
|
||||
EndOfSentenceFormat:
|
||||
active: false
|
||||
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
||||
KDocReferencesNonPublicProperty:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
OutdatedDocumentation:
|
||||
active: false
|
||||
matchTypeParameters: true
|
||||
matchDeclarationsOrder: true
|
||||
allowParamOnConstructorProperties: false
|
||||
UndocumentedPublicClass:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
searchInNestedClass: true
|
||||
searchInInnerClass: true
|
||||
searchInInnerObject: true
|
||||
searchInInnerInterface: true
|
||||
searchInProtectedClass: false
|
||||
UndocumentedPublicFunction:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
searchProtectedFunction: false
|
||||
UndocumentedPublicProperty:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
searchProtectedProperty: false
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
CognitiveComplexMethod:
|
||||
active: false
|
||||
threshold: 15
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 4
|
||||
ComplexInterface:
|
||||
active: false
|
||||
threshold: 10
|
||||
includeStaticDeclarations: false
|
||||
includePrivateDeclarations: false
|
||||
ignoreOverloaded: false
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
ignoreSingleWhenExpression: false
|
||||
ignoreSimpleWhenEntries: false
|
||||
ignoreNestingFunctions: false
|
||||
nestingFunctions:
|
||||
- 'also'
|
||||
- 'apply'
|
||||
- 'forEach'
|
||||
- 'isNotNull'
|
||||
- 'ifNull'
|
||||
- 'let'
|
||||
- 'run'
|
||||
- 'use'
|
||||
- 'with'
|
||||
LabeledExpression:
|
||||
active: false
|
||||
ignoredLabels: [ ]
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 600
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 60
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 6
|
||||
constructorThreshold: 7
|
||||
ignoreDefaultParameters: false
|
||||
ignoreDataClasses: true
|
||||
ignoreAnnotatedParameter: [ ]
|
||||
MethodOverloading:
|
||||
active: false
|
||||
threshold: 6
|
||||
NamedArguments:
|
||||
active: false
|
||||
threshold: 3
|
||||
ignoreArgumentsMatchingNames: false
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 4
|
||||
NestedScopeFunctions:
|
||||
active: false
|
||||
threshold: 1
|
||||
functions:
|
||||
- 'kotlin.apply'
|
||||
- 'kotlin.run'
|
||||
- 'kotlin.with'
|
||||
- 'kotlin.let'
|
||||
- 'kotlin.also'
|
||||
ReplaceSafeCallChainWithRun:
|
||||
active: false
|
||||
StringLiteralDuplication:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
threshold: 3
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/android/*Activity.kt', '**/fragments/*Fragment.kt' ]
|
||||
thresholdInFiles: 11
|
||||
thresholdInClasses: 11
|
||||
thresholdInInterfaces: 11
|
||||
thresholdInObjects: 11
|
||||
thresholdInEnums: 11
|
||||
ignoreDeprecated: false
|
||||
ignorePrivate: false
|
||||
ignoreOverridden: false
|
||||
ignoreAnnotatedFunctions: [ ]
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: false
|
||||
InjectDispatcher:
|
||||
active: true
|
||||
dispatcherNames:
|
||||
- 'IO'
|
||||
- 'Default'
|
||||
- 'Unconfined'
|
||||
RedundantSuspendModifier:
|
||||
active: true
|
||||
SleepInsteadOfDelay:
|
||||
active: true
|
||||
SuspendFunSwallowedCancellation:
|
||||
active: false
|
||||
SuspendFunWithCoroutineScopeReceiver:
|
||||
active: false
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: true
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: true
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyTryBlock:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames:
|
||||
- 'equals'
|
||||
- 'finalize'
|
||||
- 'hashCode'
|
||||
- 'toString'
|
||||
InstanceOfCheckForException:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
NotImplementedDeclaration:
|
||||
active: false
|
||||
ObjectExtendsThrowable:
|
||||
active: false
|
||||
PrintStackTrace:
|
||||
active: true
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
ignoreLabeled: false
|
||||
SwallowedException:
|
||||
active: true
|
||||
ignoredExceptionTypes:
|
||||
- 'InterruptedException'
|
||||
- 'MalformedURLException'
|
||||
- 'NumberFormatException'
|
||||
- 'ParseException'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: false
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
exceptions:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Exception'
|
||||
- 'IllegalArgumentException'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'IllegalStateException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'NullPointerException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
exceptionNames:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'NullPointerException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptionNames:
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
|
||||
naming:
|
||||
active: true
|
||||
BooleanPropertyNaming:
|
||||
active: false
|
||||
allowedPattern: '^(is|has|are)'
|
||||
ClassNaming:
|
||||
active: true
|
||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||
ConstructorParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
EnumNaming:
|
||||
active: true
|
||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: false
|
||||
forbiddenName: [ ]
|
||||
FunctionMaxLength:
|
||||
active: false
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: false
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
functionPattern: '[a-z][a-zA-Z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
FunctionParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
InvalidPackageDeclaration:
|
||||
active: true
|
||||
rootPackage: ''
|
||||
requireRootInDeclaration: false
|
||||
LambdaParameterNaming:
|
||||
active: false
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*|_'
|
||||
MatchingDeclarationName:
|
||||
active: false # done in ktlint
|
||||
mustBeFirst: true
|
||||
MemberNameEqualsClassName:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
NoNameShadowing:
|
||||
active: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: false
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: false
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: false
|
||||
minimumVariableNameLength: 1
|
||||
VariableNaming:
|
||||
active: true
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ArrayPrimitive:
|
||||
active: true
|
||||
CouldBeSequence:
|
||||
active: false
|
||||
threshold: 3
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
SpreadOperator:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
UnnecessaryPartOfBinaryExpression:
|
||||
active: false
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
AvoidReferentialEquality:
|
||||
active: true
|
||||
forbiddenTypePatterns:
|
||||
- 'kotlin.String'
|
||||
CastNullableToNonNullableType:
|
||||
active: false
|
||||
CastToNullableType:
|
||||
active: false
|
||||
Deprecation:
|
||||
active: false
|
||||
DontDowncastCollectionTypes:
|
||||
active: false
|
||||
DoubleMutabilityForCollection:
|
||||
active: true
|
||||
mutableTypes:
|
||||
- 'kotlin.collections.MutableList'
|
||||
- 'kotlin.collections.MutableMap'
|
||||
- 'kotlin.collections.MutableSet'
|
||||
- 'java.util.ArrayList'
|
||||
- 'java.util.LinkedHashSet'
|
||||
- 'java.util.HashSet'
|
||||
- 'java.util.LinkedHashMap'
|
||||
- 'java.util.HashMap'
|
||||
ElseCaseInsteadOfExhaustiveWhen:
|
||||
active: false
|
||||
ignoredSubjectTypes: [ ]
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: false
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: true
|
||||
IgnoredReturnValue:
|
||||
active: true
|
||||
restrictToConfig: true
|
||||
returnValueAnnotations:
|
||||
- 'CheckResult'
|
||||
- '*.CheckResult'
|
||||
- 'CheckReturnValue'
|
||||
- '*.CheckReturnValue'
|
||||
ignoreReturnValueAnnotations:
|
||||
- 'CanIgnoreReturnValue'
|
||||
- '*.CanIgnoreReturnValue'
|
||||
returnValueTypes:
|
||||
- 'kotlin.sequences.Sequence'
|
||||
- 'kotlinx.coroutines.flow.*Flow'
|
||||
- 'java.util.stream.*Stream'
|
||||
ignoreFunctionCall: [ ]
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: false
|
||||
allowExplicitReturnType: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
ignoreOnClassesPattern: ''
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: true
|
||||
MissingPackageDeclaration:
|
||||
active: false
|
||||
excludes: [ '**/*.kts' ]
|
||||
NullCheckOnMutableProperty:
|
||||
active: false
|
||||
NullableToStringCall:
|
||||
active: false
|
||||
PropertyUsedBeforeDeclaration:
|
||||
active: false
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: false
|
||||
UnnecessaryNotNullCheck:
|
||||
active: false
|
||||
UnnecessaryNotNullOperator:
|
||||
active: true
|
||||
UnnecessarySafeCall:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: true
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ]
|
||||
UnsafeCast:
|
||||
active: true
|
||||
UnusedUnaryOperator:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: true
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
AlsoCouldBeApply:
|
||||
active: false
|
||||
BracesOnIfStatements:
|
||||
active: false
|
||||
singleLine: 'never'
|
||||
multiLine: 'always'
|
||||
BracesOnWhenStatements:
|
||||
active: false
|
||||
singleLine: 'necessary'
|
||||
multiLine: 'consistent'
|
||||
CanBeNonNullable:
|
||||
active: false
|
||||
CascadingCallWrapping:
|
||||
active: false
|
||||
includeElvis: true
|
||||
ClassOrdering:
|
||||
active: false
|
||||
CollapsibleIfStatements:
|
||||
active: false
|
||||
DataClassContainsFunctions:
|
||||
active: false
|
||||
conversionFunctionPrefix:
|
||||
- 'to'
|
||||
allowOperators: false
|
||||
DataClassShouldBeImmutable:
|
||||
active: false
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: true
|
||||
maxDestructuringEntries: 3
|
||||
DoubleNegativeLambda:
|
||||
active: false
|
||||
negativeFunctions:
|
||||
- reason: 'Use `takeIf` instead.'
|
||||
value: 'takeUnless'
|
||||
- reason: 'Use `all` instead.'
|
||||
value: 'none'
|
||||
negativeFunctionNameParts:
|
||||
- 'not'
|
||||
- 'non'
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: false
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: false
|
||||
ExplicitItLambdaParameter:
|
||||
active: true
|
||||
ExpressionBodySyntax:
|
||||
active: false
|
||||
includeLineWrapping: false
|
||||
ForbiddenAnnotation:
|
||||
active: false
|
||||
annotations:
|
||||
- reason: 'it is a java annotation. Use `Suppress` instead.'
|
||||
value: 'java.lang.SuppressWarnings'
|
||||
- reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
|
||||
value: 'java.lang.Deprecated'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
|
||||
value: 'java.lang.annotation.Documented'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
|
||||
value: 'java.lang.annotation.Target'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
|
||||
value: 'java.lang.annotation.Retention'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
|
||||
value: 'java.lang.annotation.Repeatable'
|
||||
- reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
|
||||
value: 'java.lang.annotation.Inherited'
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
comments:
|
||||
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
|
||||
value: 'FIXME:'
|
||||
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
|
||||
value: 'STOPSHIP:'
|
||||
- reason: 'Forbidden TODO todo marker in comment, please do the changes.'
|
||||
value: 'TODO:'
|
||||
allowedPatterns: ''
|
||||
ForbiddenImport:
|
||||
active: false
|
||||
imports: [ ]
|
||||
forbiddenPatterns: ''
|
||||
ForbiddenMethodCall:
|
||||
active: false
|
||||
methods:
|
||||
- reason: 'print does not allow you to configure the output stream. Use a logger instead.'
|
||||
value: 'kotlin.io.print'
|
||||
- reason: 'println does not allow you to configure the output stream. Use a logger instead.'
|
||||
value: 'kotlin.io.println'
|
||||
ForbiddenSuppress:
|
||||
active: false
|
||||
rules: [ ]
|
||||
ForbiddenVoid:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
ignoreUsageInGenerics: false
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
ignoreActualFunction: true
|
||||
excludedFunctions: [ ]
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: true
|
||||
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ]
|
||||
ignoreNumbers:
|
||||
- '-1'
|
||||
- '0'
|
||||
- '1'
|
||||
- '2'
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: false
|
||||
ignoreLocalVariableDeclaration: false
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: false
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: false
|
||||
ignoreRanges: false
|
||||
ignoreExtensionFunctions: true
|
||||
MandatoryBracesLoops:
|
||||
active: false
|
||||
MaxChainedCallsOnSameLine:
|
||||
active: false
|
||||
maxChainedCalls: 5
|
||||
MaxLineLength:
|
||||
active: false # done in ktlint
|
||||
maxLineLength: 140 # default is 120. 140 to match ktlint
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: false
|
||||
excludeRawStrings: true
|
||||
MayBeConst:
|
||||
active: true
|
||||
ModifierOrder:
|
||||
active: true
|
||||
MultilineLambdaItParameter:
|
||||
active: false
|
||||
MultilineRawStringIndentation:
|
||||
active: false
|
||||
indentSize: 4
|
||||
trimmingMethods:
|
||||
- 'trimIndent'
|
||||
- 'trimMargin'
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: false # done in ktlint
|
||||
NoTabs:
|
||||
active: false
|
||||
NullableBooleanCheck:
|
||||
active: false
|
||||
ObjectLiteralToLambda:
|
||||
active: true
|
||||
OptionalAbstractKeyword:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: false
|
||||
PreferToOverPairSyntax:
|
||||
active: false
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: false
|
||||
RedundantHigherOrderMapUsage:
|
||||
active: true
|
||||
RedundantVisibilityModifierRule:
|
||||
active: false
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludedFunctions:
|
||||
- 'equals'
|
||||
excludeLabeled: false
|
||||
excludeReturnFromLambda: true
|
||||
excludeGuardClauses: false
|
||||
SafeCast:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: true
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: false
|
||||
StringShouldBeRawString:
|
||||
active: false
|
||||
maxEscapedCharacterCount: 2
|
||||
ignoredCharacters: [ ]
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludeGuardClauses: false
|
||||
TrailingWhitespace:
|
||||
active: false
|
||||
TrimMultilineRawString:
|
||||
active: false
|
||||
trimmingMethods:
|
||||
- 'trimIndent'
|
||||
- 'trimMargin'
|
||||
UnderscoresInNumericLiterals:
|
||||
active: false
|
||||
acceptableLength: 4
|
||||
allowNonStandardGrouping: false
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
UnnecessaryAnnotationUseSiteTarget:
|
||||
active: false
|
||||
UnnecessaryApply:
|
||||
active: true
|
||||
UnnecessaryBackticks:
|
||||
active: false
|
||||
UnnecessaryBracesAroundTrailingLambda:
|
||||
active: false
|
||||
UnnecessaryFilter:
|
||||
active: true
|
||||
UnnecessaryInheritance:
|
||||
active: true
|
||||
UnnecessaryInnerClass:
|
||||
active: false
|
||||
UnnecessaryLet:
|
||||
active: false
|
||||
UnnecessaryParentheses:
|
||||
active: false
|
||||
allowForUnclearPrecedence: false
|
||||
UntilInsteadOfRangeTo:
|
||||
active: false
|
||||
UnusedImports:
|
||||
active: false
|
||||
UnusedParameter:
|
||||
active: true
|
||||
allowedNames: 'ignored|expected'
|
||||
UnusedPrivateClass:
|
||||
active: true
|
||||
UnusedPrivateMember:
|
||||
active: true
|
||||
allowedNames: ''
|
||||
UnusedPrivateProperty:
|
||||
active: true
|
||||
allowedNames: '_|ignored|expected|serialVersionUID'
|
||||
excludes: [ '**/build.gradle.kts' ]
|
||||
UseAnyOrNoneInsteadOfFind:
|
||||
active: true
|
||||
UseArrayLiteralsInAnnotations:
|
||||
active: true
|
||||
UseCheckNotNull:
|
||||
active: true
|
||||
UseCheckOrError:
|
||||
active: true
|
||||
UseDataClass:
|
||||
active: false
|
||||
allowVars: false
|
||||
UseEmptyCounterpart:
|
||||
active: false
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: false
|
||||
UseIfInsteadOfWhen:
|
||||
active: false
|
||||
ignoreWhenContainingVariableDeclaration: false
|
||||
UseIsNullOrEmpty:
|
||||
active: true
|
||||
UseLet:
|
||||
active: false
|
||||
UseOrEmpty:
|
||||
active: true
|
||||
UseRequire:
|
||||
active: true
|
||||
UseRequireNotNull:
|
||||
active: true
|
||||
UseSumOfInsteadOfFlatMapSize:
|
||||
active: false
|
||||
UselessCallOnNotNull:
|
||||
active: true
|
||||
UtilityClassWithPublicConstructor:
|
||||
active: true
|
||||
VarCouldBeVal:
|
||||
active: true
|
||||
ignoreLateinitVar: false
|
||||
WildcardImport:
|
||||
active: true
|
||||
excludeImports:
|
||||
- 'java.util.*'
|
@@ -0,0 +1,4 @@
|
||||
**v125010111**
|
||||
|
||||
- Debug trying to fix context issues. (#174)
|
||||
- Changelog for v125010031
|
@@ -0,0 +1,5 @@
|
||||
**v125010131**
|
||||
|
||||
- fix: reload the adapter when it's needed. Fixes #128. (#176)
|
||||
- feat: basic auth and images loading. Fixes #172. (#175)
|
||||
- Changelog for v125010111
|
@@ -0,0 +1,6 @@
|
||||
**v125010201**
|
||||
|
||||
- fix: Handle empty url issue.
|
||||
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
|
||||
- chore: changing actions in reader fragment.
|
||||
- Changelog for v125010131
|
@@ -0,0 +1,8 @@
|
||||
**v125010241**
|
||||
|
||||
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
|
||||
- refactor: context fragments issues.
|
||||
- logs: Context issues.
|
||||
- fix: Handle empty url issue, again.
|
||||
- fix: Link not opening.
|
||||
- Changelog for v125010201
|
@@ -0,0 +1,7 @@
|
||||
**v125020411**
|
||||
|
||||
- Merge pull request 'bump' (#182) from bump into master
|
||||
- chore: non transiant R classes.
|
||||
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
|
||||
- bump
|
||||
- fix: One more missing context.
|
@@ -0,0 +1,7 @@
|
||||
**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
|
@@ -0,0 +1,4 @@
|
||||
**v125020581**
|
||||
|
||||
- fix: url can be empty ?
|
||||
- Changelog for v125020471
|
12
fastlane/metadata/android/en-US/changelogs/v125030681.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/v125030681.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
**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
|
@@ -18,12 +18,12 @@ kotlin.code.style=official
|
||||
#Android
|
||||
android.useAndroidX=true
|
||||
#android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.enableJetifier=false
|
||||
android.nonTransitiveRClass=true
|
||||
#MPP
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
ignoreGitVersion=false
|
||||
kotlin.native.cacheKind.iosX64=none
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.configureondemand=true
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Mon Nov 25 22:48:24 CET 2024
|
||||
#Sun Feb 09 14:44:52 CET 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -4,7 +4,6 @@ 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 {
|
||||
@@ -41,13 +40,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
|
||||
@@ -55,6 +54,10 @@ 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 {
|
||||
@@ -114,4 +117,4 @@ sqldelight {
|
||||
packageName.set("bou.amine.apps.readerforselfossv2.dao")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,12 +9,14 @@ 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,6 +3,7 @@ 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,16 +12,14 @@ actual fun SelfossModel.Item.getIcon(baseUrl: String): String = constructUrl(bas
|
||||
|
||||
actual fun SelfossModel.Item.getThumbnail(baseUrl: String): String = constructUrl(baseUrl, "thumbnails", thumbnail)
|
||||
|
||||
val IMAGE_EXTENSION_REGEXP = """\.(jpg|jpeg|png|webp)""".toRegex()
|
||||
|
||||
actual fun SelfossModel.Item.getImages(): ArrayList<String> {
|
||||
val allImages = ArrayList<String>()
|
||||
|
||||
for (image in Jsoup.parse(content).getElementsByTag("img")) {
|
||||
val url = image.attr("src")
|
||||
if (url.lowercase(Locale.US).contains(".jpg") ||
|
||||
url.lowercase(Locale.US).contains(".jpeg") ||
|
||||
url.lowercase(Locale.US).contains(".png") ||
|
||||
url.lowercase(Locale.US).contains(".webp")
|
||||
) {
|
||||
if (IMAGE_EXTENSION_REGEXP.containsMatchIn(url.lowercase(Locale.US))) {
|
||||
allImages.add(url)
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,16 @@
|
||||
@file:Suppress("detekt:LongParameterList")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class MercuryModel {
|
||||
@Suppress("detekt:ConstructorParameterNaming")
|
||||
@Serializable
|
||||
class ParsedContent(
|
||||
val title: String? = null,
|
||||
val content: String? = null,
|
||||
val lead_image_url: String? = null, // NOSONAR
|
||||
val lead_image_url: String? = null,
|
||||
val url: String? = null,
|
||||
val error: Boolean? = null,
|
||||
val message: String? = null,
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("detekt:LongParameterList")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.model
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
||||
@@ -18,6 +20,10 @@ import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class ModelException(
|
||||
message: String,
|
||||
) : Throwable(message)
|
||||
|
||||
class SelfossModel {
|
||||
@Serializable
|
||||
data class Tag(
|
||||
@@ -121,8 +127,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=")
|
||||
@@ -140,11 +146,7 @@ class SelfossModel {
|
||||
stringUrl = "http:$stringUrl"
|
||||
}
|
||||
|
||||
if (stringUrl.isEmptyOrNullOrNullString()) {
|
||||
throw Exception("Link $link was translated to $stringUrl, but was empty. Handle this.")
|
||||
}
|
||||
|
||||
return stringUrl
|
||||
return if (stringUrl.isEmptyOrNullOrNullString()) null else stringUrl
|
||||
}
|
||||
|
||||
fun sourceAuthorAndDate(): String {
|
||||
@@ -170,7 +172,7 @@ class SelfossModel {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this seems to be super slow.
|
||||
// this seems to be super slow.
|
||||
object TagsListSerializer : KSerializer<List<String>> {
|
||||
override fun deserialize(decoder: Decoder): List<String> =
|
||||
when (val json = ((decoder as JsonDecoder).decodeJsonElement())) {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("detekt:TooManyFunctions")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.repository
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.dao.ACTION
|
||||
@@ -11,6 +13,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.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.toEntity
|
||||
@@ -23,14 +26,15 @@ 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,
|
||||
val isConnectionAvailable: MutableStateFlow<Boolean>,
|
||||
private val connectivityService: ConnectivityService,
|
||||
private val db: ReaderForSelfossDB,
|
||||
) {
|
||||
var items = ArrayList<SelfossModel.Item>()
|
||||
var connectionMonitored = false
|
||||
|
||||
var baseUrl = appSettingsService.getBaseUrl()
|
||||
|
||||
@@ -59,7 +63,7 @@ class Repository(
|
||||
|
||||
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
|
||||
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
fetchedItems =
|
||||
api.getItems(
|
||||
displayedItems.type,
|
||||
@@ -98,7 +102,7 @@ class Repository(
|
||||
|
||||
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
|
||||
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
val offset = items.size
|
||||
fetchedItems =
|
||||
api.getItems(
|
||||
@@ -118,7 +122,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
|
||||
return if (isNetworkAvailable()) {
|
||||
return if (connectivityService.isNetworkAvailable()) {
|
||||
val items =
|
||||
api.getItems(
|
||||
itemType.type,
|
||||
@@ -127,7 +131,7 @@ class Repository(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
200,
|
||||
MAX_ITEMS_NUMBER,
|
||||
)
|
||||
return if (items.success && items.data != null) {
|
||||
items.data
|
||||
@@ -139,9 +143,10 @@ class Repository(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("detekt:ForbiddenComment")
|
||||
suspend fun reloadBadges(): Boolean {
|
||||
var success = false
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
val response = api.stats()
|
||||
if (response.success && response.data != null) {
|
||||
_badgeUnread.value = response.data.unread ?: 0
|
||||
@@ -163,7 +168,7 @@ class Repository(
|
||||
suspend fun getTags(): List<SelfossModel.Tag> {
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
return if (isNetworkAvailable() && !fetchedTags) {
|
||||
return if (connectivityService.isNetworkAvailable() && !fetchedTags) {
|
||||
val apiTags = api.tags()
|
||||
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
|
||||
resetDBTagsWithData(apiTags.data)
|
||||
@@ -180,7 +185,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
val spouts = api.spouts()
|
||||
if (spouts.success && spouts.data != null) {
|
||||
spouts.data
|
||||
@@ -196,7 +201,7 @@ class Repository(
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
|
||||
if (shouldFetch && isNetworkAvailable()) {
|
||||
if (shouldFetch && connectivityService.isNetworkAvailable()) {
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
val apiSources = api.sourcesStats()
|
||||
if (apiSources.success && apiSources.data != null) {
|
||||
@@ -218,7 +223,7 @@ class Repository(
|
||||
val isDatabaseEnabled =
|
||||
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
|
||||
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
|
||||
if (shouldFetch && isNetworkAvailable()) {
|
||||
if (shouldFetch && connectivityService.isNetworkAvailable()) {
|
||||
val apiSources = api.sourcesDetailed()
|
||||
if (apiSources.success && apiSources.data != null) {
|
||||
fetchedSources = true
|
||||
@@ -243,7 +248,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun markAsReadById(id: Int): Boolean =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
api.markAsRead(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), read = true)
|
||||
@@ -260,7 +265,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun unmarkAsReadById(id: Int): Boolean =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
api.unmarkAsRead(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), unread = true)
|
||||
@@ -277,7 +282,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun starrById(id: Int): Boolean =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
api.starr(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), starred = true)
|
||||
@@ -294,7 +299,7 @@ class Repository(
|
||||
}
|
||||
|
||||
private suspend fun unstarrById(id: Int): Boolean =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
api.unstarr(id.toString()).isSuccess
|
||||
} else {
|
||||
insertDBAction(id.toString(), starred = true)
|
||||
@@ -304,7 +309,8 @@ class Repository(
|
||||
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
|
||||
var success = false
|
||||
|
||||
if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
|
||||
if (connectivityService.isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess
|
||||
) {
|
||||
success = true
|
||||
for (item in items) {
|
||||
markAsReadLocally(item)
|
||||
@@ -364,7 +370,7 @@ class Repository(
|
||||
tags: String,
|
||||
): Boolean {
|
||||
var response = false
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
response = api
|
||||
.createSourceForVersion(
|
||||
title,
|
||||
@@ -385,7 +391,7 @@ class Repository(
|
||||
tags: String,
|
||||
): Boolean {
|
||||
var response = false
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
|
||||
}
|
||||
|
||||
@@ -397,13 +403,13 @@ class Repository(
|
||||
title: String,
|
||||
): Boolean {
|
||||
var success = false
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
val response = api.deleteSource(id)
|
||||
success = response.isSuccess
|
||||
}
|
||||
|
||||
// We filter on success or if the network isn't available
|
||||
if (success || !isNetworkAvailable()) {
|
||||
if (success || !connectivityService.isNetworkAvailable()) {
|
||||
items = ArrayList(items.filter { it.sourcetitle != title })
|
||||
setReaderItems(items)
|
||||
db.itemsQueries.deleteItemsWhereSource(title)
|
||||
@@ -413,7 +419,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun updateRemote(): Boolean =
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
api.update().data.equals("finished")
|
||||
} else {
|
||||
false
|
||||
@@ -421,7 +427,7 @@ class Repository(
|
||||
|
||||
suspend fun login(): Boolean {
|
||||
var result = false
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
try {
|
||||
val response = api.login()
|
||||
result = response.isSuccess == true
|
||||
@@ -434,7 +440,7 @@ class Repository(
|
||||
|
||||
suspend fun checkIfFetchFails(): Boolean {
|
||||
var fetchFailed = true
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.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
|
||||
@@ -448,7 +454,7 @@ class Repository(
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
try {
|
||||
val response = api.logout()
|
||||
if (!response.isSuccess) {
|
||||
@@ -476,7 +482,7 @@ class Repository(
|
||||
suspend fun updateApiInformation() {
|
||||
val apiMajorVersion = appSettingsService.getApiVersion()
|
||||
|
||||
if (isNetworkAvailable()) {
|
||||
if (connectivityService.isNetworkAvailable()) {
|
||||
val fetchedInformation = api.apiInformation()
|
||||
if (fetchedInformation.success && fetchedInformation.data != null) {
|
||||
if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
|
||||
@@ -495,8 +501,6 @@ 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)
|
||||
@@ -559,6 +563,7 @@ class Repository(
|
||||
item.id.toString(),
|
||||
)
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item> {
|
||||
try {
|
||||
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
|
||||
|
@@ -33,6 +33,7 @@ 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,3 +1,5 @@
|
||||
@file:Suppress("detekt:TooManyFunctions", "detekt:LongParameterList", "detekt:LargeClass")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
@@ -35,6 +37,8 @@ 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,
|
||||
) {
|
||||
@@ -176,7 +180,7 @@ class SelfossApi(
|
||||
},
|
||||
)
|
||||
|
||||
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
|
||||
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= VERSION_WHERE_POST_LOGIN_SHOULD_WORK // We are missing 4.1.0
|
||||
|
||||
suspend fun logout(): SuccessResponse =
|
||||
if (shouldHaveNewLogout()) {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("detekt:TooManyFunctions")
|
||||
|
||||
package bou.amine.apps.readerforselfossv2.service
|
||||
|
||||
import com.russhwolf.settings.Settings
|
||||
|
@@ -1,7 +1,19 @@
|
||||
@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,
|
||||
) {
|
||||
@@ -36,12 +48,11 @@ class AppSettingsService(
|
||||
private var notifyNewItems: Boolean? = null
|
||||
private var itemsNumber: Int? = null
|
||||
private var apiTimeout: Long? = null
|
||||
private var refreshMinutes: Long = 360
|
||||
private var refreshMinutes: Long = DEFAULT_REFRESH_MINUTES
|
||||
private var markOnScroll: Boolean? = null
|
||||
private var activeAlignment: Int? = null
|
||||
|
||||
private var fontSize: Int? = null
|
||||
private var staticBar: Boolean? = null
|
||||
private var font: String = ""
|
||||
private var theme: Int? = null
|
||||
|
||||
@@ -141,13 +152,14 @@ class AppSettingsService(
|
||||
return itemsNumber!!
|
||||
}
|
||||
|
||||
@Suppress("detekt:SwallowedException")
|
||||
private fun refreshItemsNumber() {
|
||||
itemsNumber =
|
||||
try {
|
||||
settings.getString(API_ITEMS_NUMBER, "20").toInt()
|
||||
settings.getString(API_ITEMS_NUMBER, DEFAULT_ITEMS_NUMBER.toString()).toInt()
|
||||
} catch (e: Exception) {
|
||||
settings.remove(API_ITEMS_NUMBER)
|
||||
20
|
||||
DEFAULT_ITEMS_NUMBER
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,22 +170,24 @@ 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, "60")
|
||||
val settingsTimeout = settings.getString(API_TIMEOUT, DEFAULT_API_TIMEOUT.toString())
|
||||
if (settingsTimeout.toLong() > 0) {
|
||||
settingsTimeout.toLong()
|
||||
} else {
|
||||
settings.remove(API_TIMEOUT)
|
||||
60
|
||||
DEFAULT_API_TIMEOUT
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
settings.remove(API_TIMEOUT)
|
||||
60
|
||||
DEFAULT_API_TIMEOUT
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -287,14 +301,14 @@ class AppSettingsService(
|
||||
}
|
||||
|
||||
private fun refreshRefreshMinutes() {
|
||||
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, "360").toLong()
|
||||
if (refreshMinutes <= 15) {
|
||||
refreshMinutes = 15
|
||||
refreshMinutes = settings.getString(PERIODIC_REFRESH_MINUTES, DEFAULT_REFRESH_MINUTES.toString()).toLong()
|
||||
if (refreshMinutes <= MIN_REFRESH_MINUTES) {
|
||||
refreshMinutes = MIN_REFRESH_MINUTES
|
||||
}
|
||||
}
|
||||
|
||||
fun getRefreshMinutes(): Long {
|
||||
if (refreshMinutes != 360L) {
|
||||
if (refreshMinutes != DEFAULT_REFRESH_MINUTES) {
|
||||
refreshRefreshMinutes()
|
||||
}
|
||||
return refreshMinutes
|
||||
@@ -368,18 +382,7 @@ class AppSettingsService(
|
||||
if (fontSize != null) {
|
||||
refreshFontSize()
|
||||
}
|
||||
return fontSize ?: 16
|
||||
}
|
||||
|
||||
private fun refreshStaticBarEnabled() {
|
||||
staticBar = settings.getBoolean(READER_STATIC_BAR, false)
|
||||
}
|
||||
|
||||
fun isStaticBarEnabled(): Boolean {
|
||||
if (staticBar != null) {
|
||||
refreshStaticBarEnabled()
|
||||
}
|
||||
return staticBar == true
|
||||
return fontSize ?: DEFAULT_FONT_SIZE
|
||||
}
|
||||
|
||||
private fun refreshFont() {
|
||||
@@ -434,7 +437,6 @@ class AppSettingsService(
|
||||
refreshActiveAllignment()
|
||||
refreshFontSize()
|
||||
refreshFont()
|
||||
refreshStaticBarEnabled()
|
||||
refreshCurrentTheme()
|
||||
}
|
||||
|
||||
@@ -532,8 +534,6 @@ 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"
|
||||
|
@@ -0,0 +1,46 @@
|
||||
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.Main).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,6 +4,12 @@ import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
|
||||
class DateParseException(
|
||||
message: String,
|
||||
e: Throwable? = null,
|
||||
) : Throwable(message, e)
|
||||
|
||||
@Suppress("detekt:ThrowsCount")
|
||||
fun String.toParsedDate(): Long {
|
||||
// Possible formats are
|
||||
// yyyy-mm-dd hh:mm:ss format
|
||||
@@ -21,17 +27,18 @@ fun String.toParsedDate(): Long {
|
||||
.find(this)
|
||||
?.groups
|
||||
?.get(1)
|
||||
?.value ?: throw Exception("Couldn't parse $this")
|
||||
?.value ?: throw DateParseException("Couldn't parse $this")
|
||||
} else {
|
||||
throw Exception("Unrecognized format for $this")
|
||||
throw DateParseException("Unrecognized format for $this")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw Exception("parseDate failed for $this", e)
|
||||
throw DateParseException("parseDate failed for $this", e)
|
||||
}
|
||||
|
||||
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
|
||||
}
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
expect class DateUtils() {
|
||||
companion object {
|
||||
fun parseRelativeDate(dateString: String): String
|
||||
|
@@ -17,7 +17,7 @@ fun SOURCE.toView(): SelfossModel.SourceDetail =
|
||||
this.id.toInt(),
|
||||
this.title,
|
||||
null,
|
||||
this.tags?.split(","),
|
||||
this.tags.split(","),
|
||||
this.spout,
|
||||
this.error,
|
||||
this.icon,
|
||||
@@ -74,6 +74,7 @@ fun SelfossModel.Item.toEntity(): ITEM =
|
||||
this.author,
|
||||
)
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
fun SelfossModel.Tag.getColorHexCode(): String =
|
||||
if (this.color.length == 4) { // #000
|
||||
val char1 = this.color.get(1)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.utils
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
enum class ItemType(
|
||||
val position: Int,
|
||||
val type: String,
|
||||
|
@@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.utils
|
||||
|
||||
fun String?.isEmptyOrNullOrNullString(): Boolean = this == null || this == "null" || this.isEmpty()
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
fun String.longHash(): Long {
|
||||
var h = 98764321261L
|
||||
val l = this.length
|
||||
|
@@ -2,5 +2,6 @@ package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import io.ktor.client.engine.cio.CIOEngineConfig
|
||||
|
||||
@Suppress("detekt:EmptyFunctionBlock")
|
||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.utils
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
actual class DateUtils {
|
||||
actual companion object {
|
||||
actual fun parseRelativeDate(dateString: String): String {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.utils
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
actual class DateUtils actual constructor() {
|
||||
actual companion object {
|
||||
actual fun parseRelativeDate(dateString: String): String {
|
@@ -2,5 +2,6 @@ package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import io.ktor.client.engine.cio.CIOEngineConfig
|
||||
|
||||
@Suppress("detekt:EmptyFunctionBlock")
|
||||
actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package bou.amine.apps.readerforselfossv2.utils
|
||||
|
||||
@Suppress("detekt:UtilityClassWithPublicConstructor")
|
||||
actual class DateUtils {
|
||||
actual companion object {
|
||||
actual fun parseRelativeDate(dateString: String): String {
|
||||
|
Reference in New Issue
Block a user