mirror of
https://github.com/readrops/Readrops.git
synced 2025-02-01 19:26:49 +01:00
Display folders and feeds in Drawer
This commit is contained in:
parent
cffc102b20
commit
db2b089533
@ -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) ->
|
||||
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
Loading…
x
Reference in New Issue
Block a user