Convert ListPicker to compose

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

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

@ -2,82 +2,43 @@ package org.tasks.activities
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment 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.Filter
import com.todoroo.astrid.api.GtasksFilter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.compose.pickers.ListPicker
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.filters.FilterProvider
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ListPicker : DialogFragment() { class ListPicker : DialogFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder @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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
if (savedInstanceState != null) { return dialogBuilder
filterAdapter.restore(savedInstanceState) .newDialog()
} .setNegativeButton(R.string.cancel, null)
return createDialog(filterAdapter, dialogBuilder, this::selectedList) .setContent {
} ListPicker(
selected = arguments?.getParcelable(EXTRA_SELECTED_FILTER),
override fun onResume() { onSelected = {
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( targetFragment!!.onActivityResult(
targetRequestCode, targetRequestCode,
Activity.RESULT_OK, Activity.RESULT_OK,
Intent().putExtra(EXTRA_SELECTED_FILTER, list)) Intent().putExtra(EXTRA_SELECTED_FILTER, it)
} )
dismiss()
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)
} }
.show()
} }
companion object { companion object {
const val EXTRA_SELECTED_FILTER = "extra_selected_filter" const val EXTRA_SELECTED_FILTER = "extra_selected_filter"
private const val EXTRA_NO_SELECTION = "extra_no_selection"
fun newListPicker( fun newListPicker(
selected: Filter?, targetFragment: Fragment?, requestCode: Int): ListPicker { selected: Filter?, targetFragment: Fragment?, requestCode: Int): ListPicker {
val dialog = ListPicker() val dialog = ListPicker()
@ -87,31 +48,5 @@ class ListPicker : DialogFragment() {
dialog.setTargetFragment(targetFragment, requestCode) dialog.setTargetFragment(targetFragment, requestCode)
return dialog 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.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.compose.CalendarPicker import org.tasks.compose.pickers.CalendarPicker
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import javax.inject.Inject import javax.inject.Inject

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

@ -1,9 +1,7 @@
package org.tasks.compose package org.tasks.compose.pickers
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -14,16 +12,38 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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 import androidx.compose.ui.unit.dp
@Composable @Composable
fun CheckableIconRow( fun CheckableIconRow(
icon: ImageVector, icon: Painter,
tint: Color, tint: Color,
text: String, text: String,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -32,16 +52,14 @@ fun CheckableIconRow(
.clickable { onClick() } .clickable { onClick() }
) { ) {
Icon( Icon(
imageVector = icon, painter = icon,
contentDescription = null, contentDescription = null,
tint = tint.copy(alpha = ContentAlpha.medium), tint = tint.copy(alpha = ContentAlpha.medium),
modifier = Modifier.padding(start = 16.dp, end = 32.dp, top = 12.dp, bottom = 12.dp), modifier = Modifier.padding(start = 16.dp, end = 32.dp, top = 12.dp, bottom = 12.dp),
) )
Text( Box(modifier = Modifier.weight(1f)) {
text = text, content()
style = MaterialTheme.typography.body1, }
modifier = Modifier.weight(1f),
)
if (selected) { if (selected) {
Icon( Icon(
imageVector = Icons.Outlined.Check, imageVector = Icons.Outlined.Check,
@ -49,6 +67,8 @@ fun CheckableIconRow(
tint = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.medium), tint = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.medium),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), 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.appcompat.app.AlertDialog
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
class AlertDialogBuilder internal constructor(private val context: Context) { 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 { fun setContent(content: @Composable () -> Unit): AlertDialogBuilder {
builder.setView(ComposeView(context).apply { setContent(content) }) builder.setView(ComposeView(context)
.apply {
setContent {
MdcTheme {
content()
}
}
}
)
return this return this
} }

@ -3,12 +3,15 @@ package org.tasks.themes
import android.content.Context import android.content.Context
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import com.todoroo.astrid.data.Task 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.R
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject 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 { companion object {
const val BLUE_500 = -14575885 const val BLUE_500 = -14575885

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