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,
|
||||
"restartableComposables": 489,
|
||||
"skippableComposables": 387,
|
||||
"restartableComposables": 514,
|
||||
"readonlyComposables": 0,
|
||||
"totalComposables": 495,
|
||||
"restartGroups": 489,
|
||||
"totalGroups": 598,
|
||||
"staticArguments": 762,
|
||||
"certainArguments": 314,
|
||||
"knownStableArguments": 4864,
|
||||
"knownUnstableArguments": 139,
|
||||
"unknownStableArguments": 9,
|
||||
"totalArguments": 5012,
|
||||
"totalComposables": 520,
|
||||
"restartGroups": 514,
|
||||
"totalGroups": 630,
|
||||
"staticArguments": 792,
|
||||
"certainArguments": 329,
|
||||
"knownStableArguments": 5083,
|
||||
"knownUnstableArguments": 138,
|
||||
"unknownStableArguments": 11,
|
||||
"totalArguments": 5232,
|
||||
"markedStableClasses": 0,
|
||||
"inferredStableClasses": 100,
|
||||
"inferredUnstableClasses": 345,
|
||||
"inferredStableClasses": 108,
|
||||
"inferredUnstableClasses": 334,
|
||||
"inferredUncertainClasses": 1,
|
||||
"effectivelyStableClasses": 100,
|
||||
"totalClasses": 446,
|
||||
"memoizedLambdas": 524,
|
||||
"singletonLambdas": 182,
|
||||
"singletonComposableLambdas": 90,
|
||||
"composableLambdas": 223,
|
||||
"totalLambdas": 633
|
||||
"effectivelyStableClasses": 108,
|
||||
"totalClasses": 443,
|
||||
"memoizedLambdas": 549,
|
||||
"singletonLambdas": 188,
|
||||
"singletonComposableLambdas": 94,
|
||||
"composableLambdas": 238,
|
||||
"totalLambdas": 667
|
||||
}
|
Loading…
Reference in New Issue