diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index 5091e4e36..cefe17377 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -20,13 +20,18 @@ import androidx.appcompat.view.ActionMode import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.DrawerValue import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.layout.HingePolicy @@ -36,9 +41,10 @@ import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -61,6 +67,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.tasks.BuildConfig import org.tasks.R import org.tasks.TasksApplication @@ -128,7 +135,6 @@ class MainActivity : AppCompatActivity() { /** @see android.app.Activity.onCreate */ - @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) theme.applyTheme(this) @@ -147,257 +153,270 @@ class MainActivity : AppCompatActivity() { ) setContent { - TasksTheme(theme = theme.themeBase.index) { - val navigator = rememberListDetailPaneScaffoldNavigator( - calculatePaneScaffoldDirective( - windowAdaptiveInfo = currentWindowAdaptiveInfo(), - verticalHingePolicy = HingePolicy.AlwaysAvoid, - ).copy( - horizontalPartitionSpacerSize = 0.dp, - verticalPartitionSpacerSize = 0.dp, - ), + val windowInsets = WindowInsets.systemBars.asPaddingValues() + TasksTheme(theme = theme.themeBase.index) { + val drawerState = rememberDrawerState( + initialValue = DrawerValue.Closed, + confirmStateChange = { + viewModel.setDrawerState(it == DrawerValue.Open) + true + } ) val state = viewModel.state.collectAsStateWithLifecycle().value - val isListVisible = - navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded - val isDetailVisible = - navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded - val scope = rememberCoroutineScope() - - LaunchedEffect(state.task) { - if (state.task == null) { - if (intent.finishAffinity) { - finishAffinity() - } else { - if (intent.removeTask && intent.broughtToFront) { - moveTaskToBack(true) - } - hideKeyboard() - navigator.navigateTo(pane = ThreePaneScaffoldRole.Secondary) - } - } else { - navigator.navigateTo(pane = ThreePaneScaffoldRole.Primary) - } - } - BackHandler(enabled = state.task == null) { - Timber.d("onBackPressed") - if (intent.finishAffinity) { - finishAffinity() - } else if (isDetailVisible && navigator.canNavigateBack()) { - scope.launch { - navigator.navigateBack() - } - } else { - finish() - if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) { - runBlocking { - viewModel.resetFilter() + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + drawerState = drawerState, + windowInsets = WindowInsets(0, 0, 0, 0), + ) { + val context = LocalContext.current + val settingsRequest = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + context.findActivity()?.recreate() } - } - } - } - LaunchedEffect(state.filter, state.task) { - actionMode?.finish() - actionMode = null - viewModel.closeDrawer() - } - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - key (state.filter) { - AndroidFragment( - fragmentState = rememberFragmentState(), - arguments = remember(state.filter) { - Bundle() - .apply { putParcelable(EXTRA_FILTER, state.filter) } + val scope = rememberCoroutineScope() + val bottomSearchBar = atLeastR() + TaskListDrawer( + arrangement = when { + state.menuQuery.isBlank() -> Arrangement.Top + bottomSearchBar -> Arrangement.Bottom + else -> Arrangement.Top + }, + bottomSearchBar = bottomSearchBar, + filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems, + onClick = { + when (it) { + is DrawerItem.Filter -> { + viewModel.setFilter(it.filter) + scope.launch(Dispatchers.Default) { + withContext(Dispatchers.Main) { + context.findActivity()?.hideKeyboard() + } + drawerState.close() + } + } + + is DrawerItem.Header -> { + viewModel.toggleCollapsed(it.header) + } + } + }, + onAddClick = { + scope.launch(Dispatchers.Default) { + drawerState.close() + when (it.header.addIntentRc) { + FilterProvider.REQUEST_NEW_FILTER -> + NewFilterDialog.newFilterDialog().show( + supportFragmentManager, + SubheaderClickHandler.FRAG_TAG_NEW_FILTER + ) + + REQUEST_NEW_PLACE -> + startActivityForResult( + Intent( + this@MainActivity, + LocationPickerActivity::class.java + ), + REQUEST_NEW_PLACE + ) + + REQUEST_NEW_TAGS -> + startActivityForResult( + Intent( + this@MainActivity, + TagSettingsActivity::class.java + ), + REQUEST_NEW_LIST + ) + + REQUEST_NEW_LIST -> { + val account = + caldavDao.getAccount(it.header.id.toLong()) + ?: return@launch + when (it.header.subheaderType) { + NavigationDrawerSubheader.SubheaderType.CALDAV, + NavigationDrawerSubheader.SubheaderType.TASKS, + -> + startActivityForResult( + Intent( + this@MainActivity, + account.listSettingsClass() + ) + .putExtra( + EXTRA_CALDAV_ACCOUNT, + account + ), + REQUEST_NEW_LIST + ) + + else -> {} + } + } + + else -> Timber.e("Unhandled request code: $it") + } + } + }, + onErrorClick = { + context.startActivity(Intent(context, MainPreferences::class.java)) + }, + searchBar = { + MenuSearchBar( + begForMoney = state.begForMoney, + onDrawerAction = { + scope.launch { + drawerState.close() + when (it) { + DrawerAction.PURCHASE -> + if (TasksApplication.IS_GENERIC) + context.openUri(R.string.url_donate) + else + context.startActivity( + Intent( + context, + PurchaseActivity::class.java + ) + ) + + DrawerAction.SETTINGS -> + settingsRequest.launch( + Intent( + context, + MainPreferences::class.java + ) + ) + + DrawerAction.HELP_AND_FEEDBACK -> + context.startActivity( + Intent( + context, + HelpAndFeedback::class.java + ) + ) + } + } + }, + query = state.menuQuery, + onQueryChange = { viewModel.queryMenu(it) }, + ) }, - modifier = Modifier.fillMaxSize(), ) } - }, - detailPane = { + } + ) { + val navigator = rememberListDetailPaneScaffoldNavigator( + calculatePaneScaffoldDirective( + windowAdaptiveInfo = currentWindowAdaptiveInfo(), + verticalHingePolicy = HingePolicy.AlwaysAvoid, + ).copy( + horizontalPartitionSpacerSize = 0.dp, + verticalPartitionSpacerSize = 0.dp, + ), + + ) + val isListVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded + val isDetailVisible = + navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded + val scope = rememberCoroutineScope() + + LaunchedEffect(state.task) { if (state.task == null) { - if (isListVisible && isDetailVisible) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground), - contentDescription = null, - modifier = Modifier.size(192.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + if (intent.finishAffinity) { + finishAffinity() + } else { + if (intent.removeTask && intent.broughtToFront) { + moveTaskToBack(true) } + hideKeyboard() + navigator.navigateTo(pane = ThreePaneScaffoldRole.Secondary) + } + } else { + navigator.navigateTo(pane = ThreePaneScaffoldRole.Primary) + } + } + + BackHandler(enabled = state.task == null) { + Timber.d("onBackPressed") + if (intent.finishAffinity) { + finishAffinity() + } else if (isDetailVisible && navigator.canNavigateBack()) { + scope.launch { + navigator.navigateBack() } } else { - key (state.task) { - AndroidFragment( + finish() + if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) { + runBlocking { + viewModel.resetFilter() + } + } + } + } + LaunchedEffect(state.filter, state.task) { + actionMode?.finish() + actionMode = null + drawerState.close() + } + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + key (state.filter) { + val fragment = remember { mutableStateOf(null) } + AndroidFragment( fragmentState = rememberFragmentState(), - arguments = remember(state.task) { + arguments = remember(state.filter) { Bundle() - .apply { putParcelable(EXTRA_TASK, state.task) } + .apply { putParcelable(EXTRA_FILTER, state.filter) } }, modifier = Modifier.fillMaxSize(), - ) + ) { tlf -> + fragment.value = tlf + tlf.setNavigationClickListener { + scope.launch { drawerState.open() } + } + } + LaunchedEffect(fragment, windowInsets) { + fragment.value?.applyInsets(windowInsets) + } } - } - }, - ) - - if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) { - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - confirmValueChange = { true }, - ) - ModalBottomSheet( - modifier = Modifier.statusBarsPadding(), - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - onDismissRequest = { viewModel.closeDrawer() }, - contentWindowInsets = { - WindowInsets( - left = 0, - top = 0, - right = 0, - bottom = 0 - ) }, - ) { - val context = LocalContext.current - val settingsRequest = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - context.findActivity()?.recreate() - } - val scope = rememberCoroutineScope() - val bottomSearchBar = atLeastR() - TaskListDrawer( - arrangement = when { - state.menuQuery.isBlank() -> Arrangement.Top - bottomSearchBar -> Arrangement.Bottom - else -> Arrangement.Top - }, - bottomSearchBar = bottomSearchBar, - filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems, - onClick = { - when (it) { - is DrawerItem.Filter -> { - viewModel.setFilter(it.filter) - scope.launch(Dispatchers.Default) { - sheetState.hide() - viewModel.closeDrawer() - } - } - - is DrawerItem.Header -> { - viewModel.toggleCollapsed(it.header) + detailPane = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(windowInsets), + contentAlignment = Alignment.Center, + ) { + if (state.task == null) { + if (isListVisible && isDetailVisible) { + Icon( + painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground), + contentDescription = null, + modifier = Modifier.size(192.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } - } - }, - onAddClick = { - scope.launch(Dispatchers.Default) { - sheetState.hide() - viewModel.closeDrawer() - when (it.header.addIntentRc) { - FilterProvider.REQUEST_NEW_FILTER -> - NewFilterDialog.newFilterDialog().show( - supportFragmentManager, - SubheaderClickHandler.FRAG_TAG_NEW_FILTER - ) - - REQUEST_NEW_PLACE -> - startActivityForResult( - Intent( - this@MainActivity, - LocationPickerActivity::class.java - ), - REQUEST_NEW_PLACE - ) - - REQUEST_NEW_TAGS -> - startActivityForResult( - Intent( - this@MainActivity, - TagSettingsActivity::class.java - ), - REQUEST_NEW_LIST - ) - - REQUEST_NEW_LIST -> { - val account = - caldavDao.getAccount(it.header.id.toLong()) - ?: return@launch - when (it.header.subheaderType) { - NavigationDrawerSubheader.SubheaderType.CALDAV, - NavigationDrawerSubheader.SubheaderType.TASKS, - -> - startActivityForResult( - Intent( - this@MainActivity, - account.listSettingsClass() - ) - .putExtra( - EXTRA_CALDAV_ACCOUNT, - account - ), - REQUEST_NEW_LIST - ) - - else -> {} - } - } - - else -> Timber.e("Unhandled request code: $it") + } else { + key(state.task) { + AndroidFragment( + fragmentState = rememberFragmentState(), + arguments = remember(state.task) { + Bundle() + .apply { putParcelable(EXTRA_TASK, state.task) } + }, + modifier = Modifier.fillMaxSize(), + ) } } - }, - onErrorClick = { - context.startActivity(Intent(context, MainPreferences::class.java)) - }, - searchBar = { - MenuSearchBar( - begForMoney = state.begForMoney, - onDrawerAction = { - viewModel.closeDrawer() - when (it) { - DrawerAction.PURCHASE -> - if (TasksApplication.IS_GENERIC) - context.openUri(R.string.url_donate) - else - context.startActivity( - Intent( - context, - PurchaseActivity::class.java - ) - ) - - DrawerAction.SETTINGS -> - settingsRequest.launch( - Intent( - context, - MainPreferences::class.java - ) - ) - - DrawerAction.HELP_AND_FEEDBACK -> - context.startActivity( - Intent( - context, - HelpAndFeedback::class.java - ) - ) - } - }, - query = state.menuQuery, - onQueryChange = { viewModel.queryMenu(it) }, - ) - }, - ) - } + } + }, + ) } } } diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt index 1a8fd7ec4..c93b245eb 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivityViewModel.kt @@ -119,8 +119,11 @@ class MainActivityViewModel @Inject constructor( _state.update { it.copy(menuQuery = "") } } - fun openDrawer() { - _drawerOpen.update { true } + fun setDrawerState(opened: Boolean) { + _drawerOpen.update { opened } + if (!opened) { + _state.update { it.copy(menuQuery = "") } + } } init { 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 6dd777aab..1bf37c0e4 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -29,6 +29,7 @@ import androidx.appcompat.widget.Toolbar import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.app.ShareCompat @@ -190,6 +191,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL private lateinit var search: MenuItem private var mode: ActionMode? = null lateinit var themeColor: ThemeColor + private var onClickMenu: () -> Unit = {} private lateinit var binding: FragmentTaskListBinding private val listPickerLauncher = registerForListPickerResult { val selected = taskAdapter.getSelected() @@ -267,6 +269,32 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL .launchIn(viewLifecycleOwner.lifecycleScope) } + fun setNavigationClickListener(onClick: () -> Unit) { + onClickMenu = onClick + } + + fun applyInsets(windowInsets: PaddingValues) { + val density = resources.displayMetrics.density + val actionBarHeight = TypedValue.complexToDimensionPixelSize( + getData(requireContext(), android.R.attr.actionBarSize), + resources.displayMetrics + ) + with(binding.toolbar) { + val topInset = (windowInsets.calculateTopPadding().value * density).toInt() + val params = layoutParams + params.height = actionBarHeight + topInset + layoutParams = params + updatePadding(top = topInset) + } + with(binding.bottomAppBar) { + val bottomInset = (windowInsets.calculateBottomPadding().value * density).toInt() + val params = layoutParams + params.height = actionBarHeight + bottomInset + layoutParams = params + updatePadding(bottom = bottomInset) + } + } + @OptIn(ExperimentalPermissionsApi::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -333,14 +361,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } val toolbar = run { themeColor.apply(binding.bottomAppBar) - binding.bottomAppBar.isVisible = true - binding.toolbar.navigationIcon = null binding.bottomAppBar } toolbar.setOnMenuItemClickListener(this) toolbar.setNavigationOnClickListener { activity?.hideKeyboard() - mainViewModel.openDrawer() + onClickMenu() } setupMenu(toolbar) binding.banner.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) diff --git a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt index c86b52d07..2bd20f202 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/TaskListDrawer.kt @@ -12,12 +12,10 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.mandatorySystemGestures +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -82,6 +80,7 @@ fun TaskListDrawer( val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Scaffold( modifier = Modifier + .imePadding() .nestedScroll( if (bottomSearchBar) bottomAppBarScrollBehavior.nestedScrollConnection @@ -123,33 +122,20 @@ fun TaskListDrawer( ) } } - ) { contentPadding -> + ) { paddingValues -> LazyColumn( - modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues( - top = if (bottomSearchBar) 0.dp else contentPadding.calculateTopPadding(), - bottom = if (bottomSearchBar) - maxOf( - WindowInsets.mandatorySystemGestures - .asPaddingValues() - .calculateBottomPadding(), - contentPadding.calculateBottomPadding() - ) else - 48.dp - ), + modifier = Modifier.fillMaxSize().imePadding(), + contentPadding = paddingValues, verticalArrangement = arrangement, ) { items(items = filters, key = { it.key() }) { when (it) { is DrawerItem.Filter -> FilterItem( -// modifier = Modifier.animateItemPlacement(), item = it, onClick = { onClick(it) } ) is DrawerItem.Header -> HeaderItem( -// modifier = Modifier.animateItemPlacement(), item = it, canAdd = it.canAdd, toggleCollapsed = { onClick(it) },