Show feeds as sub items of folders in FeedTab
This commit is contained in:
parent
6b50d40800
commit
5cd7ead78f
@ -15,7 +15,7 @@ val composeAppModule = module {
|
|||||||
|
|
||||||
viewModel { TimelineViewModel(get(), get()) }
|
viewModel { TimelineViewModel(get(), get()) }
|
||||||
|
|
||||||
viewModel { FeedViewModel(get()) }
|
viewModel { FeedViewModel(get(), get()) }
|
||||||
|
|
||||||
viewModel { AccountSelectionViewModel(get()) }
|
viewModel { AccountSelectionViewModel(get()) }
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.readrops.app.compose.feeds
|
package com.readrops.app.compose.feeds
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -25,10 +26,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import cafe.adriel.voyager.navigator.tab.Tab
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.readrops.app.compose.R
|
||||||
|
import com.readrops.app.compose.util.components.Placeholder
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@ -58,7 +62,9 @@ object FeedTab : Tab {
|
|||||||
FeedModalBottomSheet(
|
FeedModalBottomSheet(
|
||||||
feed = selectedFeed!!,
|
feed = selectedFeed!!,
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { showBottomSheet = false },
|
||||||
onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) },
|
onOpen = {
|
||||||
|
Log.d("TAG", "Content: ")
|
||||||
|
uriHandler.openUri(selectedFeed!!.siteUrl!!) },
|
||||||
onModify = { },
|
onModify = { },
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
)
|
)
|
||||||
@ -100,20 +106,22 @@ object FeedTab : Tab {
|
|||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is FeedsState.LoadedState -> {
|
is FeedsState.LoadedState -> {
|
||||||
val feeds = (state as FeedsState.LoadedState).feeds
|
val foldersAndFeeds = (state as FeedsState.LoadedState).foldersAndFeeds
|
||||||
|
|
||||||
if (feeds.isNotEmpty()) {
|
if (foldersAndFeeds.isNotEmpty()) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(
|
items(
|
||||||
items = feeds
|
items = foldersAndFeeds.toList()
|
||||||
) { feed ->
|
) { folderWithFeeds ->
|
||||||
FeedItem(
|
if (folderWithFeeds.first != null) {
|
||||||
feed = feed,
|
FolderExpandableItem(
|
||||||
onClick = {
|
folder = folderWithFeeds.first!!,
|
||||||
|
feeds = folderWithFeeds.second,
|
||||||
|
onFeedClick = { feed ->
|
||||||
selectedFeed = feed
|
selectedFeed = feed
|
||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onFeedLongClick = { feed ->
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
uriHandler.openUri(feed.siteUrl!!)
|
uriHandler.openUri(feed.siteUrl!!)
|
||||||
}
|
}
|
||||||
@ -121,6 +129,12 @@ object FeedTab : Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Placeholder(
|
||||||
|
text = "No feed",
|
||||||
|
painter = painterResource(R.drawable.ic_rss_feed_grey)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is FeedsState.ErrorState -> {
|
is FeedsState.ErrorState -> {
|
||||||
|
@ -2,16 +2,21 @@ package com.readrops.app.compose.feeds
|
|||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.readrops.app.compose.base.TabViewModel
|
import com.readrops.app.compose.base.TabViewModel
|
||||||
|
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||||
import com.readrops.db.Database
|
import com.readrops.db.Database
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
|
import com.readrops.db.entities.Folder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FeedViewModel(
|
class FeedViewModel(
|
||||||
private val database: Database,
|
database: Database,
|
||||||
|
private val getFoldersWithFeeds: GetFoldersWithFeeds
|
||||||
) : TabViewModel(database) {
|
) : TabViewModel(database) {
|
||||||
|
|
||||||
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
|
private val _feedsState = MutableStateFlow<FeedsState>(FeedsState.InitialState)
|
||||||
@ -19,7 +24,10 @@ class FeedViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(context = Dispatchers.IO) {
|
viewModelScope.launch(context = Dispatchers.IO) {
|
||||||
database.newFeedDao().selectFeeds()
|
accountEvent.consumeAsFlow()
|
||||||
|
.flatMapConcat { account ->
|
||||||
|
getFoldersWithFeeds.get(account.id)
|
||||||
|
}
|
||||||
.catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) }
|
.catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) }
|
||||||
.collect { _feedsState.value = FeedsState.LoadedState(it) }
|
.collect { _feedsState.value = FeedsState.LoadedState(it) }
|
||||||
}
|
}
|
||||||
@ -35,5 +43,5 @@ class FeedViewModel(
|
|||||||
sealed class FeedsState {
|
sealed class FeedsState {
|
||||||
object InitialState : FeedsState()
|
object InitialState : FeedsState()
|
||||||
data class ErrorState(val exception: Exception) : FeedsState()
|
data class ErrorState(val exception: Exception) : FeedsState()
|
||||||
data class LoadedState(val feeds: List<Feed>) : FeedsState()
|
data class LoadedState(val foldersAndFeeds: Map<Folder?, List<Feed>>) : FeedsState()
|
||||||
}
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
package com.readrops.app.compose.feeds
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import com.readrops.app.compose.R
|
||||||
|
import com.readrops.app.compose.util.theme.MediumSpacer
|
||||||
|
import com.readrops.app.compose.util.theme.spacing
|
||||||
|
import com.readrops.db.entities.Feed
|
||||||
|
import com.readrops.db.entities.Folder
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FolderExpandableItem(
|
||||||
|
folder: Folder,
|
||||||
|
feeds: List<Feed>,
|
||||||
|
onFeedClick: (Feed) -> Unit,
|
||||||
|
onFeedLongClick: (Feed) -> Unit,
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
val rotationState by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 180f else 0f,
|
||||||
|
label = "folder item arrow rotation"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 300,
|
||||||
|
easing = LinearOutSlowInEasing,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { isExpanded = isExpanded.not() }
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.spacing.shortSpacing,
|
||||||
|
vertical = MaterialTheme.spacing.veryShortSpacing,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_folder_grey),
|
||||||
|
contentDescription = folder.name
|
||||||
|
)
|
||||||
|
|
||||||
|
MediumSpacer()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = folder.name!!,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.rotate(rotationState),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
if (isExpanded) {
|
||||||
|
for (feed in feeds) {
|
||||||
|
FeedItem(
|
||||||
|
feed = feed,
|
||||||
|
onClick = { onFeedClick(feed) },
|
||||||
|
onLongClick = { onFeedLongClick(feed) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ class GetFoldersWithFeeds(
|
|||||||
id = it.feedId,
|
id = it.feedId,
|
||||||
name = it.feedName,
|
name = it.feedName,
|
||||||
iconUrl = it.feedIcon,
|
iconUrl = it.feedIcon,
|
||||||
|
siteUrl = it.feedUrl,
|
||||||
unreadCount = it.unreadCount
|
unreadCount = it.unreadCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
abstract class NewFolderDao : NewBaseDao<Folder> {
|
abstract class NewFolderDao : NewBaseDao<Folder> {
|
||||||
|
|
||||||
@Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " +
|
@Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " +
|
||||||
"Feed.icon_url As feedIcon, count(*) As unreadCount From Folder Left Join Feed " +
|
"Feed.icon_url As feedIcon, Feed.siteUrl as feedUrl, count(*) As unreadCount From Folder Left Join Feed " +
|
||||||
"On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " +
|
"On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " +
|
||||||
"Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " +
|
"Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " +
|
||||||
"And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id")
|
"And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id")
|
||||||
|
@ -18,6 +18,7 @@ data class FolderWithFeed(
|
|||||||
val feedId: Int = 0,
|
val feedId: Int = 0,
|
||||||
val feedName: String? = null,
|
val feedName: String? = null,
|
||||||
val feedIcon: String? = null,
|
val feedIcon: String? = null,
|
||||||
|
val feedUrl: String? = null,
|
||||||
val unreadCount: Int = 0
|
val unreadCount: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user