Compare commits

...

35 Commits

Author SHA1 Message Date
1893904135 fix: Handle empty url issue.
All checks were successful
Check master code / build (push) Successful in 10m53s
Create tag / build (push) Successful in 7m40s
Create tag / createTagAndChangelog (push) Successful in 38s
Create tag / release (push) Successful in 5m3s
2025-01-19 14:49:37 +01:00
a4cb28ba81 Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
All checks were successful
Check master code / build (push) Successful in 11m40s
Reviewed-on: #177
2025-01-19 13:35:18 +00:00
ae3cada1c7 chore: changing actions in reader fragment.
All checks were successful
Check PR code / Lint (pull_request) Successful in 3m21s
Check PR code / build (pull_request) Successful in 11m38s
2025-01-19 12:54:25 +00:00
309500276f Changelog for v125010131
All checks were successful
Check master code / build (push) Successful in 8m58s
2025-01-13 16:19:05 +00:00
ce255b23cd fix: reload the adapter when it's needed. Fixes #128. (#176)
All checks were successful
Check master code / build (push) Successful in 10m27s
Create tag / build (push) Successful in 7m41s
Create tag / createTagAndChangelog (push) Successful in 48s
Create tag / release (push) Successful in 5m28s
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Reviewed-on: #176
Co-authored-by: Amine <amine.bouabdallaoui@pm.me>
Co-committed-by: Amine <amine.bouabdallaoui@pm.me>
2025-01-13 15:08:39 +00:00
3b3a575dae feat: basic auth and images loading. Fixes #172. (#175)
All checks were successful
Check master code / build (push) Successful in 9m58s
Check PR code / Lint (pull_request) Successful in 3m15s
Check PR code / build (pull_request) Successful in 10m34s
## Types of changes

- [ ] I have read the **CONTRIBUTING** document.
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
- [ ] This is **NOT** translation related.

This closes issue #XXX

This is implements feature #YYY

This finishes chore #ZZZ

Reviewed-on: #175
Co-authored-by: Amine <amine.bouabdallaoui@pm.me>
Co-committed-by: Amine <amine.bouabdallaoui@pm.me>
2025-01-13 07:39:13 +00:00
7bcf4574b4 Changelog for v125010111
All checks were successful
Check master code / build (push) Successful in 9m15s
2025-01-11 20:54:28 +00:00
c79ab5e92b Debug trying to fix context issues. (#174)
All checks were successful
Check master code / build (push) Successful in 7m44s
Create tag / build (push) Successful in 6m27s
Create tag / createTagAndChangelog (push) Successful in 37s
Create tag / release (push) Successful in 4m27s
Reviewed-on: #174
Co-authored-by: Amine <amine.bouabdallaoui@pm.me>
Co-committed-by: Amine <amine.bouabdallaoui@pm.me>
2025-01-11 20:35:27 +00:00
54dbda76ab Changelog for v125010031
All checks were successful
Check master code / build (push) Successful in 15m56s
2025-01-03 09:09:18 +00:00
11c39ae87c Merge pull request 'Bump dependencies' (#173) from upgarde into master
All checks were successful
Check master code / build (push) Successful in 19m42s
Create tag / build (push) Successful in 27m4s
Create tag / createTagAndChangelog (push) Successful in 52s
Create tag / release (push) Successful in 7m41s
Reviewed-on: #173
2025-01-03 08:41:15 +00:00
6645902ec8 chore: "faster" action.
All checks were successful
Check PR code / Lint (pull_request) Successful in 5m59s
Check PR code / build (pull_request) Successful in 14m16s
2025-01-03 09:19:12 +01:00
0a07a5dfad fastlane: icon change.
Some checks failed
Check PR code / Lint (pull_request) Has been cancelled
Check PR code / build (pull_request) Has been cancelled
2025-01-03 09:18:06 +01:00
d88d38fd3b chore: ignoring a pixel issue.
All checks were successful
Check PR code / Lint (pull_request) Successful in 9m15s
Check PR code / build (pull_request) Successful in 23m30s
2025-01-02 20:53:00 +01:00
28fe38aa17 test: fixed an ui test issue.
All checks were successful
Check PR code / Lint (pull_request) Successful in 11m23s
Check PR code / build (pull_request) Successful in 19m41s
2025-01-02 20:32:53 +01:00
d524c30732 fix: center the loading thing.
All checks were successful
Check PR code / Lint (pull_request) Successful in 5m35s
Check PR code / build (pull_request) Successful in 18m15s
2024-12-31 15:31:47 +01:00
8c00aa65da test: items displaying.
All checks were successful
Check PR code / Lint (pull_request) Successful in 9m8s
Check PR code / build (pull_request) Successful in 36m8s
2024-12-31 15:23:26 +01:00
ae81261cb1 bump: sqldelight.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m47s
Check PR code / build (pull_request) Successful in 15m9s
2024-12-31 12:37:36 +01:00
03c567ee33 bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m37s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:45:11 +01:00
d23dd82fc2 bump: androix and coroutines.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m50s
Check PR code / build (pull_request) Successful in 13m29s
2024-12-31 11:06:36 +01:00
2e7a168424 bump: ktor. Closes #67.
All checks were successful
Check PR code / Lint (pull_request) Successful in 47s
Check PR code / build (pull_request) Successful in 15m22s
2024-12-31 10:03:21 +01:00
5bc2f614af Changelog for v124123651
All checks were successful
Check master code / build (push) Successful in 12m31s
2024-12-30 22:54:15 +00:00
934c112db5 Merge pull request 'Bugfixes' (#171) from bugfixes into master
All checks were successful
Check master code / build (push) Successful in 19m51s
Create tag / build (push) Successful in 21m4s
Create tag / createTagAndChangelog (push) Successful in 50s
Create tag / release (push) Successful in 3m39s
Reviewed-on: #171
2024-12-30 22:32:11 +00:00
ad7549a89f config: crowdin
All checks were successful
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 10m35s
2024-12-30 23:20:03 +01:00
fb9ceecabd chore: can links be empty ?
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m3s
Check PR code / build (pull_request) Successful in 11m14s
2024-12-30 22:45:18 +01:00
61b9fd30e0 fix: Context issues in article fragment. 2024-12-30 22:37:36 +01:00
806e56e20b fix: Context issues in fragment sheet.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m7s
Check PR code / build (pull_request) Successful in 12m15s
2024-12-30 22:12:38 +01:00
cd8b7aaf9d fix: build. 2024-12-30 22:12:21 +01:00
c25ad7621e chore: compile issue fix.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m10s
Check PR code / build (pull_request) Successful in 9m57s
2024-12-30 15:07:35 +01:00
63da3b9fe7 chore: filter some bugs. 2024-12-30 15:07:18 +01:00
1d99eeb633 bugfix: catch users using something other than selfoss. 2024-12-30 13:51:45 +01:00
162a350a8f bugfix: No browser, no link. 2024-12-30 13:51:14 +01:00
27c1bba146 translations
All checks were successful
Check PR code / Lint (pull_request) Successful in 54s
Check PR code / build (pull_request) Successful in 9m26s
2024-12-30 13:49:42 +01:00
b7f3a9877a chore: remove log. 2024-12-30 12:58:34 +01:00
47f78754dc translation
Some checks failed
Check PR code / Lint (pull_request) Successful in 46s
Check PR code / build (pull_request) Failing after 3m20s
2024-12-30 12:48:42 +01:00
1bdfb143ac Changelog for v124123641
All checks were successful
Check master code / coverage (push) Successful in 11m32s
Check master code / build (push) Successful in 15m2s
2024-12-29 22:02:12 +00:00
115 changed files with 2950 additions and 1854 deletions

36
.editorconfig Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,65 @@
**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
- chore: "faster" action.
- fastlane: icon change.
- chore: ignoring a pixel issue.
- test: fixed an ui test issue.
- fix: center the loading thing.
- test: items displaying.
- bump: sqldelight.
- bump: material, desugar jdk, jsoup, kodein, settings, napier, mock.
- bump: androix and coroutines.
- bump: ktor. Closes #67.
- Changelog for v124123651
--------------------------------------------------------------------
**v124123651
- Merge pull request 'Bugfixes' (#171) from bugfixes into master
- config: crowdin
- chore: can links be empty ?
- fix: Context issues in article fragment.
- fix: Context issues in fragment sheet.
- fix: build.
- chore: compile issue fix.
- chore: filter some bugs.
- bugfix: catch users using something other than selfoss.
- bugfix: No browser, no link.
- translations
- chore: remove log.
- translation
- Changelog for v124123641
--------------------------------------------------------------------
**v124123641
- Chore: no tests on build.
- Merge pull request 'testing' (#170) from testing into master
- fix: Displaying fixes. Fixes #155
- test: coverage
- chore: update and use multiplatform datetime
- Changelog for v124123421
--------------------------------------------------------------------
**v124123421 **v124123421
- fix: Trying to fix the serialization issue. - fix: Trying to fix the serialization issue.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.app.Activity
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
@ -23,46 +22,43 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class LoginActivityTest { class LoginActivityTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
private fun getActivity(): Activity? {
var activity: Activity? = null
activityRule.scenario.onActivity {
activity = it
}
return activity
}
@Before @Before
fun registerIdlingResource() { fun registerIdlingResource() {
IdlingRegistry.getInstance() IdlingRegistry
.getInstance()
.register(CountingIdlingResourceSingleton.countingIdlingResource) .register(CountingIdlingResourceSingleton.countingIdlingResource)
} }
@After @After
fun unregisterIdlingResource() { fun unregisterIdlingResource() {
IdlingRegistry.getInstance() IdlingRegistry
.getInstance()
.unregister(CountingIdlingResourceSingleton.countingIdlingResource) .unregister(CountingIdlingResourceSingleton.countingIdlingResource)
} }
@Test @Test
fun viewIsInitialized() { fun viewIsInitialized() {
onView(withId(R.id.urlView)).check(matches(isDisplayed())) onView(withId(R.id.urlView)).check(matches(isDisplayed()))
onView(withId(R.id.selfSigned)).check(matches(isDisplayed())).check(matches(isNotChecked())) onView(withId(R.id.selfSigned))
.check(matches(isDisplayed()))
.check(matches(isNotChecked()))
.check( .check(
matches(isClickable()) matches(isClickable()),
) )
onView(withId(R.id.withLogin)).check(matches(isDisplayed())) onView(withId(R.id.withLogin))
.check(matches(isNotChecked())).check( .check(matches(isDisplayed()))
matches(isClickable()) .check(matches(isNotChecked()))
.check(
matches(isClickable()),
) )
} }
@Test @Test
fun urlError() { fun urlError() {
performLogin("172.17.0.1:8888") performLogin("10.0.2.2:8888")
onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
} }
@ -80,4 +76,4 @@ class LoginActivityTest {
performLogin() performLogin()
onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed()))
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isChecked
@ -25,11 +26,9 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
class SettingsActivityGeneralTest { class SettingsActivityGeneralTest {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@ -37,80 +36,77 @@ class SettingsActivityGeneralTest {
fun init() { fun init() {
loginAndInitHome() loginAndInitHome()
openActionBarOverflowOrOptionsMenu( openActionBarOverflowOrOptionsMenu(
ApplicationProvider.getApplicationContext() ApplicationProvider.getApplicationContext(),
) )
onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.title_activity_settings)).perform(click())
onView(withText(R.string.pref_header_general)).perform(click()) onView(withText(R.string.pref_header_general)).perform(click())
} }
@Suppress("detekt:LongMethod")
@Test @Test
fun testGeneral() { fun testGeneral() {
onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed())) onView(withText(R.string.pref_api_items_number_title)).check(matches(isDisplayed()))
onView( onView(
withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title) withSettingsCheckboxWidget(R.string.pref_general_infinite_loading_title),
).check( ).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed())) onView(withText(R.string.pref_general_category_links)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check( onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isChecked() isDisplayed(),
) isChecked(),
) ),
) ),
onView(withSettingsCheckboxWidget(R.string.reader_static_bar_title)).check(
matches(
allOf(
isDisplayed(), not(isChecked())
)
)
)
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
) )
onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed())) onView(withText(R.string.pref_general_category_displaying)).check(matches(isDisplayed()))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check( onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxWidget(R.string.card_height_title)).check( onView(withSettingsCheckboxWidget(R.string.card_height_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check( onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(
matches( matches(
not(isEnabled()) not(isEnabled()),
) ),
) )
onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check( onView(withSettingsCheckboxWidget(R.string.switch_unread_count_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), isChecked() isDisplayed(),
) isChecked(),
) ),
),
) )
onView(withId(R.id.settings)).perform(swipeUp())
onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check( onView(withSettingsCheckboxWidget(R.string.display_all_counts_title)).check(
matches( matches(
allOf( allOf(
isDisplayed(), not(isChecked()) isDisplayed(),
) not(isChecked()),
) ),
),
) )
} }
@Suppress("detekt:ForbiddenComment")
@Test @Test
fun testGeneralActionsNumberItems() { fun testGeneralActionsNumberItems() {
onView(withText(R.string.pref_api_items_number_title)).perform(click()) onView(withText(R.string.pref_api_items_number_title)).perform(click())
@ -118,25 +114,25 @@ class SettingsActivityGeneralTest {
// Value check // Value check
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("AVC")) ).perform(replaceText("AVC"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("-1")) ).perform(replaceText("-1"))
.check(matches(withText(""))) .check(matches(withText("")))
// TODO: should check message error. Not working for api level 30+ // TODO: should check message error. Not working for api level 30+
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("300")) ).perform(replaceText("300"))
.check(matches(withText(""))) .check(matches(withText("")))
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(typeTextIntoFocusedView("300")) ).perform(typeTextIntoFocusedView("300"))
.check(matches(withText("30"))) .check(matches(withText("30")))
onView( onView(
withId(android.R.id.edit) withId(android.R.id.edit),
).perform(replaceText("10")) ).perform(replaceText("10"))
.check(matches(withText("10"))) .check(matches(withText("10")))
onView(isRoot()).perform(ViewActions.pressBack()) onView(isRoot()).perform(ViewActions.pressBack())
@ -152,21 +148,8 @@ class SettingsActivityGeneralTest {
@Test @Test
fun testGeneralActionsCheckboxes() { fun testGeneralActionsCheckboxes() {
// article viewer settings
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
isEnabled()
)
)
onView(withSettingsCheckboxWidget(R.string.pref_article_viewer_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.reader_static_bar_title)).check(
matches(
not(isEnabled())
)
)
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled()))) onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(not(isEnabled())))
onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click()) onView(withSettingsCheckboxWidget(R.string.pref_switch_card_view_title)).perform(click())
onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled())) onView(withSettingsCheckboxFrame(R.string.card_height_title)).check(matches(isEnabled()))
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -32,6 +31,7 @@ import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
@ -49,7 +49,12 @@ import org.kodein.di.instance
import java.security.MessageDigest import java.security.MessageDigest
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { private const val MIN_WIDTH_CARD_DP = 300
class HomeActivity :
AppCompatActivity(),
SearchView.OnQueryTextListener,
DIAware {
private var items: ArrayList<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var elementsShown: ItemType = ItemType.UNREAD private var elementsShown: ItemType = ItemType.UNREAD
@ -171,11 +176,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab() getElementsAccordingToTab()
} }
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
"Found null when swiping at positon $position.", this@HomeActivity,
Toast.LENGTH_LONG, "Found null when swiping at positon $position.",
).show() Toast.LENGTH_LONG,
).show()
} }
} }
} }
@ -196,19 +202,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
@Suppress("detekt:LongMethod")
private fun handleBottomBar() { private fun handleBottomBar() {
tabNewBadge = tabNewBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
tabArchiveBadge = tabArchiveBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
tabStarredBadge = tabStarredBadge =
TextBadgeItem() TextBadgeItem()
.setText("") .setText("")
.setHideOnSelect(false).hide(false) .setHideOnSelect(false)
.hide(false)
if (appSettingsService.isDisplayUnreadCountEnabled()) { if (appSettingsService.isDisplayUnreadCountEnabled()) {
lifecycleScope.launch { lifecycleScope.launch {
@ -236,14 +246,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_fiber_new_black_24dp, R.drawable.ic_tab_fiber_new_black_24dp,
getString(R.string.tab_new), getString(R.string.tab_new),
) ).setBadgeItem(tabNewBadge)
.setBadgeItem(tabNewBadge)
val tabArchive = val tabArchive =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_archive_black_24dp, R.drawable.ic_tab_archive_black_24dp,
getString(R.string.tab_read), getString(R.string.tab_read),
) ).setBadgeItem(tabArchiveBadge)
.setBadgeItem(tabArchiveBadge)
val tabStarred = val tabStarred =
BottomNavigationItem( BottomNavigationItem(
R.drawable.ic_tab_favorite_black_24dp, R.drawable.ic_tab_favorite_black_24dp,
@ -277,7 +285,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleBottomBarActions() handleBottomBarActions()
handleGDPRDialog(appSettingsService.settings.getBoolean("GDPR_shown", false)) handleGdprDialog(appSettingsService.settings.getBoolean("GDPR_shown", false))
handleRecurringTask() handleRecurringTask()
CountingIdlingResourceSingleton.increment() CountingIdlingResourceSingleton.increment()
@ -289,10 +297,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
getElementsAccordingToTab() getElementsAccordingToTab()
} }
private fun handleGDPRDialog(GDPRShown: Boolean) { private fun handleGdprDialog(gdprShown: Boolean) {
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256") val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(appSettingsService.getBaseUrl().toByteArray()) messageDigest.update(appSettingsService.getBaseUrl().toByteArray())
if (!GDPRShown) { if (!gdprShown) {
val alertDialog = AlertDialog.Builder(this).create() val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.gdpr_dialog_title)) alertDialog.setTitle(getString(R.string.gdpr_dialog_title))
alertDialog.setMessage(getString(R.string.gdpr_dialog_message)) alertDialog.setMessage(getString(R.string.gdpr_dialog_message))
@ -309,50 +317,44 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadLayoutManager() { private fun reloadLayoutManager() {
val currentManager = binding.recyclerView.layoutManager val currentManager = binding.recyclerView.layoutManager
val layoutManager: RecyclerView.LayoutManager
// This will only update the layout manager if settings changed fun gridLayoutManager() {
val layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
}
fun staggererdGridLayoutManager() {
var layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
}
when (currentManager) { when (currentManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = gridLayoutManager()
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} }
is GridLayoutManager -> is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
layoutManager = staggererdGridLayoutManager()
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
} }
else -> else ->
if (currentManager == null) { if (currentManager == null) {
if (!appSettingsService.isCardViewEnabled()) { if (!appSettingsService.isCardViewEnabled()) {
layoutManager = gridLayoutManager()
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} else { } else {
layoutManager = staggererdGridLayoutManager()
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
} }
} }
} }
@ -425,17 +427,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener) binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
} }
private fun getLastVisibleItem(): Int { private fun getLastVisibleItem(): Int =
return when (val manager = binding.recyclerView.layoutManager) { when (val manager = binding.recyclerView.layoutManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
manager.findLastCompletelyVisibleItemPositions( manager
null, .findLastCompletelyVisibleItemPositions(
).last() null,
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition() is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0 else -> 0
} }
}
private fun mayBeEmpty() = private fun mayBeEmpty() =
if (items.isEmpty()) { if (items.isEmpty()) {
@ -477,8 +479,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
private fun handleListResult(appendResults: Boolean = false) { private fun handleListResult(appendResults: Boolean = false) {
val oldManager = binding.recyclerView.layoutManager
if (appendResults) { if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
firstVisible = firstVisible =
when (oldManager) { when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
@ -491,7 +493,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
} }
} }
if (recyclerAdapter == null) { @Suppress("detekt:ComplexCondition")
if (recyclerAdapter == null ||
(
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
)
) {
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
recyclerAdapter = recyclerAdapter =
ItemCardAdapter( ItemCardAdapter(
@ -538,7 +546,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun calculateNoOfColumns(): Int { private fun calculateNoOfColumns(): Int {
val displayMetrics = resources.displayMetrics val displayMetrics = resources.displayMetrics
val dpWidth = displayMetrics.widthPixels / displayMetrics.density val dpWidth = displayMetrics.widthPixels / displayMetrics.density
return (dpWidth / 300).toInt() return (dpWidth / MIN_WIDTH_CARD_DP).toInt()
} }
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
@ -577,7 +585,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
messageRes: Int, messageRes: Int,
doFn: () -> Unit, doFn: () -> Unit,
) { ) {
AlertDialog.Builder(this@HomeActivity) AlertDialog
.Builder(this@HomeActivity)
.setMessage(messageRes) .setMessage(messageRes)
.setTitle(titleRes) .setTitle(titleRes)
.setPositiveButton(android.R.string.ok) { _, _ -> doFn() } .setPositiveButton(android.R.string.ok) { _, _ -> doFn() }
@ -586,12 +595,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
.show() .show()
} }
@Suppress("detekt:ReturnCount", "detekt:LongMethod")
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
val browserIntent = baseContext.openUrlInBrowser(AppSettingsService.BUG_URL)
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
startActivity(browserIntent)
return true return true
} }
@ -608,18 +616,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val updatedRemote = repository.updateRemote() val updatedRemote = repository.updateRemote()
if (updatedRemote) { if (updatedRemote) {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.refresh_success_response, this@HomeActivity,
Toast.LENGTH_LONG, R.string.refresh_success_response,
) Toast.LENGTH_LONG,
.show() ).show()
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.refresh_failer_message, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.refresh_failer_message,
).show() Toast.LENGTH_SHORT,
).show()
} }
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -635,25 +644,26 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items) val success = repository.markAllAsRead(items)
if (success) { if (success) {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.all_posts_read, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.all_posts_read,
).show() Toast.LENGTH_SHORT,
).show()
tabNewBadge.removeBadge() tabNewBadge.removeBadge()
getElementsAccordingToTab() getElementsAccordingToTab()
} else { } else {
Toast.makeText( Toast
this@HomeActivity, .makeText(
R.string.all_posts_not_read, this@HomeActivity,
Toast.LENGTH_SHORT, R.string.all_posts_not_read,
).show() Toast.LENGTH_SHORT,
).show()
} }
handleListResult() handleListResult()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
} }
} }
@ -663,7 +673,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
R.id.action_disconnect -> { R.id.action_disconnect -> {
needsConfirmation( needsConfirmation(
R.string.confirm_disconnect_title, R.string.confirm_disconnect_title,
R.string.confirm_disconnect_description R.string.confirm_disconnect_description,
) { ) {
runBlocking { runBlocking {
repository.logout() repository.logout()
@ -704,7 +714,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun handleRecurringTask() { private fun handleRecurringTask() {
if (appSettingsService.isPeriodicRefreshEnabled()) { if (appSettingsService.isPeriodicRefreshEnabled()) {
val myConstraints = val myConstraints =
Constraints.Builder() Constraints
.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled()) .setRequiresCharging(appSettingsService.isRefreshWhenChargingOnlyEnabled())
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
@ -713,19 +724,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
val backgroundWork = val backgroundWork =
PeriodicWorkRequestBuilder<LoadingWorker>( PeriodicWorkRequestBuilder<LoadingWorker>(
appSettingsService.getRefreshMinutes(), appSettingsService.getRefreshMinutes(),
TimeUnit.MINUTES TimeUnit.MINUTES,
) ).setConstraints(myConstraints)
.setConstraints(myConstraints)
.addTag("selfoss-loading") .addTag("selfoss-loading")
.build() .build()
WorkManager.getInstance( WorkManager
baseContext, .getInstance(
).enqueueUniquePeriodicWork( baseContext,
"selfoss-loading", ).enqueueUniquePeriodicWork(
ExistingPeriodicWorkPolicy.KEEP, "selfoss-loading",
backgroundWork ExistingPeriodicWorkPolicy.KEEP,
) backgroundWork,
)
} }
} }
} }

View File

@ -84,7 +84,9 @@ class ImageActivity : AppCompatActivity() {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private inner class ScreenSlidePagerAdapter(
fa: FragmentActivity,
) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allImages.size override fun getItemCount(): Int = allImages.size
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position]) override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])

View File

@ -30,7 +30,11 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class LoginActivity : AppCompatActivity(), DIAware { private const val MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED = 3
class LoginActivity :
AppCompatActivity(),
DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithLogin = false private var isWithLogin = false
@ -108,7 +112,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation() repository.updateApiInformation()
ACRA.errorReporter.putCustomData( ACRA.errorReporter.putCustomData(
"SELFOSS_API_VERSION", "SELFOSS_API_VERSION",
appSettingsService.getApiVersion().toString() appSettingsService.getApiVersion().toString(),
) )
CountingIdlingResourceSingleton.decrement() CountingIdlingResourceSingleton.decrement()
} }
@ -132,9 +136,18 @@ class LoginActivity : AppCompatActivity(), DIAware {
binding.passwordView.error = null binding.passwordView.error = null
// Store values at the time of the login attempt. // Store values at the time of the login attempt.
val url = binding.urlView.text.toString().trim() val url =
val login = binding.loginView.text.toString().trim() binding.urlView.text
val password = binding.passwordView.text.toString().trim() .toString()
.trim()
val login =
binding.loginView.text
.toString()
.trim()
val password =
binding.passwordView.text
.toString()
.trim()
failInvalidUrl(url) failInvalidUrl(url)
failLoginDetails(password, login) failLoginDetails(password, login)
@ -151,11 +164,12 @@ class LoginActivity : AppCompatActivity(), DIAware {
repository.updateApiInformation() repository.updateApiInformation()
} catch (e: Exception) { } catch (e: Exception) {
if (e.message?.startsWith("No transformation found") == true) { if (e.message?.startsWith("No transformation found") == true) {
Toast.makeText( Toast
applicationContext, .makeText(
R.string.application_selfoss_only, applicationContext,
Toast.LENGTH_LONG, R.string.application_selfoss_only,
).show() Toast.LENGTH_LONG,
).show()
preferenceError() preferenceError()
showProgress(false) showProgress(false)
} }
@ -205,7 +219,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
cancel = true cancel = true
binding.urlView.error = getString(R.string.login_url_problem) binding.urlView.error = getString(R.string.login_url_problem)
inValidCount++ inValidCount++
if (inValidCount == 3) { if (inValidCount == MAX_INVALID_LOGIN_BEFORE_ALERT_DISPLAYED) {
val alertDialog = AlertDialog.Builder(this).create() val alertDialog = AlertDialog.Builder(this).create()
alertDialog.setTitle(getString(R.string.warning_wrong_url)) alertDialog.setTitle(getString(R.string.warning_wrong_url))
alertDialog.setMessage(getString(R.string.text_wrong_url)) alertDialog.setMessage(getString(R.string.text_wrong_url))
@ -270,7 +284,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
return when (item.itemId) { return when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
val browserIntent = val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl)) Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.BUG_URL))
startActivity(browserIntent) startActivity(browserIntent)
return true return true
} }
@ -280,9 +294,9 @@ class LoginActivity : AppCompatActivity(), DIAware {
.withAboutIconShown(true) .withAboutIconShown(true)
.withAboutVersionShown(true) .withAboutVersionShown(true)
.withAboutSpecial2("Bug reports") .withAboutSpecial2("Bug reports")
.withAboutSpecial2Description(AppSettingsService.trackerUrl) .withAboutSpecial2Description(AppSettingsService.BUG_URL)
.withAboutSpecial1("Project Page") .withAboutSpecial1("Project Page")
.withAboutSpecial1Description(AppSettingsService.sourceUrl) .withAboutSpecial1Description(AppSettingsService.SOURCE_URL)
.start(this) .start(this)
true true
} }
@ -290,4 +304,4 @@ class LoginActivity : AppCompatActivity(), DIAware {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
} }

View File

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

View File

@ -22,7 +22,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class ReaderActivity : AppCompatActivity(), DIAware { class ReaderActivity :
AppCompatActivity(),
DIAware {
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
@ -51,6 +53,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
showMenuItem(false) showMenuItem(false)
} }
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater) binding = ActivityReaderBinding.inflate(layoutInflater)
@ -99,8 +102,9 @@ class ReaderActivity : AppCompatActivity(), DIAware {
oldInstanceState.clear() oldInstanceState.clear()
} }
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : private inner class ScreenSlidePagerAdapter(
FragmentStateAdapter(fa) { fa: FragmentActivity,
) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = allItems.size override fun getItemCount(): Int = allItems.size
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position]) override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
@ -109,25 +113,26 @@ class ReaderActivity : AppCompatActivity(), DIAware {
override fun onKeyDown( override fun onKeyDown(
keyCode: Int, keyCode: Int,
event: KeyEvent?, event: KeyEvent?,
): Boolean { ): Boolean =
return when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> { KeyEvent.KEYCODE_VOLUME_DOWN -> {
val currentFragment = val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollDown() currentFragment.volumeButtonScrollDown()
true true
} }
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> {
val currentFragment = val currentFragment =
supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment supportFragmentManager.findFragmentByTag("f" + binding.pager.currentItem) as ArticleFragment
currentFragment.scrollUp() currentFragment.volumeButtonScrollUp()
true true
} }
else -> { else -> {
super.onKeyDown(keyCode, event) super.onKeyDown(keyCode, event)
} }
} }
}
private fun alignmentMenu() { private fun alignmentMenu() {
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
@ -187,6 +192,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@ -200,10 +206,12 @@ class ReaderActivity : AppCompatActivity(), DIAware {
afterSave() afterSave()
} }
} }
R.id.align_left -> { R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT) switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment() refreshFragment()
} }
R.id.align_justify -> { R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY) switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment() refreshFragment()

View File

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

View File

@ -21,7 +21,9 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class UpsertSourceActivity : AppCompatActivity(), DIAware { class UpsertSourceActivity :
AppCompatActivity(),
DIAware {
private var existingSource: SelfossModel.SourceDetail? = null private var existingSource: SelfossModel.SourceDetail? = null
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
@ -83,6 +85,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
} }
@Suppress("detekt:SwallowedException")
private fun handleSpoutsSpinner() { private fun handleSpoutsSpinner() {
val spoutsKV = HashMap<String, String>() val spoutsKV = HashMap<String, String>()
binding.spoutsSpinner.onItemSelectedListener = binding.spoutsSpinner.onItemSelectedListener =
@ -105,11 +108,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
} }
fun handleSpoutFailure(networkIssue: Boolean = false) { fun handleSpoutFailure(networkIssue: Boolean = false) {
Toast.makeText( Toast
this@UpsertSourceActivity, .makeText(
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts, this@UpsertSourceActivity,
Toast.LENGTH_SHORT, if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
).show() Toast.LENGTH_SHORT,
).show()
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
} }
@ -170,6 +174,7 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
sourceDetailsUnavailable -> { sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val successfullyAddedSource = val successfullyAddedSource =
@ -192,11 +197,12 @@ class UpsertSourceActivity : AppCompatActivity(), DIAware {
if (successfullyAddedSource) { if (successfullyAddedSource) {
finish() finish()
} else { } else {
Toast.makeText( Toast
this@UpsertSourceActivity, .makeText(
R.string.cant_create_source, this@UpsertSourceActivity,
Toast.LENGTH_SHORT, R.string.cant_create_source,
).show() Toast.LENGTH_SHORT,
).show()
} }
} }
} }

View File

@ -8,11 +8,11 @@ import android.widget.ImageView.ScaleType
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@ -49,7 +49,10 @@ class ItemCardAdapter(
return ViewHolder(binding) return ViewHolder(binding)
} }
private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) { private fun handleClickListeners(
holderBinding: CardItemBinding,
position: Int,
) {
holderBinding.favButton.setOnClickListener { holderBinding.favButton.setOnClickListener {
val item = items[position] val item = items[position]
if (item.starred) { if (item.starred) {
@ -71,7 +74,7 @@ class ItemCardAdapter(
} }
binding.browserBtn.setOnClickListener { binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[position]) c.openItemUrlInBrowserAsNewTask(items[position])
} }
} }
@ -96,12 +99,13 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try { binding.sourceTitleAndDate.text =
itm.sourceAuthorAndDate() try {
} catch (e: Exception) { itm.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date") } catch (e: Exception) {
itm.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
} itm.sourceAuthorOnly()
}
if (!appSettingsService.isFullHeightCardsEnabled()) { if (!appSettingsService.isFullHeightCardsEnabled()) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
@ -114,16 +118,18 @@ class ItemCardAdapter(
binding.itemImage.setImageDrawable(null) binding.itemImage.setImageDrawable(null)
} else { } else {
binding.itemImage.visibility = View.VISIBLE binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage) c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
} }
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage, appSettingsService)
} }
} }
} }
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(
val binding: CardItemBinding,
) : RecyclerView.ViewHolder(binding.root)
} }

View File

@ -6,8 +6,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
@ -53,24 +53,27 @@ class ItemListAdapter(
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent)) binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
binding.sourceTitleAndDate.text = try { binding.sourceTitleAndDate.text =
itm.sourceAuthorAndDate() try {
} catch (e: Exception) { itm.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date") } catch (e: Exception) {
itm.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
} itm.sourceAuthorOnly()
}
if (itm.getThumbnail(repository.baseUrl).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
} }
} else { } else {
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage) c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage, appSettingsService)
} }
} }
} }
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(
val binding: ListItemBinding,
) : RecyclerView.ViewHolder(binding.root)
} }

View File

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

View File

@ -16,6 +16,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBindi
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -29,12 +30,14 @@ import org.kodein.di.instance
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.SourceDetail>, private val items: ArrayList<SelfossModel.SourceDetail>,
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
DIAware {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private lateinit var binding: SourceListItemBinding private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app) override val di: DI by closestDI(app)
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
@ -61,11 +64,12 @@ class SourcesListAdapter(
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount) notifyItemRangeChanged(position, itemCount)
} else { } else {
Toast.makeText( Toast
app, .makeText(
R.string.can_delete_source, app,
Toast.LENGTH_SHORT, R.string.can_delete_source,
).show() Toast.LENGTH_SHORT,
).show()
} }
} }
} }
@ -80,7 +84,7 @@ class SourcesListAdapter(
if (itm.getIcon(repository.baseUrl).isEmpty()) { if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded()) binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else { } else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage) c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
} }
if (!itm.error.isNullOrBlank()) { if (!itm.error.isNullOrBlank()) {
@ -99,5 +103,7 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) inner class ViewHolder(
val mView: ConstraintLayout,
) : RecyclerView.ViewHolder(mView)
} }

View File

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

View File

@ -1,23 +1,24 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.ActivityNotFoundException import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue.DATA_NULL_UNDEFINED
import android.view.* import android.view.GestureDetector
import android.view.InflateException
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.ImageActivity import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
@ -25,10 +26,15 @@ import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBind
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
import bou.amine.apps.readerforselfossv2.android.model.toModel import bou.amine.apps.readerforselfossv2.android.model.toModel
import bou.amine.apps.readerforselfossv2.android.model.toParcelable import bou.amine.apps.readerforselfossv2.android.model.toParcelable
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.addHomeMadeActionItem
import bou.amine.apps.readerforselfossv2.android.utils.getColorFromAttr
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapFitCenter
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.getGlideImageForResource
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.android.utils.shareLink
import bou.amine.apps.readerforselfossv2.model.MercuryModel import bou.amine.apps.readerforselfossv2.model.MercuryModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
@ -39,27 +45,34 @@ import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail import bou.amine.apps.readerforselfossv2.utils.getThumbnail
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide import com.leinardi.android.speeddial.SpeedDialView
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ktx.sendSilentlyWithAcra
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.* import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
private const val IMAGE_JPG = "image/jpg" private const val IMAGE_JPG = "image/jpg"
private const val IMAGE_PNG = "image/png"
private const val IMAGE_WEBP = "image/webp"
class ArticleFragment : Fragment(), DIAware { private const val WHITE_COLOR_HEX = 0xFFFFFF
private var fontSize: Int = 16
private const val DEFAULT_FONT_SIZE = 16
class ArticleFragment :
Fragment(),
DIAware {
private var colorOnSurface: Int = 0
private var colorSurface: Int = 0
private var fontSize: Int = DEFAULT_FONT_SIZE
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
@ -67,7 +80,7 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var contentImage: String private lateinit var contentImage: String
private lateinit var contentTitle: String private lateinit var contentTitle: String
private lateinit var allImages: ArrayList<String> private lateinit var allImages: ArrayList<String>
private lateinit var fab: FloatingActionButton private lateinit var fab: SpeedDialView
private lateinit var textAlignment: String private lateinit var textAlignment: String
private lateinit var binding: FragmentArticleBinding private lateinit var binding: FragmentArticleBinding
@ -78,7 +91,6 @@ class ArticleFragment : Fragment(), DIAware {
private var typeface: Typeface? = null private var typeface: Typeface? = null
private var resId: Int = 0 private var resId: Int = 0
private var font = "" private var font = ""
private var staticBar = false
private val mercuryApi: MercuryApi by instance() private val mercuryApi: MercuryApi by instance()
@ -90,6 +102,7 @@ class ArticleFragment : Fragment(), DIAware {
item = pi.toModel() item = pi.toModel()
} }
@Suppress("detekt:LongMethod")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -98,36 +111,33 @@ class ArticleFragment : Fragment(), DIAware {
try { try {
binding = FragmentArticleBinding.inflate(inflater, container, false) binding = FragmentArticleBinding.inflate(inflater, container, false)
url = item.getLinkDecoded() try {
url = item.getLinkDecoded()
} catch (e: Exception) {
e.sendSilentlyWithAcra()
}
colorOnSurface = requireContext().getColorFromAttr(R.attr.colorOnSurface)
colorSurface = requireContext().getColorFromAttr(R.attr.colorSurface)
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
contentImage = item.getThumbnail(repository.baseUrl) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = try { contentSource =
item.sourceAuthorAndDate() try {
} catch (e: Exception) { item.sourceAuthorAndDate()
e.sendSilentlyWithAcraWithName("Article Fragment parse date") } catch (e: Exception) {
item.sourceAuthorOnly() e.sendSilentlyWithAcraWithName("Article Fragment parse date")
} item.sourceAuthorOnly()
}
allImages = item.getImages() allImages = item.getImages()
fontSize = appSettingsService.getFontSize() fontSize = appSettingsService.getFontSize()
staticBar = appSettingsService.isStaticBarEnabled()
font = appSettingsService.getFont() font = appSettingsService.getFont()
refreshAlignment() refreshAlignment()
fab = binding.fab handleFloatingToolbar()
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource binding.source.text = contentSource
if (typeface != null) { if (typeface != null) {
@ -135,25 +145,11 @@ class ArticleFragment : Fragment(), DIAware {
} }
handleContent() handleContent()
binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide()
} else {
if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
}
},
)
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
if (context != null) { try {
AlertDialog.Builder(requireContext()) AlertDialog
.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
@ -161,9 +157,10 @@ class ArticleFragment : Fragment(), DIAware {
) { _, _ -> ) { _, _ ->
appSettingsService.disableArticleViewer() appSettingsService.disableArticleViewer()
requireActivity().finish() requireActivity().finish()
} }.create()
.create()
.show() .show()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@ -185,67 +182,84 @@ class ArticleFragment : Fragment(), DIAware {
if (!contentImage.isEmptyOrNullOrNullString() && context != null) { if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
Glide requireContext().bitmapFitCenter(contentImage, binding.imageView, appSettingsService)
.with(requireContext())
.asBitmap()
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
} }
} }
private fun handleFloatingToolbar(): FloatingToolbar { private fun handleFloatingToolbar() {
val floatingToolbar: FloatingToolbar = binding.floatingToolbar fab = binding.speedDial
if (appSettingsService.getPublicAccess()) { fab.mainFabClosedIconColor = colorOnSurface
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read) fab.mainFabOpenedIconColor = colorOnSurface
}
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent)) handleFloatingToolbarActionItems()
floatingToolbar.setClickListener( fab.setOnActionSelectedListener { actionItem ->
object : FloatingToolbar.ItemClickListener { when (actionItem.id) {
override fun onItemClick(item: MenuItem) { R.id.share_action -> requireActivity().shareLink(url, contentTitle)
when (item.itemId) { R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.unread_action ->
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) try {
R.id.unread_action -> if (this@ArticleFragment.item.unread) {
if (context != null) { CoroutineScope(Dispatchers.IO).launch {
if (this@ArticleFragment.item.unread) { repository.markAsRead(this@ArticleFragment.item)
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(this@ArticleFragment.item)
}
this@ArticleFragment.item.unread = false
Toast.makeText(
context,
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()
}
} }
else -> Unit 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")
} }
}
override fun onItemLongClick(item: MenuItem?) { else -> Unit
// We do nothing }
} false
}, }
}
private fun handleFloatingToolbarActionItems() {
fab.addHomeMadeActionItem(
R.id.share_action,
resources.getDrawable(R.drawable.ic_share_white_24dp),
R.string.reader_action_share,
colorOnSurface,
colorSurface,
requireContext(),
)
fab.addHomeMadeActionItem(
R.id.open_action,
resources.getDrawable(R.drawable.ic_open_in_browser_white_24dp),
R.string.reader_action_open,
colorOnSurface,
colorSurface,
requireContext(),
)
fab.addHomeMadeActionItem(
R.id.unread_action,
resources.getDrawable(R.drawable.ic_baseline_white_eye_24dp),
R.string.unmark,
colorOnSurface,
colorSurface,
requireContext(),
) )
return floatingToolbar
} }
private fun refreshAlignment() { private fun refreshAlignment() {
@ -257,6 +271,7 @@ class ArticleFragment : Fragment(), DIAware {
} }
} }
@Suppress("detekt:SwallowedException")
private fun getContentFromMercury() { private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@ -288,24 +303,17 @@ class ArticleFragment : Fragment(), DIAware {
contentText = data.content.orEmpty() contentText = data.content.orEmpty()
htmlToWebview() htmlToWebview()
handleLeadImage(data?.lead_image_url) handleLeadImage(data.lead_image_url)
binding.nestedScrollView.scrollTo(0, 0) binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
} }
} }
private fun handleLeadImage(lead_image_url: String?) { private fun handleLeadImage(leadImageUrl: String?) {
if (!lead_image_url.isNullOrEmpty() && context != null) { if (!leadImageUrl.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
Glide requireContext().bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
.with(requireContext())
.asBitmap()
.load(
lead_image_url,
)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
@ -318,85 +326,85 @@ class ArticleFragment : Fragment(), DIAware {
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
url: String, url: String,
): Boolean { ): Boolean =
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { if (context != null &&
try { url.isUrlValid() &&
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
} catch (e: ActivityNotFoundException) { ) {
e.sendSilentlyWithAcraWithName("activityNotFound > $url") requireContext().openUrlInBrowser(url)
}
true true
} else { } else {
false false
} }
}
@Suppress("detekt:SwallowedException", "detekt:ReturnCount")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView, view: WebView,
url: String, url: String,
): WebResourceResponse? { ): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) val (mime: String?, compression: Bitmap.CompressFormat) =
if (url.lowercase(Locale.US).contains(".jpg") || if (url
url.lowercase(Locale.US) .lowercase(Locale.US)
.contains(".jpeg") .contains(".jpg") ||
) { url.lowercase(Locale.US).contains(".jpeg")
try { ) {
val image = Pair(IMAGE_JPG, Bitmap.CompressFormat.JPEG)
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() } else if (url.lowercase(Locale.US).contains(".png")) {
return WebResourceResponse( Pair(IMAGE_PNG, Bitmap.CompressFormat.PNG)
IMAGE_JPG, } else if (url.lowercase(Locale.US).contains(".webp")) {
"UTF-8", Pair(IMAGE_WEBP, Bitmap.CompressFormat.WEBP)
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG), } else {
) return super.shouldInterceptRequest(view, url)
} catch (e: ExecutionException) {
// Do nothing
} }
} else if (url.lowercase(Locale.US).contains(".png")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
)
} catch (e: ExecutionException) {
// Do nothing
}
} else if (url.lowercase(Locale.US).contains(".webp")) {
try {
val image =
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse(
IMAGE_JPG,
"UTF-8",
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
)
} catch (e: ExecutionException) {
// Do nothing
}
}
return super.shouldInterceptRequest(view, url) try {
val image = view.getGlideImageForResource(url, appSettingsService)
return WebResourceResponse(
mime,
"UTF-8",
getBitmapInputStream(image, compression),
)
} catch (e: ExecutionException) {
return super.shouldInterceptRequest(view, url)
}
} }
} }
} }
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
private fun htmlToWebview() { private fun htmlToWebview() {
if (context != null) { val context: Context
try {
context = requireContext()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
return
}
try {
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily) val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs) val a: TypedArray = context.obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0) binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE binding.webcontent.visibility = View.VISIBLE
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
}
val colorOnSurface = TypedValue() val colorSurfaceString =
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true) String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorSurface != DATA_NULL_UNDEFINED) colorSurface else WHITE_COLOR_HEX),
)
val colorSurface = TypedValue() val colorOnSurfaceString =
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true) String.format(
"#%06X",
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
)
try {
binding.webcontent.settings.useWideViewPort = true binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false binding.webcontent.settings.javaScriptEnabled = false
@ -407,19 +415,25 @@ class ArticleFragment : Fragment(), DIAware {
GestureDetector( GestureDetector(
activity, activity,
object : GestureDetector.SimpleOnGestureListener() { object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean = performClick()
return performClick()
}
}, },
) )
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) } binding.webcontent.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(
event,
)
}
binding.webcontent.settings.layoutAlgorithm = binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
return
}
try {
var baseUrl: String? = null var baseUrl: String? = null
try { try {
val itemUrl = URL(url) val itemUrl = URL(url)
baseUrl = itemUrl.protocol + "://" + itemUrl.host baseUrl = itemUrl.protocol + "://" + itemUrl.host
@ -469,12 +483,12 @@ class ArticleFragment : Fragment(), DIAware {
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
0xFFFFFF and resources.getColor(R.color.colorAccent), WHITE_COLOR_HEX and context.resources.getColor(R.color.colorAccent),
) )
} !important; } !important;
| } | }
| *:not(a) { | *:not(a) {
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)}; | color: $colorOnSurfaceString;
| } | }
| * { | * {
| font-size: ${fontSize}px; | font-size: ${fontSize}px;
@ -482,26 +496,11 @@ class ArticleFragment : Fragment(), DIAware {
| word-break: break-word; | word-break: break-word;
| overflow:hidden; | overflow:hidden;
| line-height: 1.5em; | line-height: 1.5em;
| background-color: ${ | background-color: $colorSurfaceString;
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| } | }
| body, html { | body, html {
| background-color: ${ | background-color: $colorSurfaceString !important;
String.format( | border-color: $colorSurfaceString !important;
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| border-color: ${
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
} !important;
| padding: 0 !important; | padding: 0 !important;
| margin: 0 !important; | margin: 0 !important;
| } | }
@ -511,12 +510,7 @@ class ArticleFragment : Fragment(), DIAware {
| pre, code { | pre, code {
| white-space: pre-wrap; | white-space: pre-wrap;
| width:100%; | width:100%;
| background-color: ${ | background-color: $colorSurfaceString;
String.format(
"#%06X",
0xFFFFFF and colorSurface.data,
)
};
| } | }
| </style> | </style>
| $fontLinkAndStyle | $fontLinkAndStyle
@ -529,25 +523,27 @@ class ArticleFragment : Fragment(), DIAware {
"utf-8", "utf-8",
null, null,
) )
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is still null ?")
} }
} }
fun scrollDown() { fun volumeButtonScrollDown() {
val height = binding.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, height / 2) binding.nestedScrollView.smoothScrollBy(0, height / 2)
} }
fun scrollUp() { fun volumeButtonScrollUp() {
val height = binding.nestedScrollView.measuredHeight val height = binding.nestedScrollView.measuredHeight
binding.nestedScrollView.smoothScrollBy(0, -height / 2) binding.nestedScrollView.smoothScrollBy(0, -height / 2)
} }
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
if (context != null) { try {
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item) requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
} else { } catch (e: IllegalStateException) {
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context") e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@ -564,7 +560,8 @@ class ArticleFragment : Fragment(), DIAware {
} }
fun performClick(): Boolean { fun performClick(): Boolean {
if (allImages != null && ( if (allImages != null &&
(
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) )

View File

@ -15,12 +15,13 @@ import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon import bou.amine.apps.readerforselfossv2.utils.getIcon
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.ViewTarget import com.bumptech.glide.request.target.ViewTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -33,10 +34,15 @@ import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.instance import org.kodein.di.instance
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware { private const val DRAWABLE_SIZE = 30
class FilterSheetFragment :
BottomSheetDialogFragment(),
DIAware {
private lateinit var binding: FilterFragmentBinding private lateinit var binding: FilterFragmentBinding
override val di: DI by closestDI() override val di: DI by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private var selectedChip: Chip? = null private var selectedChip: Chip? = null
@ -52,19 +58,17 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
false, false,
) )
val context: Context? = context try {
if (context == null) {
dismiss()
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} else {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
handleTagChips(context) handleTagChips(requireContext())
handleSourceChips(context) handleSourceChips(requireContext())
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
} }
} catch (e: IllegalStateException) {
dismiss()
e.sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
} }
binding.floatingActionButton2.setOnClickListener { binding.floatingActionButton2.setOnClickListener {
@ -82,22 +86,22 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
val c = Chip(context) val c = Chip(context)
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
Glide.with(context) context.imageIntoViewTarget(
.load(source.getIcon(repository.baseUrl)) source.getIcon(repository.baseUrl),
.into( object : ViewTarget<Chip?, Drawable?>(c) {
object : ViewTarget<Chip?, Drawable?>(c) { override fun onResourceReady(
override fun onResourceReady( resource: Drawable,
resource: Drawable, transition: Transition<in Drawable?>?,
transition: Transition<in Drawable?>?, ) {
) { try {
try { c.chipIcon = resource
c.chipIcon = resource } catch (e: Exception) {
} catch (e: Exception) { e.sendSilentlyWithAcraWithName("sources > onResourceReady")
e.sendSilentlyWithAcraWithName("sources > onResourceReady")
}
} }
}, }
) },
appSettingsService,
)
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@ -155,8 +159,8 @@ class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
} }
gd.setColor(gdColor) gd.setColor(gdColor)
gd.shape = GradientDrawable.RECTANGLE gd.shape = GradientDrawable.RECTANGLE
gd.setSize(30, 30) gd.setSize(DRAWABLE_SIZE, DRAWABLE_SIZE)
gd.cornerRadius = 30F gd.cornerRadius = DRAWABLE_SIZE.toFloat()
c.chipIcon = gd c.chipIcon = gd
} catch (e: Exception) { } catch (e: Exception) {
e.sendSilentlyWithAcraWithName("tags > GradientDrawable") e.sendSilentlyWithAcraWithName("tags > GradientDrawable")

View File

@ -6,15 +6,21 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
import com.bumptech.glide.Glide import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapWithCache
import com.bumptech.glide.load.engine.DiskCacheStrategy import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import com.bumptech.glide.request.RequestOptions import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.instance
class ImageFragment : Fragment() { class ImageFragment :
Fragment(),
DIAware {
override val di: DI by closestDI()
private val appSettingsService: AppSettingsService by instance()
private lateinit var imageUrl: String private lateinit var imageUrl: String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null private var _binding: FragmentImageBinding? = null
private val binding get() = _binding val binding get() = _binding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -31,11 +37,7 @@ class ImageFragment : Fragment() {
val view = binding?.root val view = binding?.root
binding!!.photoView.visibility = View.VISIBLE binding!!.photoView.visibility = View.VISIBLE
Glide.with(requireActivity()) requireActivity().bitmapWithCache(imageUrl, binding!!.photoView, appSettingsService)
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
return view return view
} }

View File

@ -2,24 +2,22 @@ package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context import android.content.Context
import android.webkit.URLUtil import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.preloadImage
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getImages import bou.amine.apps.readerforselfossv2.utils.getImages
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
fun SelfossModel.Item.preloadImages(context: Context): Boolean { fun SelfossModel.Item.preloadImages(
context: Context,
appSettingsService: AppSettingsService,
): Boolean {
val imageUrls = this.getImages() val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try { try {
for (url in imageUrls) { for (url in imageUrls) {
if (URLUtil.isValidUrl(url)) { if (URLUtil.isValidUrl(url)) {
Glide.with(context).asBitmap() context.preloadImage(url, appSettingsService)
.apply(glideOptions)
.load(url).submit()
} }
} }
} catch (e: Error) { } catch (e: Error) {

View File

@ -1,7 +1,5 @@
package bou.amine.apps.readerforselfossv2.android.settings package bou.amine.apps.readerforselfossv2.android.settings
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.InputFilter import android.text.InputFilter
@ -16,14 +14,22 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowser
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.API_ITEMS_NUMBER
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.CURRENT_THEME
import bou.amine.apps.readerforselfossv2.service.AppSettingsService.Companion.READER_FONT_SIZE
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
const val MAX_ITEMS_NUMBER = 200
private const val MIN_ITEMS_NUMBER = 1
class SettingsActivity : class SettingsActivity :
AppCompatActivity(), AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
@ -62,15 +68,14 @@ class SettingsActivity :
outState.putCharSequence(TITLE_TAG, title) outState.putCharSequence(TITLE_TAG, title)
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean =
return if (supportFragmentManager.popBackStackImmediate()) { if (supportFragmentManager.popBackStackImmediate()) {
supportActionBar?.title = getText(R.string.title_activity_settings) supportActionBar?.title = getText(R.string.title_activity_settings)
false false
} else { } else {
super.onBackPressed() super.onBackPressed()
true true
} }
}
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
@ -79,15 +84,17 @@ class SettingsActivity :
// Instantiate the new Fragment // Instantiate the new Fragment
val args = pref.extras val args = pref.extras
val fragment = val fragment =
supportFragmentManager.fragmentFactory.instantiate( supportFragmentManager.fragmentFactory
classLoader, .instantiate(
pref.fragment.toString(), classLoader,
).apply { pref.fragment.toString(),
arguments = args ).apply {
setTargetFragment(caller, 0) arguments = args
} setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment // Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction() supportFragmentManager
.beginTransaction()
.replace(R.id.settings, fragment) .replace(R.id.settings, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
@ -103,9 +110,11 @@ class SettingsActivity :
) { ) {
setPreferencesFromResource(R.xml.pref_main, rootKey) setPreferencesFromResource(R.xml.pref_main, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener = preferenceManager.findPreference<Preference>(CURRENT_THEME)?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue -> Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯ AppCompatDelegate.setDefaultNightMode(
newValue.toString().toInt(),
) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true true
} }
@ -129,7 +138,8 @@ class SettingsActivity :
) { ) {
setPreferencesFromResource(R.xml.pref_general, rootKey) setPreferencesFromResource(R.xml.pref_general, rootKey)
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number") val editTextPreference =
preferenceManager.findPreference<EditTextPreference>(API_ITEMS_NUMBER)
editTextPreference?.setOnBindEditTextListener { editText -> editTextPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.filters = editText.filters =
@ -137,10 +147,14 @@ class SettingsActivity :
InputFilter { source, _, _, dest, _, _ -> InputFilter { source, _, _, dest, _, _ ->
try { try {
val input: Int = (dest.toString() + source.toString()).toInt() val input: Int = (dest.toString() + source.toString()).toInt()
if (input in 1..200) return@InputFilter null if (input in MIN_ITEMS_NUMBER..MAX_ITEMS_NUMBER) return@InputFilter null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
nfe.sendSilentlyWithAcraWithName("GeneralPreferenceFragment") Toast
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show() .makeText(
activity,
R.string.items_number_should_be_number,
Toast.LENGTH_LONG,
).show()
} }
"" ""
}, },
@ -156,7 +170,7 @@ class SettingsActivity :
) { ) {
setPreferencesFromResource(R.xml.pref_viewer, rootKey) setPreferencesFromResource(R.xml.pref_viewer, rootKey)
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size") val fontSize = preferenceManager.findPreference<EditTextPreference>(READER_FONT_SIZE)
fontSize?.setOnBindEditTextListener { editText -> fontSize?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_NUMBER editText.inputType = InputType.TYPE_CLASS_NUMBER
editText.addTextChangedListener { editText.addTextChangedListener {
@ -213,25 +227,9 @@ class SettingsActivity :
} }
} }
class ThemePreferenceFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
setPreferencesFromResource(R.xml.pref_theme, rootKey)
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, newValue ->
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
true
}
}
}
class LinksPreferenceFragment : PreferenceFragmentCompat() { class LinksPreferenceFragment : PreferenceFragmentCompat() {
private fun openUrl(uri: Uri?) { private fun openUrl(url: String) {
val browserIntent = Intent(Intent.ACTION_VIEW, uri) context?.openUrlInBrowser(url)
startActivity(browserIntent)
} }
override fun onCreatePreferences( override fun onCreatePreferences(
@ -242,19 +240,19 @@ class SettingsActivity :
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.trackerUrl)) openUrl(AppSettingsService.BUG_URL)
true true
} }
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.sourceUrl)) openUrl(AppSettingsService.SOURCE_URL)
false false
} }
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
openUrl(Uri.parse(AppSettingsService.translationUrl)) openUrl(AppSettingsService.TRANSLATION_URL)
false false
} }
} }

View File

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

View File

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

View File

@ -2,7 +2,11 @@ package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink( fun Context.shareLink(
@ -16,9 +20,24 @@ fun Context.shareLink(
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle) sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.type = "text/plain" sendIntent.type = "text/plain"
startActivity( startActivity(
Intent.createChooser( Intent
sendIntent, .createChooser(
getString(R.string.share), sendIntent,
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), getString(R.string.share),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
) )
} }
@ColorInt
fun Context.getColorFromAttr(
@AttrRes attrColor: Int,
resolveRefs: Boolean = true,
): Int {
val typedValue = TypedValue()
try {
this.theme.resolveAttribute(attrColor, typedValue, resolveRefs)
} catch (e: Throwable) {
e.sendSilentlyWithAcraWithName("ColorFromAttr")
}
return typedValue.data
}

View File

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

View File

@ -1,6 +1,7 @@
package bou.amine.apps.readerforselfossv2.android.utils package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -24,21 +25,19 @@ fun Context.openItemUrl(
app: Activity, app: Activity,
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
Toast.makeText( Toast
this, .makeText(
this.getString(R.string.cant_open_invalid_url), this,
Toast.LENGTH_LONG, this.getString(R.string.cant_open_invalid_url),
).show() Toast.LENGTH_LONG,
).show()
} else { } else {
if (articleViewer) { if (articleViewer) {
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
} else { } else {
val intent = Intent(Intent.ACTION_VIEW) this.openUrlInBrowserAsNewTask(linkDecoded)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(linkDecoded.toStringUriWithHttp())
startActivity(intent)
} }
} }
} }
@ -56,11 +55,30 @@ fun String.isBaseUrlInvalid(): Boolean {
return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash) return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash)
} }
fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) { fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
}
fun Context.openUrlInBrowserAsNewTask(url: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp()) intent.data = Uri.parse(url)
startActivity(intent) this.mayBeStartActivity(intent)
}
fun Context.openUrlInBrowser(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
this.mayBeStartActivity(intent)
}
@Suppress("detekt:SwallowedException")
fun Context.mayBeStartActivity(intent: Intent) {
try {
this.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, getString(R.string.no_browser), Toast.LENGTH_SHORT).show()
}
} }
class LinkOnTouchListener : View.OnTouchListener { class LinkOnTouchListener : View.OnTouchListener {

View File

@ -1,4 +1,4 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android.utils.acra
import org.acra.ACRA import org.acra.ACRA
import org.acra.ktx.sendSilentlyWithAcra import org.acra.ktx.sendSilentlyWithAcra

View File

@ -0,0 +1,26 @@
package bou.amine.apps.readerforselfossv2.android.utils.acra
import android.content.Context
import android.os.DeadSystemException
import com.google.auto.service.AutoService
import org.acra.builder.ReportBuilder
import org.acra.config.CoreConfiguration
import org.acra.config.ReportingAdministrator
import org.acra.data.CrashReportData
@AutoService(ReportingAdministrator::class)
class AcraReportingAdministrator : ReportingAdministrator {
override fun shouldStartCollecting(
context: Context,
config: CoreConfiguration,
reportBuilder: ReportBuilder,
): Boolean =
reportBuilder.exception !is DeadSystemException &&
(reportBuilder.exception != null && reportBuilder.exception!!::class.simpleName != "CannotDeliverBroadcastException")
override fun shouldSendReport(
context: Context,
config: CoreConfiguration,
crashReportData: CrashReportData,
): Boolean = crashReportData.get("BRAND") != "redroid"
}

View File

@ -1,6 +1,13 @@
package bou.amine.apps.readerforselfossv2.android.utils.bottombar package bou.amine.apps.readerforselfossv2.android.utils.bottombar
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import bou.amine.apps.readerforselfossv2.android.R
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
fun TextBadgeItem.removeBadge(): TextBadgeItem { fun TextBadgeItem.removeBadge(): TextBadgeItem {
this.setText("") this.setText("")
@ -9,3 +16,25 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
} }
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
@Suppress("detekt:LongParameterList")
fun SpeedDialView.addHomeMadeActionItem(
@IdRes actionId: Int,
actionIcon: Drawable,
@StringRes labelId: Int,
colorOnSurface: Int,
colorSurface: Int,
context: Context,
) {
this.addActionItem(
SpeedDialActionItem
.Builder(actionId, actionIcon)
.setFabBackgroundColor(context.resources.getColor(R.color.colorAccent))
.setFabImageTintColor(colorOnSurface)
.setLabel(context.getString(labelId))
.setLabelClickable(false)
.setLabelBackgroundColor(colorOnSurface)
.setLabelColor(colorSurface)
.create(),
)
}

View File

@ -2,40 +2,134 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.webkit.WebView
import android.widget.ImageView import android.widget.ImageView
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.ViewTarget
import com.google.android.material.chip.Chip
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
private const val PRELOAD_IMAGE_TIMEOUT = 10000
@OptIn(ExperimentalEncodingApi::class)
fun String.toGlideUrl(appSettingsService: AppSettingsService): GlideUrl {
if (this.isEmptyOrNullOrNullString()) {
return GlideUrl("")
}
if (appSettingsService.getBasicUserName().isNotEmpty()) {
val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))
return GlideUrl(
this,
LazyHeaders
.Builder()
.addHeader("Authorization", "Basic $authBuf")
.build(),
)
} else {
return GlideUrl(
this,
)
}
}
fun WebView.getGlideImageForResource(
url: String,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
.load(url.toGlideUrl(appSettingsService))
.submit()
.get()
fun Context.preloadImage(
url: String,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(PRELOAD_IMAGE_TIMEOUT))
.load(url.toGlideUrl(appSettingsService))
.submit()
fun Context.imageIntoViewTarget(
url: String,
target: ViewTarget<Chip?, Drawable?>,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.load(url.toGlideUrl(appSettingsService))
.into(target)
fun Context.bitmapWithCache(
url: String,
iv: ImageView,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL))
.load(url.toGlideUrl(appSettingsService))
.into(iv)
fun Context.bitmapCenterCrop( fun Context.bitmapCenterCrop(
url: String, url: String,
iv: ImageView, iv: ImageView,
) = Glide.with(this) appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap() .asBitmap()
.load(url) .load(url.toGlideUrl(appSettingsService))
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(iv) .into(iv)
fun Context.bitmapFitCenter(
url: String,
iv: ImageView,
appSettingsService: AppSettingsService,
) = Glide
.with(this)
.asBitmap()
.load(url.toGlideUrl(appSettingsService))
.apply(RequestOptions.fitCenterTransform())
.into(iv)
fun Context.circularDrawable( fun Context.circularDrawable(
url: String, url: String,
view: CircleImageView, view: CircleImageView,
appSettingsService: AppSettingsService,
) { ) {
view.textView.text = "" view.textView.text = ""
Glide.with(this) Glide
.load(url) .with(this)
.load(url.toGlideUrl(appSettingsService))
.into(view.imageView) .into(view.imageView)
} }
private const val BITMAP_INPUT_STREAM_COMPRESSION_QUALITY = 80
fun getBitmapInputStream( fun getBitmapInputStream(
bitmap: Bitmap, bitmap: Bitmap,
compressFormat: Bitmap.CompressFormat, compressFormat: Bitmap.CompressFormat,
): InputStream { ): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream) bitmap.compress(compressFormat, BITMAP_INPUT_STREAM_COMPRESSION_QUALITY, byteArrayOutputStream)
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
return ByteArrayInputStream(bitmapData) return ByteArrayInputStream(bitmapData)
} }

View File

@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android.utils.network
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
lateinit var s: Snackbar lateinit var s: Snackbar
@ -11,19 +10,13 @@ lateinit var s: Snackbar
fun isNetworkAccessible(context: Context): Boolean { fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return when { return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false else -> false
}
} else {
val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting
} }
} }

View File

@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AppViewModel(private val repository: Repository) : ViewModel() { class AppViewModel(
private val repository: Repository,
) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>() private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow() val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true private var wasConnected = true
@ -19,11 +21,10 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
if (isConnected && !wasConnected && repository.connectionMonitored) { if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true) _networkAvailableProvider.emit(true)
wasConnected = true wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) } else if (!isConnected && wasConnected && repository.connectionMonitored) {
{ _networkAvailableProvider.emit(false)
_networkAvailableProvider.emit(false) wasConnected = false
wasConnected = false }
}
} }
} }
} }

View File

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

View File

@ -71,35 +71,13 @@
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<FrameLayout <com.leinardi.android.speeddial.SpeedDialView
android:layout_width="match_parent" android:id="@+id/speedDial"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|bottom|end" android:layout_gravity="bottom|end"
app:layout_constraintBottom_toBottomOf="parent" app:layout_behavior="@string/speeddial_scrolling_view_snackbar_behavior"
app:layout_constraintEnd_toEndOf="parent" app:sdMainFabClosedSrc="@drawable/ic_add_white_24dp" />
app:layout_constraintStart_toStartOf="parent">
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
android:id="@+id/floatingToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
app:floatingMenu="@menu/reader_toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:src="@drawable/ic_add_white_24dp"
app:backgroundTint="?attr/colorAccent"
app:fabSize="mini"
app:rippleColor="?attr/colorAccentDark" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/progressBar" android:id="@+id/progressBar"
@ -119,4 +97,5 @@
android:progressTint="?attr/colorAccent" /> android:progressTint="?attr/colorAccent" />
</FrameLayout> </FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -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>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Lector per a Selfoss"</string> <string name="app_name">"Lector per a Selfoss"</string>
<string name="title_activity_login">"Inicia la sessió"</string> <string name="title_activity_login">"Inicia la sessió"</string>
<string name="prompt_password">"Contrasenya"</string> <string name="prompt_password">"Contrasenya"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"La contrasenya és massa curta"</string> <string name="error_invalid_password">"La contrasenya és massa curta"</string>
<string name="error_field_required">"Camp necessari"</string> <string name="error_field_required">"Camp necessari"</string>
<string name="prompt_url">"URL"</string> <string name="prompt_url">"URL"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Autenticació (si és necessària)"</string> <string name="withLoginSwitch">"Autenticació (si és necessària)"</string>
<string name="login_url_problem">"Pot ser que falti una \"/\" al final de l'url."</string> <string name="login_url_problem">"Pot ser que falti una \"/\" al final de l'url."</string>
<string name="prompt_login">"Nom d'usuari"</string> <string name="prompt_login">"Nom d'usuari"</string>
@ -22,13 +23,6 @@
<string name="wrong_infos">"Torneu a comprovar la informació."</string> <string name="wrong_infos">"Torneu a comprovar la informació."</string>
<string name="all_posts_not_read">"No s'han llegit totes les publicacions"</string> <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="all_posts_read">"S'han llegit totes les publicacions"</string>
<string name="nothing_here">"No hi ha res"</string>
<string name="tab_new">"Nou"</string>
<string name="tab_read">"Tot"</string>
<string name="tab_favs">"Preferits"</string>
<string name="action_about">"Quant a"</string>
<string name="marked_as_read">"Element llegit"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Desfés"</string> <string name="undo_string">"Desfés"</string>
<string name="addStringNoUrl">"Inicieu la sessió per afegir fonts."</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_get_sources">"No es pot obtenir la llista de fonts."</string>
@ -90,7 +84,7 @@
<string name="pref_switch_items_caching">Guarda els elements per utilitzar-los sense connexió</string> <string name="pref_switch_items_caching">Guarda els elements per utilitzar-los sense connexió</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="network_connectivity_lost">"Network connection lost"</string> <string name="network_connectivity_lost">"Sense connexió!"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string> <string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sincronitza els articles</string> <string name="pref_switch_periodic_refresh">Sincronitza els articles</string>
<string name="pref_switch_periodic_refresh_off">Els articles no se sincronitzaran en segon pla</string> <string name="pref_switch_periodic_refresh_off">Els articles no se sincronitzaran en segon pla</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"No hi ha res"</string>
<string name="tab_new">"Nou"</string>
<string name="tab_read">"Tot"</string>
<string name="tab_favs">"Preferits"</string>
<string name="action_about">"Quant a"</string>
<string name="marked_as_read">"Element llegit"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader für selfoss"</string> <string name="app_name">"Reader für Selfoss"</string>
<string name="title_activity_login">"Anmelden"</string> <string name="title_activity_login">"Anmelden"</string>
<string name="prompt_password">"Passwort"</string> <string name="prompt_password">"Passwort"</string>
<string name="action_sign_in">"Fortfahren"</string> <string name="action_sign_in">"Fortfahren"</string>
<string name="error_invalid_password">"Passwort ist nicht lang genug"</string> <string name="error_invalid_password">"Passwort ist nicht lang genug"</string>
<string name="error_field_required">"Pflichtfeld"</string> <string name="error_field_required">"Pflichtfeld"</string>
<string name="prompt_url">"URL"</string> <string name="prompt_url">"URL"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Anmeldung erforderlich?"</string> <string name="withLoginSwitch">"Anmeldung erforderlich?"</string>
<string name="login_url_problem">"Ups. Du musst eventuell ein \"/\" am Ende der URL anhängen."</string> <string name="login_url_problem">"Ups. Du musst eventuell ein \"/\" am Ende der URL anhängen."</string>
<string name="prompt_login">"Benutzername"</string> <string name="prompt_login">"Benutzername"</string>
@ -22,22 +23,15 @@
<string name="wrong_infos">"Überprüfe deine Angaben noch einmal."</string> <string name="wrong_infos">"Überprüfe deine Angaben noch einmal."</string>
<string name="all_posts_not_read">"Nicht alle Beiträge wurden gelesen"</string> <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="all_posts_read">"Alle Beiträge wurden gelesen"</string>
<string name="nothing_here">"Keine Einträge vorhanden"</string>
<string name="tab_new">"Neu"</string>
<string name="tab_read">"Alle"</string>
<string name="tab_favs">"Favoriten"</string>
<string name="action_about">"Über"</string>
<string name="marked_as_read">"Artikel gelesen"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Rückgängig"</string> <string name="undo_string">"Rückgängig"</string>
<string name="addStringNoUrl">"Melde dich an um Quellen hinzuzufügen."</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_get_sources">"Quellen können nicht abgerufen werden."</string>
<string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string> <string name="cant_create_source">"Quelle kann nicht gespeichert werden."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string> <string name="cant_get_spouts_no_network">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Fehler beim Laden der Spouts-Liste, möglicherweise aufgrund eines API-Fehlers."</string>
<string name="form_not_complete">"Das Formular ist nicht vollständig"</string> <string name="form_not_complete">"Das Formular ist nicht vollständig"</string>
<string name="pref_header_links">"Links"</string> <string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string> <string name="issue_tracker_link">"Ticketsystem"</string>
<string name="issue_tracker_summary">"Melde einen Bug oder rege ein neues Feature an"</string> <string name="issue_tracker_summary">"Melde einen Bug oder rege ein neues Feature an"</string>
<string name="warning_wrong_url">"WARNUNG"</string> <string name="warning_wrong_url">"WARNUNG"</string>
<string name="pref_switch_card_view_title">"Kachelansicht"</string> <string name="pref_switch_card_view_title">"Kachelansicht"</string>
@ -64,71 +58,75 @@
<string name="filter_item_tags">Tags</string> <string name="filter_item_tags">Tags</string>
<string name="filter_item_sources">Quellen</string> <string name="filter_item_sources">Quellen</string>
<string name="menu_home_search">Suche</string> <string name="menu_home_search">Suche</string>
<string name="can_delete_source">Can\'t delete the source</string> <string name="can_delete_source">Quelle konnte nicht gelöscht werden</string>
<string name="base_url_error">Beim Versuch deine Selfoss-Instanz zu erreichen ist ein Fehler aufgetreten. Solltet dieser Fehler bestehen bleiben, trete bitte mit mir in Kontakt.</string> <string name="base_url_error">Beim Versuch deine Selfoss-Instanz zu erreichen ist ein Fehler aufgetreten. Solltet dieser Fehler bestehen bleiben, trete bitte mit mir in Kontakt.</string>
<string name="pref_header_theme">Designs</string> <string name="pref_header_theme">Designs</string>
<string name="pref_selfoss_category">selfoss API</string> <string name="pref_selfoss_category">selfoss API</string>
<string name="pref_api_items_number_title">Loaded items number</string> <string name="pref_api_items_number_title">Anzahl der zu ladenden Artikel</string>
<string name="pref_general_infinite_loading_title">Load more articles on scroll</string> <string name="pref_general_infinite_loading_title">Weitere Artikel beim Navigieren laden</string>
<string name="translation">Übersetzung</string> <string name="translation">Übersetzung</string>
<string name="cant_open_invalid_url">The item url is invalid. I\'m looking into solving this issue so the app won\'t crash.</string> <string name="cant_open_invalid_url">Der Artikel-Link ist ungültig. Ich such nach einer Lösung dieses Problems, damit die App nicht abstürzt.</string>
<string name="items_number_should_be_number">The items number should be an integer.</string> <string name="items_number_should_be_number">Die Anzahl der Artikel sollte eine Ganzzahl sein.</string>
<string name="reader_action_open">Im Browser öffnen</string> <string name="reader_action_open">Im Browser öffnen</string>
<string name="reader_action_share">Teilen</string> <string name="reader_action_share">Teilen</string>
<string name="pref_switch_actions_pager_scroll_on">Mark articles as read when swiping between articles.</string> <string name="pref_switch_actions_pager_scroll_on">Artikel als gelesen markieren, wenn zwischen den Artikeln gewischt wird.</string>
<string name="add_to_favs_reader">Zu Favoriten hinzufügen</string> <string name="add_to_favs_reader">Zu Favoriten hinzufügen</string>
<string name="pref_content_reader_font_size">Article reader content font size</string> <string name="pref_content_reader_font_size">Schriftgröße im Lesemodus</string>
<string name="pref_header_viewer">Article viewer</string> <string name="pref_header_viewer">Lesemodus</string>
<string name="refresh_dialog_message">This will refresh your Selfoss instance.</string> <string name="refresh_dialog_message">Dies wird die Selfoss-Instanz aktualisieren.</string>
<string name="markall_dialog_message">Dies wird alle Elemente als gelesen markieren.</string> <string name="markall_dialog_message">Dies wird alle Elemente als gelesen markieren.</string>
<string name="pref_switch_actions_pager_scroll">Beim Wischen als gelesen markieren</string> <string name="pref_switch_actions_pager_scroll">Beim Wischen als gelesen markieren</string>
<string name="pref_switch_actions_pager_scroll_off">Don\'t mark articles as read when swiping.</string> <string name="pref_switch_actions_pager_scroll_off">Artikel nicht als gelesen markieren, wenn zwischen den Artikeln gewischt wird.</string>
<string name="unmark">Eintrag als ungelesen markieren</string> <string name="unmark">Eintrag als ungelesen markieren</string>
<string name="pref_header_offline">Offline and cache</string> <string name="pref_header_offline">Offline-Modus und Cache</string>
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string> <string name="pref_switch_items_caching_off">Artikel werden nicht lokal zwischengespeichert wodurch die App nicht offline nutzbar ist.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string> <string name="pref_switch_items_caching_on">Artikel werden lokal zwischengespeichert wodurch die App offline nutzbar ist.</string>
<string name="pref_switch_items_caching">Save items for offline use</string> <string name="pref_switch_items_caching">Artikel lokal zwischenspeichern</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Nach neuen Quellen und Tags suchen</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Diese Funktion sollte deaktiviert werden, wenn der Server übermäßig viele Datenbankanfragen erhält.</string>
<string name="network_connectivity_lost">"Die Netzwerkverbindung wurde unterbrochen"</string> <string name="network_connectivity_lost">"Die Netzwerkverbindung wurde unterbrochen"</string>
<string name="network_connectivity_retrieved">"Netzwerkverbindung ist jetzt verfügbar"</string> <string name="network_connectivity_retrieved">"Netzwerkverbindung ist jetzt verfügbar"</string>
<string name="pref_switch_periodic_refresh">Synchronisiere Artikel</string> <string name="pref_switch_periodic_refresh">Synchronisiere Artikel</string>
<string name="pref_switch_periodic_refresh_off">Artikel werden nicht im Hintergrund synchronisiert</string> <string name="pref_switch_periodic_refresh_off">Artikel werden nicht im Hintergrund synchronisiert</string>
<string name="pref_switch_periodic_refresh_on">Die Artikel werden regelmäßig synchronisiert</string> <string name="pref_switch_periodic_refresh_on">Die Artikel werden regelmäßig synchronisiert</string>
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string> <string name="pref_periodic_refresh_minutes_title"><![CDATA[Aktualisierungsintervall (>= 15 Minuten)]]></string>
<string name="pref_switch_refresh_when_charging">Nur aktualisieren, wenn das Telefon aufgeladen wird</string> <string name="pref_switch_refresh_when_charging">Nur aktualisieren, wenn das Telefon aufgeladen wird</string>
<string name="loading_notification_title">Lädt…</string> <string name="loading_notification_title">Lädt…</string>
<string name="loading_notification_text">Selfoss synchronisiert Ihre Artikel</string> <string name="loading_notification_text">Selfoss synchronisiert Ihre Artikel</string>
<string name="notification_channel_sync">Sync notification</string> <string name="notification_channel_sync">Synchronisationsbenachrichtigung</string>
<string name="new_items_channel_sync">New items notification</string> <string name="new_items_channel_sync">Benachrichtigung bei neuen Artikeln</string>
<string name="new_items_notification_title">New items !</string> <string name="new_items_notification_title">Neue Artikel!</string>
<string name="new_items_notification_text">%1$d new items loaded.</string> <string name="new_items_notification_text">%1$d neue Artikel geladen.</string>
<string name="pref_switch_notify_new_items">Notify on new items synced.</string> <string name="pref_switch_notify_new_items">Benachrichtigung bei neuen Artikeln</string>
<string name="shortcut_offline">Offline</string> <string name="shortcut_offline">Offline</string>
<string name="pref_api_timeout">API-Zeitüberschreitung</string> <string name="pref_api_timeout">API-Zeitüberschreitung</string>
<string name="pref_header_experimental">Experimentell</string> <string name="pref_header_experimental">Experimentell</string>
<string name="webview_dialog_issue_message">Webview not available. Disabling the article viewer to avoid any future crashes. Will load articles inside of your browser from now on.</string> <string name="webview_dialog_issue_message">Webview ist nicht verfügbar. Deaktiviere den Lesemodus, um zukünftige Abstürze zu vermeiden. Lade von nun an die Nachrichten in deinen Browser.</string>
<string name="webview_dialog_issue_title">Webview issue</string> <string name="webview_dialog_issue_title">Webview-Probleme</string>
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Linksbündig</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Blocksatz</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Schriftgröße im Lesemodus</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string> <string name="remove_source">Quelle entfernen</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string> <string name="pref_theme_title">Heller/Dunkler Modus</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string> <string name="mode_dark">Dunkler Modus</string>
<string name="remove_source">Remove source</string> <string name="mode_system">Systemeinstellungen übernehmen</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="mode_light">Heller Modus</string>
<string name="mode_dark">Dark mode</string> <string name="gdpr_dialog_title">Diese App teilt keine persönlichen Daten.</string>
<string name="mode_system">Follow the system setting</string> <string name="gdpr_dialog_message"><![CDATA[Das Senden von Absturzberichten ist jetzt aktiviert. Die Funktion kann auf der Einstellungsseite deaktiviert werden, beachte aber bitte, dass Absturzberichte für die Anwendungsentwicklung von entscheidender Bedeutung sind.]]></string>
<string name="mode_light">Light mode</string> <string name="crash_toast_text">Die App ist abgestürzt. Details werden an den Entwickler gesendet.</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="pref_switch_disable_acra">"Automatische Fehlerberichterstattung deaktivieren."</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="menu_home_filter">Filter</string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="application_selfoss_only">Diese App funktioniert nur mit einer Selfoss-Instanz, nicht mit einzelnen RSS-Feeds.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="menu_home_sources">Quellen</string>
<string name="menu_home_filter">Filters</string> <string name="update_source">Quelle aktualisieren</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="confirm_disconnect_title">Verbindung trennen?</string>
<string name="menu_home_sources">Sources</string> <string name="confirm_disconnect_description">Die Verbindung zur Selfoss-Instanz wird getrennt.</string>
<string name="update_source">Update source</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="nothing_here">"Keine Einträge vorhanden"</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="tab_new">"Neu"</string>
<string name="disable_ssl">Disable SSL</string> <string name="tab_read">"Alle"</string>
<string name="tab_favs">"Favoriten"</string>
<string name="action_about">"Über"</string>
<string name="marked_as_read">"Artikel gelesen"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Lector para Selfoss"</string> <string name="app_name">"Lector para Selfoss"</string>
<string name="title_activity_login">"Iniciar sesión"</string> <string name="title_activity_login">"Iniciar sesión"</string>
<string name="prompt_password">"Contraseña"</string> <string name="prompt_password">"Contraseña"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"La contraseña no es suficientemente larga"</string> <string name="error_invalid_password">"La contraseña no es suficientemente larga"</string>
<string name="error_field_required">"Campo requerido"</string> <string name="error_field_required">"Campo requerido"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Inicio de sesión requerido ?"</string> <string name="withLoginSwitch">"Inicio de sesión requerido ?"</string>
<string name="login_url_problem">"Oops. Puede que necesite añadir un \"/\" al final de la url."</string> <string name="login_url_problem">"Oops. Puede que necesite añadir un \"/\" al final de la url."</string>
<string name="prompt_login">"Nombre de usuario"</string> <string name="prompt_login">"Nombre de usuario"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Revise sus datos de nuevo."</string> <string name="wrong_infos">"Revise sus datos de nuevo."</string>
<string name="all_posts_not_read">"No todas las publicaciones fueron leídas"</string> <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="all_posts_read">"Todas las publicaciones fueron leídas"</string>
<string name="nothing_here">"Nada aquí"</string>
<string name="tab_new">"Nuevo"</string>
<string name="tab_read">"Todo"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Acerca de"</string>
<string name="marked_as_read">"Artículo leído"</string>
<string name="marked_as_unread">"Artículo no leído"</string>
<string name="undo_string">"Deshacer"</string> <string name="undo_string">"Deshacer"</string>
<string name="addStringNoUrl">"Iniciar sesión para añadir fuentes."</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_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_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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"No se puede obtener la lista de fuentes."</string>
<string name="form_not_complete">"El formulario no está completo"</string> <string name="form_not_complete">"El formulario no está completo"</string>
<string name="pref_header_links">"Enlaces"</string> <string name="pref_header_links">"Enlaces"</string>
<string name="issue_tracker_link">"Rastreador de Incidencias"</string> <string name="issue_tracker_link">"Rastreador de Incidencias"</string>
@ -90,7 +84,7 @@
<string name="pref_switch_items_caching">Guardar elementos para uso sin conexión</string> <string name="pref_switch_items_caching">Guardar elementos para uso sin conexión</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="network_connectivity_lost">"Network connection lost"</string> <string name="network_connectivity_lost">"Sin conexión!"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string> <string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sincronizar artículos</string> <string name="pref_switch_periodic_refresh">Sincronizar artículos</string>
<string name="pref_switch_periodic_refresh_off">Los artículos no se sincronizarán en segundo plano</string> <string name="pref_switch_periodic_refresh_off">Los artículos no se sincronizarán en segundo plano</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Alinear a la izquierda</string> <string name="reader_text_align_left">Alinear a la izquierda</string>
<string name="reader_text_align_justify">Justificado</string> <string name="reader_text_align_justify">Justificado</string>
<string name="settings_reader_font">Modo lectura</string> <string name="settings_reader_font">Modo lectura</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Nada aquí"</string>
<string name="tab_new">"Nuevo"</string>
<string name="tab_read">"Todo"</string>
<string name="tab_favs">"Favoritos"</string>
<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> </resources>

View File

@ -1,134 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Log in"</string>
<string name="prompt_password">"Password"</string>
<string name="action_sign_in">"Go"</string>
<string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string>
<string name="withLoginSwitch">"Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"Username"</string>
<string name="readAll">"Read all"</string>
<string name="action_disconnect">"Disconnect"</string>
<string name="title_activity_settings">"Settings"</string>
<string name="pref_header_general">"General"</string>
<string name="add_source_hint_tags">"Tag1, Tag2, Tag3"</string>
<string name="add_source_hint_url">"Link"</string>
<string name="add_source_hint_name">"Name"</string>
<string name="add_source">"Add a source"</string>
<string name="add_source_save">"Save"</string>
<string name="wrong_infos">"Check your details again."</string>
<string name="all_posts_not_read">"All posts weren't read"</string>
<string name="all_posts_read">"All posts were read"</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"All"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</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>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string>
<string name="form_not_complete">"The form is not complete"</string>
<string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Issue Tracker"</string>
<string name="issue_tracker_summary">"Report a bug or ask for a new feature"</string>
<string name="warning_wrong_url">"WARNING"</string>
<string name="pref_switch_card_view_title">"Card View"</string>
<string name="share">"Share"</string>
<string name="switch_unread_count">"Display the unread count as a badge for the bottom bar."</string>
<string name="switch_unread_count_title">"Display unread count"</string>
<string name="display_all_counts_title">"Display count for favorite and read"</string>
<string name="text_wrong_url">"You seem to be trying to use an invalid URL. Make sure it is correct, and if the problem persists, contact me (via the store contact link). Please note that the app needs you to be using Selfoss. You can't access RSS feeds without it."</string>
<string name="pref_article_viewer_title">"Open links inside the app"</string>
<string name="pref_article_viewer_on">"Articles will open inside the app"</string>
<string name="pref_article_viewer_off">"Articles will open with your default browser"</string>
<string name="pref_general_category_links">"Link handling"</string>
<string name="pref_general_category_displaying">"Displaying"</string>
<string name="pref_switch_card_view_on">"The articles will be displayed as cards"</string>
<string name="pref_switch_card_view_off">"The articles will be displayed as a list"</string>
<string name="menu_home_refresh">"Update remote"</string>
<string name="refresh_success_response">"The remote is updated, you can now reload the articles list"</string>
<string name="refresh_failer_message">"The update didn't work, try again later, or check your selfoss logs."</string>
<string name="refresh_in_progress">"Refresh in progress"</string>
<string name="card_height_title">Full height cards</string>
<string name="card_height_on">Cards height will adjust to its content</string>
<string name="card_height_off">Card height will be fixed</string>
<string name="source_code">Source code</string>
<string name="filter_item_tags">Tags</string>
<string name="filter_item_sources">Sources</string>
<string name="menu_home_search">Search</string>
<string name="can_delete_source">Can\'t delete the source…</string>
<string name="base_url_error">There was an issue when trying to communicate with your Selfoss Instance. If the issue persists, please get in touch with me.</string>
<string name="pref_header_theme">Themes</string>
<string name="pref_selfoss_category">Selfoss Api</string>
<string name="pref_api_items_number_title">Loaded items number</string>
<string name="pref_general_infinite_loading_title">Load more articles on scroll</string>
<string name="translation">Translation</string>
<string name="cant_open_invalid_url">The item url is invalid. I\'m looking into solving this issue so the app won\'t crash.</string>
<string name="items_number_should_be_number">The items number should be an integer.</string>
<string name="reader_action_open">Open in browser</string>
<string name="reader_action_share">Share</string>
<string name="pref_switch_actions_pager_scroll_on">Mark articles as read when swiping between articles.</string>
<string name="add_to_favs_reader">Add to favorites</string>
<string name="pref_content_reader_font_size">Article reader content font size</string>
<string name="pref_header_viewer">Article viewer</string>
<string name="refresh_dialog_message">This will refresh your Selfoss instance.</string>
<string name="markall_dialog_message">This will mark all the items as read.</string>
<string name="pref_switch_actions_pager_scroll">Mark as read on swipe</string>
<string name="pref_switch_actions_pager_scroll_off">Don\'t mark articles as read when swiping.</string>
<string name="unmark">Mark item as unread</string>
<string name="pref_header_offline">Offline and cache</string>
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
<string name="pref_switch_refresh_when_charging">Only refresh when phone is charging</string>
<string name="loading_notification_title">Loading …</string>
<string name="loading_notification_text">Selfoss is syncing your articles</string>
<string name="notification_channel_sync">Sync notification</string>
<string name="new_items_channel_sync">New items notification</string>
<string name="new_items_notification_title">New items !</string>
<string name="new_items_notification_text">%1$d new items loaded.</string>
<string name="pref_switch_notify_new_items">Notify on new items synced.</string>
<string name="shortcut_offline">Offline</string>
<string name="pref_api_timeout">Api Timeout</string>
<string name="pref_header_experimental">Experimental</string>
<string name="webview_dialog_issue_message">Webview not available. Disabling the article viewer to avoid any future crashes. Will load articles inside of your browser from now on.</string>
<string name="webview_dialog_issue_title">Webview issue</string>
<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>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
<string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Login"</string> <string name="title_activity_login">"Login"</string>
<string name="prompt_password">"Mot de passe"</string> <string name="prompt_password">"Mot de passe"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Mot de passe trop court"</string> <string name="error_invalid_password">"Mot de passe trop court"</string>
<string name="error_field_required">"Champ requis"</string> <string name="error_field_required">"Champ requis"</string>
<string name="prompt_url">"Url Selfoss"</string> <string name="prompt_url">"Url Selfoss"</string>
<string name="disable_ssl">"Désactiver la vérification SSL"</string>
<string name="withLoginSwitch">"Avec login ?"</string> <string name="withLoginSwitch">"Avec login ?"</string>
<string name="login_url_problem">"Petit souci. Il manque peut-être un / à la fin ?"</string> <string name="login_url_problem">"Petit souci. Il manque peut-être un / à la fin ?"</string>
<string name="prompt_login">"Utilisateur"</string> <string name="prompt_login">"Utilisateur"</string>
@ -22,16 +23,9 @@
<string name="wrong_infos">"Vérifiez vos informations."</string> <string name="wrong_infos">"Vérifiez vos informations."</string>
<string name="all_posts_not_read">"Tous les posts n'ont pas été lus"</string> <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="all_posts_read">"Tous les posts sont lus"</string>
<string name="nothing_here">"Il n'y a rien ici !"</string>
<string name="tab_new">"Non lus"</string>
<string name="tab_read">"Tous"</string>
<string name="tab_favs">"Favoris"</string>
<string name="action_about">"À propos"</string>
<string name="marked_as_read">"Marqué comme lu"</string>
<string name="marked_as_unread">"Marqué comme non lu"</string>
<string name="undo_string">"Annuler"</string> <string name="undo_string">"Annuler"</string>
<string name="addStringNoUrl">"Identifiez-vous pour ajouter une source."</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_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_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> <string name="cant_get_spouts_no_network">"Impossible d'obtenir la liste des spouts en raison d'un problème de réseau."</string>
<string name="cant_get_spouts">"Impossible d'obtenir la liste des spouts. Cela pourrait venir de l'api."</string> <string name="cant_get_spouts">"Impossible d'obtenir la liste des spouts. Cela pourrait venir de l'api."</string>
@ -42,13 +36,13 @@
<string name="warning_wrong_url">"ATTENTION"</string> <string name="warning_wrong_url">"ATTENTION"</string>
<string name="pref_switch_card_view_title">"Vue en carte"</string> <string name="pref_switch_card_view_title">"Vue en carte"</string>
<string name="share">"Partager"</string> <string name="share">"Partager"</string>
<string name="switch_unread_count">"Afficher le nombre d'articles non lus sur la barre en bas de l'écran"</string> <string name="switch_unread_count">"Afficher le nombre d'articles non lus sur la barre en bas de l'écran."</string>
<string name="switch_unread_count_title">"Afficher le nombre de non lus"</string> <string name="switch_unread_count_title">"Afficher le nombre de non lus"</string>
<string name="display_all_counts_title">"Afficher le nombre de favoris et d'articles lus"</string> <string name="display_all_counts_title">"Afficher le nombre de favoris et d'articles lus"</string>
<string name="text_wrong_url">"Vous semblez essayer de vous connecter avec une URL invalide. Assurez-vous que c'est la bonne, et si le problème persiste, contactez-moi via le lien du play store. Notez aussi que l'application ne peut fonctionner sans l'application web Selfoss. Vous ne pouvez pas utiliser l'application pour accéder directement aux flux RSS."</string> <string name="text_wrong_url">"Vous semblez essayer de vous connecter avec une URL invalide. Assurez-vous que c'est la bonne, et si le problème persiste, contactez-moi via le lien du play store. Notez aussi que l'application ne peut fonctionner sans l'application web Selfoss. Vous ne pouvez pas utiliser l'application pour accéder directement aux flux RSS."</string>
<string name="pref_article_viewer_title">"Ouvrir les liens dans l'application"</string> <string name="pref_article_viewer_title">"Ouvrir les liens dans l'application"</string>
<string name="pref_article_viewer_on">"Les articles s'ouvriront dans l'application"</string> <string name="pref_article_viewer_on">"Les articles s'ouvriront dans l'application"</string>
<string name="pref_article_viewer_off">"Les articles s'ouvriront dans votre naviguateur par défaut"</string> <string name="pref_article_viewer_off">"Les articles s'ouvriront dans votre navigateur par défaut"</string>
<string name="pref_general_category_links">"Gestion des liens"</string> <string name="pref_general_category_links">"Gestion des liens"</string>
<string name="pref_general_category_displaying">"Affichage"</string> <string name="pref_general_category_displaying">"Affichage"</string>
<string name="pref_switch_card_view_on">"Les articles seront affichés en forme de carte"</string> <string name="pref_switch_card_view_on">"Les articles seront affichés en forme de carte"</string>
@ -65,7 +59,7 @@
<string name="filter_item_sources">Sources</string> <string name="filter_item_sources">Sources</string>
<string name="menu_home_search">Rechercher</string> <string name="menu_home_search">Rechercher</string>
<string name="can_delete_source">Impossible de supprimer la source…</string> <string name="can_delete_source">Impossible de supprimer la source…</string>
<string name="base_url_error">Il y a eu un souci lors de la communication avec votre instance Selfoss. Si le problèmes persiste, contactez-moi pour trouver une solution.</string> <string name="base_url_error">Il y a eu un souci lors de la communication avec votre instance Selfoss. Si le problème persiste, contactez-moi pour trouver une solution.</string>
<string name="pref_header_theme">Thèmes</string> <string name="pref_header_theme">Thèmes</string>
<string name="pref_selfoss_category">Api Selfoss</string> <string name="pref_selfoss_category">Api Selfoss</string>
<string name="pref_api_items_number_title">Nombre d\'articles chargés</string> <string name="pref_api_items_number_title">Nombre d\'articles chargés</string>
@ -81,7 +75,7 @@
<string name="pref_header_viewer">Lecteur d\'articles</string> <string name="pref_header_viewer">Lecteur d\'articles</string>
<string name="refresh_dialog_message">En validant, votre instance Selfoss sera mise à jour.</string> <string name="refresh_dialog_message">En validant, votre instance Selfoss sera mise à jour.</string>
<string name="markall_dialog_message">Marquer tous les éléments comme lus ?</string> <string name="markall_dialog_message">Marquer tous les éléments comme lus ?</string>
<string name="pref_switch_actions_pager_scroll">Marquer comme lu à la navigation.</string> <string name="pref_switch_actions_pager_scroll">Marquer comme lu à la navigation</string>
<string name="pref_switch_actions_pager_scroll_off">Ne pas marquer les articles comme lus à la navigation.</string> <string name="pref_switch_actions_pager_scroll_off">Ne pas marquer les articles comme lus à la navigation.</string>
<string name="unmark">Marquer l\'article comme non lu</string> <string name="unmark">Marquer l\'article comme non lu</string>
<string name="pref_header_offline">Hors ligne et cache</string> <string name="pref_header_offline">Hors ligne et cache</string>
@ -93,9 +87,9 @@
<string name="network_connectivity_lost">"Connexion au réseau perdue"</string> <string name="network_connectivity_lost">"Connexion au réseau perdue"</string>
<string name="network_connectivity_retrieved">"Connexion réseau de nouveau disponible"</string> <string name="network_connectivity_retrieved">"Connexion réseau de nouveau disponible"</string>
<string name="pref_switch_periodic_refresh">Synchroniser les articles</string> <string name="pref_switch_periodic_refresh">Synchroniser les articles</string>
<string name="pref_switch_periodic_refresh_off">Les articles ne seront pas synchronisés en arrière plan</string> <string name="pref_switch_periodic_refresh_off">Les articles ne seront pas synchronisés en arrière-plan</string>
<string name="pref_switch_periodic_refresh_on">Articles seront périodiquement synchronisées</string> <string name="pref_switch_periodic_refresh_on">Articles seront périodiquement synchronisés</string>
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Interval de synchronisation ( >= 15 minutes)]]></string> <string name="pref_periodic_refresh_minutes_title"><![CDATA[Intervalle de synchronisation ( >= 15 minutes)]]></string>
<string name="pref_switch_refresh_when_charging">Synchroniser uniquement lorsque le téléphone est en charge</string> <string name="pref_switch_refresh_when_charging">Synchroniser uniquement lorsque le téléphone est en charge</string>
<string name="loading_notification_title">Chargement …</string> <string name="loading_notification_title">Chargement …</string>
<string name="loading_notification_text">Selfoss synchronise vos articles</string> <string name="loading_notification_text">Selfoss synchronise vos articles</string>
@ -107,28 +101,32 @@
<string name="shortcut_offline">Hors ligne</string> <string name="shortcut_offline">Hors ligne</string>
<string name="pref_api_timeout">Timeout de l\'api</string> <string name="pref_api_timeout">Timeout de l\'api</string>
<string name="pref_header_experimental">Expérimental</string> <string name="pref_header_experimental">Expérimental</string>
<string name="webview_dialog_issue_message">Pas de Webview disponible. Désactivation du lecteur d\'article pour éviter les futures erreurs. Les articles seront lu via votre navigateur à l\'avenir.</string> <string name="webview_dialog_issue_message">Pas de Webview disponible. Désactivation du lecteur d\'article pour éviter les futures erreurs. Les articles seront lus via votre navigateur à l\'avenir.</string>
<string name="webview_dialog_issue_title">Problème de Webview</string> <string name="webview_dialog_issue_title">Problème de Webview</string>
<string name="reader_text_align_left">Aligner à gauche</string> <string name="reader_text_align_left">Aligner à gauche</string>
<string name="reader_text_align_justify">Justifier le texte</string> <string name="reader_text_align_justify">Justifier le texte</string>
<string name="settings_reader_font">Police du lecteur d\'articles</string> <string name="settings_reader_font">Police du lecteur d\'articles</string>
<string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
<string name="reader_static_bar_on">La barre sera affichée</string>
<string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
<string name="remove_source">Supprimer la source</string> <string name="remove_source">Supprimer la source</string>
<string name="pref_theme_title">Thème Clair/Sombre</string> <string name="pref_theme_title">Thème Clair/Sombre</string>
<string name="mode_dark">Thème sombre</string> <string name="mode_dark">Thème sombre</string>
<string name="mode_system">Utiliser les paramètres système</string> <string name="mode_system">Utiliser les paramètres système</string>
<string name="mode_light">Thème clair</string> <string name="mode_light">Thème clair</string>
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">L\'application ne partage aucune information personnelle.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Le rapport de plantage est activés par défaut. Il peut être désactivé depuis les paramètres de l\'application. Notez que les rapports de plantage sont essentiels pour le développement de l\'application.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">Un bug s\'est produit. Le développeur en sera informé.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Désactiver les rapports de plantage."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filtres</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">Cette application ne fonctionne qu\'avec l\'api de Selfoss, et aucun autre.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Mise à jour des sources</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Se déconnecter ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">Vous allez être déconnecté de votre instance Selfoss.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Il n'y a rien ici !"</string>
<string name="tab_new">"Non lus"</string>
<string name="tab_read">"Tous"</string>
<string name="tab_favs">"Favoris"</string>
<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> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Lector para selfoss"</string> <string name="app_name">"Lector para selfoss"</string>
<string name="title_activity_login">"Conectar"</string> <string name="title_activity_login">"Conectar"</string>
<string name="prompt_password">"Contrasinal"</string> <string name="prompt_password">"Contrasinal"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"O contrasinal non é suficientemente longo"</string> <string name="error_invalid_password">"O contrasinal non é suficientemente longo"</string>
<string name="error_field_required">"Campo requirido"</string> <string name="error_field_required">"Campo requirido"</string>
<string name="prompt_url">"URL"</string> <string name="prompt_url">"URL"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"É preciso iniciar sesión?"</string> <string name="withLoginSwitch">"É preciso iniciar sesión?"</string>
<string name="login_url_problem">"Ups! Pode que precises engadir un \"/\" o final da URL."</string> <string name="login_url_problem">"Ups! Pode que precises engadir un \"/\" o final da URL."</string>
<string name="prompt_login">"Nome de usuario"</string> <string name="prompt_login">"Nome de usuario"</string>
@ -22,13 +23,6 @@
<string name="wrong_infos">"Comprobar os teus detalles de novo."</string> <string name="wrong_infos">"Comprobar os teus detalles de novo."</string>
<string name="all_posts_not_read">"Non se leron todas as publicacións"</string> <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="all_posts_read">"Leronse todas as publicacións"</string>
<string name="nothing_here">"Non hai nada aquí"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Todos"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Acerca de"</string>
<string name="marked_as_read">"Elemento lido"</string>
<string name="marked_as_unread">"Elemento non lido"</string>
<string name="undo_string">"Desfacer"</string> <string name="undo_string">"Desfacer"</string>
<string name="addStringNoUrl">"Accede pra engadir fontes."</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_get_sources">"Non se pode obter a lista de fontes."</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Aliñar á esquerda</string> <string name="reader_text_align_left">Aliñar á esquerda</string>
<string name="reader_text_align_justify">Xustificado</string> <string name="reader_text_align_justify">Xustificado</string>
<string name="settings_reader_font">Modo lector</string> <string name="settings_reader_font">Modo lector</string>
<string name="reader_static_bar_title">Barra inferior estática na vista de artigos</string>
<string name="reader_static_bar_on">A barra inferior mostrarase sempre</string>
<string name="reader_static_bar_off">A barra inferior pode mostrarse a través do botón flotante</string>
<string name="remove_source">Eliminar fonte</string> <string name="remove_source">Eliminar fonte</string>
<string name="pref_theme_title">Modo Claro/Escuro</string> <string name="pref_theme_title">Modo Claro/Escuro</string>
<string name="mode_dark">Modo escuro</string> <string name="mode_dark">Modo escuro</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">A aplicación non comparte ningún dato persoal seu.</string> <string name="gdpr_dialog_title">A aplicación non comparte ningún dato persoal seu.</string>
<string name="gdpr_dialog_message"><![CDATA[O envío de informes de erros está habilitado. Pode deshabilitarse dende a páxina de axustes. Ten en conta que os informes de erros son esenciais para o desenvolvemento da aplicación.]]></string> <string name="gdpr_dialog_message"><![CDATA[O envío de informes de erros está habilitado. Pode deshabilitarse dende a páxina de axustes. Ten en conta que os informes de erros son esenciais para o desenvolvemento da aplicación.]]></string>
<string name="crash_toast_text">Ocurriu un erro. Enviando os detalles o desenvolvedor.</string> <string name="crash_toast_text">Ocurriu un erro. Enviando os detalles o desenvolvedor.</string>
<string name="pref_switch_disable_acra">"Deshabilitar o reporte automático de erros. "</string> <string name="pref_switch_disable_acra">"Deshabilitar o reporte automático de erros."</string>
<string name="menu_home_filter">Filtros</string> <string name="menu_home_filter">Filtros</string>
<string name="application_selfoss_only">Esta aplicación só funciona cunha instancia de Selfoss, e con ningún outro filtro RSS.</string> <string name="application_selfoss_only">Esta aplicación só funciona cunha instancia de Selfoss, e con ningún outro filtro RSS.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Non hai nada aquí"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Todos"</string>
<string name="tab_favs">"Favoritos"</string>
<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> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Masuk"</string> <string name="title_activity_login">"Masuk"</string>
<string name="prompt_password">"Kata sandi"</string> <string name="prompt_password">"Kata sandi"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Kata sandinya tidak cukup panjang"</string> <string name="error_invalid_password">"Kata sandinya tidak cukup panjang"</string>
<string name="error_field_required">"Kolom wajib diisi"</string> <string name="error_field_required">"Kolom wajib diisi"</string>
<string name="prompt_url">"URL"</string> <string name="prompt_url">"URL"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Harus masuk?"</string> <string name="withLoginSwitch">"Harus masuk?"</string>
<string name="login_url_problem">"Ups. Anda mungkin harus menambahkan \"/\" di akhir url."</string> <string name="login_url_problem">"Ups. Anda mungkin harus menambahkan \"/\" di akhir url."</string>
<string name="prompt_login">"Nama pengguna"</string> <string name="prompt_login">"Nama pengguna"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Periksa kembali detail Anda."</string> <string name="wrong_infos">"Periksa kembali detail Anda."</string>
<string name="all_posts_not_read">"Semua pos belum dibaca"</string> <string name="all_posts_not_read">"Semua pos belum dibaca"</string>
<string name="all_posts_read">"Semua pos sudah dibaca"</string> <string name="all_posts_read">"Semua pos sudah dibaca"</string>
<string name="nothing_here">"Tidak ada di sini"</string>
<string name="tab_new">"Baru"</string>
<string name="tab_read">"Semua"</string>
<string name="tab_favs">"Favorit"</string>
<string name="action_about">"Tentang"</string>
<string name="marked_as_read">"Membaca item"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Urung"</string> <string name="undo_string">"Urung"</string>
<string name="addStringNoUrl">"Masuk untuk menambah sumber."</string> <string name="addStringNoUrl">"Masuk untuk menambah sumber."</string>
<string name="cant_get_sources">"Tidak bisa mendapatkan daftar 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_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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Tidak bisa masuk ke daftar Spouts."</string>
<string name="form_not_complete">"Formulirnya belum selesai"</string> <string name="form_not_complete">"Formulirnya belum selesai"</string>
<string name="pref_header_links">"Tautan"</string> <string name="pref_header_links">"Tautan"</string>
<string name="issue_tracker_link">"Pelacak Masalah"</string> <string name="issue_tracker_link">"Pelacak Masalah"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Tidak ada di sini"</string>
<string name="tab_new">"Baru"</string>
<string name="tab_read">"Semua"</string>
<string name="tab_favs">"Favorit"</string>
<string name="action_about">"Tentang"</string>
<string name="marked_as_read">"Membaca item"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Lettore RSS per Selfoss"</string> <string name="app_name">"Lettore RSS per Selfoss"</string>
<string name="title_activity_login">"Accedi"</string> <string name="title_activity_login">"Accedi"</string>
<string name="prompt_password">"Password"</string> <string name="prompt_password">"Password"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"La password non è sufficientemente lunga"</string> <string name="error_invalid_password">"La password non è sufficientemente lunga"</string>
<string name="error_field_required">"Campo obbligatorio"</string> <string name="error_field_required">"Campo obbligatorio"</string>
<string name="prompt_url">"URL"</string> <string name="prompt_url">"URL"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"È richiesto l'accesso?"</string> <string name="withLoginSwitch">"È richiesto l'accesso?"</string>
<string name="login_url_problem">"Oops. Potrebbe essere necessario aggiungere un \"/\" alla fine dell'url."</string> <string name="login_url_problem">"Oops. Potrebbe essere necessario aggiungere un \"/\" alla fine dell'url."</string>
<string name="prompt_login">"Nome utente"</string> <string name="prompt_login">"Nome utente"</string>
@ -22,13 +23,6 @@
<string name="wrong_infos">"Controlla nuovamente i dati."</string> <string name="wrong_infos">"Controlla nuovamente i dati."</string>
<string name="all_posts_not_read">"All posts weren't read"</string> <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="all_posts_read">"Tutti i messaggi sono stati letti"</string>
<string name="nothing_here">"Non c'è niente qui"</string>
<string name="tab_new">"Nuovi"</string>
<string name="tab_read">"Tutti"</string>
<string name="tab_favs">"Preferiti"</string>
<string name="action_about">"Informazioni"</string>
<string name="marked_as_read">"Articolo letto"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Annulla"</string> <string name="undo_string">"Annulla"</string>
<string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string> <string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string>
<string name="cant_get_sources">"Can't get sources list."</string> <string name="cant_get_sources">"Can't get sources list."</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Non c'è niente qui"</string>
<string name="tab_new">"Nuovi"</string>
<string name="tab_read">"Tutti"</string>
<string name="tab_favs">"Preferiti"</string>
<string name="action_about">"Informazioni"</string>
<string name="marked_as_read">"Articolo letto"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"로그인"</string> <string name="title_activity_login">"로그인"</string>
<string name="prompt_password">"비밀번호"</string> <string name="prompt_password">"비밀번호"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"패스워드가 짧습니다."</string> <string name="error_invalid_password">"패스워드가 짧습니다."</string>
<string name="error_field_required">"필수 항목"</string> <string name="error_field_required">"필수 항목"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"로그인이 필요합니까?"</string> <string name="withLoginSwitch">"로그인이 필요합니까?"</string>
<string name="login_url_problem">"죄송합니다. Url의 끝에 \"/\"를 추가할 필요가 있습니다."</string> <string name="login_url_problem">"죄송합니다. Url의 끝에 \"/\"를 추가할 필요가 있습니다."</string>
<string name="prompt_login">"사용자 이름"</string> <string name="prompt_login">"사용자 이름"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"세부 정보를 다시 확인하세요."</string> <string name="wrong_infos">"세부 정보를 다시 확인하세요."</string>
<string name="all_posts_not_read">"모든 게시물을 읽지 않았습니다."</string> <string name="all_posts_not_read">"모든 게시물을 읽지 않았습니다."</string>
<string name="all_posts_read">"모든 게시물을 읽었습니다."</string> <string name="all_posts_read">"모든 게시물을 읽었습니다."</string>
<string name="nothing_here">"비어있음"</string>
<string name="tab_new">"새로운"</string>
<string name="tab_read">"전체"</string>
<string name="tab_favs">"즐겨찾기"</string>
<string name="action_about">"정보"</string>
<string name="marked_as_read">"항목 읽기"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"실행 취소"</string> <string name="undo_string">"실행 취소"</string>
<string name="addStringNoUrl">"로그인 소스를 추가 해야 합니다."</string> <string name="addStringNoUrl">"로그인 소스를 추가 해야 합니다."</string>
<string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string> <string name="cant_get_sources">"소스 리스트를 얻을 수 없습니다."</string>
<string name="cant_create_source">"소스를 만들 수 없습니다."</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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Spouts 목록을 가져올 수 없습니다."</string>
<string name="form_not_complete">"양식이 완료되지 않았습니다."</string> <string name="form_not_complete">"양식이 완료되지 않았습니다."</string>
<string name="pref_header_links">"링크"</string> <string name="pref_header_links">"링크"</string>
<string name="issue_tracker_link">"이슈 트래커"</string> <string name="issue_tracker_link">"이슈 트래커"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"비어있음"</string>
<string name="tab_new">"새로운"</string>
<string name="tab_read">"전체"</string>
<string name="tab_favs">"즐겨찾기"</string>
<string name="action_about">"정보"</string>
<string name="marked_as_read">"항목 읽기"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Selfoss Reader"</string> <string name="app_name">"Selfoss Reader"</string>
<string name="title_activity_login">"Inloggen"</string> <string name="title_activity_login">"Inloggen"</string>
<string name="prompt_password">"Wachtwoord"</string> <string name="prompt_password">"Wachtwoord"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Wachtwoord niet lang genoeg"</string> <string name="error_invalid_password">"Wachtwoord niet lang genoeg"</string>
<string name="error_field_required">"Dit veld is verplicht"</string> <string name="error_field_required">"Dit veld is verplicht"</string>
<string name="prompt_url">"Selfoss server"</string> <string name="prompt_url">"Selfoss server"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Authenticatie vereist?"</string> <string name="withLoginSwitch">"Authenticatie vereist?"</string>
<string name="login_url_problem">"Oeps, ben je soms de \"/\" vergeten aan het eind?"</string> <string name="login_url_problem">"Oeps, ben je soms de \"/\" vergeten aan het eind?"</string>
<string name="prompt_login">"Gebruikersnaam"</string> <string name="prompt_login">"Gebruikersnaam"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Controleer de gegevens nogmaals."</string> <string name="wrong_infos">"Controleer de gegevens nogmaals."</string>
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string> <string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string> <string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
<string name="nothing_here">"Niets gevonden"</string>
<string name="tab_new">"Nieuw"</string>
<string name="tab_read">"Alle"</string>
<string name="tab_favs">"Favorieten"</string>
<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> <string name="undo_string">"Ongedaan maken"</string>
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string> <string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string> <string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string>
<string name="cant_create_source">"Kan bron niet creëeren"</string> <string name="cant_create_source">"Kan bron niet creëeren"</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Ophalen spouts mislukt"</string>
<string name="form_not_complete">"Formulier is niet volledig ingevuld"</string> <string name="form_not_complete">"Formulier is niet volledig ingevuld"</string>
<string name="pref_header_links">"Links"</string> <string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Bug tracker"</string> <string name="issue_tracker_link">"Bug tracker"</string>
@ -92,7 +86,7 @@
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="network_connectivity_lost">"Network connection lost"</string> <string name="network_connectivity_lost">"Network connection lost"</string>
<string name="network_connectivity_retrieved">"Network connection is now available"</string> <string name="network_connectivity_retrieved">"Network connection is now available"</string>
<string name="pref_switch_periodic_refresh">Sync articles</string> <string name="pref_switch_periodic_refresh">Artikel synchronisieren</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string> <string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>
<string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string> <string name="pref_switch_periodic_refresh_on">Articles will periodically be synced</string>
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string> <string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Niets gevonden"</string>
<string name="tab_new">"Nieuw"</string>
<string name="tab_read">"Alle"</string>
<string name="tab_favs">"Favorieten"</string>
<string name="action_about">"Over"</string>
<string name="marked_as_read">"Artikel gelezen"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Entrar"</string> <string name="title_activity_login">"Entrar"</string>
<string name="prompt_password">"Senha"</string> <string name="prompt_password">"Senha"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Senha muito pequena"</string> <string name="error_invalid_password">"Senha muito pequena"</string>
<string name="error_field_required">"Campo obrigatório"</string> <string name="error_field_required">"Campo obrigatório"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"É necessário o login ?"</string> <string name="withLoginSwitch">"É necessário o login ?"</string>
<string name="login_url_problem">"Oops. Talvez você precise adicionar uma \"/\" no final da url."</string> <string name="login_url_problem">"Oops. Talvez você precise adicionar uma \"/\" no final da url."</string>
<string name="prompt_login">"Usuário"</string> <string name="prompt_login">"Usuário"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Verifique os detalhes novamente."</string> <string name="wrong_infos">"Verifique os detalhes novamente."</string>
<string name="all_posts_not_read">"Nenhum post foi lido"</string> <string name="all_posts_not_read">"Nenhum post foi lido"</string>
<string name="all_posts_read">"Todos os posts foram lidos"</string> <string name="all_posts_read">"Todos os posts foram lidos"</string>
<string name="nothing_here">"Nada aqui"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Todos"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Desfazer"</string> <string name="undo_string">"Desfazer"</string>
<string name="addStringNoUrl">"Faça login para adicionar fontes."</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_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_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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Não é possível obter a lista de spouts."</string>
<string name="form_not_complete">"O formulário não está completo"</string> <string name="form_not_complete">"O formulário não está completo"</string>
<string name="pref_header_links">"Links"</string> <string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Rastreador de problemas"</string> <string name="issue_tracker_link">"Rastreador de problemas"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Nada aqui"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Todos"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Leitor para Selfoss"</string> <string name="app_name">"Leitor para Selfoss"</string>
<string name="title_activity_login">"Iniciar sessão"</string> <string name="title_activity_login">"Iniciar sessão"</string>
<string name="prompt_password">"Palavra passe"</string> <string name="prompt_password">"Palavra passe"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Senha não é longa o suficiente"</string> <string name="error_invalid_password">"Senha não é longa o suficiente"</string>
<string name="error_field_required">"Campo obrigatório"</string> <string name="error_field_required">"Campo obrigatório"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"É necessário fazer login?"</string> <string name="withLoginSwitch">"É necessário fazer login?"</string>
<string name="login_url_problem">"Uups. Você pode precisar adicionar uma \"/\" no final da url."</string> <string name="login_url_problem">"Uups. Você pode precisar adicionar uma \"/\" no final da url."</string>
<string name="prompt_login">"Nome do usuário"</string> <string name="prompt_login">"Nome do usuário"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Verifique seus dados novamente."</string> <string name="wrong_infos">"Verifique seus dados novamente."</string>
<string name="all_posts_not_read">"Todas as postagens não foram lidas"</string> <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="all_posts_read">"Todas as postagens foram lidas"</string>
<string name="nothing_here">"Nada aqui"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Tudo"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Desfazer"</string> <string name="undo_string">"Desfazer"</string>
<string name="addStringNoUrl">"Logar para adicionar fontes."</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_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_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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Não é possível obter a lista de bicos."</string>
<string name="form_not_complete">"O formulário não está completo"</string> <string name="form_not_complete">"O formulário não está completo"</string>
<string name="pref_header_links">"Links"</string> <string name="pref_header_links">"Links"</string>
<string name="issue_tracker_link">"Rastreador de problemas"</string> <string name="issue_tracker_link">"Rastreador de problemas"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Nada aqui"</string>
<string name="tab_new">"Novo"</string>
<string name="tab_read">"Tudo"</string>
<string name="tab_favs">"Favoritos"</string>
<string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"පිවිසෙන්න"</string> <string name="title_activity_login">"පිවිසෙන්න"</string>
<string name="prompt_password">"මුර පදය"</string> <string name="prompt_password">"මුර පදය"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Password not long enough"</string> <string name="error_invalid_password">"Password not long enough"</string>
<string name="error_field_required">"Field required"</string> <string name="error_field_required">"Field required"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Login required ?"</string> <string name="withLoginSwitch">"Login required ?"</string>
<string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string> <string name="login_url_problem">"Oops. You may need to add a \"/\" at the end of the url."</string>
<string name="prompt_login">"පරිශීලක නාමය"</string> <string name="prompt_login">"පරිශීලක නාමය"</string>
@ -22,13 +23,6 @@
<string name="wrong_infos">"Check your details again."</string> <string name="wrong_infos">"Check your details again."</string>
<string name="all_posts_not_read">"All posts weren't read"</string> <string name="all_posts_not_read">"All posts weren't read"</string>
<string name="all_posts_read">"All posts were read"</string> <string name="all_posts_read">"All posts were read"</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"සියල්ල"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"මේ ගැන"</string>
<string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Undo"</string> <string name="undo_string">"Undo"</string>
<string name="addStringNoUrl">"Log in to add sources."</string> <string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string> <string name="cant_get_sources">"Can't get sources list."</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"සියල්ල"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"මේ ගැන"</string>
<string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Selfoss için okuyucu"</string> <string name="app_name">"Selfoss için okuyucu"</string>
<string name="title_activity_login">"Giriş"</string> <string name="title_activity_login">"Giriş"</string>
<string name="prompt_password">"Şifre"</string> <string name="prompt_password">"Şifre"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"Parola yeterince uzun değil"</string> <string name="error_invalid_password">"Parola yeterince uzun değil"</string>
<string name="error_field_required">"Alan gereklidir"</string> <string name="error_field_required">"Alan gereklidir"</string>
<string name="prompt_url">"Url"</string> <string name="prompt_url">"Url"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"Kullanıcı Girişi Gerekli?"</string> <string name="withLoginSwitch">"Kullanıcı Girişi Gerekli?"</string>
<string name="login_url_problem">"Oops. Url'nin sonuna \"/\" eklemek gerekebilir."</string> <string name="login_url_problem">"Oops. Url'nin sonuna \"/\" eklemek gerekebilir."</string>
<string name="prompt_login">"Kullanıcı adı"</string> <string name="prompt_login">"Kullanıcı adı"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"Detaylarınızı tekrar kontrol edin."</string> <string name="wrong_infos">"Detaylarınızı tekrar kontrol edin."</string>
<string name="all_posts_not_read">"Tüm mesajlar okunmadı"</string> <string name="all_posts_not_read">"Tüm mesajlar okunmadı"</string>
<string name="all_posts_read">"Tüm mesajlar okundu"</string> <string name="all_posts_read">"Tüm mesajlar okundu"</string>
<string name="nothing_here">"Burada hiçbir şey yok"</string>
<string name="tab_new">"Yeni"</string>
<string name="tab_read">"Tüm"</string>
<string name="tab_favs">"Favoriler"</string>
<string name="action_about">"Hakkında"</string>
<string name="marked_as_read">"Öğeleri oku"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Geri al"</string> <string name="undo_string">"Geri al"</string>
<string name="addStringNoUrl">"Kaynakları eklemek için giriş yapın."</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_get_sources">"Kaynakları listesi alınamıyor."</string>
<string name="cant_create_source">"Kaynak oluşturulamı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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"Spouts listesine girilemiyor."</string>
<string name="form_not_complete">"Form tamamlanamadı"</string> <string name="form_not_complete">"Form tamamlanamadı"</string>
<string name="pref_header_links">"Bağlantılar"</string> <string name="pref_header_links">"Bağlantılar"</string>
<string name="issue_tracker_link">"Sorun İzleyici"</string> <string name="issue_tracker_link">"Sorun İzleyici"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Burada hiçbir şey yok"</string>
<string name="tab_new">"Yeni"</string>
<string name="tab_read">"Tüm"</string>
<string name="tab_favs">"Favoriler"</string>
<string name="action_about">"Hakkında"</string>
<string name="marked_as_read">"Öğeleri oku"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Selfoss 阅读器"</string> <string name="app_name">"Selfoss 阅读器"</string>
<string name="title_activity_login">"登录"</string> <string name="title_activity_login">"登录"</string>
<string name="prompt_password">"密码"</string> <string name="prompt_password">"密码"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"密码不够长"</string> <string name="error_invalid_password">"密码不够长"</string>
<string name="error_field_required">"必填字段"</string> <string name="error_field_required">"必填字段"</string>
<string name="prompt_url">"网址"</string> <string name="prompt_url">"网址"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"需要登录?"</string> <string name="withLoginSwitch">"需要登录?"</string>
<string name="login_url_problem">"哎呀。您可能需要在网址的末尾添加一个 \"/\"。"</string> <string name="login_url_problem">"哎呀。您可能需要在网址的末尾添加一个 \"/\"。"</string>
<string name="prompt_login">"用户名"</string> <string name="prompt_login">"用户名"</string>
@ -22,13 +23,6 @@
<string name="wrong_infos">"再次检查您的详细信息。"</string> <string name="wrong_infos">"再次检查您的详细信息。"</string>
<string name="all_posts_not_read">"所有帖子都未读"</string> <string name="all_posts_not_read">"所有帖子都未读"</string>
<string name="all_posts_read">"所有帖子已读"</string> <string name="all_posts_read">"所有帖子已读"</string>
<string name="nothing_here">"暂无内容!"</string>
<string name="tab_new">"新建"</string>
<string name="tab_read">"所有"</string>
<string name="tab_favs">"收藏夹"</string>
<string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未读条目"</string>
<string name="undo_string">"撤销"</string> <string name="undo_string">"撤销"</string>
<string name="addStringNoUrl">"登录以添加数据源。"</string> <string name="addStringNoUrl">"登录以添加数据源。"</string>
<string name="cant_get_sources">"无法获取数据列表。"</string> <string name="cant_get_sources">"无法获取数据列表。"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">左对齐</string> <string name="reader_text_align_left">左对齐</string>
<string name="reader_text_align_justify">左右对齐</string> <string name="reader_text_align_justify">左右对齐</string>
<string name="settings_reader_font">阅读器字体</string> <string name="settings_reader_font">阅读器字体</string>
<string name="reader_static_bar_title">文章查看器中的静态底部栏</string>
<string name="reader_static_bar_on">底部栏将始终显示</string>
<string name="reader_static_bar_off">底部栏可以通过浮动按钮显示</string>
<string name="remove_source">删除源</string> <string name="remove_source">删除源</string>
<string name="pref_theme_title">浅色/深色模式</string> <string name="pref_theme_title">浅色/深色模式</string>
<string name="mode_dark">深色模式</string> <string name="mode_dark">深色模式</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">该应用不分享任何关于您的个人数据。</string> <string name="gdpr_dialog_title">该应用不分享任何关于您的个人数据。</string>
<string name="gdpr_dialog_message"><![CDATA[崩溃报告发送现已启用。 可以从设置页面禁用它。 请记住,崩溃报告对于应用程序开发是必需的。]]></string> <string name="gdpr_dialog_message"><![CDATA[崩溃报告发送现已启用。 可以从设置页面禁用它。 请记住,崩溃报告对于应用程序开发是必需的。]]></string>
<string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string> <string name="crash_toast_text">发生崩溃。请将细节发送给开发人员。</string>
<string name="pref_switch_disable_acra">"禁用自动错误报告 "</string> <string name="pref_switch_disable_acra">"禁用自动错误报告"</string>
<string name="menu_home_filter">筛选器</string> <string name="menu_home_filter">筛选器</string>
<string name="application_selfoss_only">此应用只适用于 Selfoss 实例,不适用于其他 RSS 。</string> <string name="application_selfoss_only">此应用只适用于 Selfoss 实例,不适用于其他 RSS 。</string>
<string name="menu_home_sources"></string> <string name="menu_home_sources"></string>
<string name="update_source">更新源</string> <string name="update_source">更新源</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">断开连接?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">您将断开与 selfoss 实例的连接。</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"暂无内容!"</string>
<string name="tab_new">"新建"</string>
<string name="tab_read">"所有"</string>
<string name="tab_favs">"收藏夹"</string>
<string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未读条目"</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Selfoss 阅读器"</string> <string name="app_name">"Selfoss 阅读器"</string>
<string name="title_activity_login">"登录"</string> <string name="title_activity_login">"登录"</string>
<string name="prompt_password">"密码"</string> <string name="prompt_password">"密码"</string>
@ -7,6 +7,7 @@
<string name="error_invalid_password">"密码不够长"</string> <string name="error_invalid_password">"密码不够长"</string>
<string name="error_field_required">"欄位必填"</string> <string name="error_field_required">"欄位必填"</string>
<string name="prompt_url">"网址"</string> <string name="prompt_url">"网址"</string>
<string name="disable_ssl">"Disable SSL"</string>
<string name="withLoginSwitch">"需要登入?"</string> <string name="withLoginSwitch">"需要登入?"</string>
<string name="login_url_problem">"哎呀。您可能需要在网址的末尾添加一个 \"/\"。"</string> <string name="login_url_problem">"哎呀。您可能需要在网址的末尾添加一个 \"/\"。"</string>
<string name="prompt_login">"使用者名稱"</string> <string name="prompt_login">"使用者名稱"</string>
@ -22,19 +23,12 @@
<string name="wrong_infos">"再次检查您的详细信息。"</string> <string name="wrong_infos">"再次检查您的详细信息。"</string>
<string name="all_posts_not_read">"所有帖子都未读"</string> <string name="all_posts_not_read">"所有帖子都未读"</string>
<string name="all_posts_read">"所有帖子已读"</string> <string name="all_posts_read">"所有帖子已读"</string>
<string name="nothing_here">"暂无内容!"</string>
<string name="tab_new">"新建"</string>
<string name="tab_read">"所有"</string>
<string name="tab_favs">"收藏夹"</string>
<string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未讀項目"</string>
<string name="undo_string">"撤销"</string> <string name="undo_string">"撤销"</string>
<string name="addStringNoUrl">"登录以添加数据源。"</string> <string name="addStringNoUrl">"登录以添加数据源。"</string>
<string name="cant_get_sources">"无法获取数据列表。"</string> <string name="cant_get_sources">"无法获取数据列表。"</string>
<string name="cant_create_source">"无法创建源数据。"</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> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
<string name="cant_get_spouts">"Can't get spouts list. There may ben an api issue."</string> <string name="cant_get_spouts">"无法获取数据列表"</string>
<string name="form_not_complete">"窗体未完成"</string> <string name="form_not_complete">"窗体未完成"</string>
<string name="pref_header_links">"链接"</string> <string name="pref_header_links">"链接"</string>
<string name="issue_tracker_link">"问题追踪器"</string> <string name="issue_tracker_link">"问题追踪器"</string>
@ -112,9 +106,6 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -123,12 +114,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="disable_ssl">Disable SSL</string> <string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"暂无内容!"</string>
<string name="tab_new">"新建"</string>
<string name="tab_read">"所有"</string>
<string name="tab_favs">"收藏夹"</string>
<string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未讀項目"</string>
</resources> </resources>

View 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>

View File

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">"Reader for Selfoss"</string> <string name="app_name">"Reader for Selfoss"</string>
<string name="title_activity_login">"Log in"</string> <string name="title_activity_login">"Log in"</string>
<string name="prompt_password">"Password"</string> <string name="prompt_password">"Password"</string>
@ -22,13 +22,6 @@
<string name="wrong_infos">"Check your details again."</string> <string name="wrong_infos">"Check your details again."</string>
<string name="all_posts_not_read">"All posts weren't read"</string> <string name="all_posts_not_read">"All posts weren't read"</string>
<string name="all_posts_read">"All posts were read"</string> <string name="all_posts_read">"All posts were read"</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"All"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string>
<string name="undo_string">"Undo"</string> <string name="undo_string">"Undo"</string>
<string name="addStringNoUrl">"Log in to add sources."</string> <string name="addStringNoUrl">"Log in to add sources."</string>
<string name="cant_get_sources">"Can't get sources list."</string> <string name="cant_get_sources">"Can't get sources list."</string>
@ -55,8 +48,7 @@
<string name="pref_switch_card_view_off">"The articles will be displayed as a list"</string> <string name="pref_switch_card_view_off">"The articles will be displayed as a list"</string>
<string name="menu_home_refresh">"Update remote"</string> <string name="menu_home_refresh">"Update remote"</string>
<string name="refresh_success_response">"The remote is updated, you can now reload the articles list"</string> <string name="refresh_success_response">"The remote is updated, you can now reload the articles list"</string>
<string <string name="refresh_failer_message">"The update didn't work, try again later, or check your selfoss logs."</string>
name="refresh_failer_message">"The update didn't work, try again later, or check your selfoss logs."</string>
<string name="refresh_in_progress">"Refresh in progress"</string> <string name="refresh_in_progress">"Refresh in progress"</string>
<string name="card_height_title">Full height cards</string> <string name="card_height_title">Full height cards</string>
<string name="card_height_on">Cards height will adjust to its content</string> <string name="card_height_on">Cards height will adjust to its content</string>
@ -116,9 +108,6 @@
<string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string> <string name="source_code_pro_font_id" translatable="false">source_code_pro_medium</string>
<string name="open_sans_font_id" translatable="false">open_sans</string> <string name="open_sans_font_id" translatable="false">open_sans</string>
<string name="roboto_font_id" translatable="false">roboto</string> <string name="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
<string name="remove_source">Remove source</string> <string name="remove_source">Remove source</string>
<string name="pref_theme_title">Light/Dark mode</string> <string name="pref_theme_title">Light/Dark mode</string>
<string name="mode_dark">Dark mode</string> <string name="mode_dark">Dark mode</string>
@ -127,11 +116,19 @@
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string> <string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string> <string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string> <string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string> <string name="pref_switch_disable_acra">"Disable automatic bug reporting."</string>
<string name="menu_home_filter">Filters</string> <string name="menu_home_filter">Filters</string>
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string> <string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
<string name="menu_home_sources">Sources</string> <string name="menu_home_sources">Sources</string>
<string name="update_source">Update source</string> <string name="update_source">Update source</string>
<string name="confirm_disconnect_title">Disconnect ?</string> <string name="confirm_disconnect_title">Disconnect ?</string>
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string> <string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
<string name="no_browser">Can\'t open a link without a browser installed</string>
<string name="nothing_here">"Nothing here"</string>
<string name="tab_new">"New"</string>
<string name="tab_read">"All"</string>
<string name="tab_favs">"Favorites"</string>
<string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string>
</resources> </resources>

View File

@ -30,14 +30,6 @@
android:summaryOn="@string/pref_article_viewer_on" android:summaryOn="@string/pref_article_viewer_on"
android:title="@string/pref_article_viewer_title" android:title="@string/pref_article_viewer_title"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<SwitchPreference
android:defaultValue="false"
android:dependency="prefer_article_viewer"
android:key="reader_static_bar"
android:summaryOff="@string/reader_static_bar_off"
android:summaryOn="@string/reader_static_bar_on"
android:title="@string/reader_static_bar_title"
app:iconSpaceReserved="false"/>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_general_category_displaying"> android:title="@string/pref_general_category_displaying">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,3 @@
buildscript {
dependencies {
// SqlDelight
classpath("com.squareup.sqldelight:gradle-plugin:1.5.5")
}
}
plugins { plugins {
//trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("8.7.3").apply(false) id("com.android.application").version("8.7.3").apply(false)

View File

@ -3,3 +3,4 @@ files:
translation: /androidApp/src/main/res/values-%android_code%/%original_file_name% translation: /androidApp/src/main/res/values-%android_code%/%original_file_name%
translate_attributes: '0' translate_attributes: '0'
content_segmentation: '0' content_segmentation: '0'
preserve_hierarchy: true

786
detekt.yml Normal file
View 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.*'

View File

@ -0,0 +1,8 @@
**v124123641**
- Chore: no tests on build.
- Merge pull request 'testing' (#170) from testing into master
- fix: Displaying fixes. Fixes #155
- test: coverage
- chore: update and use multiplatform datetime
- Changelog for v124123421

View File

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

View File

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

View File

@ -0,0 +1,4 @@
**v125010111**
- Debug trying to fix context issues. (#174)
- Changelog for v125010031

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -18,7 +18,7 @@ kotlin.code.style=official
#Android #Android
android.useAndroidX=true android.useAndroidX=true
#android.nonTransitiveRClass=true #android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=false
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
#MPP #MPP
kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.enableCInteropCommonization=true

View File

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

View File

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

View File

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

View File

@ -8,16 +8,20 @@ class NaiveTrustManager : X509TrustManager {
override fun checkClientTrusted( override fun checkClientTrusted(
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) {} ) {
// Nothing
}
override fun checkServerTrusted( override fun checkServerTrusted(
chain: Array<out X509Certificate>?, chain: Array<out X509Certificate>?,
authType: String?, authType: String?,
) {} ) {
// Nothing
}
override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf() override fun getAcceptedIssuers(): Array<out X509Certificate> = arrayOf()
} }
actual fun setupInsecureHTTPEngine(config: CIOEngineConfig) { actual fun setupInsecureHttpEngine(config: CIOEngineConfig) {
config.https.trustManager = NaiveTrustManager() config.https.trustManager = NaiveTrustManager()
} }

View File

@ -1,9 +1,9 @@
package bou.amine.apps.readerforselfossv2.utils package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import io.github.aakira.napier.Napier import kotlinx.datetime.Clock
import kotlinx.datetime.*
@Suppress("detekt:UtilityClassWithPublicConstructor")
actual class DateUtils { actual class DateUtils {
actual companion object { actual companion object {
actual fun parseRelativeDate(dateString: String): String { actual fun parseRelativeDate(dateString: String): String {

View File

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

View File

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

View File

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

View File

@ -1,13 +1,16 @@
@file:Suppress("detekt:LongParameterList")
package bou.amine.apps.readerforselfossv2.model package bou.amine.apps.readerforselfossv2.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class MercuryModel { class MercuryModel {
@Suppress("detekt:ConstructorParameterNaming")
@Serializable @Serializable
class ParsedContent( class ParsedContent(
val title: String? = null, val title: String? = null,
val content: String? = null, val content: String? = null,
val lead_image_url: String? = null, // NOSONAR val lead_image_url: String? = null,
val url: String? = null, val url: String? = null,
val error: Boolean? = null, val error: Boolean? = null,
val message: String? = null, val message: String? = null,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,23 +3,28 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.StatusAndData import bou.amine.apps.readerforselfossv2.model.StatusAndData
import bou.amine.apps.readerforselfossv2.model.SuccessResponse import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.call.* import io.ktor.client.call.body
import io.ktor.client.request.* import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.* import io.ktor.client.request.delete
import io.ktor.client.statement.* import io.ktor.client.request.forms.submitForm
import io.ktor.http.* import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.url
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import io.ktor.http.Parameters
import io.ktor.http.isSuccess
suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse { suspend fun responseOrSuccessIf404(r: HttpResponse?): SuccessResponse =
return if (r != null && r.status === HttpStatusCode.NotFound) { if (r != null && r.status === HttpStatusCode.NotFound) {
SuccessResponse(true) SuccessResponse(true)
} else { } else {
maybeResponse(r) maybeResponse(r)
} }
}
suspend fun maybeResponse(r: HttpResponse?): SuccessResponse { suspend fun maybeResponse(r: HttpResponse?): SuccessResponse =
return if (r != null && r.status.isSuccess()) { if (r != null && r.status.isSuccess()) {
r.body() r.body()
} else { } else {
if (r != null) { if (r != null) {
@ -27,13 +32,17 @@ suspend fun maybeResponse(r: HttpResponse?): SuccessResponse {
} }
SuccessResponse(false) SuccessResponse(false)
} }
}
@Suppress("detekt:SwallowedException")
suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> { suspend inline fun <reified T> bodyOrFailure(r: HttpResponse?): StatusAndData<T> {
return if (r != null && r.status.isSuccess()) { try {
StatusAndData.succes(r.body()) return if (r != null && r.status.isSuccess()) {
} else { StatusAndData.succes(r.body())
StatusAndData.error() } else {
StatusAndData.error()
}
} catch (e: Throwable) {
return StatusAndData.error()
} }
} }

View File

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

View File

@ -1,3 +1,5 @@
@file:Suppress("detekt:TooManyFunctions")
package bou.amine.apps.readerforselfossv2.service package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
@ -121,4 +123,4 @@ class ACRASettings : Settings {
longs.remove(key) longs.remove(key)
strings.remove(key) strings.remove(key)
} }
} }

View File

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

View File

@ -4,6 +4,12 @@ import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
class DateParseException(
message: String,
e: Throwable? = null,
) : Throwable(message, e)
@Suppress("detekt:ThrowsCount")
fun String.toParsedDate(): Long { fun String.toParsedDate(): Long {
// Possible formats are // Possible formats are
// yyyy-mm-dd hh:mm:ss format // yyyy-mm-dd hh:mm:ss format
@ -17,17 +23,22 @@ fun String.toParsedDate(): Long {
if (this.matches(oldVersionFormat)) { if (this.matches(oldVersionFormat)) {
this.replace(" ", "T") this.replace(" ", "T")
} else if (this.matches(newVersionFormat)) { } else if (this.matches(newVersionFormat)) {
newVersionFormat.find(this)?.groups?.get(1)?.value ?: throw Exception("Couldn't parse $this") newVersionFormat
.find(this)
?.groups
?.get(1)
?.value ?: throw DateParseException("Couldn't parse $this")
} else { } else {
throw Exception("Unrecognized format for $this") throw DateParseException("Unrecognized format for $this")
} }
} catch (e: Exception) { } catch (e: Exception) {
throw Exception("parseDate failed for $this", e) throw DateParseException("parseDate failed for $this", e)
} }
return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() return LocalDateTime.parse(isoDateString).toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds()
} }
@Suppress("detekt:UtilityClassWithPublicConstructor")
expect class DateUtils() { expect class DateUtils() {
companion object { companion object {
fun parseRelativeDate(dateString: String): String fun parseRelativeDate(dateString: String): String

Some files were not shown because too many files have changed in this diff Show More