Display folders and feeds in Drawer

This commit is contained in:
Shinokuni 2023-08-20 17:31:35 +02:00
parent cffc102b20
commit db2b089533
7 changed files with 331 additions and 14 deletions

View File

@ -4,6 +4,7 @@ import com.readrops.app.compose.account.AccountViewModel
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
import com.readrops.app.compose.feeds.FeedViewModel
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineViewModel
import com.readrops.db.entities.account.Account
@ -12,7 +13,7 @@ import org.koin.dsl.module
val composeAppModule = module {
viewModel { TimelineViewModel(get()) }
viewModel { TimelineViewModel(get(), get()) }
viewModel { FeedViewModel(get()) }
@ -20,6 +21,8 @@ val composeAppModule = module {
viewModel { AccountViewModel(get()) }
single { GetFoldersWithFeeds(get()) }
// repositories
factory<BaseRepository> { (account: Account) ->

View File

@ -39,6 +39,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.readrops.app.compose.R
import com.readrops.app.compose.item.ItemScreen
import com.readrops.app.compose.timelime.drawer.TimelineDrawer
import com.readrops.app.compose.util.theme.spacing
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
@ -78,6 +79,7 @@ object TimelineTab : Tab {
}
)
val closeDrawer = { scope.launch { drawerState.close() } }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
@ -85,9 +87,15 @@ object TimelineTab : Tab {
viewModel = viewModel,
onClickDefaultItem = {
viewModel.updateDrawerDefaultItem(it)
scope.launch {
drawerState.close()
}
closeDrawer()
},
onFolderClick = {
viewModel.updateDrawerFolderSelection(it)
closeDrawer()
},
onFeedClick = {
viewModel.updateDrawerFeedSelection(it)
closeDrawer()
}
)
}

View File

@ -3,6 +3,8 @@ package com.readrops.app.compose.timelime
import androidx.compose.runtime.Immutable
import androidx.lifecycle.viewModelScope
import com.readrops.app.compose.base.TabViewModel
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.app.compose.timelime.drawer.DrawerDefaultItemsSelection
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
@ -11,6 +13,8 @@ import com.readrops.db.queries.ItemsQueryBuilder
import com.readrops.db.queries.QueryFilters
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
@ -21,6 +25,7 @@ import kotlinx.coroutines.launch
class TimelineViewModel(
private val database: Database,
private val getFoldersWithFeeds: GetFoldersWithFeeds,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : TabViewModel(database) {
@ -38,11 +43,23 @@ class TimelineViewModel(
accountEvent.consumeAsFlow().collectLatest { account ->
val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = account.id))
database.newItemDao().selectAll(query)
.catch { _timelineState.value = TimelineState.Error(Exception(it)) }
.collect {
_timelineState.value = TimelineState.Loaded(it)
val items = async {
database.newItemDao().selectAll(query)
.catch { _timelineState.value = TimelineState.Error(Exception(it)) }
.collect {
_timelineState.value = TimelineState.Loaded(it)
}
}
val drawer = async {
_drawerState.update {
it.copy(
foldersAndFeeds = getFoldersWithFeeds.get(account.id)
)
}
}
awaitAll(items, drawer)
}
}
}
@ -59,7 +76,21 @@ class TimelineViewModel(
}
fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) {
_drawerState.update { it.copy(selection = selection) }
_drawerState.update {
it.copy(
selection = selection,
selectedFolderId = 0,
selectedFeedId = 0,
)
}
}
fun updateDrawerFolderSelection(folderId: Int) {
_drawerState.update { it.copy(selectedFolderId = folderId, selectedFeedId = 0) }
}
fun updateDrawerFeedSelection(feedId: Int) {
_drawerState.update { it.copy(selectedFeedId = feedId, selectedFolderId = 0) }
}
}
@ -76,8 +107,8 @@ sealed class TimelineState {
@Immutable
data class DrawerState(
val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES,
val folderSelection: Int = 0,
val feedSelection: Int = 0,
val selectedFolderId: Int = 0,
val selectedFeedId: Int = 0,
val foldersAndFeeds: Map<Folder?, List<Feed>> = emptyMap()
)

View File

@ -0,0 +1,59 @@
package com.readrops.app.compose.timelime.drawer
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.readrops.app.compose.util.theme.DrawerSpacing
@Composable
fun DrawerFeedItem(
label: @Composable () -> Unit,
icon: @Composable () -> Unit,
badge: @Composable () -> Unit,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val colors = NavigationDrawerItemDefaults.colors()
Surface(
selected = selected,
onClick = onClick,
color = colors.containerColor(selected = selected).value,
shape = CircleShape,
modifier = modifier
.height(36.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 16.dp, end = 24.dp)
) {
val iconColor = colors.iconColor(selected).value
CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
DrawerSpacing()
Box(Modifier.weight(1f)) {
val labelColor = colors.textColor(selected).value
CompositionLocalProvider(LocalContentColor provides labelColor, content = label)
}
DrawerSpacing()
val badgeColor = colors.badgeColor(selected).value
CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge)
}
}
}

View File

@ -0,0 +1,140 @@
package com.readrops.app.compose.timelime.drawer
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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 androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.readrops.app.compose.R
import com.readrops.app.compose.util.theme.DrawerSpacing
import com.readrops.db.entities.Feed
@Composable
fun DrawerFolderItem(
label: @Composable () -> Unit,
icon: @Composable () -> Unit,
badge: @Composable () -> Unit,
selected: Boolean,
onClick: () -> Unit,
feeds: List<Feed>,
selectedFeed: Int,
onFeedClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val colors = NavigationDrawerItemDefaults.colors()
var isExpanded by remember { mutableStateOf(false) }
val rotationState by animateFloatAsState(
targetValue = if (isExpanded) 180f else 0f,
label = "drawer item arrow rotation"
)
Column(
modifier = Modifier.animateContentSize(
animationSpec = tween(
durationMillis = 300,
easing = LinearOutSlowInEasing,
)
)
) {
Surface(
selected = selected,
onClick = onClick,
color = colors.containerColor(selected = selected).value,
shape = CircleShape,
modifier = modifier
.height(56.dp)
.fillMaxWidth()
.animateContentSize(
animationSpec = tween(
durationMillis = 300,
easing = LinearOutSlowInEasing,
)
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 16.dp, end = 24.dp)
) {
val iconColor = colors.iconColor(selected).value
CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)
DrawerSpacing()
Box(Modifier.weight(1f)) {
val labelColor = colors.textColor(selected).value
CompositionLocalProvider(LocalContentColor provides labelColor, content = label)
}
DrawerSpacing()
val badgeColor = colors.badgeColor(selected).value
CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge)
DrawerSpacing()
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null,
modifier = Modifier
.clickable { isExpanded = isExpanded.not() }
.rotate(rotationState),
)
}
}
if (isExpanded && feeds.isNotEmpty()) {
for (feed in feeds) {
DrawerFeedItem(
label = {
Text(
text = feed.name!!,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
icon = {
AsyncImage(
model = feed.iconUrl,
contentDescription = feed.name,
placeholder = painterResource(id = R.drawable.ic_folder_grey),
modifier = Modifier.size(24.dp)
)
},
badge = { Text(feed.unreadCount.toString()) },
selected = feed.id == selectedFeed,
onClick = { onFeedClick(feed.id) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
}

View File

@ -1,8 +1,12 @@
package com.readrops.app.compose.timelime
package com.readrops.app.compose.timelime.drawer
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Divider
@ -17,8 +21,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.readrops.app.compose.R
import com.readrops.app.compose.timelime.TimelineViewModel
import com.readrops.app.compose.util.theme.spacing
enum class DrawerDefaultItemsSelection {
@ -32,10 +39,17 @@ enum class DrawerDefaultItemsSelection {
fun TimelineDrawer(
viewModel: TimelineViewModel,
onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit,
onFolderClick: (Int) -> Unit,
onFeedClick: (Int) -> Unit,
) {
val state by viewModel.drawerState.collectAsState()
val scrollState = rememberScrollState()
ModalDrawerSheet {
ModalDrawerSheet(
modifier = Modifier
.fillMaxHeight()
.verticalScroll(scrollState)
) {
Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing))
DrawerDefaultItems(
@ -44,6 +58,65 @@ fun TimelineDrawer(
)
DrawerDivider()
Column {
for (folderEntry in state.foldersAndFeeds) {
val folder = folderEntry.key
if (folder != null) {
DrawerFolderItem(
label = {
Text(
text = folder.name!!,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
icon = {
Icon(
painterResource(id = R.drawable.ic_folder_grey),
contentDescription = null
)
},
badge = {
Text(folderEntry.value.sumOf { it.unreadCount }.toString())
},
selected = state.selectedFolderId == folder.id,
onClick = { onFolderClick(folder.id) },
feeds = folderEntry.value,
selectedFeed = state.selectedFeedId,
onFeedClick = { onFeedClick(it) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
} else {
val feeds = folderEntry.value
for (feed in feeds) {
DrawerFeedItem(
label = {
Text(
text = feed.name!!,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
icon = {
AsyncImage(
model = feed.iconUrl,
contentDescription = feed.name,
placeholder = painterResource(id = R.drawable.ic_folder_grey),
modifier = Modifier.size(24.dp)
)
},
badge = { Text(feed.unreadCount.toString()) },
selected = feed.id == state.selectedFeedId,
onClick = { onFeedClick(feed.id) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
}
}
}

View File

@ -39,4 +39,7 @@ fun MediumSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.mediumSpacing))
fun LargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.largeSpacing))
@Composable
fun VeryLargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryLargeSpacing))
fun VeryLargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryLargeSpacing))
@Composable
fun DrawerSpacing() = Spacer(Modifier.size(MaterialTheme.spacing.drawerSpacing))