Convert ListPicker to compose

pull/1964/head
Alex Baker 2 years ago
parent 5f3b706b81
commit d157bbec53

@ -718,11 +718,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
R.id.move_tasks -> {
lifecycleScope.launch {
val singleFilter = taskMover.getSingleFilter(selected)
val fragment = if (singleFilter == null) {
newListPicker(this@TaskListFragment, REQUEST_MOVE_TASKS)
} else {
newListPicker(singleFilter, this@TaskListFragment, REQUEST_MOVE_TASKS)
}
val fragment = newListPicker(
selected = singleFilter,
targetFragment = this@TaskListFragment,
requestCode = REQUEST_MOVE_TASKS
)
fragment.show(parentFragmentManager, FRAG_TAG_REMOTE_LIST_PICKER)
}
true

@ -2,82 +2,43 @@ package org.tasks.activities
import android.app.Activity
import android.app.Dialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.adapter.FilterAdapter
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.compose.pickers.ListPicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.filters.FilterProvider
import javax.inject.Inject
@AndroidEntryPoint
class ListPicker : DialogFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var filterAdapter: FilterAdapter
@Inject lateinit var filterProvider: FilterProvider
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private val refreshReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
if (savedInstanceState != null) {
filterAdapter.restore(savedInstanceState)
}
return createDialog(filterAdapter, dialogBuilder, this::selectedList)
}
override fun onResume() {
super.onResume()
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
refresh()
}
override fun onPause() {
super.onPause()
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
filterAdapter.save(outState)
}
private fun selectedList(list: Filter) {
targetFragment!!.onActivityResult(
targetRequestCode,
Activity.RESULT_OK,
Intent().putExtra(EXTRA_SELECTED_FILTER, list))
}
private fun refresh() {
val noSelection = requireArguments().getBoolean(EXTRA_NO_SELECTION, false)
val selected: Filter? = if (noSelection) null else arguments?.getParcelable(EXTRA_SELECTED_FILTER)
lifecycleScope.launch {
val items = filterProvider.listPickerItems()
filterAdapter.setData(items, selected)
}
return dialogBuilder
.newDialog()
.setNegativeButton(R.string.cancel, null)
.setContent {
ListPicker(
selected = arguments?.getParcelable(EXTRA_SELECTED_FILTER),
onSelected = {
targetFragment!!.onActivityResult(
targetRequestCode,
Activity.RESULT_OK,
Intent().putExtra(EXTRA_SELECTED_FILTER, it)
)
dismiss()
},
)
}
.show()
}
companion object {
const val EXTRA_SELECTED_FILTER = "extra_selected_filter"
private const val EXTRA_NO_SELECTION = "extra_no_selection"
fun newListPicker(
selected: Filter?, targetFragment: Fragment?, requestCode: Int): ListPicker {
val dialog = ListPicker()
@ -87,31 +48,5 @@ class ListPicker : DialogFragment() {
dialog.setTargetFragment(targetFragment, requestCode)
return dialog
}
fun newListPicker(targetFragment: Fragment?, requestCode: Int): ListPicker {
val dialog = ListPicker()
val arguments = Bundle()
arguments.putBoolean(EXTRA_NO_SELECTION, true)
dialog.arguments = arguments
dialog.setTargetFragment(targetFragment, requestCode)
return dialog
}
private fun createDialog(
filterAdapter: FilterAdapter,
dialogBuilder: DialogBuilder,
handler: (Filter) -> Unit): AlertDialog {
val builder = dialogBuilder
.newDialog()
.setNegativeButton(R.string.cancel, null)
.setSingleChoiceItems(filterAdapter,-1) { dialog: DialogInterface, which: Int ->
val item = filterAdapter.getItem(which)
if (item is GtasksFilter || item is CaldavFilter) {
handler(item as Filter)
}
dialog.dismiss()
}
return builder.show()
}
}
}

@ -0,0 +1,106 @@
package org.tasks.activities
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.FilterListItem
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
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.R
import org.tasks.billing.Inventory
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider
import org.tasks.themes.CustomIcons
import javax.inject.Inject
@HiltViewModel
class ListPickerViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val filterProvider: FilterProvider,
private val localBroadcastManager: LocalBroadcastManager,
private val inventory: Inventory,
private val colorProvider: ColorProvider,
private val preferences: Preferences,
private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao,
) : ViewModel() {
data class ViewState(
val filters: List<FilterListItem> = emptyList(),
)
private val refreshReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
}
}
private val _viewState = MutableStateFlow(ViewState())
val viewState: StateFlow<ViewState>
get() = _viewState.asStateFlow()
fun onClick(subheader: NavigationDrawerSubheader) = viewModelScope.launch {
val collapsed = !subheader.isCollapsed
when (subheader.subheaderType) {
NavigationDrawerSubheader.SubheaderType.PREFERENCE ->
preferences.setBoolean(subheader.id.toInt(), collapsed)
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS ->
googleTaskDao.setCollapsed(subheader.id, collapsed)
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
NavigationDrawerSubheader.SubheaderType.ETESYNC ->
caldavDao.setCollapsed(subheader.id, collapsed)
}
localBroadcastManager.broadcastRefreshList()
}
private fun refresh() {
viewModelScope.launch {
val items = filterProvider.listPickerItems()
_viewState.update {
it.copy(filters = items)
}
}
}
fun getIcon(filter: Filter): Int {
if (filter.icon < 1000 || inventory.hasPro) {
val icon = CustomIcons.getIconResId(filter.icon)
if (icon != null) {
return icon
}
}
return R.drawable.ic_list_24px
}
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 context.getColor(R.color.text_primary)
}
override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
init {
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
refresh()
}
}

@ -7,7 +7,7 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.compose.CalendarPicker
import org.tasks.compose.pickers.CalendarPicker
import org.tasks.dialogs.DialogBuilder
import javax.inject.Inject

@ -1,4 +1,4 @@
package org.tasks.compose
package org.tasks.compose.pickers
import android.Manifest
import android.content.res.Configuration
@ -7,13 +7,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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
@ -24,6 +22,7 @@ import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R
import org.tasks.calendars.AndroidCalendar
import org.tasks.calendars.CalendarPickerViewModel
import org.tasks.compose.collectAsStateLifecycleAware
@OptIn(ExperimentalPermissionsApi::class)
@Composable
@ -61,28 +60,26 @@ fun CalendarPickerList(
onSelected: (AndroidCalendar?) -> Unit,
) {
val selectedCalendar = calendars.find { it.id == selected }
MdcTheme {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 12.dp)
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 12.dp)
) {
CheckableIconRow(
icon = painterResource(id = R.drawable.ic_outline_block_24),
tint = MaterialTheme.colors.onSurface,
text = stringResource(id = R.string.dont_add_to_calendar),
selected = selectedCalendar == null,
onClick = { onSelected(null) },
)
calendars.forEach {
CheckableIconRow(
icon = Icons.Outlined.Block,
tint = MaterialTheme.colors.onSurface,
text = stringResource(id = R.string.dont_add_to_calendar),
selected = selectedCalendar == null,
onClick = { onSelected(null) },
icon = painterResource(id = R.drawable.ic_outline_event_24px),
tint = Color(it.color),
text = it.name,
selected = selectedCalendar == it,
onClick = { onSelected(it) }
)
calendars.forEach {
CheckableIconRow(
icon = Icons.Outlined.Event,
tint = Color(it.color),
text = it.name,
selected = selectedCalendar == it,
onClick = { onSelected(it) }
)
}
}
}
}

@ -1,9 +1,7 @@
package org.tasks.compose
package org.tasks.compose.pickers
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
@ -14,16 +12,38 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
@Composable
fun CheckableIconRow(
icon: ImageVector,
icon: Painter,
tint: Color,
text: String,
selected: Boolean,
onClick: () -> Unit,
) {
CheckableIconRow(
icon = icon,
tint = tint,
selected = selected,
onClick = onClick,
content = {
Text(
text = text,
style = MaterialTheme.typography.body1,
)
}
)
}
@Composable
fun CheckableIconRow(
icon: Painter,
tint: Color,
selected: Boolean,
onClick: () -> Unit,
content: @Composable () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -32,16 +52,14 @@ fun CheckableIconRow(
.clickable { onClick() }
) {
Icon(
imageVector = icon,
painter = icon,
contentDescription = null,
tint = tint.copy(alpha = ContentAlpha.medium),
modifier = Modifier.padding(start = 16.dp, end = 32.dp, top = 12.dp, bottom = 12.dp),
)
Text(
text = text,
style = MaterialTheme.typography.body1,
modifier = Modifier.weight(1f),
)
Box(modifier = Modifier.weight(1f)) {
content()
}
if (selected) {
Icon(
imageVector = Icons.Outlined.Check,
@ -49,6 +67,8 @@ fun CheckableIconRow(
tint = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.medium),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
)
} else {
Spacer(modifier = Modifier.width(56.dp))
}
}
}
}

@ -0,0 +1,55 @@
package org.tasks.compose.pickers
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.KeyboardArrowUp
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun CollapsibleRow(
text: String,
collapsed: Boolean,
onClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = text,
style = MaterialTheme.typography.body2.copy(
fontWeight = FontWeight.Medium,
),
modifier = Modifier.weight(1f),
)
val rotationAngle by animateFloatAsState(
targetValue = if (collapsed) -180f else 0f,
animationSpec = tween(durationMillis = 250,easing = FastOutLinearInEasing)
)
Icon(
imageVector = Icons.Outlined.KeyboardArrowUp,
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
modifier = Modifier.rotate(rotationAngle)
)
}
}

@ -0,0 +1,82 @@
package org.tasks.compose.pickers
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.todoroo.astrid.api.Filter
import org.tasks.R
import org.tasks.activities.ListPickerViewModel
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.filters.NavigationDrawerSubheader
@Composable
fun ListPicker(
viewModel: ListPickerViewModel = viewModel(),
selected: Filter?,
onSelected: (Filter) -> Unit,
) {
val filters = viewModel.viewState.collectAsStateLifecycleAware().value.filters
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 12.dp)
) {
filters.forEach { filter ->
when (filter) {
is NavigationDrawerSubheader -> {
CollapsibleRow(
text = filter.listingTitle,
collapsed = filter.isCollapsed,
onClick = { viewModel.onClick(filter) },
)
}
is Filter -> {
CheckableIconRow(
icon = painterResource(id = viewModel.getIcon(filter)),
tint = Color(viewModel.getColor(filter)),
selected = filter == selected,
onClick = { onSelected(filter) },
) {
Row(verticalAlignment = CenterVertically) {
Text(
text = filter.listingTitle,
style = MaterialTheme.typography.body2.copy(
fontWeight = FontWeight.Medium
),
modifier = Modifier.weight(1f),
)
if (filter.principals > 0) {
Icon(
painter = painterResource(
id = when (filter.principals) {
1 -> R.drawable.ic_outline_perm_identity_24px
in 2..Int.MAX_VALUE -> R.drawable.ic_outline_people_outline_24
else -> 0
}
),
modifier = Modifier.alpha(ContentAlpha.medium),
contentDescription = null,
)
}
}
}
}
}
}
}
}

@ -7,6 +7,7 @@ import android.widget.ListAdapter
import androidx.appcompat.app.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class AlertDialogBuilder internal constructor(private val context: Context) {
@ -75,7 +76,15 @@ class AlertDialogBuilder internal constructor(private val context: Context) {
}
fun setContent(content: @Composable () -> Unit): AlertDialogBuilder {
builder.setView(ComposeView(context).apply { setContent(content) })
builder.setView(ComposeView(context)
.apply {
setContent {
MdcTheme {
content()
}
}
}
)
return this
}

@ -3,12 +3,15 @@ package org.tasks.themes
import android.content.Context
import androidx.annotation.ColorInt
import com.todoroo.astrid.data.Task
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.preferences.Preferences
import javax.inject.Inject
class ColorProvider @Inject constructor(@param:ActivityContext private val context: Context, preferences: Preferences) {
class ColorProvider @Inject constructor(
@param:ApplicationContext private val context: Context,
preferences: Preferences
) {
companion object {
const val BLUE_500 = -14575885

@ -209,6 +209,7 @@ object CustomIcons {
1183 to R.drawable.ic_outline_forum_24,
1184 to R.drawable.ic_twitter_logo_black,
1185 to R.drawable.ic_outline_person_add_24,
1186 to R.drawable.ic_outline_block_24,
)
@JvmStatic

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
</vector>
Loading…
Cancel
Save