mirror of https://github.com/tasks/tasks
New drawer
parent
7fd5647cb8
commit
b5748aa8e6
@ -0,0 +1,174 @@
|
|||||||
|
package com.todoroo.astrid.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.todoroo.astrid.api.CaldavFilter
|
||||||
|
import com.todoroo.astrid.api.CustomFilter
|
||||||
|
import com.todoroo.astrid.api.Filter
|
||||||
|
import com.todoroo.astrid.api.Filter.Companion.NO_COUNT
|
||||||
|
import com.todoroo.astrid.api.GtasksFilter
|
||||||
|
import com.todoroo.astrid.api.TagFilter
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.tasks.LocalBroadcastManager
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.Tasks.Companion.IS_GENERIC
|
||||||
|
import org.tasks.billing.Inventory
|
||||||
|
import org.tasks.compose.drawer.DrawerItem
|
||||||
|
import org.tasks.data.CaldavDao
|
||||||
|
import org.tasks.data.TaskDao
|
||||||
|
import org.tasks.filters.FilterProvider
|
||||||
|
import org.tasks.filters.NavigationDrawerSubheader
|
||||||
|
import org.tasks.filters.PlaceFilter
|
||||||
|
import org.tasks.preferences.DefaultFilterProvider
|
||||||
|
import org.tasks.preferences.Preferences
|
||||||
|
import org.tasks.themes.ColorProvider
|
||||||
|
import org.tasks.themes.CustomIcons
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
class MainActivityViewModel @Inject constructor(
|
||||||
|
private val defaultFilterProvider: DefaultFilterProvider,
|
||||||
|
private val filterProvider: FilterProvider,
|
||||||
|
private val taskDao: TaskDao,
|
||||||
|
private val localBroadcastManager: LocalBroadcastManager,
|
||||||
|
private val inventory: Inventory,
|
||||||
|
private val colorProvider: ColorProvider,
|
||||||
|
private val caldavDao: CaldavDao,
|
||||||
|
private val preferences: Preferences,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val begForMoney: Boolean = false,
|
||||||
|
val filter: Filter? = null,
|
||||||
|
val drawerOpen: Boolean = false,
|
||||||
|
val drawerItems: ImmutableList<DrawerItem> = persistentListOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(State())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val refreshReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
LocalBroadcastManager.REFRESH,
|
||||||
|
LocalBroadcastManager.REFRESH_LIST -> updateFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFilter(filter: Filter) {
|
||||||
|
_state.update { it.copy(filter = filter) }
|
||||||
|
defaultFilterProvider.lastViewedFilter = filter
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDrawerOpen(open: Boolean) {
|
||||||
|
_state.update { it.copy(drawerOpen = open) }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
|
||||||
|
updateFilters()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
localBroadcastManager.unregisterReceiver(refreshReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFilters() = viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
filterProvider
|
||||||
|
.drawerItems()
|
||||||
|
.map { item ->
|
||||||
|
when (item) {
|
||||||
|
is Filter ->
|
||||||
|
DrawerItem.Filter(
|
||||||
|
title = item.title ?: "",
|
||||||
|
icon = getIcon(item),
|
||||||
|
color = getColor(item),
|
||||||
|
count = item.count.takeIf { it != NO_COUNT } ?: try {
|
||||||
|
taskDao.count(item)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
0
|
||||||
|
},
|
||||||
|
shareCount = if (item is CaldavFilter) item.principals else 0,
|
||||||
|
type = { item },
|
||||||
|
)
|
||||||
|
is NavigationDrawerSubheader ->
|
||||||
|
DrawerItem.Header(
|
||||||
|
title = item.title ?: "",
|
||||||
|
collapsed = item.isCollapsed,
|
||||||
|
hasError = item.error,
|
||||||
|
canAdd = item.addIntent != null,
|
||||||
|
type = { item },
|
||||||
|
)
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { filters -> _state.update { it.copy(drawerItems = filters.toPersistentList()) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getColor(filter: Filter): Int {
|
||||||
|
if (filter.tint != 0) {
|
||||||
|
val color = colorProvider.getThemeColor(filter.tint, true)
|
||||||
|
if (color.isFree || inventory.purchasedThemes()) {
|
||||||
|
return color.primaryColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIcon(filter: Filter): Int {
|
||||||
|
if (filter.icon < 1000 || filter.icon == CustomIcons.PLACE || inventory.hasPro) {
|
||||||
|
val icon = CustomIcons.getIconResId(filter.icon)
|
||||||
|
if (icon != null) {
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when (filter) {
|
||||||
|
is TagFilter -> R.drawable.ic_outline_label_24px
|
||||||
|
is GtasksFilter,
|
||||||
|
is CaldavFilter -> R.drawable.ic_list_24px
|
||||||
|
|
||||||
|
is CustomFilter -> R.drawable.ic_outline_filter_list_24px
|
||||||
|
is PlaceFilter -> R.drawable.ic_outline_place_24px
|
||||||
|
else -> filter.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleCollapsed(subheader: NavigationDrawerSubheader) = viewModelScope.launch {
|
||||||
|
val collapsed = !subheader.isCollapsed
|
||||||
|
when (subheader.subheaderType) {
|
||||||
|
NavigationDrawerSubheader.SubheaderType.PREFERENCE -> {
|
||||||
|
preferences.setBoolean(subheader.id.toInt(), collapsed)
|
||||||
|
localBroadcastManager.broadcastRefreshList()
|
||||||
|
}
|
||||||
|
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS,
|
||||||
|
NavigationDrawerSubheader.SubheaderType.CALDAV,
|
||||||
|
NavigationDrawerSubheader.SubheaderType.TASKS,
|
||||||
|
NavigationDrawerSubheader.SubheaderType.ETESYNC -> {
|
||||||
|
caldavDao.setCollapsed(subheader.id, collapsed)
|
||||||
|
localBroadcastManager.broadcastRefreshList()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package com.todoroo.astrid.adapter
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.tasks.databinding.FilterAdapterActionBinding
|
|
||||||
import org.tasks.filters.NavigationDrawerAction
|
|
||||||
import org.tasks.themes.DrawableUtil
|
|
||||||
|
|
||||||
class ActionViewHolder internal constructor(
|
|
||||||
private val context: Context,
|
|
||||||
itemView: View,
|
|
||||||
private val onClick: (NavigationDrawerAction) -> Unit
|
|
||||||
) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
|
|
||||||
private val row: View
|
|
||||||
private val text: TextView
|
|
||||||
private val icon: ImageView
|
|
||||||
|
|
||||||
init {
|
|
||||||
FilterAdapterActionBinding.bind(itemView).let {
|
|
||||||
row = it.row
|
|
||||||
text = it.text
|
|
||||||
icon = it.icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(filter: NavigationDrawerAction) {
|
|
||||||
text.text = filter.title
|
|
||||||
icon.setImageDrawable(DrawableUtil.getWrapped(context, filter.icon))
|
|
||||||
row.setOnClickListener {
|
|
||||||
onClick.invoke(filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package com.todoroo.astrid.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class SeparatorViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package org.tasks.compose.drawer
|
||||||
|
|
||||||
|
enum class DrawerAction {
|
||||||
|
PURCHASE, CUSTOMIZE_DRAWER, SETTINGS, HELP_AND_FEEDBACK
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package org.tasks.compose.drawer
|
||||||
|
|
||||||
|
import org.tasks.filters.NavigationDrawerSubheader
|
||||||
|
|
||||||
|
sealed interface DrawerItem {
|
||||||
|
data class Filter(
|
||||||
|
val title: String,
|
||||||
|
val icon: Int,
|
||||||
|
val color: Int = 0,
|
||||||
|
val count: Int = 0,
|
||||||
|
val shareCount: Int = 0,
|
||||||
|
val selected: Boolean = false,
|
||||||
|
val type: () -> com.todoroo.astrid.api.Filter,
|
||||||
|
) : DrawerItem
|
||||||
|
data class Header(
|
||||||
|
val title: String,
|
||||||
|
val collapsed: Boolean,
|
||||||
|
val hasError: Boolean,
|
||||||
|
val canAdd: Boolean,
|
||||||
|
val type: () -> NavigationDrawerSubheader,
|
||||||
|
) : DrawerItem
|
||||||
|
}
|
||||||
@ -0,0 +1,318 @@
|
|||||||
|
package org.tasks.compose.drawer
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.mandatorySystemGestures
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.PeopleOutline
|
||||||
|
import androidx.compose.material.icons.outlined.PermIdentity
|
||||||
|
import androidx.compose.material.icons.outlined.SyncProblem
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.android.material.composethemeadapter.MdcTheme
|
||||||
|
import com.todoroo.astrid.api.FilterImpl
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.Tasks.Companion.IS_GENERIC
|
||||||
|
import org.tasks.extensions.formatNumber
|
||||||
|
import org.tasks.filters.NavigationDrawerSubheader
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TaskListDrawer(
|
||||||
|
begForMoney: Boolean,
|
||||||
|
filters: ImmutableList<DrawerItem>,
|
||||||
|
onClick: (DrawerItem) -> Unit,
|
||||||
|
onDrawerAction: (DrawerAction) -> Unit,
|
||||||
|
onAddClick: (DrawerItem.Header) -> Unit,
|
||||||
|
onErrorClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val insets = WindowInsets.mandatorySystemGestures
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
bottom = insets
|
||||||
|
.asPaddingValues()
|
||||||
|
.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(items = filters) {
|
||||||
|
when (it) {
|
||||||
|
is DrawerItem.Filter -> FilterItem(item = it, onClick = { onClick(it) })
|
||||||
|
is DrawerItem.Header -> HeaderItem(
|
||||||
|
item = it,
|
||||||
|
canAdd = it.canAdd,
|
||||||
|
toggleCollapsed = { onClick(it) },
|
||||||
|
onAddClick = { onAddClick(it) },
|
||||||
|
onErrorClick = onErrorClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
if (begForMoney) {
|
||||||
|
item {
|
||||||
|
MenuAction(
|
||||||
|
icon = R.drawable.ic_outline_attach_money_24px,
|
||||||
|
title = if (IS_GENERIC) R.string.TLA_menu_donate else R.string.name_your_price
|
||||||
|
) {
|
||||||
|
onDrawerAction(DrawerAction.PURCHASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MenuAction(
|
||||||
|
icon = R.drawable.ic_outline_edit_24px,
|
||||||
|
title = R.string.manage_drawer
|
||||||
|
) {
|
||||||
|
onDrawerAction(DrawerAction.CUSTOMIZE_DRAWER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MenuAction(
|
||||||
|
icon = R.drawable.ic_outline_settings_24px,
|
||||||
|
title = R.string.TLA_menu_settings
|
||||||
|
) {
|
||||||
|
onDrawerAction(DrawerAction.SETTINGS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
MenuAction(
|
||||||
|
icon = R.drawable.ic_outline_help_outline_24px,
|
||||||
|
title = R.string.help_and_feedback
|
||||||
|
) {
|
||||||
|
onDrawerAction(DrawerAction.HELP_AND_FEEDBACK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilterItem(
|
||||||
|
item: DrawerItem.Filter,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
MenuRow(
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
if (item.icon != -1) {
|
||||||
|
DrawerIcon(icon = item.icon, color = item.color)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = item.title,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (item.shareCount > 0) {
|
||||||
|
Icon(
|
||||||
|
imageVector = when (item.shareCount) {
|
||||||
|
1 -> Icons.Outlined.PermIdentity
|
||||||
|
else -> Icons.Outlined.PeopleOutline
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onSurface.copy(
|
||||||
|
alpha = ContentAlpha.medium
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.width(48.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
) {
|
||||||
|
if (item.count > 0) {
|
||||||
|
val locale = LocalConfiguration.current.locales[0]
|
||||||
|
Text(
|
||||||
|
text = locale.formatNumber(item.count),
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuAction(
|
||||||
|
icon: Int,
|
||||||
|
title: Int,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
MenuRow(onClick = onClick) {
|
||||||
|
DrawerIcon(icon = icon)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = title),
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DrawerIcon(icon: Int, color: Int = 0) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = when (color) {
|
||||||
|
0 -> MaterialTheme.colors.onSurface
|
||||||
|
else -> Color(color)
|
||||||
|
}.copy(alpha = ContentAlpha.medium)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HeaderItem(
|
||||||
|
item: DrawerItem.Header,
|
||||||
|
canAdd: Boolean,
|
||||||
|
toggleCollapsed: () -> Unit,
|
||||||
|
onAddClick: () -> Unit,
|
||||||
|
onErrorClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
|
MenuRow(
|
||||||
|
padding = PaddingValues(start = 16.dp),
|
||||||
|
onClick = toggleCollapsed,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = item.title,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
IconButton(onClick = toggleCollapsed) {
|
||||||
|
val rotation by animateFloatAsState(
|
||||||
|
targetValue = if (item.collapsed) 0f else 180f,
|
||||||
|
animationSpec = tween(250),
|
||||||
|
label = "arrow rotation",
|
||||||
|
)
|
||||||
|
Timber.d("rotation: $rotation")
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.rotate(rotation),
|
||||||
|
imageVector = Icons.Outlined.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (canAdd) {
|
||||||
|
IconButton(onClick = onAddClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.hasError) {
|
||||||
|
IconButton(onClick = onErrorClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.SyncProblem,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuRow(
|
||||||
|
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(padding)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun MenuPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
TaskListDrawer(
|
||||||
|
filters = persistentListOf(
|
||||||
|
DrawerItem.Filter(
|
||||||
|
title = "My Tasks",
|
||||||
|
icon = R.drawable.ic_outline_all_inbox_24px,
|
||||||
|
type = { FilterImpl() },
|
||||||
|
),
|
||||||
|
DrawerItem.Header(
|
||||||
|
title = "Filters",
|
||||||
|
collapsed = false,
|
||||||
|
canAdd = true,
|
||||||
|
hasError = false,
|
||||||
|
type = {
|
||||||
|
NavigationDrawerSubheader(
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
NavigationDrawerSubheader.SubheaderType.PREFERENCE,
|
||||||
|
0L,
|
||||||
|
0,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onClick = {},
|
||||||
|
onDrawerAction = {},
|
||||||
|
begForMoney = true,
|
||||||
|
onAddClick = {},
|
||||||
|
onErrorClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.tasks.extensions
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.util.TypedValue
|
|
||||||
|
|
||||||
val Number.dp: Float
|
|
||||||
get() = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
this.toFloat(),
|
|
||||||
Resources.getSystem().displayMetrics
|
|
||||||
)
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package org.tasks.filters
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import com.todoroo.astrid.api.FilterListItem
|
|
||||||
|
|
||||||
data class NavigationDrawerAction(
|
|
||||||
val title: String,
|
|
||||||
val icon: Int,
|
|
||||||
val requestCode: Int,
|
|
||||||
val intent: Intent? = null,
|
|
||||||
) : FilterListItem {
|
|
||||||
override val itemType = FilterListItem.Type.ACTION
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: FilterListItem) = this == other
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package org.tasks.filters
|
|
||||||
|
|
||||||
import com.todoroo.astrid.api.FilterListItem
|
|
||||||
|
|
||||||
class NavigationDrawerSeparator : FilterListItem {
|
|
||||||
override val itemType = FilterListItem.Type.SEPARATOR
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: FilterListItem): Boolean {
|
|
||||||
return other is NavigationDrawerSeparator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
package org.tasks.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import com.todoroo.astrid.adapter.NavigationDrawerAdapter
|
|
||||||
import com.todoroo.astrid.api.Filter
|
|
||||||
import com.todoroo.astrid.api.FilterListItem
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.tasks.R
|
|
||||||
import org.tasks.billing.PurchaseActivity
|
|
||||||
import org.tasks.extensions.Context.openUri
|
|
||||||
import org.tasks.filters.NavigationDrawerAction
|
|
||||||
import org.tasks.intents.TaskIntents
|
|
||||||
import org.tasks.preferences.Preferences
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class NavigationDrawerFragment : BottomSheetDialogFragment() {
|
|
||||||
@Inject lateinit var adapter: NavigationDrawerAdapter
|
|
||||||
@Inject lateinit var preferences: Preferences
|
|
||||||
|
|
||||||
override fun getTheme() = R.style.CustomBottomSheetDialog
|
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
|
||||||
private val viewModel: NavigationDrawerViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
arguments?.getParcelable<Filter>(EXTRA_SELECTED)?.let {
|
|
||||||
viewModel.setSelected(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
|
|
||||||
dialog.setOnShowListener {
|
|
||||||
val bottomSheet =
|
|
||||||
dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
|
||||||
val behavior = BottomSheetBehavior.from(bottomSheet!!)
|
|
||||||
behavior.skipCollapsed = true
|
|
||||||
if (preferences.isTopAppBar) {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
} else if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val layout = inflater.inflate(R.layout.fragment_navigation_drawer, container, false)
|
|
||||||
recyclerView = layout.findViewById(R.id.recycler_view)
|
|
||||||
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
|
|
||||||
adapter.setOnClick(this::onFilterItemSelected)
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
viewModel
|
|
||||||
.viewState
|
|
||||||
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
|
|
||||||
.onEach {
|
|
||||||
adapter.setSelected(it.selected)
|
|
||||||
adapter.submitList(it.filters)
|
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFilterItemSelected(item: FilterListItem?) {
|
|
||||||
if (item is Filter) {
|
|
||||||
viewModel.setSelected(item)
|
|
||||||
activity?.startActivity(TaskIntents.getTaskListIntent(activity, item))
|
|
||||||
} else if (item is NavigationDrawerAction) {
|
|
||||||
when (item.requestCode) {
|
|
||||||
REQUEST_PURCHASE ->
|
|
||||||
startActivity(Intent(context, PurchaseActivity::class.java))
|
|
||||||
REQUEST_DONATE -> context?.openUri(R.string.url_donate)
|
|
||||||
else -> item.intent?.let {
|
|
||||||
activity?.startActivityForResult(it, item.requestCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val REQUEST_NEW_LIST = 10100
|
|
||||||
const val REQUEST_SETTINGS = 10101
|
|
||||||
const val REQUEST_PURCHASE = 10102
|
|
||||||
const val REQUEST_DONATE = 10103
|
|
||||||
const val REQUEST_NEW_PLACE = 10104
|
|
||||||
const val REQUEST_NEW_FILTER = 101015
|
|
||||||
private const val EXTRA_SELECTED = "extra_selected"
|
|
||||||
|
|
||||||
fun newNavigationDrawer(selected: Filter?): NavigationDrawerFragment {
|
|
||||||
val fragment = NavigationDrawerFragment()
|
|
||||||
fragment.arguments = Bundle().apply {
|
|
||||||
putParcelable(EXTRA_SELECTED, selected)
|
|
||||||
}
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
package org.tasks.ui
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.todoroo.astrid.api.Filter
|
|
||||||
import com.todoroo.astrid.api.Filter.Companion.NO_COUNT
|
|
||||||
import com.todoroo.astrid.api.FilterListItem
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.tasks.LocalBroadcastManager
|
|
||||||
import org.tasks.data.TaskDao
|
|
||||||
import org.tasks.filters.FilterProvider
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class NavigationDrawerViewModel @Inject constructor(
|
|
||||||
private val filterProvider: FilterProvider,
|
|
||||||
private val taskDao: TaskDao,
|
|
||||||
private val localBroadcastManager: LocalBroadcastManager,
|
|
||||||
) : ViewModel() {
|
|
||||||
data class ViewState(
|
|
||||||
val selected: Filter? = null,
|
|
||||||
val filters: List<FilterListItem> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _viewState = MutableStateFlow(ViewState())
|
|
||||||
private val refreshReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
when (intent?.action) {
|
|
||||||
LocalBroadcastManager.REFRESH,
|
|
||||||
LocalBroadcastManager.REFRESH_LIST -> updateFilters()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val viewState: StateFlow<ViewState>
|
|
||||||
get() = _viewState.asStateFlow()
|
|
||||||
|
|
||||||
fun setSelected(filter: Filter?) {
|
|
||||||
_viewState.update { it.copy(selected = filter) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFilters() = viewModelScope.launch {
|
|
||||||
filterProvider
|
|
||||||
.navDrawerItems()
|
|
||||||
.onEach {
|
|
||||||
if (it is Filter && it.count == NO_COUNT) {
|
|
||||||
it.count = try {
|
|
||||||
taskDao.count(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.let { filters -> _viewState.update { it.copy(filters = filters) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
localBroadcastManager.unregisterReceiver(refreshReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
|
|
||||||
updateFilters()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/row"
|
|
||||||
android:background="@drawable/drawer_background_selector"
|
|
||||||
android:foreground="?attr/selectableItemBackground"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="12dp"
|
|
||||||
android:paddingBottom="12dp"
|
|
||||||
android:paddingStart="@dimen/keyline_first"
|
|
||||||
android:paddingEnd="0dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/icon"
|
|
||||||
android:src="@drawable/ic_outline_add_24px"
|
|
||||||
app:tint="@color/text_primary"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:background="@null"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:alpha="@dimen/alpha_secondary"
|
|
||||||
android:scaleType="center"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_toEndOf="@id/icon"
|
|
||||||
android:background="@null"
|
|
||||||
android:paddingStart="@dimen/keyline_second"
|
|
||||||
android:paddingEnd="@dimen/keyline_first"
|
|
||||||
android:duplicateParentState="true"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:fontFamily="@string/font_fontFamily_medium"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:lines="1"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="@color/text_primary"
|
|
||||||
tools:ignore="UnusedAttribute"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/drawer_background_selector"
|
|
||||||
android:focusable="false"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/divider"
|
|
||||||
android:paddingBottom="4dp"
|
|
||||||
style="@style/horizontal_divider"
|
|
||||||
android:layout_gravity="top" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/recycler_view"
|
|
||||||
android:paddingTop="@dimen/bottom_sheet_corners"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scrollbarStyle="outsideOverlay"
|
|
||||||
android:scrollbars="vertical"/>
|
|
||||||
@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"skippableComposables": 365,
|
"skippableComposables": 387,
|
||||||
"restartableComposables": 489,
|
"restartableComposables": 514,
|
||||||
"readonlyComposables": 0,
|
"readonlyComposables": 0,
|
||||||
"totalComposables": 495,
|
"totalComposables": 520,
|
||||||
"restartGroups": 489,
|
"restartGroups": 514,
|
||||||
"totalGroups": 598,
|
"totalGroups": 630,
|
||||||
"staticArguments": 762,
|
"staticArguments": 792,
|
||||||
"certainArguments": 314,
|
"certainArguments": 329,
|
||||||
"knownStableArguments": 4864,
|
"knownStableArguments": 5083,
|
||||||
"knownUnstableArguments": 139,
|
"knownUnstableArguments": 138,
|
||||||
"unknownStableArguments": 9,
|
"unknownStableArguments": 11,
|
||||||
"totalArguments": 5012,
|
"totalArguments": 5232,
|
||||||
"markedStableClasses": 0,
|
"markedStableClasses": 0,
|
||||||
"inferredStableClasses": 100,
|
"inferredStableClasses": 108,
|
||||||
"inferredUnstableClasses": 345,
|
"inferredUnstableClasses": 334,
|
||||||
"inferredUncertainClasses": 1,
|
"inferredUncertainClasses": 1,
|
||||||
"effectivelyStableClasses": 100,
|
"effectivelyStableClasses": 108,
|
||||||
"totalClasses": 446,
|
"totalClasses": 443,
|
||||||
"memoizedLambdas": 524,
|
"memoizedLambdas": 549,
|
||||||
"singletonLambdas": 182,
|
"singletonLambdas": 188,
|
||||||
"singletonComposableLambdas": 90,
|
"singletonComposableLambdas": 94,
|
||||||
"composableLambdas": 223,
|
"composableLambdas": 238,
|
||||||
"totalLambdas": 633
|
"totalLambdas": 667
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue