Compare commits

..

12 Commits

Author SHA1 Message Date
80d609a692 fix: Infinite scroll needs loading stats.
Some checks failed
Check PR code / Lint (pull_request) Successful in 1m15s
Check PR code / translations (pull_request) Successful in 46s
Check PR code / build (pull_request) Failing after 20m4s
2025-03-30 15:21:51 +02:00
266a157f20 fix: do not reload items on resume. 2025-03-23 21:10:03 +01:00
ceba58e98f Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
All checks were successful
Check master code / build (push) Successful in 8m27s
Reviewed-on: #190
2025-03-16 13:14:44 +00:00
c3ee07dd85 Refactor star icon handling
All checks were successful
Check PR code / translations (pull_request) Successful in 1m4s
Check PR code / Lint (pull_request) Successful in 1m25s
Check PR code / build (pull_request) Successful in 11m52s
Extracted all favorite handling to two functions. Makes it a little bit more readable.
2025-03-12 16:07:49 +01:00
93d99192b3 Don't restart activity changing alignment
When changing alignment in the reader we were restarting the reader activity to reload. Doing this led to reloading the article which was initially opened every time you changed alignment. Now, when changing the alignment we retain all existing fragments but command all of them to update their alignment setting.
2025-03-12 16:07:49 +01:00
359dec2ca0 Changelog for v125030711 2025-03-12 11:39:57 +00:00
62354ec70a Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
All checks were successful
Check master code / build (push) Successful in 18m28s
Create tag / build (push) Successful in 18m21s
Create tag / createTagAndChangelog (push) Successful in 53s
Create tag / release (push) Successful in 4m19s
Reviewed-on: #192
2025-03-12 11:20:14 +00:00
18a17251ac chore: check changes for translations and android.
All checks were successful
Check PR code / translations (pull_request) Successful in 2m30s
Check PR code / Lint (pull_request) Successful in 2m36s
Check PR code / build (pull_request) Successful in 16m14s
2025-03-11 22:20:07 +01:00
5e91724ee2 fix: initial status loading issues.
Some checks failed
Check PR code / Lint (pull_request) Successful in 3m56s
Check PR code / translations (pull_request) Successful in 1m32s
Check PR code / build (pull_request) Has been cancelled
2025-03-11 22:04:42 +01:00
212d259a33 Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
All checks were successful
Check master code / build (push) Successful in 14m19s
Reviewed-on: #189
2025-03-11 13:48:57 +00:00
3bf60f1146 chore: new connectivity dep. Closes #84.
All checks were successful
Check PR code / Lint (pull_request) Successful in 1m2s
Check PR code / translations (pull_request) Successful in 1m24s
Check PR code / build (pull_request) Successful in 14m10s
2025-03-10 21:11:39 +01:00
ef13e300f0 Changelog for v125030681 2025-03-09 17:41:13 +00:00
18 changed files with 314 additions and 263 deletions

View File

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

View File

@ -29,7 +29,16 @@ jobs:
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'
@ -42,8 +51,10 @@ jobs:
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'
@ -56,17 +67,18 @@ jobs:
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-changes.outputs.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-changes.outputs.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

View File

@ -1,3 +1,29 @@
**v125030711
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681
--------------------------------------------------------------------
**v125030681
- chore: do not send reports on simulators.
- Merge pull request 'chore: do not send reports on simulators.' (#188) from chore-acra-simulator into master
- chore: do not send reports on simulators.
- Merge pull request 'fix: Url validation was not failing login. Added tests.' (#186) from fix-invalid-url into master
- Merge pull request 'chore: crowding ci integration.' (#187) from chore-crowdin-ci into master
- chore: we don't need to check if the url is valid in upsert screen.
- fix: Url validation was not failing login. Added tests.
- chore: crowding ci integration.
- Show a confirmation dialog before deleting sources (#185)
- Changelog for v125020581
--------------------------------------------------------------------
**v125020581
- fix: url can be empty ?

View File

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

View File

@ -122,6 +122,7 @@ class HomeActivity :
lastFetchDone = false
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
items.clear()
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
CountingIdlingResourceSingleton.decrement()
@ -462,19 +463,24 @@ class HomeActivity :
appendResults: Boolean,
itemType: ItemType,
) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items =
if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
@Suppress("detekt:ComplexCondition")
if ((appendResults && items.size > 0) || (!appendResults && items.size == 0)) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items =
if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
CountingIdlingResourceSingleton.decrement()
}
} else {
handleListResult()
CountingIdlingResourceSingleton.decrement()
}
}
@ -534,7 +540,10 @@ class HomeActivity :
}
private fun reloadBadges() {
if (appSettingsService.isDisplayUnreadCountEnabled() || appSettingsService.isDisplayAllCountEnabled()) {
if (appSettingsService.isInfiniteLoadingEnabled() ||
appSettingsService.isDisplayUnreadCountEnabled() ||
appSettingsService.isDisplayAllCountEnabled()
) {
CountingIdlingResourceSingleton.increment()
CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges()

View File

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

View File

@ -37,22 +37,6 @@ class ReaderActivity :
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
} else {
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
}
}
private fun canFavorite() {
showMenuItem(true)
}
private fun canRemoveFromFavorite() {
showMenuItem(false)
}
@Suppress("detekt:SwallowedException")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -73,14 +57,21 @@ class ReaderActivity :
finish()
}
try {
readItem(allItems[currentItem])
} catch (e: IndexOutOfBoundsException) {
finish()
}
readItem()
binding.pager.adapter = ScreenSlidePagerAdapter(this)
binding.pager.setCurrentItem(currentItem, false)
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
currentItem = position
updateStarIcon()
readItem()
}
},
)
}
override fun onResume() {
@ -89,14 +80,20 @@ class ReaderActivity :
binding.indicator.setViewPager(binding.pager)
}
private fun readItem(item: SelfossModel.Item) {
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
private fun readItem() {
val item = allItems.getOrNull(currentItem)
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess() && item != null) {
CoroutineScope(Dispatchers.IO).launch {
repository.markAsRead(item)
}
}
}
private fun updateStarIcon() {
val isStarred = allItems.getOrNull(currentItem)?.starred ?: false
toolbarMenu.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE)
}
override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState)
oldInstanceState.clear()
@ -141,8 +138,7 @@ class ReaderActivity :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.reader_menu, menu)
menuInflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu
alignmentMenu()
@ -150,87 +146,50 @@ class ReaderActivity :
if (appSettingsService.getPublicAccess()) {
menu.removeItem(R.id.star)
} else {
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (!allItems.isNullOrEmpty() && allItems.size >= position) {
if (allItems[position].starred) {
canRemoveFromFavorite()
} else {
canFavorite()
}
readItem(allItems[position])
}
}
},
)
updateStarIcon()
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
canFavorite()
}
when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
return true
}
R.id.star -> {
if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch {
repository.unstarr(allItems[binding.pager.currentItem])
}
afterUnsave()
} else {
CoroutineScope(Dispatchers.IO).launch {
repository.starr(allItems[binding.pager.currentItem])
}
afterSave()
}
}
R.id.align_left -> {
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
refreshFragment()
}
R.id.align_justify -> {
switchAlignmentSetting(AppSettingsService.JUSTIFY)
refreshFragment()
}
android.R.id.home -> onBackPressedDispatcher.onBackPressed()
R.id.star -> toggleFavorite()
R.id.align_left -> switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
R.id.align_justify -> switchAlignmentSetting(AppSettingsService.JUSTIFY)
}
return super.onOptionsItemSelected(item)
}
private fun switchAlignmentSetting(allignment: Int) {
appSettingsService.changeAllignment(allignment)
alignmentMenu()
private fun toggleFavorite() {
val item = allItems.getOrNull(currentItem) ?: return
val starred = item.starred
CoroutineScope(Dispatchers.IO).launch {
if (starred) {
repository.unstarr(item)
} else {
repository.starr(item)
}
}
item.toggleStar()
updateStarIcon()
}
private fun refreshFragment() {
finish()
overridePendingTransition(0, 0)
startActivity(intent)
overridePendingTransition(0, 0)
private fun switchAlignmentSetting(alignment: Int) {
appSettingsService.changeAllignment(alignment)
alignmentMenu()
val fragmentManager = supportFragmentManager
val fragments = fragmentManager.fragments
for (fragment in fragments) {
if (fragment is ArticleFragment) {
fragment.refreshAlignment()
}
}
}
}

View File

@ -30,7 +30,7 @@ import org.kodein.di.instance
class ItemCardAdapter(
override val app: Activity,
override val items: ArrayList<SelfossModel.Item>,
override var items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
override lateinit var binding: CardItemBinding

View File

@ -21,7 +21,7 @@ import org.kodein.di.instance
class ItemListAdapter(
override val app: Activity,
override val items: ArrayList<SelfossModel.Item>,
override var items: ArrayList<SelfossModel.Item>,
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
override lateinit var binding: ListItemBinding

View File

@ -21,7 +21,7 @@ import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
RecyclerView.Adapter<VH>(),
DIAware {
abstract val items: ArrayList<SelfossModel.Item>
abstract var items: ArrayList<SelfossModel.Item>
abstract val repository: Repository
abstract val binding: ViewBinding
abstract val appSettingsService: AppSettingsService
@ -31,8 +31,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> :
protected val c: Context get() = app.baseContext
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
this.items.clear()
this.items.addAll(items)
this.items = items
updateHomeItems(items)
notifyDataSetChanged()
}

View File

@ -42,6 +42,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import bou.amine.apps.readerforselfossv2.service.ConnectivityService
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getImages
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
@ -88,6 +89,7 @@ class ArticleFragment :
override val di: DI by closestDI()
private val repository: Repository by instance()
private val appSettingsService: AppSettingsService by instance()
private val connectivityService: ConnectivityService by instance()
private var typeface: Typeface? = null
private var resId: Int = 0
@ -168,7 +170,7 @@ class ArticleFragment :
private fun handleContent() {
if (contentText.isEmptyOrNullOrNullString()) {
if (repository.isNetworkAvailable() && url.isUrlValid()) {
if (connectivityService.isNetworkAvailable() && url.isUrlValid()) {
getContentFromMercury(url!!)
}
} else {
@ -261,13 +263,15 @@ class ArticleFragment :
)
}
private fun refreshAlignment() {
fun refreshAlignment() {
textAlignment =
when (appSettingsService.getActiveAllignment()) {
1 -> "justify"
2 -> "left"
else -> "justify"
}
htmlToWebview()
}
@Suppress("detekt:SwallowedException")

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
**v125030711**
- Merge pull request 'fix: initial status loading issues.' (#192) from connectivity into master
- chore: check changes for translations and android.
- fix: initial status loading issues.
- Merge pull request 'chore: new connectivity dep. Closes #84.' (#189) from connectivity into master
- chore: new connectivity dep. Closes #84.
- Changelog for v125030681

View File

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

View File

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

View File

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