diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index 0ecd1e90a..2dead2f31 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -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 diff --git a/app/src/main/java/org/tasks/activities/ListPicker.kt b/app/src/main/java/org/tasks/activities/ListPicker.kt index cbd97b9d9..2a278a559 100644 --- a/app/src/main/java/org/tasks/activities/ListPicker.kt +++ b/app/src/main/java/org/tasks/activities/ListPicker.kt @@ -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() - } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/activities/ListPickerViewModel.kt b/app/src/main/java/org/tasks/activities/ListPickerViewModel.kt new file mode 100644 index 000000000..99bf3227e --- /dev/null +++ b/app/src/main/java/org/tasks/activities/ListPickerViewModel.kt @@ -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 = emptyList(), + ) + + private val refreshReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + refresh() + } + } + + private val _viewState = MutableStateFlow(ViewState()) + val viewState: StateFlow + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/calendars/CalendarPicker.kt b/app/src/main/java/org/tasks/calendars/CalendarPicker.kt index a307b026d..346e99c15 100644 --- a/app/src/main/java/org/tasks/calendars/CalendarPicker.kt +++ b/app/src/main/java/org/tasks/calendars/CalendarPicker.kt @@ -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 diff --git a/app/src/main/java/org/tasks/compose/CalendarPicker.kt b/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt similarity index 77% rename from app/src/main/java/org/tasks/compose/CalendarPicker.kt rename to app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt index 9225c3d39..3405f3e1e 100644 --- a/app/src/main/java/org/tasks/compose/CalendarPicker.kt +++ b/app/src/main/java/org/tasks/compose/pickers/CalendarPicker.kt @@ -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) } - ) - } } } } diff --git a/app/src/main/java/org/tasks/compose/CheckableIconRow.kt b/app/src/main/java/org/tasks/compose/pickers/CheckableIconRow.kt similarity index 64% rename from app/src/main/java/org/tasks/compose/CheckableIconRow.kt rename to app/src/main/java/org/tasks/compose/pickers/CheckableIconRow.kt index db5dd69ed..84a17497b 100644 --- a/app/src/main/java/org/tasks/compose/CheckableIconRow.kt +++ b/app/src/main/java/org/tasks/compose/pickers/CheckableIconRow.kt @@ -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)) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/pickers/CollapsibleRow.kt b/app/src/main/java/org/tasks/compose/pickers/CollapsibleRow.kt new file mode 100644 index 000000000..94ed20704 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/pickers/CollapsibleRow.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/pickers/ListPicker.kt b/app/src/main/java/org/tasks/compose/pickers/ListPicker.kt new file mode 100644 index 000000000..25591e17f --- /dev/null +++ b/app/src/main/java/org/tasks/compose/pickers/ListPicker.kt @@ -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, + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.kt b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.kt index 5db00efce..e3109a166 100644 --- a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.kt +++ b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.kt @@ -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 } diff --git a/app/src/main/java/org/tasks/themes/ColorProvider.kt b/app/src/main/java/org/tasks/themes/ColorProvider.kt index fd92ec3f2..531b69212 100644 --- a/app/src/main/java/org/tasks/themes/ColorProvider.kt +++ b/app/src/main/java/org/tasks/themes/ColorProvider.kt @@ -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 diff --git a/app/src/main/java/org/tasks/themes/CustomIcons.kt b/app/src/main/java/org/tasks/themes/CustomIcons.kt index d72aab85d..5e462476b 100644 --- a/app/src/main/java/org/tasks/themes/CustomIcons.kt +++ b/app/src/main/java/org/tasks/themes/CustomIcons.kt @@ -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 diff --git a/app/src/main/res/drawable/ic_outline_block_24.xml b/app/src/main/res/drawable/ic_outline_block_24.xml new file mode 100644 index 000000000..1e478d2aa --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_block_24.xml @@ -0,0 +1,5 @@ + + +