@ -5,6 +5,7 @@
* /
package com.todoroo.astrid.activity
import android.app.ActivityOptions
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
@ -21,12 +22,13 @@ 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.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
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
@ -50,6 +52,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra
@ -64,10 +68,8 @@ import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCreator
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
@ -89,10 +91,8 @@ import org.tasks.data.entity.Task
import org.tasks.data.listSettingsClass
import org.tasks.dialogs.NewFilterDialog
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.hideKeyboard
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
@ -100,6 +100,8 @@ import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_PLACE
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_TAGS
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter
import org.tasks.kmp.org.tasks.compose.TouchSlopMultiplier
import org.tasks.kmp.org.tasks.compose.rememberImeState
import org.tasks.location.LocationPickerActivity
import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.preferences.DefaultFilterProvider
@ -137,7 +139,7 @@ class MainActivity : AppCompatActivity() {
* /
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
theme . applyTheme ( this )
theme . themeBase. set ( this )
currentNightMode = nightMode
currentPro = inventory . hasPro
@ -153,8 +155,6 @@ class MainActivity : AppCompatActivity() {
)
setContent {
val windowInsets = WindowInsets . systemBars . asPaddingValues ( )
TasksTheme ( theme = theme . themeBase . index ) {
val drawerState = rememberDrawerState (
initialValue = DrawerValue . Closed ,
@ -164,259 +164,283 @@ class MainActivity : AppCompatActivity() {
}
)
val state = viewModel . state . collectAsStateWithLifecycle ( ) . value
ModalNavigationDrawer (
drawerState = drawerState ,
drawerContent = {
ModalDrawerSheet (
drawerState = drawerState ,
windowInsets = WindowInsets ( 0 , 0 , 0 , 0 ) ,
) {
val context = LocalContext . current
val settingsRequest = rememberLauncherForActivityResult (
ActivityResultContracts . StartActivityForResult ( )
val currentWindowInsets = WindowInsets . systemBars . asPaddingValues ( )
val windowInsets = remember { mutableStateOf ( currentWindowInsets ) }
val keyboard = LocalSoftwareKeyboardController . current
LaunchedEffect ( currentWindowInsets ) {
Timber . d ( " insets: $currentWindowInsets " )
if ( currentWindowInsets . calculateTopPadding ( ) != 0. dp || currentWindowInsets . calculateBottomPadding ( ) != 0. dp ) {
windowInsets . value = currentWindowInsets
}
}
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
TouchSlopMultiplier {
ModalNavigationDrawer (
drawerState = drawerState ,
gesturesEnabled = isListVisible ,
drawerContent = {
ModalDrawerSheet (
drawerState = drawerState ,
windowInsets = WindowInsets ( 0 , 0 , 0 , 0 ) ,
) {
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 ) {
withContext ( Dispatchers . Main ) {
context . findActivity ( ) ?. hideKeyboard ( )
val context = LocalContext . current
val settingsRequest = rememberLauncherForActivityResult (
ActivityResultContracts . StartActivityForResult ( )
) {
// Activity.recreate caused window inset problems
restartActivity ( )
}
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 {
drawerState . close ( )
keyboard ?. hide ( )
}
drawerState . close ( )
}
}
is DrawerItem . Header -> {
viewModel . toggleCollapsed ( it . header )
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 ( )
} ,
onAddClick = {
scope . launch {
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
)
. putExtra (
EXTRA _CALDAV _ACCOUNT ,
account
) ,
REQUEST _NEW _LIST
)
else -> { }
else -> { }
}
}
}
else -> Timber . e ( " Unhandled request code: $it " )
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 (
} ,
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 ,
PurchaseActivity :: class . java
MainPreferences :: class . java
)
)
DrawerAction . SETTINGS ->
settingsRequest . launch (
Intent (
context ,
MainPreferences :: class . java
)
)
DrawerAction . HELP _AND _FEEDBACK ->
context . startActivity (
Intent (
context ,
HelpAndFeedback :: class . java
DrawerAction . HELP _AND _FEEDBACK ->
context . startActivity (
Intent (
context ,
HelpAndFeedback :: class . java
)
)
)
}
}
}
} ,
query = state . menuQuery ,
onQueryChange = { viewModel . queryMenu ( it ) } ,
)
},
)
} ,
query = state . menuQuery ,
onQueryChange = { viewModel . queryMenu ( it ) } ,
)
},
)
}
}
}
) {
val navigator = rememberListDetailPaneScaffoldNavigator (
calculatePaneScaffoldDirective (
windowAdaptiveInfo = currentWindowAdaptiveInfo ( ) ,
verticalHingePolicy = HingePolicy . AlwaysAvoid ,
) . copy (
horizontalPartitionSpacerSize = 0. dp ,
verticalPartitionSpacerSize = 0. dp ,
) ,
) {
val scope = rememberCoroutineScope ( )
)
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 ) {
LaunchedEffect ( state . task ) {
if ( state . task == null ) {
if ( intent . finishAffinity ) {
finishAffinity ( )
} else {
if ( intent . removeTask && intent . broughtToFront ) {
moveTaskToBack ( true )
}
keyboard ?. hide ( )
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 {
if ( intent . removeTask && intent . broughtToFront ) {
moveTaskToBack ( true )
finish ( )
if ( ! preferences . getBoolean ( R . string . p _open _last _viewed _list , true ) ) {
runBlocking {
viewModel . resetFilter ( )
}
}
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 ( )
}
LaunchedEffect ( state . filter , state . task ) {
actionMode ?. finish ( )
actionMode = null
if ( state . task == null ) {
keyboard ?. hide ( )
}
drawerState . close ( )
}
}
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 < TaskListFragment ? > ( null ) }
AndroidFragment < TaskListFragment > (
fragmentState = rememberFragmentState ( ) ,
arguments = remember ( state . filter ) {
Bundle ( )
. apply { putParcelable ( EXTRA _FILTER , state . filter ) }
} ,
modifier = Modifier . fillMaxSize ( ) ,
) { tlf ->
fragment . value = tlf
tlf . setNavigationClickListener {
scope . launch { drawerState . open ( ) }
ListDetailPaneScaffold (
directive = navigator . scaffoldDirective ,
value = navigator . scaffoldValue ,
listPane = {
key ( state . filter ) {
val fragment = remember { mutableStateOf < TaskListFragment ? > ( null ) }
AndroidFragment < TaskListFragment > (
fragmentState = rememberFragmentState ( ) ,
arguments = remember ( state . filter ) {
Bundle ( )
. apply { putParcelable ( EXTRA _FILTER , state . filter ) }
} ,
modifier = Modifier . fillMaxSize ( ) ,
) { tlf ->
fragment . value = tlf
tlf . applyInsets ( windowInsets . value )
tlf . setNavigationClickListener {
scope . launch { drawerState . open ( ) }
}
}
}
LaunchedEffect ( fragment , windowInsets ) {
fragment . value ?. applyInsets ( windowInsets )
}
}
} ,
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 ,
)
LaunchedEffect ( fragment , windowInsets ) {
fragment . value ?. applyInsets ( windowInsets . value )
}
} else {
key ( state . task ) {
AndroidFragment < TaskEditFragment > (
fragmentState = rememberFragmentState ( ) ,
arguments = remember ( state . task ) {
Bundle ( )
. apply { putParcelable ( EXTRA _TASK , state . task ) }
} ,
modifier = Modifier . fillMaxSize ( ) ,
)
}
} ,
detailPane = {
val direction = LocalLayoutDirection . current
Box (
modifier = Modifier
. fillMaxSize ( )
. padding (
top = windowInsets . value . calculateTopPadding ( ) ,
start = windowInsets . value . calculateStartPadding ( direction ) ,
end = windowInsets . value . calculateEndPadding ( direction ) ,
bottom = if ( rememberImeState ( ) . value )
WindowInsets . ime . asPaddingValues ( ) . calculateBottomPadding ( )
else
windowInsets . value . calculateBottomPadding ( )
) ,
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 ,
)
}
} else {
key ( state . task ) {
AndroidFragment < TaskEditFragment > (
fragmentState = rememberFragmentState ( ) ,
arguments = remember ( state . task ) {
Bundle ( )
. apply { putParcelable ( EXTRA _TASK , state . task ) }
} ,
modifier = Modifier . fillMaxSize ( ) ,
)
}
}
}
}
} ,
)
} ,
)
}
}
}
}
@ -501,7 +525,7 @@ class MainActivity : AppCompatActivity() {
override fun onResume ( ) {
super . onResume ( )
if ( currentNightMode != nightMode || currentPro != inventory . hasPro ) {
re create ( )
re startActivity ( )
return
}
if ( preferences . getBoolean ( R . string . p _just _updated , false ) ) {
@ -520,6 +544,17 @@ class MainActivity : AppCompatActivity() {
actionMode = mode
}
private fun restartActivity ( ) {
finish ( )
startActivity (
Intent ( this , MainActivity :: class . java ) ,
ActivityOptions . makeCustomAnimation (
this @MainActivity ,
android . R . anim . fade _in , android . R . anim . fade _out
) . toBundle ( )
)
}
companion object {
/** For indicating the new list screen should be launched at fragment setup time */
const val OPEN _FILTER = " open_filter " // $NON-NLS-1$