Compare commits

...

6 Commits

Author SHA1 Message Date
9e140aefa6 Add filters only with selfoss >= 2.19
Source filters have been implemented in 2014 (commit 849957ca) but there is no reliable way to determine whether the selfoss instance does support source filters.
2025-04-13 20:01:56 +02:00
566930ba84 Fix sourceDetails unit tests 2025-04-13 00:01:46 +02:00
b1259fdc92 Support source filter editing
The API for filter is undocumented on the Selfoss API reference, but does exist. We can now allow users setting and modifying filters inside the application.
2025-04-12 23:58:12 +02:00
ceba58e98f Merge pull request 'Fix alignment changes resetting reader article position' (#190) from davidoskky/ReaderForSelfoss-multiplatform:alignment into master
Reviewed-on: Louvorg/ReaderForSelfoss-multiplatform#190
2025-03-16 13:14:44 +00:00
c3ee07dd85 Refactor star icon handling
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
9 changed files with 184 additions and 109 deletions

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

@ -65,6 +65,7 @@ class UpsertSourceActivity :
private fun initFields(items: Map<String, SelfossModel.Spout>) {
binding.nameInput.setText(existingSource!!.title)
binding.tags.setText(existingSource!!.tags?.joinToString(", "))
binding.filter.setText(existingSource!!.filter)
binding.sourceUri.setText(existingSource!!.params?.url)
binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
binding.progress.visibility = View.GONE
@ -169,6 +170,7 @@ class UpsertSourceActivity :
url,
mSpoutsValue!!,
binding.tags.text.toString(),
binding.filter.text.toString(),
)
} else {
repository.createSource(
@ -176,6 +178,7 @@ class UpsertSourceActivity :
url,
mSpoutsValue!!,
binding.tags.text.toString(),
binding.filter.text.toString(),
)
}
if (successfullyAddedSource) {

View File

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

@ -64,15 +64,28 @@
android:id="@+id/tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_tags"
android:inputType="text"
android:minHeight="48dp"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceUri" />
<EditText
android:id="@+id/filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:autofillHints="false"
android:hint="@string/add_source_hint_filter"
android:inputType="text"
android:minHeight="48dp"
android:textColorHint="?android:textColorPrimary"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tags" />
<Spinner
android:id="@+id/spoutsSpinner"
android:layout_width="match_parent"
@ -80,7 +93,7 @@
android:minHeight="48dp"
android:layout_marginTop="16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tags" />
app:layout_constraintTop_toBottomOf="@+id/filter" />
<Button
android:id="@+id/saveBtn"

View File

@ -17,6 +17,7 @@
<string name="add_source_hint_tags">"Tag1, Tag2, Tag3"</string>
<string name="add_source_hint_url">"Link"</string>
<string name="add_source_hint_name">"Name"</string>
<string name="add_source_hint_filter">/Filter/</string>
<string name="add_source">"Add a source"</string>
<string name="add_source_save">"Save"</string>
<string name="wrong_infos">"Check your details again."</string>

View File

@ -44,6 +44,8 @@ private const val FEED_URL = "https://test.com/feed"
private const val TAGS = "Test, New"
private const val FILTER = "/filter/"
private const val NUMBER_ARTICLES = 100
private const val NUMBER_UNREAD = 50
private const val NUMBER_STARRED = 20
@ -836,7 +838,7 @@ class RepositoryTest {
@Test
fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(true)
initializeRepository()
@ -848,6 +850,7 @@ class RepositoryTest {
FEED_URL,
SPOUT,
TAGS,
FILTER,
)
}
@ -857,6 +860,7 @@ class RepositoryTest {
any(),
any(),
any(),
any(),
)
}
assertSame(true, response)
@ -864,7 +868,7 @@ class RepositoryTest {
@Test
fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(false)
initializeRepository()
@ -876,6 +880,7 @@ class RepositoryTest {
FEED_URL,
SPOUT,
TAGS,
FILTER,
)
}
@ -885,6 +890,7 @@ class RepositoryTest {
any(),
any(),
any(),
any(),
)
}
assertSame(false, response)
@ -892,7 +898,7 @@ class RepositoryTest {
@Test
fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any()) } returns
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(true)
initializeRepository(false)
@ -904,6 +910,7 @@ class RepositoryTest {
FEED_URL,
SPOUT,
TAGS,
FILTER,
)
}
@ -913,6 +920,7 @@ class RepositoryTest {
any(),
any(),
any(),
any(),
)
}
assertSame(false, response)

View File

@ -102,6 +102,7 @@ class SelfossModel {
override var error: String? = null,
override var icon: String? = null,
var params: SourceParams? = null,
var filter: String? = null,
) : Source
@Serializable

View File

@ -368,6 +368,7 @@ class Repository(
url: String,
spout: String,
tags: String,
filter: String,
): Boolean {
var response = false
if (connectivityService.isNetworkAvailable()) {
@ -377,6 +378,7 @@ class Repository(
url,
spout,
tags,
filter,
).isSuccess == true
}
@ -389,10 +391,11 @@ class Repository(
url: String,
spout: String,
tags: String,
filter: String,
): Boolean {
var response = false
if (connectivityService.isNetworkAvailable()) {
response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true
response = api.updateSourceForVersion(id, title, url, spout, tags, filter).isSuccess == true
}
return response

View File

@ -638,17 +638,59 @@ class SelfossApi(
url: String,
spout: String,
tags: String,
filter: String,
): SuccessResponse =
maybeResponse(
if (appSettingsService.getApiVersion() > 1) {
createSource("tags[]", title, url, spout, tags)
createSource(title, url, spout, tags, filter)
} else {
createSource("tags", title, url, spout, tags)
createSourceOld(title, url, spout, tags)
},
)
private suspend fun createSource(
tagsParamName: String,
title: String,
url: String,
spout: String,
tags: String,
filter: String,
): HttpResponse? =
client.tryToSubmitForm(
url = url("/source"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append("filter", filter)
append("tags[]", tags)
},
block = {
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun createSourceOld(
title: String,
url: String,
spout: String,
@ -665,7 +707,7 @@ class SelfossApi(
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
append("tags", tags)
},
block = {
if (appSettingsService
@ -694,18 +736,61 @@ class SelfossApi(
url: String,
spout: String,
tags: String,
filter: String,
): SuccessResponse =
maybeResponse(
if (appSettingsService.getApiVersion() > 1) {
updateSource(id, "tags[]", title, url, spout, tags)
updateSource(id, title, url, spout, tags, filter)
} else {
updateSource(id, "tags", title, url, spout, tags)
updateSourceOld(id, title, url, spout, tags)
},
)
private suspend fun updateSource(
id: Int,
tagsParamName: String,
title: String,
url: String,
spout: String,
tags: String,
filter: String,
): HttpResponse? =
client.tryToSubmitForm(
url = url("/source/$id"),
formParameters =
Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title)
append("url", url)
append("spout", spout)
append("tags[]", tags)
append("filter", filter)
},
block = {
if (appSettingsService
.getBasicUserName()
.isNotEmpty() &&
appSettingsService.getBasicPassword().isNotEmpty()
) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword(),
),
),
)
}
}
},
)
private suspend fun updateSourceOld(
id: Int,
title: String,
url: String,
spout: String,
@ -722,7 +807,7 @@ class SelfossApi(
append("title", title)
append("url", url)
append("spout", spout)
append(tagsParamName, tags)
append("tags", tags)
},
block = {
if (appSettingsService