@ -5,6 +5,7 @@
* /
* /
package com.todoroo.astrid.activity
package com.todoroo.astrid.activity
import android.app.ActivityOptions
import android.content.Intent
import android.content.Intent
import android.graphics.Color
import android.graphics.Color
import android.os.Bundle
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.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme
@ -50,6 +52,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra
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.dao.TaskDao
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.R
import org.tasks.TasksApplication
import org.tasks.TasksApplication
@ -89,10 +91,8 @@ import org.tasks.data.entity.Task
import org.tasks.data.listSettingsClass
import org.tasks.data.listSettingsClass
import org.tasks.dialogs.NewFilterDialog
import org.tasks.dialogs.NewFilterDialog
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.hideKeyboard
import org.tasks.filters.Filter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
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.FilterProvider.Companion.REQUEST_NEW_TAGS
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter
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
import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.DefaultFilterProvider
@ -137,7 +139,7 @@ class MainActivity : AppCompatActivity() {
* /
* /
override fun onCreate ( savedInstanceState : Bundle ? ) {
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
super . onCreate ( savedInstanceState )
theme . applyTheme ( this )
theme . themeBase. set ( this )
currentNightMode = nightMode
currentNightMode = nightMode
currentPro = inventory . hasPro
currentPro = inventory . hasPro
@ -153,8 +155,6 @@ class MainActivity : AppCompatActivity() {
)
)
setContent {
setContent {
val windowInsets = WindowInsets . systemBars . asPaddingValues ( )
TasksTheme ( theme = theme . themeBase . index ) {
TasksTheme ( theme = theme . themeBase . index ) {
val drawerState = rememberDrawerState (
val drawerState = rememberDrawerState (
initialValue = DrawerValue . Closed ,
initialValue = DrawerValue . Closed ,
@ -164,259 +164,283 @@ class MainActivity : AppCompatActivity() {
}
}
)
)
val state = viewModel . state . collectAsStateWithLifecycle ( ) . value
val state = viewModel . state . collectAsStateWithLifecycle ( ) . value
val currentWindowInsets = WindowInsets . systemBars . asPaddingValues ( )
ModalNavigationDrawer (
val windowInsets = remember { mutableStateOf ( currentWindowInsets ) }
drawerState = drawerState ,
val keyboard = LocalSoftwareKeyboardController . current
drawerContent = {
ModalDrawerSheet (
LaunchedEffect ( currentWindowInsets ) {
drawerState = drawerState ,
Timber . d ( " insets: $currentWindowInsets " )
windowInsets = WindowInsets ( 0 , 0 , 0 , 0 ) ,
if ( currentWindowInsets . calculateTopPadding ( ) != 0. dp || currentWindowInsets . calculateBottomPadding ( ) != 0. dp ) {
) {
windowInsets . value = currentWindowInsets
val context = LocalContext . current
}
val settingsRequest = rememberLauncherForActivityResult (
}
ActivityResultContracts . StartActivityForResult ( )
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 context = LocalContext . current
}
val settingsRequest = rememberLauncherForActivityResult (
val scope = rememberCoroutineScope ( )
ActivityResultContracts . StartActivityForResult ( )
val bottomSearchBar = atLeastR ( )
) {
TaskListDrawer (
// Activity.recreate caused window inset problems
arrangement = when {
restartActivity ( )
state . menuQuery . isBlank ( ) -> Arrangement . Top
}
bottomSearchBar -> Arrangement . Bottom
val scope = rememberCoroutineScope ( )
else -> Arrangement . Top
val bottomSearchBar = atLeastR ( )
} ,
TaskListDrawer (
bottomSearchBar = bottomSearchBar ,
arrangement = when {
filters = if ( state . menuQuery . isNotEmpty ( ) ) state . searchItems else state . drawerItems ,
state . menuQuery . isBlank ( ) -> Arrangement . Top
onClick = {
bottomSearchBar -> Arrangement . Bottom
when ( it ) {
else -> Arrangement . Top
is DrawerItem . Filter -> {
} ,
viewModel . setFilter ( it . filter )
bottomSearchBar = bottomSearchBar ,
scope . launch ( Dispatchers . Default ) {
filters = if ( state . menuQuery . isNotEmpty ( ) ) state . searchItems else state . drawerItems ,
withContext ( Dispatchers . Main ) {
onClick = {
context . findActivity ( ) ?. hideKeyboard ( )
when ( it ) {
is DrawerItem . Filter -> {
viewModel . setFilter ( it . filter )
scope . launch {
drawerState . close ( )
keyboard ?. hide ( )
}
}
drawerState . close ( )
}
}
}
is DrawerItem . Header -> {
is DrawerItem . Header -> {
viewModel . toggleCollapsed ( it . header )
viewModel . toggleCollapsed ( it . header )
}
}
}
}
} ,
} ,
onAddClick = {
onAddClick = {
scope . launch {
scope . launch ( Dispatchers . Default ) {
drawerState . close ( )
drawerState . close ( )
when ( it . header . addIntentRc ) {
when ( it . header . addIntentRc ) {
FilterProvider . REQUEST _NEW _FILTER ->
FilterProvider . REQUEST _NEW _FILTER ->
NewFilterDialog . newFilterDialog ( ) . show (
NewFilterDialog . newFilterDialog ( ) . show (
supportFragmentManager ,
supportFragmentManager ,
SubheaderClickHandler . FRAG _TAG _NEW _FILTER
SubheaderClickHandler . FRAG _TAG _NEW _FILTER
)
)
REQUEST _NEW _PLACE ->
REQUEST _NEW _PLACE ->
startActivityForResult (
startActivityForResult (
Intent (
Intent (
this @MainActivity ,
this @MainActivity ,
LocationPickerActivity :: class . java
LocationPickerActivity :: class . java
) ,
) ,
REQUEST _NEW _PLACE
REQUEST _NEW _PLACE
)
)
REQUEST _NEW _TAGS ->
REQUEST _NEW _TAGS ->
startActivityForResult (
startActivityForResult (
Intent (
Intent (
this @MainActivity ,
this @MainActivity ,
TagSettingsActivity :: class . java
TagSettingsActivity :: class . java
) ,
) ,
REQUEST _NEW _LIST
REQUEST _NEW _LIST
)
)
REQUEST _NEW _LIST -> {
REQUEST _NEW _LIST -> {
val account =
val account =
caldavDao . getAccount ( it . header . id . toLong ( ) )
caldavDao . getAccount ( it . header . id . toLong ( ) )
?: return @launch
?: return @launch
when ( it . header . subheaderType ) {
when ( it . header . subheaderType ) {
NavigationDrawerSubheader . SubheaderType . CALDAV ,
NavigationDrawerSubheader . SubheaderType . CALDAV ,
NavigationDrawerSubheader . SubheaderType . TASKS ,
NavigationDrawerSubheader . SubheaderType . TASKS ,
->
->
startActivityForResult (
startActivityForResult (
Intent (
Intent (
this @MainActivity ,
this @MainActivity ,
account . listSettingsClass ( )
account . listSettingsClass ( )
)
. putExtra (
EXTRA _CALDAV _ACCOUNT ,
account
) ,
REQUEST _NEW _LIST
)
)
. putExtra (
EXTRA _CALDAV _ACCOUNT ,
else -> { }
account
}
) ,
REQUEST _NEW _LIST
)
else -> { }
}
}
}
else -> Timber . e ( " Unhandled request code: $it " )
else -> Timber . e ( " Unhandled request code: $it " )
}
}
}
}
} ,
} ,
onErrorClick = {
onErrorClick = {
context . startActivity ( Intent ( context , MainPreferences :: class . java ) )
context . startActivity ( Intent ( context , MainPreferences :: class . java ) )
} ,
} ,
searchBar = {
searchBar = {
MenuSearchBar (
MenuSearchBar (
begForMoney = state . begForMoney ,
begForMoney = state . begForMoney ,
onDrawerAction = {
onDrawerAction = {
scope . launch {
scope . launch {
drawerState . close ( )
drawerState . close ( )
when ( it ) {
when ( it ) {
DrawerAction . PURCHASE ->
DrawerAction . PURCHASE ->
if ( TasksApplication . IS _GENERIC )
if ( TasksApplication . IS _GENERIC )
context . openUri ( R . string . url _donate )
context . openUri ( R . string . url _donate )
else
else
context . startActivity (
context . startActivity (
Intent (
context ,
PurchaseActivity :: class . java
)
)
DrawerAction . SETTINGS ->
settingsRequest . launch (
Intent (
Intent (
context ,
context ,
PurchaseActivity :: class . java
MainPreferences :: class . java
)
)
)
)
DrawerAction . SETTINGS ->
DrawerAction . HELP _AND _FEEDBACK ->
settingsRequest . launch (
context . startActivity (
Intent (
Intent (
context ,
context ,
MainPreferences :: class . java
HelpAndFeedback :: class . java
)
)
)
DrawerAction . HELP _AND _FEEDBACK ->
context . startActivity (
Intent (
context ,
HelpAndFeedback :: class . java
)
)
)
}
}
}
}
} ,
} ,
query = state . menuQuery ,
query = state . menuQuery ,
onQueryChange = { viewModel . queryMenu ( it ) } ,
onQueryChange = { viewModel . queryMenu ( it ) } ,
)
)
},
},
)
)
}
}
}
}
) {
) {
val scope = rememberCoroutineScope ( )
val navigator = rememberListDetailPaneScaffoldNavigator (
calculatePaneScaffoldDirective (
windowAdaptiveInfo = currentWindowAdaptiveInfo ( ) ,
verticalHingePolicy = HingePolicy . AlwaysAvoid ,
) . copy (
horizontalPartitionSpacerSize = 0. dp ,
verticalPartitionSpacerSize = 0. dp ,
) ,
)
LaunchedEffect ( state . task ) {
val isListVisible =
if ( state . task == null ) {
navigator . scaffoldValue [ ListDetailPaneScaffoldRole . List ] == PaneAdaptedValue . Expanded
if ( intent . finishAffinity ) {
val isDetailVisible =
finishAffinity ( )
navigator . scaffoldValue [ ListDetailPaneScaffoldRole . Detail ] == PaneAdaptedValue . Expanded
} else {
val scope = rememberCoroutineScope ( )
if ( intent . removeTask && intent . broughtToFront ) {
moveTaskToBack ( true )
LaunchedEffect ( state . task ) {
}
if ( state . task == null ) {
keyboard ?. hide ( )
navigator . navigateTo ( pane = ThreePaneScaffoldRole . Secondary )
}
} else {
navigator . navigateTo ( pane = ThreePaneScaffoldRole . Primary )
}
}
BackHandler ( enabled = state . task == null ) {
Timber . d ( " onBackPressed " )
if ( intent . finishAffinity ) {
if ( intent . finishAffinity ) {
finishAffinity ( )
finishAffinity ( )
} else if ( isDetailVisible && navigator . canNavigateBack ( ) ) {
scope . launch {
navigator . navigateBack ( )
}
} else {
} else {
if ( intent . removeTask && intent . broughtToFront ) {
finish ( )
moveTaskToBack ( true )
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 )
}
}
}
LaunchedEffect ( state . filter , state . task ) {
actionMode ?. finish ( )
BackHandler ( enabled = state . task == null ) {
actionMode = null
Timber . d ( " onBackPressed " )
if ( state . task == null ) {
if ( intent . finishAffinity ) {
keyboard ?. hide ( )
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 ( )
}
}
}
drawerState . close ( )
}
}
}
ListDetailPaneScaffold (
LaunchedEffect ( state . filter , state . task ) {
directive = navigator . scaffoldDirective ,
actionMode ?. finish ( )
value = navigator . scaffoldValue ,
actionMode = null
listPane = {
drawerState . close ( )
key ( state . filter ) {
}
val fragment = remember { mutableStateOf < TaskListFragment ? > ( null ) }
ListDetailPaneScaffold (
AndroidFragment < TaskListFragment > (
directive = navigator . scaffoldDirective ,
fragmentState = rememberFragmentState ( ) ,
value = navigator . scaffoldValue ,
arguments = remember ( state . filter ) {
listPane = {
Bundle ( )
key ( state . filter ) {
. apply { putParcelable ( EXTRA _FILTER , state . filter ) }
val fragment = remember { mutableStateOf < TaskListFragment ? > ( null ) }
} ,
AndroidFragment < TaskListFragment > (
modifier = Modifier . fillMaxSize ( ) ,
fragmentState = rememberFragmentState ( ) ,
) { tlf ->
arguments = remember ( state . filter ) {
fragment . value = tlf
Bundle ( )
tlf . applyInsets ( windowInsets . value )
. apply { putParcelable ( EXTRA _FILTER , state . filter ) }
tlf . setNavigationClickListener {
} ,
scope . launch { drawerState . open ( ) }
modifier = Modifier . fillMaxSize ( ) ,
}
) { tlf ->
fragment . value = tlf
tlf . setNavigationClickListener {
scope . launch { drawerState . open ( ) }
}
}
}
LaunchedEffect ( fragment , windowInsets ) {
LaunchedEffect ( fragment , windowInsets ) {
fragment . value ?. applyInsets ( windowInsets . value )
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 ,
)
}
}
} else {
}
key ( state . task ) {
} ,
AndroidFragment < TaskEditFragment > (
detailPane = {
fragmentState = rememberFragmentState ( ) ,
val direction = LocalLayoutDirection . current
arguments = remember ( state . task ) {
Box (
Bundle ( )
modifier = Modifier
. apply { putParcelable ( EXTRA _TASK , state . task ) }
. fillMaxSize ( )
} ,
. padding (
modifier = Modifier . fillMaxSize ( ) ,
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 ( ) {
override fun onResume ( ) {
super . onResume ( )
super . onResume ( )
if ( currentNightMode != nightMode || currentPro != inventory . hasPro ) {
if ( currentNightMode != nightMode || currentPro != inventory . hasPro ) {
re create ( )
re startActivity ( )
return
return
}
}
if ( preferences . getBoolean ( R . string . p _just _updated , false ) ) {
if ( preferences . getBoolean ( R . string . p _just _updated , false ) ) {
@ -520,6 +544,17 @@ class MainActivity : AppCompatActivity() {
actionMode = mode
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 {
companion object {
/** For indicating the new list screen should be launched at fragment setup time */
/** For indicating the new list screen should be launched at fragment setup time */
const val OPEN _FILTER = " open_filter " // $NON-NLS-1$
const val OPEN _FILTER = " open_filter " // $NON-NLS-1$