diff --git a/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt b/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt index 032de40f..756a5e23 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt +++ b/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt @@ -103,6 +103,8 @@ class LocalRSSRepository( } } + // sort by date + newItems.sort() database.itemDao().insert(newItems) .zip(newItems) .forEach { (id, item) -> item.id = id.toInt() } diff --git a/app/src/main/java/com/readrops/app/timelime/FilterBottomSheet.kt b/app/src/main/java/com/readrops/app/timelime/FilterBottomSheet.kt index 48f5b7e5..a5b0cb05 100644 --- a/app/src/main/java/com/readrops/app/timelime/FilterBottomSheet.kt +++ b/app/src/main/java/com/readrops/app/timelime/FilterBottomSheet.kt @@ -2,41 +2,66 @@ package com.readrops.app.timelime import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth 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.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme 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.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.readrops.app.R +import com.readrops.app.util.DefaultPreview 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.spacing +import com.readrops.db.filters.OrderField import com.readrops.db.filters.OrderType import com.readrops.db.filters.QueryFilters +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterBottomSheet( - onSetShowReadItemsState: () -> Unit, - onSetSortTypeState: () -> Unit, filters: QueryFilters, - onDismiss: () -> Unit, + onSetShowReadItems: () -> Unit, + onSetOrderField: () -> Unit, + onSetOrderType: () -> Unit, + onDismiss: () -> Unit ) { + val tooltipState = rememberTooltipState(isPersistent = true) + val coroutineScope = rememberCoroutineScope() + ModalBottomSheet( - onDismissRequest = onDismiss + onDismissRequest = onDismiss, + //sheetState = rememberStandardBottomSheetState() ) { Column( modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing) ) { Text( - text = stringResource(R.string.filters) + text = stringResource(R.string.filters), + style = MaterialTheme.typography.titleMedium ) ShortSpacer() @@ -45,11 +70,11 @@ fun FilterBottomSheet( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable(onClick = onSetShowReadItemsState) + .clickable(onClick = onSetShowReadItems) ) { Checkbox( checked = filters.showReadItems, - onCheckedChange = { onSetShowReadItemsState() } + onCheckedChange = { onSetShowReadItems() } ) ShortSpacer() @@ -61,25 +86,102 @@ fun FilterBottomSheet( ShortSpacer() - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onSetSortTypeState) + Column( + modifier = Modifier.width(IntrinsicSize.Max) ) { - Checkbox( - checked = filters.orderType == OrderType.ASC, - onCheckedChange = { onSetSortTypeState() } - ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + 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() - Text( - text = stringResource(R.string.show_oldest_articles_first) - ) + SingleChoiceSegmentedButtonRow { + 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() } } +} + +@DefaultPreview +@Composable +private fun FilterBottomSheetPreview() { + ReadropsTheme { + FilterBottomSheet( + onSetShowReadItems = {}, + onSetOrderType = {}, + onSetOrderField = {}, + filters = QueryFilters(), + onDismiss = {} + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt index 7c79e049..25917a78 100644 --- a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt +++ b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt @@ -21,6 +21,7 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import com.readrops.db.entities.Item 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 @@ -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) { _timelineState.update { it.copy( diff --git a/app/src/main/java/com/readrops/app/timelime/TimelineTab.kt b/app/src/main/java/com/readrops/app/timelime/TimelineTab.kt index cdeb2365..2574029d 100644 --- a/app/src/main/java/com/readrops/app/timelime/TimelineTab.kt +++ b/app/src/main/java/com/readrops/app/timelime/TimelineTab.kt @@ -68,6 +68,7 @@ import com.readrops.app.util.components.RefreshScreen import com.readrops.app.util.components.dialog.TwoChoicesDialog import com.readrops.app.util.theme.spacing import com.readrops.db.filters.MainFilter +import com.readrops.db.filters.OrderField import com.readrops.db.filters.OrderType import com.readrops.db.filters.SubFilter import com.readrops.db.pojo.ItemWithFeed @@ -208,10 +209,19 @@ object TimelineTab : Tab { is DialogState.FilterSheet -> { FilterBottomSheet( filters = state.filters, - onSetShowReadItemsState = { + onSetShowReadItems = { screenModel.setShowReadItemsState(!state.filters.showReadItems) }, - onSetSortTypeState = { + onSetOrderField = { + screenModel.setOrderFieldState( + if (state.filters.orderField == OrderField.ID) { + OrderField.DATE + } else { + OrderField.ID + } + ) + }, + onSetOrderType = { screenModel.setOrderTypeState( if (state.filters.orderType == OrderType.DESC) { OrderType.ASC diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c775275b..341518d4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -170,4 +170,11 @@ Fichier téléchargé ! Merci de fournir l\'URL entière de l\'API Merci de fournir l\'URL racine du service + 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. + Date + Identifiant + Ascendant + Descendant + Ordonner par + Avec comme direction \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63743968..67e856c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -180,4 +180,11 @@ Downloaded file! Please provide the full API URL Please provide the service root URL + 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. + Date + Identifier + Ascending + Descending + Order by + With direction \ No newline at end of file diff --git a/db/src/androidTest/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/androidTest/java/com/readrops/db/ItemsQueryBuilderTest.kt index 6472b86a..544478fa 100644 --- a/db/src/androidTest/java/com/readrops/db/ItemsQueryBuilderTest.kt +++ b/db/src/androidTest/java/com/readrops/db/ItemsQueryBuilderTest.kt @@ -4,11 +4,12 @@ import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.readrops.db.filters.OrderType 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.queries.ItemsQueryBuilder -import com.readrops.db.filters.QueryFilters import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import org.junit.After @@ -42,7 +43,7 @@ class ItemsQueryBuilderTest { with(query.sql) { assertTrue(contains("Feed.account_id = 1")) - assertTrue(contains("pub_date DESC")) + assertTrue(contains("Item.id DESC")) assertFalse(contains("read = 0 And")) } @@ -51,8 +52,11 @@ class ItemsQueryBuilderTest { @Test fun feedFilterCaseTest() { - val queryFilters = QueryFilters(accountId = 1, subFilter = SubFilter.FEED, - feedId = 15) + val queryFilters = QueryFilters( + accountId = 1, + subFilter = SubFilter.FEED, + feedId = 15 + ) val query = ItemsQueryBuilder.buildItemsQuery(queryFilters) database.query(query) @@ -82,8 +86,12 @@ class ItemsQueryBuilderTest { @Test fun oldestSortCaseTest() { - val queryFilters = QueryFilters(accountId = 1, orderType = OrderType.ASC, - showReadItems = false) + val queryFilters = QueryFilters( + accountId = 1, + orderType = OrderType.ASC, + orderField = OrderField.DATE, + showReadItems = false + ) val query = ItemsQueryBuilder.buildItemsQuery(queryFilters) database.query(query) @@ -92,12 +100,15 @@ class ItemsQueryBuilderTest { assertTrue(contains("read = 0")) assertTrue(contains("pub_date ASC")) } - } @Test 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) database.query(query) diff --git a/db/src/main/java/com/readrops/db/filters/Filters.kt b/db/src/main/java/com/readrops/db/filters/Filters.kt index 99affbef..85541a4d 100644 --- a/db/src/main/java/com/readrops/db/filters/Filters.kt +++ b/db/src/main/java/com/readrops/db/filters/Filters.kt @@ -12,6 +12,11 @@ enum class SubFilter { ALL } +enum class OrderField { + DATE, + ID +} + enum class OrderType { DESC, ASC @@ -24,5 +29,6 @@ data class QueryFilters( val accountId: Int = 0, val mainFilter: MainFilter = MainFilter.ALL, val subFilter: SubFilter = SubFilter.ALL, + val orderField: OrderField = OrderField.ID, val orderType: OrderType = OrderType.DESC, ) \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index 8b401cab..785f210b 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -3,6 +3,7 @@ package com.readrops.db.queries import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQueryBuilder 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 @@ -40,10 +41,6 @@ object ItemsQueryBuilder { private const val SEPARATE_STATE_JOIN = "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 = buildQuery(queryFilters, separateState) @@ -55,13 +52,14 @@ object ItemsQueryBuilder { if (accountId == 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") - val columns = if (separateState) + val columns = if (separateState) { COLUMNS.plus(SEPARATE_STATE_COLUMNS) - else + } else { COLUMNS.plus(OTHER_COLUMNS) + } val selectAllJoin = if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN @@ -69,7 +67,7 @@ object ItemsQueryBuilder { SupportSQLiteQueryBuilder.builder(selectAllJoin).run { columns(columns) 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() } @@ -108,5 +106,18 @@ object ItemsQueryBuilder { 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") + } + } + } }