Compare commits

..

1 Commits

Author SHA1 Message Date
eaa6eee531 chore: changing actions in reader fragment.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m23s
Check PR code / build (pull_request) Successful in 13m39s
2025-01-13 16:54:25 +01:00
53 changed files with 467 additions and 733 deletions

View File

@ -1,10 +0,0 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
base_path: "../../../"
files:
- source: /androidApp/src/main/res/values/strings.xml
translation: /androidApp/src/main/res/values-%android_code%/%original_file_name%
translate_attributes: '0'
content_segmentation: '0'
preserve_hierarchy: true

View File

@ -10,52 +10,36 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: "Check android app changes"
id: check-android-changes
uses: tj-actions/changed-files@v45
with:
files: |
androidApp/src/**
- name: Fetch tags - name: Fetch tags
if: steps.check-android-changes.outputs.any_modified == 'true'
run: git fetch --tags -p run: git fetch --tags -p
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
if: steps.check-android-changes.outputs.any_modified == 'true'
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: gradle cache: gradle
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/actions/setup-gradle@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
- uses: android-actions/setup-android@v3 - uses: android-actions/setup-android@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
- name: Configure gradle... - name: Configure gradle...
if: steps.check-android-changes.outputs.any_modified == 'true'
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties
- name: Build and test - name: Build and test
if: steps.check-android-changes.outputs.any_modified == 'true'
run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done
# TESTS ARE RUN LOCALLY - uses: KengoTODA/actions-setup-docker-compose@v1
# - uses: KengoTODA/actions-setup-docker-compose@v1 with:
# with: version: "2.23.3"
# version: "2.23.3" - name: run selfoss
# - name: run selfoss run: |
# run: | docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
# docker compose -f .gitea/workflows/assets/docker-compose.yml up -d
- name: coverage - name: coverage
if: steps.check-android-changes.outputs.any_modified == 'true'
run: | run: |
./gradlew :koverHtmlReport ./gradlew :koverHtmlReport
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: steps.check-android-changes.outputs.any_modified == 'true'
with: with:
name: coverage name: coverage
path: build/reports/kover/html path: build/reports/kover/html
retention-days: 1 retention-days: 1
overwrite: true overwrite: true
include-hidden-files: true include-hidden-files: true
# TESTS ARE RUN LOCALLY - name: Clean
# - name: Clean if: always()
# if: always() run: |
# run: | docker compose -f .gitea/workflows/assets/docker-compose.yml stop
# docker compose -f .gitea/workflows/assets/docker-compose.yml stop

View File

@ -16,7 +16,6 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: master
- name: Config git - name: Config git
run: | run: |
git config --global user.email aminecmi+giteadrone@pm.me git config --global user.email aminecmi+giteadrone@pm.me
@ -51,7 +50,7 @@ jobs:
followtags: true followtags: true
ssh_key: ${{ secrets.PRIVATE_KEY }} ssh_key: ${{ secrets.PRIVATE_KEY }}
tags: true tags: true
branch: master branch: release
- name: copy file via ssh password - name: copy file via ssh password
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
@ -125,4 +124,4 @@ jobs:
priority: high priority: high
convert_markdown: true convert_markdown: true
body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }} body: Nouveau fichier de mapping pour la version ${{ steps.version.outputs.VERSION }}
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt

View File

@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
- chore-crowdin-ci
jobs: jobs:
Lint: Lint:
@ -24,68 +23,6 @@ jobs:
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build'
- name: Detecting... - name: Detecting...
run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt'
translations:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Check translations changes"
id: check-translations-changes
uses: tj-actions/changed-files@v45
with:
files: |
androidApp/src/main/res/values/strings.xml
- name: upload translation sources
if: steps.check-api-changes.outputs.any_modified == 'true'
uses: crowdin/github-action@v2
with:
config: './.gitea/workflows/assets/crowdin.yml'
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
push_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: wait
if: steps.check-api-changes.outputs.any_modified == 'true'
run: sleep 10s
- name: download translations
if: steps.check-api-changes.outputs.any_modified == 'true'
uses: crowdin/github-action@v2
with:
config: './.gitea/workflows/assets/crowdin.yml'
upload_sources: false
upload_translations: false
download_translations: true
create_pull_request: false
push_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Check for uncommitted changes
if: steps.check-api-changes.outputs.any_modified == 'true'
id: check-changes
uses: mskri/check-uncommitted-changes-action@v1.0.1
- name: Commit Changes
if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
run: |
git config --global user.email aminecmi+giteadrone@pm.me
git config --global user.name giteadrone
git add ./androidApp/src/main/res/*
git commit -m "translation: translation files"
- name: Push changes
if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != ''
uses: appleboy/git-push-action@v1.0.0
with:
author_name: giteadrone
author_email: aminecmi+giteadrone@pm.me
remote: ${{ secrets.REMOTE_URL }}
ssh_key: ${{ secrets.PRIVATE_KEY }}
branch: ${{ github.head_ref || github.ref_name }}
build: build:
needs: Lint needs: Lint
uses: ./.gitea/workflows/common_build.yml uses: ./.gitea/workflows/common_build.yml

4
.gitignore vendored
View File

@ -323,6 +323,4 @@ fabric.properties
crowdin.properties crowdin.properties
.kotlin/ .kotlin/
build-cache/ build-cache/
act

View File

@ -1,73 +1,3 @@
**v125030681
- chore: do not send reports on simulators.
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
- chore: do not send reports on simulators.
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
- chore: we don't need to check if the url is valid in upsert screen.
- fix: Url validation was not failing login. Added tests.
- chore: crowding ci integration.
- Show a confirmation dialog before deleting sources (#185)
- Changelog for v125020581
--------------------------------------------------------------------
**v125020581
- fix: url can be empty ?
- Changelog for v125020471
--------------------------------------------------------------------
**v125020471
- chore: no more docker-compose.
- bump: gradle plugin.
- Merge pull request 'fix: check index exists.' (#183) from fix-index into master
- fix: check index exists.
- Changelog for v125020411
--------------------------------------------------------------------
**v125020411
- Merge pull request 'bump' (#182) from bump into master
- chore: non transiant R classes.
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
- bump
- fix: One more missing context.
--------------------------------------------------------------------
**v125010241
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
- refactor: context fragments issues.
- logs: Context issues.
- fix: Handle empty url issue, again.
- fix: Link not opening.
- Changelog for v125010201
--------------------------------------------------------------------
**v125010201
- fix: Handle empty url issue.
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
- chore: changing actions in reader fragment.
- Changelog for v125010131
--------------------------------------------------------------------
**v125010131
- fix: reload the adapter when it's needed. Fixes #128. (#176)
- feat: basic auth and images loading. Fixes #172. (#175)
- Changelog for v125010111
--------------------------------------------------------------------
**v125010111 **v125010111
- Debug trying to fix context issues. (#174) - Debug trying to fix context issues. (#174)

View File

@ -12,38 +12,28 @@ plugins {
id("app.cash.sqldelight") version "2.0.2" id("app.cash.sqldelight") version "2.0.2"
} }
fun Project.execWithOutput( fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
cmd: String, val result: String = ByteArrayOutputStream().use { outputStream ->
ignore: Boolean = false, project.exec {
): String { commandLine = cmd.split(" ")
val result: String = standardOutput = outputStream
ByteArrayOutputStream().use { outputStream -> isIgnoreExitValue = ignore
project.exec {
commandLine = cmd.split(" ")
standardOutput = outputStream
isIgnoreExitValue = ignore
}
outputStream.toString()
} }
outputStream.toString()
}
return result return result
} }
fun gitVersion(): String { fun gitVersion(): String {
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true) val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
val process = val process = if (maybeTagOfCurrentCommit.isEmpty()) {
if (maybeTagOfCurrentCommit.isEmpty()) { println("No tag on current commit. Will take the latest one.")
println("No tag on current commit. Will take the latest one.") execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1") } else {
} else { println("Tag found on current commit")
println("Tag found on current commit") execWithOutput("git -C ../ describe --contains HEAD")
execWithOutput("git -C ../ describe --contains HEAD") }
} return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
return process
.replace("^0", "")
.replace("'", "")
.substring(1)
.replace("\\.", "")
.trim()
} }
fun versionCodeFromGit(): Int { fun versionCodeFromGit(): Int {
@ -126,6 +116,7 @@ android {
isIncludeAndroidResources = true isIncludeAndroidResources = true
} }
} }
} }
dependencies { dependencies {
@ -150,7 +141,7 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.0") implementation("androidx.constraintlayout:constraintlayout:2.2.0")
implementation("org.jsoup:jsoup:1.18.3") implementation("org.jsoup:jsoup:1.18.3")
// multidex //multidex
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
// About // About
@ -171,28 +162,31 @@ dependencies {
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0") implementation("androidx.viewpager2:viewpager2:1.1.0")
// Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.23.1") implementation("org.kodein.di:kodein-di:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x:7.23.1")
implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1") implementation("org.kodein.di:kodein-di-framework-android-x-viewmodel:7.23.1")
// Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0") implementation("com.russhwolf:multiplatform-settings-no-arg:1.3.0")
// Logging //Logging
implementation("io.github.aakira:napier:2.7.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.15.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")
// Network information
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
// SQLDELIGHT // SQLDELIGHT
implementation("app.cash.sqldelight:android-driver:2.0.2") 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.13.14") testImplementation("io.mockk:mockk:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
@ -216,12 +210,11 @@ tasks.withType<Test> {
useJUnit() useJUnit()
testLogging { testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = events = setOf(
setOf( org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR, )
)
showStandardStreams = true showStandardStreams = true
} }
} }
@ -234,4 +227,4 @@ aboutLibraries {
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
} }

View File

@ -56,7 +56,7 @@ class HomeActivityTest {
fun testMenuActions() { fun testMenuActions() {
onView(withId(R.id.action_search)).perform(click()) onView(withId(R.id.action_search)).perform(click())
onView( onView(
withId(com.google.android.material.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())

View File

@ -60,23 +60,9 @@ class LoginActivityTest {
fun urlError() { fun urlError() {
performLogin("10.0.2.2: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.login_url_problem)))
}
@Test
fun connectError() {
performLogin("http://10.0.2.2:8889")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos)))
} }
@Test
fun urlSlashError() {
performLogin("https://google.fr/toto")
onView(withId(R.id.urlView)).perform(click())
onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem)))
}
@Test @Test
fun multiError() { fun multiError() {
onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click())

View File

@ -31,7 +31,7 @@ import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.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.openUrlInBrowserAsNewTask 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
@ -317,44 +317,50 @@ class HomeActivity :
private fun reloadLayoutManager() { private fun reloadLayoutManager() {
val currentManager = binding.recyclerView.layoutManager val currentManager = binding.recyclerView.layoutManager
val layoutManager: RecyclerView.LayoutManager
fun gridLayoutManager() { // This will only update the layout manager if settings changed
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()) {
gridLayoutManager() layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} }
is GridLayoutManager -> is GridLayoutManager ->
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
staggererdGridLayoutManager() layoutManager =
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()) {
gridLayoutManager() layoutManager =
GridLayoutManager(
this,
calculateNoOfColumns(),
)
binding.recyclerView.layoutManager = layoutManager
} else { } else {
staggererdGridLayoutManager() layoutManager =
StaggeredGridLayoutManager(
calculateNoOfColumns(),
StaggeredGridLayoutManager.VERTICAL,
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
} }
} }
} }
@ -479,8 +485,8 @@ class HomeActivity :
} }
private fun handleListResult(appendResults: Boolean = false) { private fun handleListResult(appendResults: Boolean = false) {
val oldManager = binding.recyclerView.layoutManager
if (appendResults) { if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
firstVisible = firstVisible =
when (oldManager) { when (oldManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
@ -493,13 +499,7 @@ class HomeActivity :
} }
} }
@Suppress("detekt:ComplexCondition") if (recyclerAdapter == null) {
if (recyclerAdapter == null ||
(
(recyclerAdapter is ItemListAdapter && appSettingsService.isCardViewEnabled()) ||
(recyclerAdapter is ItemCardAdapter && !appSettingsService.isCardViewEnabled())
)
) {
if (appSettingsService.isCardViewEnabled()) { if (appSettingsService.isCardViewEnabled()) {
recyclerAdapter = recyclerAdapter =
ItemCardAdapter( ItemCardAdapter(
@ -599,7 +599,7 @@ class HomeActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.issue_tracker -> { R.id.issue_tracker -> {
baseContext.openUrlInBrowserAsNewTask(AppSettingsService.BUG_URL) baseContext.openUrlInBrowser(AppSettingsService.BUG_URL)
return true return true
} }

View File

@ -149,10 +149,9 @@ class LoginActivity :
.toString() .toString()
.trim() .trim()
val cancelUrl = failInvalidUrl(url) failInvalidUrl(url)
if (cancelUrl) return failLoginDetails(password, login)
val cancelDetails = failLoginDetails(password, login)
if (cancelDetails) return
showProgress(true) showProgress(true)
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked) appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
@ -194,7 +193,7 @@ class LoginActivity :
private fun failLoginDetails( private fun failLoginDetails(
password: String, password: String,
login: String, login: String,
): Boolean { ) {
var lastFocusedView: View? = null var lastFocusedView: View? = null
var cancel = false var cancel = false
if (isWithLogin) { if (isWithLogin) {
@ -211,10 +210,9 @@ class LoginActivity :
} }
} }
maybeCancelAndFocusView(cancel, lastFocusedView) maybeCancelAndFocusView(cancel, lastFocusedView)
return cancel
} }
private fun failInvalidUrl(url: String): Boolean { private fun failInvalidUrl(url: String) {
val focusView = binding.urlView val focusView = binding.urlView
var cancel = false var cancel = false
if (url.isBaseUrlInvalid()) { if (url.isBaseUrlInvalid()) {
@ -234,7 +232,6 @@ class LoginActivity :
} }
} }
maybeCancelAndFocusView(cancel, focusView) maybeCancelAndFocusView(cancel, focusView)
return cancel
} }
private fun maybeCancelAndFocusView( private fun maybeCancelAndFocusView(

View File

@ -10,16 +10,18 @@ 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.android.testing.TestingHelper import bou.amine.apps.readerforselfossv2.android.testing.TestingHelper
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory import bou.amine.apps.readerforselfossv2.dao.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.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 bou.amine.apps.readerforselfossv2.service.ConnectivityService import com.github.ln_12.library.ConnectivityStatus
import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.DebugAntilog
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
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA import org.acra.ACRA
import org.acra.ReportField import org.acra.ReportField
@ -42,21 +44,27 @@ class MyApp :
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<ConnectivityService>() with singleton { ConnectivityService() }
bind<Repository>() with bind<Repository>() with
singleton { singleton {
Repository( Repository(
instance(), instance(),
instance(), instance(),
instance(), isConnectionAvailable,
instance(), instance(),
) )
} }
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
} }
private val repository: Repository by instance() private val repository: Repository by instance()
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance() private val driverFactory: DriverFactory by instance()
private val connectivityService: ConnectivityService by instance()
@Suppress("detekt:ForbiddenComment")
// TODO: handle with the "previous" way
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -69,12 +77,13 @@ class MyApp :
ProcessLifecycleOwner.get().lifecycle.addObserver( ProcessLifecycleOwner.get().lifecycle.addObserver(
AppLifeCycleObserver( AppLifeCycleObserver(
connectivityService, connectivityStatus,
repository,
), ),
) )
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
connectivityService.networkAvailableProvider.collect { networkAvailable -> viewModel.networkAvailableProvider.collect { networkAvailable ->
val toastMessage = val toastMessage =
if (networkAvailable) { if (networkAvailable) {
repository.handleDBActions() repository.handleDBActions()
@ -100,7 +109,6 @@ class MyApp :
super.attachBaseContext(base) super.attachBaseContext(base)
initAcra { initAcra {
sendReportsInDevMode = false
reportFormat = StringFormat.JSON reportFormat = StringFormat.JSON
reportContent = reportContent =
listOf( listOf(
@ -180,15 +188,18 @@ class MyApp :
} }
class AppLifeCycleObserver( class AppLifeCycleObserver(
val connectivityService: ConnectivityService, val connectivityStatus: ConnectivityStatus,
val repository: Repository,
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
connectivityService.start() repository.connectionMonitored = true
connectivityStatus.start()
} }
override fun onPause(owner: LifecycleOwner) { override fun onPause(owner: LifecycleOwner) {
connectivityService.stop() repository.connectionMonitored = false
connectivityStatus.stop()
super.onPause(owner) super.onPause(owner)
} }
} }

View File

@ -161,14 +161,12 @@ class ReaderActivity :
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
if (!allItems.isNullOrEmpty() && allItems.size >= position) { if (allItems[position].starred) {
if (allItems[position].starred) { canRemoveFromFavorite()
canRemoveFromFavorite() } else {
} else { canFavorite()
canFavorite()
}
readItem(allItems[position])
} }
readItem(allItems[position])
} }
}, },
) )

View File

@ -9,9 +9,11 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,6 +31,7 @@ class UpsertSourceActivity :
override val di by closestDI() override val di by closestDI()
private val repository: Repository by instance() private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -73,7 +76,13 @@ class UpsertSourceActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
handleSpoutsSpinner()
val baseUrl = appSettingsService.getBaseUrl()
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner()
}
} }
@Suppress("detekt:SwallowedException") @Suppress("detekt:SwallowedException")
@ -148,6 +157,13 @@ class UpsertSourceActivity :
} }
} }
private fun mustLoginToAddSource() {
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
val i = Intent(this, LoginActivity::class.java)
startActivity(i)
finish()
}
private fun handleSaveSource() { private fun handleSaveSource() {
val url = binding.sourceUri.text.toString() val url = binding.sourceUri.text.toString()

View File

@ -6,8 +6,9 @@ import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout
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.UpsertSourceActivity import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
@ -31,21 +32,69 @@ class SourcesListAdapter(
private val items: ArrayList<SelfossModel.SourceDetail>, private val items: ArrayList<SelfossModel.SourceDetail>,
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(),
DIAware { DIAware {
private val c: Context = app.baseContext
private 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 appSettingsService: AppSettingsService by instance()
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int, viewType: Int,
): ViewHolder { ): ViewHolder {
val binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding.root)
} }
override fun onBindViewHolder( override fun onBindViewHolder(
holder: ViewHolder, holder: ViewHolder,
position: Int, position: Int,
) { ) {
holder.bind(items[position], position) val itm = items[position]
val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener {
val (id, title) = items[position]
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(id, title)
if (successfullyDeletedSource) {
items.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
} else {
Toast
.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT,
).show()
}
}
}
holder.mView.setOnClickListener {
val source = items[position]
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
} else {
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage, appSettingsService)
}
if (!itm.error.isNullOrBlank()) {
binding.errorText.visibility = View.VISIBLE
binding.errorText.text = itm.error
} else {
binding.errorText.visibility = View.GONE
}
binding.sourceTitle.text = itm.title.getHtmlDecoded()
} }
override fun getItemId(position: Int) = position.toLong() override fun getItemId(position: Int) = position.toLong()
@ -55,72 +104,6 @@ class SourcesListAdapter(
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
inner class ViewHolder( inner class ViewHolder(
val binding: SourceListItemBinding, val mView: ConstraintLayout,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(mView)
private val context: Context = app.applicationContext
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
fun bind(
source: SelfossModel.SourceDetail,
position: Int,
) {
binding.apply {
sourceTitle.text = source.title.getHtmlDecoded()
if (source.getIcon(repository.baseUrl).isEmpty()) {
itemImage.setBackgroundAndText(source.title.getHtmlDecoded())
} else {
context.circularDrawable(source.getIcon(repository.baseUrl), itemImage, appSettingsService)
}
errorText.apply {
visibility = if (!source.error.isNullOrBlank()) View.VISIBLE else View.GONE
text = source.error
}
deleteBtn.setOnClickListener { showDeleteConfirmationDialog(source, position) }
root.setOnClickListener {
repository.setSelectedSource(source)
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
}
}
}
private fun showDeleteConfirmationDialog(
source: SelfossModel.SourceDetail,
position: Int,
) {
AlertDialog
.Builder(app)
.setTitle(app.getString(R.string.confirm_delete_title))
.setMessage(app.getString(R.string.confirm_delete_message, source.title))
.setPositiveButton(android.R.string.ok) { _, _ -> deleteSource(source, position) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun deleteSource(
source: SelfossModel.SourceDetail,
position: Int,
) {
CoroutineScope(Dispatchers.IO).launch {
val successfullyDeletedSource = repository.deleteSource(source.id, source.title)
launch(Dispatchers.Main) {
if (successfullyDeletedSource) {
items.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
} else {
Toast
.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT,
).show()
}
}
}
}
}
} }

View File

@ -33,16 +33,14 @@ 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.glide.getGlideImageForResource
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.android.utils.openItemUrlInBrowserAsNewTask
import bou.amine.apps.readerforselfossv2.android.utils.openUrlInBrowserAsNewTask 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
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.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
@ -76,7 +74,7 @@ class ArticleFragment :
private var colorSurface: Int = 0 private var colorSurface: Int = 0
private var fontSize: Int = DEFAULT_FONT_SIZE private var fontSize: Int = DEFAULT_FONT_SIZE
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
private var url: String? = null private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
private lateinit var contentSource: String private lateinit var contentSource: String
private lateinit var contentImage: String private lateinit var contentImage: String
@ -89,7 +87,6 @@ class ArticleFragment :
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 val appSettingsService: AppSettingsService by instance()
private val connectivityService: ConnectivityService by instance()
private var typeface: Typeface? = null private var typeface: Typeface? = null
private var resId: Int = 0 private var resId: Int = 0
@ -120,8 +117,8 @@ class ArticleFragment :
e.sendSilentlyWithAcra() e.sendSilentlyWithAcra()
} }
colorOnSurface = getColorFromAttr(com.google.android.material.R.attr.colorOnSurface) colorOnSurface = requireContext().getColorFromAttr(R.attr.colorOnSurface)
colorSurface = getColorFromAttr(com.google.android.material.R.attr.colorSurface) colorSurface = requireContext().getColorFromAttr(R.attr.colorSurface)
contentText = item.content contentText = item.content
contentTitle = item.title.getHtmlDecoded() contentTitle = item.title.getHtmlDecoded()
@ -150,11 +147,11 @@ class ArticleFragment :
handleContent() handleContent()
} catch (e: InflateException) { } catch (e: InflateException) {
e.sendSilentlyWithAcraWithName("webview not available") e.sendSilentlyWithAcraWithName("webview not available")
maybeIfContext { try {
AlertDialog AlertDialog
.Builder(it) .Builder(requireContext())
.setMessage(it.getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(it.getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton( .setPositiveButton(
android.R.string.ok, android.R.string.ok,
) { _, _ -> ) { _, _ ->
@ -162,6 +159,8 @@ class ArticleFragment :
requireActivity().finish() requireActivity().finish()
}.create() }.create()
.show() .show()
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }
@ -170,8 +169,8 @@ class ArticleFragment :
private fun handleContent() { private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) { if (contentText.isEmptyOrNullOrNullString()) {
if (connectivityService.isNetworkAvailable() && url.isUrlValid()) { if (repository.isNetworkAvailable()) {
getContentFromMercury(url!!) getContentFromMercury()
} }
} else { } else {
binding.titleView.text = contentTitle binding.titleView.text = contentTitle
@ -183,7 +182,7 @@ class ArticleFragment :
if (!contentImage.isEmptyOrNullOrNullString() && context != null) { if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE binding.imageView.visibility = View.VISIBLE
maybeIfContext { it.bitmapFitCenter(contentImage, binding.imageView, appSettingsService) } requireContext().bitmapFitCenter(contentImage, binding.imageView, appSettingsService)
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
@ -195,39 +194,39 @@ class ArticleFragment :
fab.mainFabClosedIconColor = colorOnSurface fab.mainFabClosedIconColor = colorOnSurface
fab.mainFabOpenedIconColor = colorOnSurface fab.mainFabOpenedIconColor = colorOnSurface
maybeIfContext { handleFloatingToolbarActionItems(it) } handleFloatingToolbarActionItems()
fab.setOnActionSelectedListener { actionItem -> fab.setOnActionSelectedListener { actionItem ->
when (actionItem.id) { when (actionItem.id) {
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item) R.id.open_action -> requireActivity().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> R.id.unread_action ->
if (this@ArticleFragment.item.unread) { try {
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 }
maybeIfContext { this@ArticleFragment.item.unread = false
Toast Toast
.makeText( .makeText(
it, requireContext(),
R.string.marked_as_read, R.string.marked_as_read,
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
).show() ).show()
} } else {
} else { CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { repository.unmarkAsRead(this@ArticleFragment.item)
repository.unmarkAsRead(this@ArticleFragment.item) }
} this@ArticleFragment.item.unread = true
this@ArticleFragment.item.unread = true
maybeIfContext {
Toast Toast
.makeText( .makeText(
it, context,
R.string.marked_as_unread, R.string.marked_as_unread,
Toast.LENGTH_LONG, Toast.LENGTH_LONG,
).show() ).show()
} }
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
else -> Unit else -> Unit
@ -236,14 +235,14 @@ class ArticleFragment :
} }
} }
private fun handleFloatingToolbarActionItems(c: Context) { private fun handleFloatingToolbarActionItems() {
fab.addHomeMadeActionItem( fab.addHomeMadeActionItem(
R.id.share_action, R.id.share_action,
resources.getDrawable(R.drawable.ic_share_white_24dp), resources.getDrawable(R.drawable.ic_share_white_24dp),
R.string.reader_action_share, R.string.reader_action_share,
colorOnSurface, colorOnSurface,
colorSurface, colorSurface,
c, requireContext(),
) )
fab.addHomeMadeActionItem( fab.addHomeMadeActionItem(
R.id.open_action, R.id.open_action,
@ -251,7 +250,7 @@ class ArticleFragment :
R.string.reader_action_open, R.string.reader_action_open,
colorOnSurface, colorOnSurface,
colorSurface, colorSurface,
c, requireContext(),
) )
fab.addHomeMadeActionItem( fab.addHomeMadeActionItem(
R.id.unread_action, R.id.unread_action,
@ -259,7 +258,7 @@ class ArticleFragment :
R.string.unmark, R.string.unmark,
colorOnSurface, colorOnSurface,
colorSurface, colorSurface,
c, requireContext(),
) )
} }
@ -273,7 +272,7 @@ class ArticleFragment :
} }
@Suppress("detekt:SwallowedException") @Suppress("detekt:SwallowedException")
private fun getContentFromMercury(url: String) { private fun getContentFromMercury() {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -312,11 +311,9 @@ class ArticleFragment :
} }
private fun handleLeadImage(leadImageUrl: String?) { private fun handleLeadImage(leadImageUrl: String?) {
if (!leadImageUrl.isNullOrEmpty()) { if (!leadImageUrl.isNullOrEmpty() && context != null) {
maybeIfContext { binding.imageView.visibility = View.VISIBLE
binding.imageView.visibility = View.VISIBLE requireContext().bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
it.bitmapFitCenter(leadImageUrl, binding.imageView, appSettingsService)
}
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
@ -330,10 +327,11 @@ class ArticleFragment :
view: WebView?, view: WebView?,
url: String, url: String,
): Boolean = ): Boolean =
if (url.isUrlValid() && if (context != null &&
url.isUrlValid() &&
binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) { ) {
maybeIfContext { it.openUrlInBrowserAsNewTask(url) } requireContext().openUrlInBrowser(url)
true true
} else { } else {
false false
@ -376,14 +374,23 @@ class ArticleFragment :
@Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale") @Suppress("detekt:LongMethod", "detekt:ImplicitDefaultLocale")
private fun htmlToWebview() { private fun htmlToWebview() {
maybeIfContext { 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 = it.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
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context issue when setting attributes, but context wasn't null before")
} }
binding.webcontent.visibility = View.VISIBLE
val colorSurfaceString = val colorSurfaceString =
String.format( String.format(
@ -397,12 +404,13 @@ class ArticleFragment :
WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0), WHITE_COLOR_HEX and (if (colorOnSurface != DATA_NULL_UNDEFINED) colorOnSurface else 0),
) )
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
try { try {
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
handleImageLoading()
val gestureDetector = val gestureDetector =
GestureDetector( GestureDetector(
activity, activity,
@ -416,50 +424,49 @@ class ArticleFragment :
event, event,
) )
} }
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Gesture detector issue ?") e.sendSilentlyWithAcraWithName("Context is null but wasn't, and that's causing issues with webview config")
return return
} }
binding.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
var baseUrl: String? = null
try { try {
val itemUrl = URL(url.orEmpty()) var baseUrl: String? = null
baseUrl = itemUrl.protocol + "://" + itemUrl.host try {
} catch (e: MalformedURLException) { val itemUrl = URL(url)
e.sendSilentlyWithAcraWithName("htmlToWebview > ${url.orEmpty()}") baseUrl = itemUrl.protocol + "://" + itemUrl.host
} } catch (e: MalformedURLException) {
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
}
val fontName: String = val fontName =
maybeIfContext {
when (font) { when (font) {
it.getString(R.string.open_sans_font_id) -> "Open Sans" getString(R.string.open_sans_font_id) -> "Open Sans"
it.getString(R.string.roboto_font_id) -> "Roboto" getString(R.string.roboto_font_id) -> "Roboto"
it.getString(R.string.source_code_pro_font_id) -> "Source Code Pro" getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
else -> "" else -> ""
} }
}?.toString().orEmpty()
val fontLinkAndStyle = val fontLinkAndStyle =
if (fontName.isNotEmpty()) { if (font.isNotEmpty()) {
"""<link href="https://fonts.googleapis.com/css?family=${ """<link href="https://fonts.googleapis.com/css?family=${
fontName.replace( fontName.replace(
" ", " ",
"+", "+",
) )
}" rel="stylesheet"> }" rel="stylesheet">
|<style> |<style>
| * { | * {
| font-family: '$fontName'; | font-family: '$fontName';
| } | }
|</style> |</style>
""".trimMargin() """.trimMargin()
} else { } else {
"" ""
} }
try {
binding.webcontent.loadDataWithBaseURL( binding.webcontent.loadDataWithBaseURL(
baseUrl, baseUrl,
"""<html> """<html>
@ -476,7 +483,7 @@ class ArticleFragment :
| color: ${ | color: ${
String.format( String.format(
"#%06X", "#%06X",
WHITE_COLOR_HEX and (maybeIfContext { it.resources.getColor(R.color.colorAccent) } as Int), WHITE_COLOR_HEX and context.resources.getColor(R.color.colorAccent),
) )
} !important; } !important;
| } | }
@ -533,8 +540,10 @@ class ArticleFragment :
private fun openInBrowserAfterFailing() { private fun openInBrowserAfterFailing() {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
maybeIfContext { try {
it.openItemUrlInBrowserAsNewTask(this@ArticleFragment.item) requireContext().openItemUrlInBrowserAsNewTask(this@ArticleFragment.item)
} catch (e: IllegalStateException) {
e.sendSilentlyWithAcraWithName("Context required is null")
} }
} }

View File

@ -1,5 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
@ -16,7 +17,6 @@ import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget
import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
@ -60,8 +60,8 @@ class FilterSheetFragment :
try { try {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
handleTagChips() handleTagChips(requireContext())
handleSourceChips() handleSourceChips(requireContext())
binding.progressBar2.visibility = GONE binding.progressBar2.visibility = GONE
binding.filterView.visibility = VISIBLE binding.filterView.visibility = VISIBLE
@ -79,39 +79,29 @@ class FilterSheetFragment :
return binding.root return binding.root
} }
private suspend fun handleSourceChips() { private suspend fun handleSourceChips(context: Context) {
val sourceGroup = binding.sourcesGroup val sourceGroup = binding.sourcesGroup
repository.getSourcesDetailsOrStats().forEachIndexed { _, source -> repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
val c: Chip? = val c = Chip(context)
maybeIfContext {
Chip(it)
} as Chip?
if (c == null) {
return
}
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
maybeIfContext { context.imageIntoViewTarget(
it.imageIntoViewTarget( source.getIcon(repository.baseUrl),
source.getIcon(repository.baseUrl), 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, },
) appSettingsService,
} )
c.text = source.title.getHtmlDecoded() c.text = source.title.getHtmlDecoded()
@ -147,17 +137,13 @@ class FilterSheetFragment :
} }
} }
private suspend fun handleTagChips() { private suspend fun handleTagChips(context: Context) {
val tagGroup = binding.tagsGroup val tagGroup = binding.tagsGroup
val tags = repository.getTags() val tags = repository.getTags()
tags.forEachIndexed { _, tag -> tags.forEachIndexed { _, tag ->
val c: Chip? = maybeIfContext { Chip(it) } as Chip? val c = Chip(context)
if (c == null) {
return
}
c.ellipsize = TextUtils.TruncateAt.END c.ellipsize = TextUtils.TruncateAt.END
c.text = tag.tag c.text = tag.tag

View File

@ -5,57 +5,39 @@ import android.content.Intent
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.fragment.app.Fragment
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun Context.shareLink( fun Context.shareLink(
itemUrl: String?, itemUrl: String,
itemTitle: String, itemTitle: String,
) { ) {
if (itemUrl.isUrlValid()) { val sendIntent = Intent()
val sendIntent = Intent() sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK sendIntent.action = Intent.ACTION_SEND
sendIntent.action = Intent.ACTION_SEND sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl!!.toStringUriWithHttp()) sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle) sendIntent.type = "text/plain"
sendIntent.type = "text/plain" startActivity(
startActivity( Intent
Intent .createChooser(
.createChooser( sendIntent,
sendIntent, getString(R.string.share),
getString(R.string.share), ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), )
)
}
} }
@ColorInt @ColorInt
fun Fragment.getColorFromAttr( fun Context.getColorFromAttr(
@AttrRes attrColor: Int, @AttrRes attrColor: Int,
resolveRefs: Boolean = true, resolveRefs: Boolean = true,
): Int { ): Int {
val typedValue = TypedValue() val typedValue = TypedValue()
maybeIfContextWithLog { this.requireContext().theme.resolveAttribute(attrColor, typedValue, resolveRefs) } try {
this.theme.resolveAttribute(attrColor, typedValue, resolveRefs)
} catch (e: Throwable) {
e.sendSilentlyWithAcraWithName("ColorFromAttr")
}
return typedValue.data return typedValue.data
} }
@Suppress("detekt:SwallowedException")
fun Fragment.maybeIfContext(fn: (Context) -> Any): Any? {
try {
return fn(this.requireContext())
} catch (e: Exception) {
// Do nothing
return null
}
}
fun Fragment.maybeIfContextWithLog(fn: (Context) -> Any): Any? {
try {
return fn(this.requireContext())
} catch (e: Exception) {
e.sendSilentlyWithAcraWithName("Fragment context issue...")
return null
}
}

View File

@ -15,12 +15,12 @@ import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.ReaderActivity import bou.amine.apps.readerforselfossv2.android.ReaderActivity
import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.openItemUrl( fun Context.openItemUrl(
currentItem: Int, currentItem: Int,
linkDecoded: String?, linkDecoded: String,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity, app: Activity,
) { ) {
@ -37,13 +37,12 @@ fun Context.openItemUrl(
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
} else { } else {
this.openUrlInBrowserAsNewTask(linkDecoded!!) this.openUrlInBrowserAsNewTask(linkDecoded)
} }
} }
} }
fun String?.isUrlValid(): Boolean = fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
!this.isEmptyOrNullOrNullString() && this!!.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlInvalid(): Boolean { fun String.isBaseUrlInvalid(): Boolean {
val baseUrl = this.toHttpUrlOrNull() val baseUrl = this.toHttpUrlOrNull()
@ -57,16 +56,14 @@ fun String.isBaseUrlInvalid(): Boolean {
} }
fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) { fun Context.openItemUrlInBrowserAsNewTask(i: SelfossModel.Item) {
this.openUrlInBrowserAsNewTask(i.getLinkDecoded()) this.openUrlInBrowserAsNewTask(i.getLinkDecoded().toStringUriWithHttp())
} }
fun Context.openUrlInBrowserAsNewTask(url: String?) { fun Context.openUrlInBrowserAsNewTask(url: String) {
if (url.isUrlValid()) { val intent = Intent(Intent.ACTION_VIEW)
val intent = Intent(Intent.ACTION_VIEW) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.data = Uri.parse(url)
intent.data = Uri.parse(url) this.mayBeStartActivity(intent)
this.mayBeStartActivity(intent)
}
} }
fun Context.openUrlInBrowser(url: String) { fun Context.openUrlInBrowser(url: String) {

View File

@ -22,5 +22,5 @@ class AcraReportingAdministrator : ReportingAdministrator {
context: Context, context: Context,
config: CoreConfiguration, config: CoreConfiguration,
crashReportData: CrashReportData, crashReportData: CrashReportData,
): Boolean = crashReportData.get("BRAND") != "redroid" && !crashReportData.get("PHONE_MODEL").toString().startsWith("sdk_gphone") ): Boolean = crashReportData.get("BRAND") != "redroid"
} }

View File

@ -7,7 +7,6 @@ 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.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.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
@ -23,12 +22,8 @@ import kotlin.io.encoding.ExperimentalEncodingApi
private const val PRELOAD_IMAGE_TIMEOUT = 10000 private const val PRELOAD_IMAGE_TIMEOUT = 10000
@Suppress("detekt:ReturnCount")
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
fun String.toGlideUrl(appSettingsService: AppSettingsService): Any { // GlideUrl Or String fun String.toGlideUrl(appSettingsService: AppSettingsService): GlideUrl {
if (this.isEmptyOrNullOrNullString()) {
return ""
}
if (appSettingsService.getBasicUserName().isNotEmpty()) { if (appSettingsService.getBasicUserName().isNotEmpty()) {
val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}" val authString = "${appSettingsService.getBasicUserName()}:${appSettingsService.getBasicPassword()}"
val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8)) val authBuf = Base64.encode(authString.toByteArray(Charsets.UTF_8))

View File

@ -0,0 +1,32 @@
package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bou.amine.apps.readerforselfossv2.repository.Repository
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class AppViewModel(
private val repository: Repository,
) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var wasConnected = true
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
if (repository.connectionMonitored) {
if (isConnected && !wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(true)
wasConnected = true
} else if (!isConnected && wasConnected && repository.connectionMonitored) {
_networkAvailableProvider.emit(false)
wasConnected = false
}
}
}
}
}
}

View File

@ -24,6 +24,7 @@
<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="undo_string">"Desfés"</string> <string name="undo_string">"Desfés"</string>
<string name="addStringNoUrl">"Inicieu la sessió per afegir fonts."</string>
<string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string> <string name="cant_get_sources">"No es pot obtenir la llista de fonts."</string>
<string name="cant_create_source">"No es pot crear la font."</string> <string name="cant_create_source">"No es pot crear la font."</string>
<string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string> <string name="cant_get_spouts_no_network">"Can't get spouts list because of a network issue."</string>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Quant a"</string> <string name="action_about">"Quant a"</string>
<string name="marked_as_read">"Element llegit"</string> <string name="marked_as_read">"Element llegit"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="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="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">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string> <string name="cant_get_spouts_no_network">"Fehler beim Laden der Spouts-Liste aufgrund von Netzwerkproblemen."</string>
@ -106,6 +107,7 @@
<string name="reader_text_align_justify">Blocksatz</string> <string name="reader_text_align_justify">Blocksatz</string>
<string name="settings_reader_font">Schriftgröße im Lesemodus</string> <string name="settings_reader_font">Schriftgröße im Lesemodus</string>
<string name="remove_source">Quelle entfernen</string> <string name="remove_source">Quelle entfernen</string>
<string name="pref_theme_title">Heller/Dunkler Modus</string>
<string name="mode_dark">Dunkler Modus</string> <string name="mode_dark">Dunkler Modus</string>
<string name="mode_system">Systemeinstellungen übernehmen</string> <string name="mode_system">Systemeinstellungen übernehmen</string>
<string name="mode_light">Heller Modus</string> <string name="mode_light">Heller Modus</string>
@ -127,6 +129,4 @@
<string name="action_about">"Über"</string> <string name="action_about">"Über"</string>
<string name="marked_as_read">"Artikel gelesen"</string> <string name="marked_as_read">"Artikel gelesen"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Deshacer"</string> <string name="undo_string">"Deshacer"</string>
<string name="addStringNoUrl">"Iniciar sesión para añadir fuentes."</string>
<string name="cant_get_sources">"No se puede obtener la lista de fuentes."</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Acerca de"</string> <string name="action_about">"Acerca de"</string>
<string name="marked_as_read">"Artículo leído"</string> <string name="marked_as_read">"Artículo leído"</string>
<string name="marked_as_unread">"Artículo no leído"</string> <string name="marked_as_unread">"Artículo no leído"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Annuler"</string> <string name="undo_string">"Annuler"</string>
<string name="addStringNoUrl">"Identifiez-vous pour ajouter une source."</string>
<string name="cant_get_sources">"Impossible de récupérer la liste des sources."</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="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="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>
@ -127,6 +129,4 @@
<string name="action_about">"À propos"</string> <string name="action_about">"À propos"</string>
<string name="marked_as_read">"Marqué comme lu"</string> <string name="marked_as_read">"Marqué comme lu"</string>
<string name="marked_as_unread">"Marqué comme non lu"</string> <string name="marked_as_unread">"Marqué comme non lu"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Desfacer"</string> <string name="undo_string">"Desfacer"</string>
<string name="addStringNoUrl">"Accede pra engadir fontes."</string>
<string name="cant_get_sources">"Non se pode obter a lista de fontes."</string> <string name="cant_get_sources">"Non se pode obter a lista de fontes."</string>
<string name="cant_create_source">"Non se pode crear unha fonte."</string> <string name="cant_create_source">"Non se pode crear unha fonte."</string>
<string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string> <string name="cant_get_spouts_no_network">"Non se pode obter a lista de spouts por mor dun erro de rede."</string>
@ -106,6 +107,7 @@
<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="remove_source">Eliminar fonte</string> <string name="remove_source">Eliminar fonte</string>
<string name="pref_theme_title">Modo Claro/Escuro</string>
<string name="mode_dark">Modo escuro</string> <string name="mode_dark">Modo escuro</string>
<string name="mode_system">Seguir axustes do sistema</string> <string name="mode_system">Seguir axustes do sistema</string>
<string name="mode_light">Modo claro</string> <string name="mode_light">Modo claro</string>
@ -127,6 +129,4 @@
<string name="action_about">"Acerca de"</string> <string name="action_about">"Acerca de"</string>
<string name="marked_as_read">"Elemento lido"</string> <string name="marked_as_read">"Elemento lido"</string>
<string name="marked_as_unread">"Elemento non lido"</string> <string name="marked_as_unread">"Elemento non lido"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Urung"</string> <string name="undo_string">"Urung"</string>
<string name="addStringNoUrl">"Masuk untuk menambah sumber."</string>
<string name="cant_get_sources">"Tidak bisa mendapatkan daftar sumber."</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Tentang"</string> <string name="action_about">"Tentang"</string>
<string name="marked_as_read">"Membaca item"</string> <string name="marked_as_read">"Membaca item"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Annulla"</string> <string name="undo_string">"Annulla"</string>
<string name="addStringNoUrl">"Autenticati per aggiungere fonti."</string>
<string name="cant_get_sources">"Can't get sources list."</string> <string name="cant_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</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_no_network">"Can't get spouts list because of a network issue."</string>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Informazioni"</string> <string name="action_about">"Informazioni"</string>
<string name="marked_as_read">"Articolo letto"</string> <string name="marked_as_read">"Articolo letto"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"실행 취소"</string> <string name="undo_string">"실행 취소"</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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"정보"</string> <string name="action_about">"정보"</string>
<string name="marked_as_read">"항목 읽기"</string> <string name="marked_as_read">"항목 읽기"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<string name="all_posts_not_read">"Fout bij markeren als gelezen"</string> <string name="all_posts_not_read">"Fout bij markeren als gelezen"</string>
<string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string> <string name="all_posts_read">"Alle artikelen gemarkeerd als gelezen"</string>
<string name="undo_string">"Ongedaan maken"</string> <string name="undo_string">"Ongedaan maken"</string>
<string name="addStringNoUrl">"Login om bronnen toe te voegen"</string>
<string name="cant_get_sources">"Kan de lijst met bronnen niet ophalen"</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Over"</string> <string name="action_about">"Over"</string>
<string name="marked_as_read">"Artikel gelezen"</string> <string name="marked_as_read">"Artikel gelezen"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Desfazer"</string> <string name="undo_string">"Desfazer"</string>
<string name="addStringNoUrl">"Faça login para adicionar fontes."</string>
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Sobre"</string> <string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string> <string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Desfazer"</string> <string name="undo_string">"Desfazer"</string>
<string name="addStringNoUrl">"Logar para adicionar fontes."</string>
<string name="cant_get_sources">"Não é possível obter a lista de fontes."</string> <string name="cant_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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Sobre"</string> <string name="action_about">"Sobre"</string>
<string name="marked_as_read">"Item lido"</string> <string name="marked_as_read">"Item lido"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"Undo"</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_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</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_no_network">"Can't get spouts list because of a network issue."</string>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"මේ ගැන"</string> <string name="action_about">"මේ ගැන"</string>
<string name="marked_as_read">"Item read"</string> <string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="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="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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"Hakkında"</string> <string name="action_about">"Hakkında"</string>
<string name="marked_as_read">"Öğeleri oku"</string> <string name="marked_as_read">"Öğeleri oku"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"撤销"</string> <string name="undo_string">"撤销"</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">"由于网络问题,无法获取 spouts 列表。"</string> <string name="cant_get_spouts_no_network">"由于网络问题,无法获取 spouts 列表。"</string>
@ -106,6 +107,7 @@
<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="remove_source">删除源</string> <string name="remove_source">删除源</string>
<string name="pref_theme_title">浅色/深色模式</string>
<string name="mode_dark">深色模式</string> <string name="mode_dark">深色模式</string>
<string name="mode_system">遵循系统设置</string> <string name="mode_system">遵循系统设置</string>
<string name="mode_light">浅色模式</string> <string name="mode_light">浅色模式</string>
@ -127,6 +129,4 @@
<string name="action_about">"关于我们"</string> <string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string> <string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未读条目"</string> <string name="marked_as_unread">"未读条目"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -24,6 +24,7 @@
<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="undo_string">"撤销"</string> <string name="undo_string">"撤销"</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>
@ -106,6 +107,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -127,6 +129,4 @@
<string name="action_about">"关于我们"</string> <string name="action_about">"关于我们"</string>
<string name="marked_as_read">"已读"</string> <string name="marked_as_read">"已读"</string>
<string name="marked_as_unread">"未讀項目"</string> <string name="marked_as_unread">"未讀項目"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -23,6 +23,7 @@
<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="undo_string">"Undo"</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_get_sources">"Can't get sources list."</string>
<string name="cant_create_source">"Can't create source."</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_no_network">"Can't get spouts list because of a network issue."</string>
@ -108,6 +109,7 @@
<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="remove_source">Remove source</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_dark">Dark mode</string>
<string name="mode_system">Follow the system setting</string> <string name="mode_system">Follow the system setting</string>
<string name="mode_light">Light mode</string> <string name="mode_light">Light mode</string>
@ -129,6 +131,4 @@
<string name="action_about">"About"</string> <string name="action_about">"About"</string>
<string name="marked_as_read">"Item read"</string> <string name="marked_as_read">"Item read"</string>
<string name="marked_as_unread">"Item unread"</string> <string name="marked_as_unread">"Item unread"</string>
<string name="confirm_delete_title">Confirm Deletion</string>
<string name="confirm_delete_message">Are you sure you want to delete the following source?\n%s</string>
</resources> </resources>

View File

@ -11,7 +11,6 @@ import bou.amine.apps.readerforselfossv2.model.SuccessResponse
import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.repository.Repository
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.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.toView import bou.amine.apps.readerforselfossv2.utils.toView
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
@ -25,6 +24,7 @@ import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotSame import junit.framework.TestCase.assertNotSame
import junit.framework.TestCase.assertSame import junit.framework.TestCase.assertSame
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Before import org.junit.Before
@ -52,12 +52,15 @@ 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 connectivityService = mockk<ConnectivityService>()
private lateinit var repository: Repository private lateinit var repository: Repository
private fun initializeRepository(isNetworkAvailable: Boolean = true) { private fun initializeRepository(
every { connectivityService.isNetworkAvailable() } returns isNetworkAvailable isConnectionAvailable: MutableStateFlow<Boolean> =
repository = Repository(api, appSettingsService, connectivityService, db) MutableStateFlow(
true,
),
) {
repository = Repository(api, appSettingsService, isConnectionAvailable, db)
runBlocking { runBlocking {
repository.updateApiInformation() repository.updateApiInformation()
@ -107,7 +110,7 @@ class RepositoryTest {
fun instantiate_repository_without_api_version() { fun instantiate_repository_without_api_version() {
every { appSettingsService.getApiVersion() } returns -1 every { appSettingsService.getApiVersion() } returns -1
initializeRepository(false) initializeRepository(MutableStateFlow(false))
coVerify(exactly = 0) { api.apiInformation() } coVerify(exactly = 0) { api.apiInformation() }
coVerify(exactly = 0) { api.stats() } coVerify(exactly = 0) { api.stats() }
@ -284,7 +287,7 @@ class RepositoryTest {
fun get_newer_items_without_connectivity() { fun get_newer_items_without_connectivity() {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
} }
@ -311,7 +314,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
repository.setTagFilter(SelfossModel.Tag("Test", "red", 3)) repository.setTagFilter(SelfossModel.Tag("Test", "red", 3))
runBlocking { runBlocking {
repository.getNewerItems() repository.getNewerItems()
@ -339,7 +342,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
repository.setSourceFilter( repository.setSourceFilter(
SelfossModel.SourceDetail( SelfossModel.SourceDetail(
1, 1,
@ -454,7 +457,7 @@ class RepositoryTest {
var success: Boolean var success: Boolean
initializeRepository(false) initializeRepository(MutableStateFlow(false))
runBlocking { runBlocking {
success = repository.reloadBadges() success = repository.reloadBadges()
} }
@ -474,7 +477,7 @@ class RepositoryTest {
var success: Boolean var success: Boolean
initializeRepository(false) initializeRepository(MutableStateFlow(false))
runBlocking { runBlocking {
success = repository.reloadBadges() success = repository.reloadBadges()
} }
@ -569,7 +572,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -587,7 +590,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -604,7 +607,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -622,7 +625,7 @@ class RepositoryTest {
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testTags: List<SelfossModel.Tag> var testTags: List<SelfossModel.Tag>
runBlocking { runBlocking {
testTags = repository.getTags() testTags = repository.getTags()
@ -772,7 +775,7 @@ class RepositoryTest {
@Test @Test
fun get_sources_without_connection() { fun get_sources_without_connection() {
val (_, sourcesDB) = prepareSources() val (_, sourcesDB) = prepareSources()
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -789,7 +792,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns true every { appSettingsService.isUpdateSourcesEnabled() } returns true
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -806,7 +809,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns true every { appSettingsService.isItemCachingEnabled() } returns true
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -823,7 +826,7 @@ class RepositoryTest {
every { appSettingsService.isItemCachingEnabled() } returns false every { appSettingsService.isItemCachingEnabled() } returns false
every { appSettingsService.isUpdateSourcesEnabled() } returns false every { appSettingsService.isUpdateSourcesEnabled() } returns false
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var testSources: List<SelfossModel.Source> var testSources: List<SelfossModel.Source>
runBlocking { runBlocking {
testSources = repository.getSourcesDetails() testSources = repository.getSourcesDetails()
@ -895,7 +898,7 @@ class RepositoryTest {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = response =
@ -952,7 +955,7 @@ class RepositoryTest {
fun delete_source_without_connection() { fun delete_source_without_connection() {
coEvery { api.deleteSource(any()) } returns SuccessResponse(false) coEvery { api.deleteSource(any()) } returns SuccessResponse(false)
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.deleteSource(5, "src") response = repository.deleteSource(5, "src")
@ -1025,7 +1028,7 @@ class RepositoryTest {
data = "undocumented...", data = "undocumented...",
) )
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.updateRemote() response = repository.updateRemote()
@ -1067,7 +1070,7 @@ class RepositoryTest {
fun login_but_without_connection() { fun login_but_without_connection() {
coEvery { api.login() } returns SuccessResponse(success = true) coEvery { api.login() } returns SuccessResponse(success = true)
initializeRepository(false) initializeRepository(MutableStateFlow(false))
var response: Boolean var response: Boolean
runBlocking { runBlocking {
response = repository.login() response = repository.login()
@ -1147,7 +1150,7 @@ class RepositoryTest {
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(false) initializeRepository(MutableStateFlow(false))
prepareSearch() prepareSearch()
runBlocking { runBlocking {
repository.tryToCacheItemsAndGetNewOnes() repository.tryToCacheItemsAndGetNewOnes()

View File

@ -1,7 +1,7 @@
plugins { plugins {
// trick: for the same plugin versions in all sub-modules //trick: for the same plugin versions in all sub-modules
id("com.android.application").version("8.8.1").apply(false) id("com.android.application").version("8.7.3").apply(false)
id("com.android.library").version("8.8.1").apply(false) id("com.android.library").version("8.7.3").apply(false)
id("org.jetbrains.kotlin.android").version("2.1.0").apply(false) id("org.jetbrains.kotlin.android").version("2.1.0").apply(false)
kotlin("multiplatform").version("2.1.0").apply(false) kotlin("multiplatform").version("2.1.0").apply(false)
id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false) id("com.mikepenz.aboutlibraries.plugin").version("10.5.1").apply(false)
@ -16,6 +16,7 @@ allprojects {
} }
} }
tasks.register("clean", Delete::class) { tasks.register("clean", Delete::class) {
delete(layout.buildDirectory) delete(layout.buildDirectory)
} }
@ -23,4 +24,4 @@ tasks.register("clean", Delete::class) {
dependencies { dependencies {
kover(project(":shared")) kover(project(":shared"))
kover(project(":androidApp")) kover(project(":androidApp"))
} }

View File

@ -1,5 +0,0 @@
**v125010131**
- fix: reload the adapter when it's needed. Fixes #128. (#176)
- feat: basic auth and images loading. Fixes #172. (#175)
- Changelog for v125010111

View File

@ -1,6 +0,0 @@
**v125010201**
- fix: Handle empty url issue.
- Merge pull request 'Removed the floating bar.' (#177) from floating-bar into master
- chore: changing actions in reader fragment.
- Changelog for v125010131

View File

@ -1,8 +0,0 @@
**v125010241**
- Merge pull request 'fix: Link not opening.' (#178) from fix-open-link into master
- refactor: context fragments issues.
- logs: Context issues.
- fix: Handle empty url issue, again.
- fix: Link not opening.
- Changelog for v125010201

View File

@ -1,7 +0,0 @@
**v125020411**
- Merge pull request 'bump' (#182) from bump into master
- chore: non transiant R classes.
- Merge pull request 'fix: One more missing context.' (#181) from fix-one-more-context into master
- bump
- fix: One more missing context.

View File

@ -1,7 +0,0 @@
**v125020471**
- chore: no more docker-compose.
- bump: gradle plugin.
- Merge pull request 'fix: check index exists.' (#183) from fix-index into master
- fix: check index exists.
- Changelog for v125020411

View File

@ -1,4 +0,0 @@
**v125020581**
- fix: url can be empty ?
- Changelog for v125020471

View File

@ -1,12 +0,0 @@
**v125030681**
- chore: do not send reports on simulators.
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
- chore: do not send reports on simulators.
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
- chore: we don't need to check if the url is valid in upsert screen.
- fix: Url validation was not failing login. Added tests.
- chore: crowding ci integration.
- Show a confirmation dialog before deleting sources (#185)
- Changelog for v125020581

View File

@ -19,11 +19,11 @@ kotlin.code.style=official
android.useAndroidX=true android.useAndroidX=true
#android.nonTransitiveRClass=true #android.nonTransitiveRClass=true
android.enableJetifier=false android.enableJetifier=false
android.nonTransitiveRClass=true android.nonTransitiveRClass=false
#MPP #MPP
kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.enableCInteropCommonization=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
ignoreGitVersion=false ignoreGitVersion=false
kotlin.native.cacheKind.iosX64=none kotlin.native.cacheKind.iosX64=none
org.gradle.configureondemand=true org.gradle.configureondemand=true

View File

@ -1,6 +1,6 @@
#Sun Feb 09 14:44:52 CET 2025 #Mon Nov 25 22:48:24 CET 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -4,6 +4,7 @@ object SqlDelight {
const val runtime = "app.cash.sqldelight:runtime:2.0.2" const val runtime = "app.cash.sqldelight:runtime:2.0.2"
const val android = "app.cash.sqldelight:android-driver:2.0.2" const val android = "app.cash.sqldelight:android-driver:2.0.2"
const val native = "app.cash.sqldelight:native-driver:2.0.2" const val native = "app.cash.sqldelight:native-driver:2.0.2"
} }
plugins { plugins {
@ -40,13 +41,13 @@ kotlin {
implementation("org.jsoup:jsoup:1.15.4") implementation("org.jsoup:jsoup:1.15.4")
// Dependency Injection //Dependency Injection
implementation("org.kodein.di:kodein-di:7.14.0") implementation("org.kodein.di:kodein-di:7.14.0")
// Settings //Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC") implementation("com.russhwolf:multiplatform-settings-no-arg:1.0.0-RC")
// Logging //Logging
implementation("io.github.aakira:napier:2.6.1") implementation("io.github.aakira:napier:2.6.1")
// Sql // Sql
@ -54,10 +55,6 @@ kotlin {
// Sql // Sql
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
// Connectivity
implementation("dev.jordond.connectivity:connectivity-core:1.2.0")
implementation("dev.jordond.connectivity:connectivity-device:1.2.0")
} }
} }
val commonTest by getting { val commonTest by getting {
@ -117,4 +114,4 @@ sqldelight {
packageName.set("bou.amine.apps.readerforselfossv2.dao") packageName.set("bou.amine.apps.readerforselfossv2.dao")
} }
} }
} }

View File

@ -127,8 +127,8 @@ class SelfossModel {
val tags: List<String>, val tags: List<String>,
val author: String? = null, val author: String? = null,
) { ) {
fun getLinkDecoded(): String? { fun getLinkDecoded(): String {
var stringUrl: String? var stringUrl: String
stringUrl = stringUrl =
if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) { if (link.contains("//news.google.com/news/") && link.contains("&amp;url=")) {
link.substringAfter("&amp;url=") link.substringAfter("&amp;url=")
@ -146,7 +146,11 @@ class SelfossModel {
stringUrl = "http:$stringUrl" stringUrl = "http:$stringUrl"
} }
return if (stringUrl.isEmptyOrNullOrNullString()) null else stringUrl if (stringUrl.isEmptyOrNullOrNullString()) {
throw ModelException("Link $link was translated to $stringUrl, but was empty. Handle this.")
}
return stringUrl
} }
fun sourceAuthorAndDate(): String { fun sourceAuthorAndDate(): String {

View File

@ -13,7 +13,6 @@ 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.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.ItemType import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.toEntity import bou.amine.apps.readerforselfossv2.utils.toEntity
@ -31,10 +30,11 @@ 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,
private val connectivityService: ConnectivityService, val isConnectionAvailable: MutableStateFlow<Boolean>,
private val db: ReaderForSelfossDB, private val db: ReaderForSelfossDB,
) { ) {
var items = ArrayList<SelfossModel.Item>() var items = ArrayList<SelfossModel.Item>()
var connectionMonitored = false
var baseUrl = appSettingsService.getBaseUrl() var baseUrl = appSettingsService.getBaseUrl()
@ -63,7 +63,7 @@ class Repository(
suspend fun getNewerItems(): ArrayList<SelfossModel.Item> { suspend fun getNewerItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
fetchedItems = fetchedItems =
api.getItems( api.getItems(
displayedItems.type, displayedItems.type,
@ -102,7 +102,7 @@ class Repository(
suspend fun getOlderItems(): ArrayList<SelfossModel.Item> { suspend fun getOlderItems(): ArrayList<SelfossModel.Item> {
var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error() var fetchedItems: StatusAndData<List<SelfossModel.Item>> = StatusAndData.error()
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
val offset = items.size val offset = items.size
fetchedItems = fetchedItems =
api.getItems( api.getItems(
@ -122,7 +122,7 @@ class Repository(
} }
private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> { private suspend fun getMaxItemsForBackground(itemType: ItemType): List<SelfossModel.Item> {
return if (connectivityService.isNetworkAvailable()) { return if (isNetworkAvailable()) {
val items = val items =
api.getItems( api.getItems(
itemType.type, itemType.type,
@ -146,7 +146,7 @@ class Repository(
@Suppress("detekt:ForbiddenComment") @Suppress("detekt:ForbiddenComment")
suspend fun reloadBadges(): Boolean { suspend fun reloadBadges(): Boolean {
var success = false var success = false
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.stats() val response = api.stats()
if (response.success && response.data != null) { if (response.success && response.data != null) {
_badgeUnread.value = response.data.unread ?: 0 _badgeUnread.value = response.data.unread ?: 0
@ -168,7 +168,7 @@ class Repository(
suspend fun getTags(): List<SelfossModel.Tag> { suspend fun getTags(): List<SelfossModel.Tag> {
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
return if (connectivityService.isNetworkAvailable() && !fetchedTags) { return if (isNetworkAvailable() && !fetchedTags) {
val apiTags = api.tags() val apiTags = api.tags()
if (apiTags.success && apiTags.data != null && isDatabaseEnabled) { if (apiTags.success && apiTags.data != null && isDatabaseEnabled) {
resetDBTagsWithData(apiTags.data) resetDBTagsWithData(apiTags.data)
@ -185,7 +185,7 @@ class Repository(
} }
suspend fun getSpouts(): Map<String, SelfossModel.Spout> = suspend fun getSpouts(): Map<String, SelfossModel.Spout> =
if (connectivityService.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
@ -201,7 +201,7 @@ class Repository(
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && connectivityService.isNetworkAvailable()) { if (shouldFetch && isNetworkAvailable()) {
if (appSettingsService.getPublicAccess()) { if (appSettingsService.getPublicAccess()) {
val apiSources = api.sourcesStats() val apiSources = api.sourcesStats()
if (apiSources.success && apiSources.data != null) { if (apiSources.success && apiSources.data != null) {
@ -223,7 +223,7 @@ class Repository(
val isDatabaseEnabled = val isDatabaseEnabled =
appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled()
val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true
if (shouldFetch && connectivityService.isNetworkAvailable()) { if (shouldFetch && isNetworkAvailable()) {
val apiSources = api.sourcesDetailed() val apiSources = api.sourcesDetailed()
if (apiSources.success && apiSources.data != null) { if (apiSources.success && apiSources.data != null) {
fetchedSources = true fetchedSources = true
@ -248,7 +248,7 @@ class Repository(
} }
private suspend fun markAsReadById(id: Int): Boolean = private suspend fun markAsReadById(id: Int): Boolean =
if (connectivityService.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)
@ -265,7 +265,7 @@ class Repository(
} }
private suspend fun unmarkAsReadById(id: Int): Boolean = private suspend fun unmarkAsReadById(id: Int): Boolean =
if (connectivityService.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)
@ -282,7 +282,7 @@ class Repository(
} }
private suspend fun starrById(id: Int): Boolean = private suspend fun starrById(id: Int): Boolean =
if (connectivityService.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)
@ -299,7 +299,7 @@ class Repository(
} }
private suspend fun unstarrById(id: Int): Boolean = private suspend fun unstarrById(id: Int): Boolean =
if (connectivityService.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)
@ -309,8 +309,7 @@ class Repository(
suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean { suspend fun markAllAsRead(items: ArrayList<SelfossModel.Item>): Boolean {
var success = false var success = false
if (connectivityService.isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess if (isNetworkAvailable() && api.markAllAsRead(items.map { it.id.toString() }).isSuccess) {
) {
success = true success = true
for (item in items) { for (item in items) {
markAsReadLocally(item) markAsReadLocally(item)
@ -370,7 +369,7 @@ class Repository(
tags: String, tags: String,
): Boolean { ): Boolean {
var response = false var response = false
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
response = api response = api
.createSourceForVersion( .createSourceForVersion(
title, title,
@ -391,7 +390,7 @@ class Repository(
tags: String, tags: String,
): Boolean { ): Boolean {
var response = false var response = false
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
} }
@ -403,13 +402,13 @@ class Repository(
title: String, title: String,
): Boolean { ): Boolean {
var success = false var success = false
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
val response = api.deleteSource(id) val response = api.deleteSource(id)
success = response.isSuccess success = response.isSuccess
} }
// We filter on success or if the network isn't available // We filter on success or if the network isn't available
if (success || !connectivityService.isNetworkAvailable()) { if (success || !isNetworkAvailable()) {
items = ArrayList(items.filter { it.sourcetitle != title }) items = ArrayList(items.filter { it.sourcetitle != title })
setReaderItems(items) setReaderItems(items)
db.itemsQueries.deleteItemsWhereSource(title) db.itemsQueries.deleteItemsWhereSource(title)
@ -419,7 +418,7 @@ class Repository(
} }
suspend fun updateRemote(): Boolean = suspend fun updateRemote(): Boolean =
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
api.update().data.equals("finished") api.update().data.equals("finished")
} else { } else {
false false
@ -427,7 +426,7 @@ class Repository(
suspend fun login(): Boolean { suspend fun login(): Boolean {
var result = false var result = false
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
try { try {
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
@ -440,7 +439,7 @@ class Repository(
suspend fun checkIfFetchFails(): Boolean { suspend fun checkIfFetchFails(): Boolean {
var fetchFailed = true var fetchFailed = true
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
try { try {
// Trying to fetch one item, and check someone is trying to use the app with // Trying to fetch one item, and check someone is trying to use the app with
// a random rss feed, that would throw a NoTransformationFoundException // a random rss feed, that would throw a NoTransformationFoundException
@ -454,7 +453,7 @@ class Repository(
} }
suspend fun logout() { suspend fun logout() {
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
try { try {
val response = api.logout() val response = api.logout()
if (!response.isSuccess) { if (!response.isSuccess) {
@ -482,7 +481,7 @@ class Repository(
suspend fun updateApiInformation() { suspend fun updateApiInformation() {
val apiMajorVersion = appSettingsService.getApiVersion() val apiMajorVersion = appSettingsService.getApiVersion()
if (connectivityService.isNetworkAvailable()) { if (isNetworkAvailable()) {
val fetchedInformation = api.apiInformation() val fetchedInformation = api.apiInformation()
if (fetchedInformation.success && fetchedInformation.data != null) { if (fetchedInformation.success && fetchedInformation.data != null) {
if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) { if (fetchedInformation.data.getApiMajorVersion() != apiMajorVersion) {
@ -501,6 +500,8 @@ class Repository(
} }
} }
fun isNetworkAvailable() = isConnectionAvailable.value && !offlineOverride
private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList() private fun getDBActions(): List<ACTION> = db.actionsQueries.actions().executeAsList()
private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id) private fun deleteDBAction(action: ACTION) = db.actionsQueries.deleteAction(action.id)

View File

@ -1,46 +0,0 @@
package bou.amine.apps.readerforselfossv2.service
import dev.jordond.connectivity.Connectivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class ConnectivityService {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private var currentStatus = true
private lateinit var connectivity: Connectivity
fun start() {
connectivity = Connectivity()
connectivity.start()
CoroutineScope(Dispatchers.Main).launch {
connectivity.statusUpdates.collect { status ->
when (status) {
is Connectivity.Status.Connected -> {
if (!currentStatus) {
currentStatus = true
_networkAvailableProvider.emit(true)
}
}
is Connectivity.Status.Disconnected -> {
if (currentStatus) {
currentStatus = false
_networkAvailableProvider.emit(false)
}
}
}
}
}
}
fun isNetworkAvailable(): Boolean = currentStatus
fun stop() {
currentStatus = true
connectivity.stop()
}
}