Restore Order By Item.id in TimelineTab main query

This commit is contained in:
Shinokuni 2024-09-03 16:49:43 +02:00
parent e6c880a79f
commit cf9d307f00
9 changed files with 207 additions and 38 deletions

View File

@ -103,6 +103,8 @@ class LocalRSSRepository(
} }
} }
// sort by date
newItems.sort()
database.itemDao().insert(newItems) database.itemDao().insert(newItems)
.zip(newItems) .zip(newItems)
.forEach { (id, item) -> item.id = id.toInt() } .forEach { (id, item) -> item.id = id.toInt() }

View File

@ -2,41 +2,66 @@ package com.readrops.app.timelime
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.RichTooltip
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.readrops.app.R import com.readrops.app.R
import com.readrops.app.util.DefaultPreview
import com.readrops.app.util.theme.LargeSpacer import com.readrops.app.util.theme.LargeSpacer
import com.readrops.app.util.theme.MediumSpacer
import com.readrops.app.util.theme.ReadropsTheme
import com.readrops.app.util.theme.ShortSpacer import com.readrops.app.util.theme.ShortSpacer
import com.readrops.app.util.theme.spacing import com.readrops.app.util.theme.spacing
import com.readrops.db.filters.OrderField
import com.readrops.db.filters.OrderType import com.readrops.db.filters.OrderType
import com.readrops.db.filters.QueryFilters import com.readrops.db.filters.QueryFilters
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun FilterBottomSheet( fun FilterBottomSheet(
onSetShowReadItemsState: () -> Unit,
onSetSortTypeState: () -> Unit,
filters: QueryFilters, filters: QueryFilters,
onDismiss: () -> Unit, onSetShowReadItems: () -> Unit,
onSetOrderField: () -> Unit,
onSetOrderType: () -> Unit,
onDismiss: () -> Unit
) { ) {
val tooltipState = rememberTooltipState(isPersistent = true)
val coroutineScope = rememberCoroutineScope()
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss onDismissRequest = onDismiss,
//sheetState = rememberStandardBottomSheetState()
) { ) {
Column( Column(
modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing) modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing)
) { ) {
Text( Text(
text = stringResource(R.string.filters) text = stringResource(R.string.filters),
style = MaterialTheme.typography.titleMedium
) )
ShortSpacer() ShortSpacer()
@ -45,11 +70,11 @@ fun FilterBottomSheet(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onSetShowReadItemsState) .clickable(onClick = onSetShowReadItems)
) { ) {
Checkbox( Checkbox(
checked = filters.showReadItems, checked = filters.showReadItems,
onCheckedChange = { onSetShowReadItemsState() } onCheckedChange = { onSetShowReadItems() }
) )
ShortSpacer() ShortSpacer()
@ -61,25 +86,102 @@ fun FilterBottomSheet(
ShortSpacer() ShortSpacer()
Row( Column(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.width(IntrinsicSize.Max)
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onSetSortTypeState)
) { ) {
Checkbox( Row(
checked = filters.orderType == OrderType.ASC, verticalAlignment = Alignment.CenterVertically
onCheckedChange = { onSetSortTypeState() } ) {
Text(text = stringResource(R.string.order_by))
TooltipBox(
positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
tooltip = {
RichTooltip(
title = { Text(text = stringResource(id = R.string.order_by)) }
) {
Text(
text = stringResource(R.string.order_field_tooltip),
) )
}
},
state = tooltipState
) {
IconButton(
onClick = {
coroutineScope.launch {
tooltipState.show()
}
}
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null
)
}
}
}
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth()
) {
SegmentedButton(
selected = filters.orderField == OrderField.ID,
onClick = onSetOrderField,
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
) {
Text(text = stringResource(R.string.identifier))
}
SegmentedButton(
selected = filters.orderField == OrderField.DATE,
onClick = onSetOrderField,
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
) {
Text(text = stringResource(R.string.date))
}
}
MediumSpacer()
Text(text = stringResource(R.string.with_direction))
ShortSpacer() ShortSpacer()
Text( SingleChoiceSegmentedButtonRow {
text = stringResource(R.string.show_oldest_articles_first) SegmentedButton(
) selected = filters.orderType == OrderType.ASC,
onClick = onSetOrderType,
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
) {
Text(text = stringResource(R.string.ascending))
}
SegmentedButton(
selected = filters.orderType == OrderType.DESC,
onClick = onSetOrderType,
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
) {
Text(text = stringResource(R.string.descending))
}
}
} }
LargeSpacer() LargeSpacer()
} }
} }
} }
@DefaultPreview
@Composable
private fun FilterBottomSheetPreview() {
ReadropsTheme {
FilterBottomSheet(
onSetShowReadItems = {},
onSetOrderType = {},
onSetOrderField = {},
filters = QueryFilters(),
onDismiss = {}
)
}
}

View File

@ -21,6 +21,7 @@ import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item import com.readrops.db.entities.Item
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.OrderField
import com.readrops.db.filters.OrderType import com.readrops.db.filters.OrderType
import com.readrops.db.filters.QueryFilters import com.readrops.db.filters.QueryFilters
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
@ -374,6 +375,18 @@ class TimelineScreenModel(
} }
} }
fun setOrderFieldState(orderField: OrderField) {
_timelineState.update {
it.copy(
filters = updateFilters {
it.filters.copy(
orderField = orderField
)
}
)
}
}
fun setOrderTypeState(orderType: OrderType) { fun setOrderTypeState(orderType: OrderType) {
_timelineState.update { _timelineState.update {
it.copy( it.copy(

View File

@ -68,6 +68,7 @@ import com.readrops.app.util.components.RefreshScreen
import com.readrops.app.util.components.dialog.TwoChoicesDialog import com.readrops.app.util.components.dialog.TwoChoicesDialog
import com.readrops.app.util.theme.spacing import com.readrops.app.util.theme.spacing
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.OrderField
import com.readrops.db.filters.OrderType import com.readrops.db.filters.OrderType
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.pojo.ItemWithFeed
@ -208,10 +209,19 @@ object TimelineTab : Tab {
is DialogState.FilterSheet -> { is DialogState.FilterSheet -> {
FilterBottomSheet( FilterBottomSheet(
filters = state.filters, filters = state.filters,
onSetShowReadItemsState = { onSetShowReadItems = {
screenModel.setShowReadItemsState(!state.filters.showReadItems) screenModel.setShowReadItemsState(!state.filters.showReadItems)
}, },
onSetSortTypeState = { onSetOrderField = {
screenModel.setOrderFieldState(
if (state.filters.orderField == OrderField.ID) {
OrderField.DATE
} else {
OrderField.ID
}
)
},
onSetOrderType = {
screenModel.setOrderTypeState( screenModel.setOrderTypeState(
if (state.filters.orderType == OrderType.DESC) { if (state.filters.orderType == OrderType.DESC) {
OrderType.ASC OrderType.ASC

View File

@ -170,4 +170,11 @@
<string name="downloaded_file">Fichier téléchargé !</string> <string name="downloaded_file">Fichier téléchargé !</string>
<string name="provide_full_url">Merci de fournir l\'URL entière de l\'API</string> <string name="provide_full_url">Merci de fournir l\'URL entière de l\'API</string>
<string name="provide_root_url">Merci de fournir l\'URL racine du service</string> <string name="provide_root_url">Merci de fournir l\'URL racine du service</string>
<string name="order_field_tooltip">Les flux RSS contiennent toujours d\'anciens articles qu\'il est peu probable de voir apparaître dans votre timeline si vous la classez par date. Le classement par identifiant d\'article vous permet d\'afficher tous les nouveaux articles insérés, quelle que soit leur date.</string>
<string name="date">Date</string>
<string name="identifier">Identifiant</string>
<string name="ascending">Ascendant</string>
<string name="descending">Descendant</string>
<string name="order_by">Ordonner par</string>
<string name="with_direction">Avec comme direction</string>
</resources> </resources>

View File

@ -180,4 +180,11 @@
<string name="downloaded_file">Downloaded file!</string> <string name="downloaded_file">Downloaded file!</string>
<string name="provide_full_url">Please provide the full API URL</string> <string name="provide_full_url">Please provide the full API URL</string>
<string name="provide_root_url">Please provide the service root URL</string> <string name="provide_root_url">Please provide the service root URL</string>
<string name="order_field_tooltip">RSS feeds always contain old articles it is unlikely you will see in your timeline if you order it by date. Ordering by article identifier lets you show all new inserted articles whatever date they might have.</string>
<string name="date">Date</string>
<string name="identifier">Identifier</string>
<string name="ascending">Ascending</string>
<string name="descending">Descending</string>
<string name="order_by">Order by</string>
<string name="with_direction">With direction</string>
</resources> </resources>

View File

@ -4,11 +4,12 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.readrops.db.filters.OrderType
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.OrderField
import com.readrops.db.filters.OrderType
import com.readrops.db.filters.QueryFilters
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.ItemsQueryBuilder
import com.readrops.db.filters.QueryFilters
import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import org.junit.After import org.junit.After
@ -42,7 +43,7 @@ class ItemsQueryBuilderTest {
with(query.sql) { with(query.sql) {
assertTrue(contains("Feed.account_id = 1")) assertTrue(contains("Feed.account_id = 1"))
assertTrue(contains("pub_date DESC")) assertTrue(contains("Item.id DESC"))
assertFalse(contains("read = 0 And")) assertFalse(contains("read = 0 And"))
} }
@ -51,8 +52,11 @@ class ItemsQueryBuilderTest {
@Test @Test
fun feedFilterCaseTest() { fun feedFilterCaseTest() {
val queryFilters = QueryFilters(accountId = 1, subFilter = SubFilter.FEED, val queryFilters = QueryFilters(
feedId = 15) accountId = 1,
subFilter = SubFilter.FEED,
feedId = 15
)
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters) val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
database.query(query) database.query(query)
@ -82,8 +86,12 @@ class ItemsQueryBuilderTest {
@Test @Test
fun oldestSortCaseTest() { fun oldestSortCaseTest() {
val queryFilters = QueryFilters(accountId = 1, orderType = OrderType.ASC, val queryFilters = QueryFilters(
showReadItems = false) accountId = 1,
orderType = OrderType.ASC,
orderField = OrderField.DATE,
showReadItems = false
)
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters) val query = ItemsQueryBuilder.buildItemsQuery(queryFilters)
database.query(query) database.query(query)
@ -92,12 +100,15 @@ class ItemsQueryBuilderTest {
assertTrue(contains("read = 0")) assertTrue(contains("read = 0"))
assertTrue(contains("pub_date ASC")) assertTrue(contains("pub_date ASC"))
} }
} }
@Test @Test
fun separateStateTest() { fun separateStateTest() {
val queryFilters = QueryFilters(accountId = 1, showReadItems = false, mainFilter = MainFilter.STARS) val queryFilters = QueryFilters(
accountId = 1,
showReadItems = false,
mainFilter = MainFilter.STARS
)
val query = ItemsQueryBuilder.buildItemsQuery(queryFilters, true) val query = ItemsQueryBuilder.buildItemsQuery(queryFilters, true)
database.query(query) database.query(query)

View File

@ -12,6 +12,11 @@ enum class SubFilter {
ALL ALL
} }
enum class OrderField {
DATE,
ID
}
enum class OrderType { enum class OrderType {
DESC, DESC,
ASC ASC
@ -24,5 +29,6 @@ data class QueryFilters(
val accountId: Int = 0, val accountId: Int = 0,
val mainFilter: MainFilter = MainFilter.ALL, val mainFilter: MainFilter = MainFilter.ALL,
val subFilter: SubFilter = SubFilter.ALL, val subFilter: SubFilter = SubFilter.ALL,
val orderField: OrderField = OrderField.ID,
val orderType: OrderType = OrderType.DESC, val orderType: OrderType = OrderType.DESC,
) )

View File

@ -3,6 +3,7 @@ package com.readrops.db.queries
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQueryBuilder import androidx.sqlite.db.SupportSQLiteQueryBuilder
import com.readrops.db.filters.MainFilter import com.readrops.db.filters.MainFilter
import com.readrops.db.filters.OrderField
import com.readrops.db.filters.OrderType import com.readrops.db.filters.OrderType
import com.readrops.db.filters.QueryFilters import com.readrops.db.filters.QueryFilters
import com.readrops.db.filters.SubFilter import com.readrops.db.filters.SubFilter
@ -40,10 +41,6 @@ object ItemsQueryBuilder {
private const val SEPARATE_STATE_JOIN = private const val SEPARATE_STATE_JOIN =
"LEFT JOIN ItemState On Item.remote_id = ItemState.remote_id" "LEFT JOIN ItemState On Item.remote_id = ItemState.remote_id"
private const val ORDER_BY_DESC = "pub_date DESC"
private const val ORDER_BY_ASC = "pub_date ASC"
fun buildItemsQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery = fun buildItemsQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery =
buildQuery(queryFilters, separateState) buildQuery(queryFilters, separateState)
@ -55,13 +52,14 @@ object ItemsQueryBuilder {
if (accountId == 0) if (accountId == 0)
throw IllegalArgumentException("AccountId must be greater than 0") throw IllegalArgumentException("AccountId must be greater than 0")
if (queryFilters.subFilter == SubFilter.FEED && feedId == 0) if (subFilter == SubFilter.FEED && feedId == 0)
throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER") throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER")
val columns = if (separateState) val columns = if (separateState) {
COLUMNS.plus(SEPARATE_STATE_COLUMNS) COLUMNS.plus(SEPARATE_STATE_COLUMNS)
else } else {
COLUMNS.plus(OTHER_COLUMNS) COLUMNS.plus(OTHER_COLUMNS)
}
val selectAllJoin = val selectAllJoin =
if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN
@ -69,7 +67,7 @@ object ItemsQueryBuilder {
SupportSQLiteQueryBuilder.builder(selectAllJoin).run { SupportSQLiteQueryBuilder.builder(selectAllJoin).run {
columns(columns) columns(columns)
selection(buildWhereClause(this@with, separateState), null) selection(buildWhereClause(this@with, separateState), null)
orderBy(if (orderType == OrderType.DESC) this@ItemsQueryBuilder.ORDER_BY_DESC else this@ItemsQueryBuilder.ORDER_BY_ASC) orderBy(buildOrderByClause(orderField, orderType))
create() create()
} }
@ -108,5 +106,18 @@ object ItemsQueryBuilder {
toString() toString()
} }
private fun buildOrderByClause(orderField: OrderField, orderType: OrderType): String {
return buildString {
when (orderField) {
OrderField.ID -> append("Item.id ")
else -> append("pub_date ")
}
when (orderType) {
OrderType.DESC -> append("DESC")
else -> append("ASC")
}
}
}
} }