Adaptive layout

pull/3336/head
Alex Baker 10 months ago
parent c48cfc32d1
commit 1f659c3dc6

@ -154,6 +154,7 @@ dependencies {
implementation(projects.data) implementation(projects.data)
implementation(projects.kmp) implementation(projects.kmp)
implementation(projects.icons) implementation(projects.icons)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) { implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit") exclude(group = "junit")
@ -205,6 +206,7 @@ dependencies {
implementation(libs.persistent.cookiejar) implementation(libs.persistent.cookiejar)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference) implementation(libs.androidx.preference)

@ -8,46 +8,65 @@ package com.todoroo.astrid.activity
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.HingePolicy
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
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.rememberModalBottomSheetState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.AndroidUtilities.atLeastR import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK
import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER
import com.todoroo.astrid.adapter.SubheaderClickHandler 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.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.TasksApplication import org.tasks.TasksApplication
@ -66,9 +85,7 @@ import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.data.getLocation
import org.tasks.data.listSettingsClass import org.tasks.data.listSettingsClass
import org.tasks.databinding.TaskListActivityBinding
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.findActivity
@ -91,12 +108,12 @@ import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment
import org.tasks.ui.MainActivityEvent import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus import org.tasks.ui.MainActivityEventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class)
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@ -117,7 +134,6 @@ class MainActivity : AppCompatActivity() {
private var currentNightMode = 0 private var currentNightMode = 0
private var currentPro = false private var currentPro = false
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private lateinit var binding: TaskListActivityBinding
/** @see android.app.Activity.onCreate /** @see android.app.Activity.onCreate
*/ */
@ -127,10 +143,6 @@ class MainActivity : AppCompatActivity() {
theme.applyTheme(this) theme.applyTheme(this)
currentNightMode = nightMode currentNightMode = nightMode
currentPro = inventory.hasPro currentPro = inventory.hasPro
binding = TaskListActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
logIntent("onCreate")
handleIntent()
enableEdgeToEdge( enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto( statusBarStyle = SystemBarStyle.auto(
@ -143,9 +155,112 @@ class MainActivity : AppCompatActivity() {
) )
) )
binding.composeView.setContent { setContent {
if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) {
TasksTheme(theme = theme.themeBase.index) { TasksTheme(theme = theme.themeBase.index) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
verticalHingePolicy = HingePolicy.AlwaysAvoid,
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
defaultPanePreferredWidth = screenWidth / 2,
),
)
val state = viewModel.state.collectAsStateWithLifecycle().value
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 = navigator.canNavigateBack() && state.task == null) {
if (intent.finishAffinity) {
finishAffinity()
} else if (isDetailVisible) {
scope.launch {
navigator.navigateBack()
}
} else {
finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
viewModel.resetFilter()
}
}
}
}
val taskListState = key (state.filter) {
rememberFragmentState()
}
val taskEditState = key (state.task) {
rememberFragmentState()
}
LaunchedEffect(state.filter, state.task) {
clearUi()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AndroidFragment<TaskListFragment>(
fragmentState = taskListState,
arguments = remember (state.filter) {
Bundle()
.apply { putParcelable(EXTRA_FILTER, state.filter) }
},
modifier = Modifier.fillMaxSize(),
)
},
detailPane = {
if (state.task == null) {
if (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,
)
}
}
} else {
AndroidFragment<TaskEditFragment>(
fragmentState = taskEditState,
arguments = remember(state.task) {
Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) }
},
modifier = Modifier.fillMaxSize(),
onUpdate = {
Timber.d("On updated")
}
)
}
},
)
if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) {
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true, skipPartiallyExpanded = true,
confirmValueChange = { true }, confirmValueChange = { true },
@ -164,7 +279,6 @@ class MainActivity : AppCompatActivity() {
) )
}, },
) { ) {
val state = viewModel.state.collectAsStateWithLifecycle().value
val context = LocalContext.current val context = LocalContext.current
val settingsRequest = rememberLauncherForActivityResult( val settingsRequest = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
@ -227,10 +341,12 @@ class MainActivity : AppCompatActivity() {
REQUEST_NEW_LIST -> { REQUEST_NEW_LIST -> {
val account = val account =
caldavDao.getAccount(it.header.id.toLong()) ?: return@launch caldavDao.getAccount(it.header.id.toLong())
?: 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,
@ -297,6 +413,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
logIntent("onCreate")
handleIntent()
eventBus eventBus
.onEach(this::process) .onEach(this::process)
@ -307,77 +425,6 @@ class MainActivity : AppCompatActivity() {
updateSystemBars(viewModel.state.value.filter) updateSystemBars(viewModel.state.value.filter)
} }
} }
viewModel
.state
.flowWithLifecycle(lifecycle)
.map { it.filter to it.task }
.distinctUntilChanged()
.onEach { (newFilter, task) ->
Timber.d("filter: $newFilter task: $task")
val existingTlf =
supportFragmentManager.findFragmentByTag(FRAG_TAG_TASK_LIST) as TaskListFragment?
val existingFilter = existingTlf?.getFilter()
val tlf = if (
existingFilter != null
&& existingFilter.areItemsTheSame(newFilter)
&& existingFilter == newFilter
// && check if manual sort changed
) {
existingTlf
} else {
clearUi()
TaskListFragment.newTaskListFragment(newFilter)
}
val existingTef =
supportFragmentManager.findFragmentByTag(FRAG_TAG_TASK_EDIT) as TaskEditFragment?
val transaction = supportFragmentManager.beginTransaction()
if (task == null) {
if (intent.finishAffinity) {
finishAffinity()
} else if (existingTef != null) {
if (intent.removeTask && intent.broughtToFront) {
moveTaskToBack(true)
}
hideKeyboard()
transaction
.replace(R.id.detail, newEmptyTaskEditFragment())
.runOnCommit {
if (isSinglePaneLayout) {
binding.master.visibility = View.VISIBLE
binding.detail.visibility = View.GONE
}
}
}
} else if (task != existingTef?.task) {
existingTef?.save(remove = false)
transaction
.replace(R.id.detail, newTaskEditFragment(task), FRAG_TAG_TASK_EDIT)
.runOnCommit {
if (isSinglePaneLayout) {
binding.detail.visibility = View.VISIBLE
binding.master.visibility = View.GONE
}
}
} else if (task == existingTef.task) {
transaction
.runOnCommit {
if (isSinglePaneLayout) {
binding.detail.visibility = View.VISIBLE
binding.master.visibility = View.GONE
}
}
}
defaultFilterProvider.setLastViewedFilter(newFilter)
theme
.withThemeColor(getFilterColor(newFilter))
.applyToContext(this) // must happen before committing fragment
transaction
.replace(R.id.master, tlf, FRAG_TAG_TASK_LIST)
.runOnCommit { updateSystemBars(newFilter) }
.commit()
}
.launchIn(lifecycleScope)
} }
private fun process(event: MainActivityEvent) = when (event) { private fun process(event: MainActivityEvent) = when (event) {
@ -494,30 +541,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
private suspend fun newTaskEditFragment(task: Task): TaskEditFragment {
AndroidUtilities.assertMainThread()
clearUi()
return coroutineScope {
withContext(Dispatchers.Default) {
val freshTask = async { if (task.isNew) task else taskDao.fetch(task.id) ?: task }
val list = async { defaultFilterProvider.getList(task) }
val location = async { locationDao.getLocation(task, preferences) }
val tags = async { tagDataDao.getTags(task) }
val alarms = async { alarmDao.getAlarms(task) }
newTaskEditFragment(
freshTask.await(),
list.await(),
location.await(),
tags.await(),
alarms.await(),
)
}
}
}
private val isSinglePaneLayout: Boolean
get() = !resources.getBoolean(R.bool.two_pane_layout)
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
actionMode = mode actionMode = mode

@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -44,10 +43,7 @@ import org.tasks.compose.edit.ListRow
import org.tasks.compose.edit.PriorityRow import org.tasks.compose.edit.PriorityRow
import org.tasks.compose.edit.TaskEditScreen import org.tasks.compose.edit.TaskEditScreen
import org.tasks.compose.edit.TitleRow import org.tasks.compose.edit.TitleRow
import org.tasks.data.Location
import org.tasks.data.dao.UserActivityDao import org.tasks.data.dao.UserActivityDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
@ -55,7 +51,6 @@ import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.is24HourFormat import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.hideKeyboard import org.tasks.extensions.hideKeyboard
import org.tasks.filters.Filter
import org.tasks.kmp.org.tasks.time.DateStyle import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
@ -118,15 +113,6 @@ class TaskEditFragment : Fragment() {
} }
TasksTheme(theme = theme.themeBase.index,) { TasksTheme(theme = theme.themeBase.index,) {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
BackHandler {
if (viewState.backButtonSavesTask) {
lifecycleScope.launch {
save()
}
} else {
discardButtonClick()
}
}
LaunchedEffect(viewState.isNew) { LaunchedEffect(viewState.isNew) {
if (!viewState.isNew) { if (!viewState.isNew) {
notificationManager.cancel(viewState.task.id) notificationManager.cancel(viewState.task.id)
@ -140,7 +126,15 @@ class TaskEditFragment : Fragment() {
.value, .value,
save = { lifecycleScope.launch { save() } }, save = { lifecycleScope.launch { save() } },
discard = { discardButtonClick() }, discard = { discardButtonClick() },
onBackPressed = { activity?.onBackPressed() }, onBackPressed = {
if (viewState.backButtonSavesTask) {
lifecycleScope.launch {
save()
}
} else {
discardButtonClick()
}
},
delete = { deleteButtonClick() }, delete = { deleteButtonClick() },
openBeastModeSettings = { openBeastModeSettings = {
editViewModel.hideBeastModeHint(click = true) editViewModel.hideBeastModeHint(click = true)
@ -351,10 +345,6 @@ class TaskEditFragment : Fragment() {
companion object { companion object {
const val EXTRA_TASK = "extra_task" const val EXTRA_TASK = "extra_task"
const val EXTRA_LIST = "extra_list"
const val EXTRA_LOCATION = "extra_location"
const val EXTRA_TAGS = "extra_tags"
const val EXTRA_ALARMS = "extra_alarms"
const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker" const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
@ -368,24 +358,6 @@ class TaskEditFragment : Fragment() {
newDateTime(this).endOfDay().isBeforeNow newDateTime(this).endOfDay().isBeforeNow
} }
fun newTaskEditFragment(
task: Task,
list: Filter,
location: Location?,
tags: ArrayList<TagData>,
alarms: ArrayList<Alarm>,
): TaskEditFragment {
val taskEditFragment = TaskEditFragment()
val arguments = Bundle()
arguments.putParcelable(EXTRA_TASK, task)
arguments.putParcelable(EXTRA_LIST, list)
arguments.putParcelable(EXTRA_LOCATION, location)
arguments.putParcelableArrayList(EXTRA_TAGS, tags)
arguments.putParcelableArrayList(EXTRA_ALARMS, alarms)
taskEditFragment.arguments = arguments
return taskEditFragment
}
fun Modifier.gesturesDisabled(disabled: Boolean = true) = fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) { if (disabled) {
pointerInput(Unit) { pointerInput(Unit) {

@ -73,7 +73,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
@ -278,13 +277,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) { requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) {
if (search.isActionViewExpanded) { if (search.isActionViewExpanded) {
search.collapseActionView() search.collapseActionView()
} else {
requireActivity().finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
mainViewModel.resetFilter()
}
}
} }
} }
binding = FragmentTaskListBinding.inflate(inflater, container, false) binding = FragmentTaskListBinding.inflate(inflater, container, false)
@ -1103,7 +1095,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
const val ACTION_DELETED = "action_deleted" const val ACTION_DELETED = "action_deleted"
private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids" private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"
private const val VOICE_RECOGNITION_REQUEST_CODE = 1234 private const val VOICE_RECOGNITION_REQUEST_CODE = 1234
private const val EXTRA_FILTER = "extra_filter" const val EXTRA_FILTER = "extra_filter"
private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker" private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker"
private const val FRAG_TAG_PRIORITY_PICKER = "frag_tag_priority_picker" private const val FRAG_TAG_PRIORITY_PICKER = "frag_tag_priority_picker"
private const val REQUEST_TAG_TASKS = 10106 private const val REQUEST_TAG_TASKS = 10106

@ -1,6 +1,7 @@
package org.tasks.compose.edit package org.tasks.compose.edit
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -50,6 +51,9 @@ fun TaskEditScreen(
deleteComment: (UserActivity) -> Unit, deleteComment: (UserActivity) -> Unit,
content: @Composable (Int) -> Unit, content: @Composable (Int) -> Unit,
) { ) {
BackHandler {
onBackPressed()
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(

@ -163,8 +163,8 @@ class DefaultFilterProvider @Inject constructor(
} }
} }
suspend fun getList(task: Task): Filter { suspend fun getList(task: Task): CaldavFilter {
var originalList: Filter? = null var originalList: CaldavFilter? = null
if (task.isNew) { if (task.isNew) {
if (task.hasTransitory(GoogleTask.KEY)) { if (task.hasTransitory(GoogleTask.KEY)) {
val listId = task.getTransitory<String>(GoogleTask.KEY)!! val listId = task.getTransitory<String>(GoogleTask.KEY)!!

@ -53,8 +53,9 @@ class SubtaskControlSet : TaskEditControlFragment() {
listViewModel = ViewModelProvider(requireParentFragment())[TaskListViewModel::class.java] listViewModel = ViewModelProvider(requireParentFragment())[TaskListViewModel::class.java]
setContent { setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
val originalState = viewModel.originalState.collectAsStateWithLifecycle().value
SubtaskRow( SubtaskRow(
originalFilter = viewModel.originalState.list, originalFilter = originalState.list,
filter = viewState.list, filter = viewState.list,
hasParent = viewState.hasParent, hasParent = viewState.hasParent,
existingSubtasks = if (viewModel.viewState.collectAsStateWithLifecycle().value.isNew) { existingSubtasks = if (viewModel.viewState.collectAsStateWithLifecycle().value.isNew) {

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -18,7 +17,6 @@ abstract class TaskEditControlFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val composeView = ComposeView(requireActivity()) val composeView = ComposeView(requireActivity())
composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
viewModel = ViewModelProvider(requireParentFragment())[TaskEditViewModel::class.java] viewModel = ViewModelProvider(requireParentFragment())[TaskEditViewModel::class.java]
bind(composeView) bind(composeView)
createView(savedInstanceState) createView(savedInstanceState)

@ -59,6 +59,8 @@ import org.tasks.data.entity.Alarm.Companion.whenDue
import org.tasks.data.entity.Alarm.Companion.whenOverdue import org.tasks.data.entity.Alarm.Companion.whenOverdue
import org.tasks.data.entity.Alarm.Companion.whenStarted import org.tasks.data.entity.Alarm.Companion.whenStarted
import org.tasks.data.entity.Attachment import org.tasks.data.entity.Attachment
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.FORCE_CALDAV_SYNC import org.tasks.data.entity.FORCE_CALDAV_SYNC
import org.tasks.data.entity.FORCE_MICROSOFT_SYNC import org.tasks.data.entity.FORCE_MICROSOFT_SYNC
@ -69,11 +71,13 @@ import org.tasks.data.entity.Task.Companion.NOTIFY_MODE_NONSTOP
import org.tasks.data.entity.Task.Companion.hasDueTime import org.tasks.data.entity.Task.Companion.hasDueTime
import org.tasks.data.entity.TaskAttachment import org.tasks.data.entity.TaskAttachment
import org.tasks.data.entity.UserActivity import org.tasks.data.entity.UserActivity
import org.tasks.data.getLocation
import org.tasks.data.setPicture import org.tasks.data.setPicture
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
@ -107,6 +111,7 @@ class TaskEditViewModel @Inject constructor(
private val userActivityDao: UserActivityDao, private val userActivityDao: UserActivityDao,
private val alarmDao: AlarmDao, private val alarmDao: AlarmDao,
private val taskAttachmentDao: TaskAttachmentDao, private val taskAttachmentDao: TaskAttachmentDao,
private val defaultFilterProvider: DefaultFilterProvider,
) : ViewModel() { ) : ViewModel() {
data class ViewState( data class ViewState(
val task: Task, val task: Task,
@ -143,11 +148,7 @@ class TaskEditViewModel @Inject constructor(
?.apply { notes = notes?.stripCarriageReturns() } // copying here broke tests 🙄 ?.apply { notes = notes?.stripCarriageReturns() } // copying here broke tests 🙄
?: throw IllegalArgumentException("task is null") ?: throw IllegalArgumentException("task is null")
private var _originalState: ViewState private val _originalState = MutableStateFlow(
val originalState: ViewState
get() = _originalState
private val _viewState = MutableStateFlow(
ViewState( ViewState(
task = task, task = task,
showBeastModeHint = !preferences.shownBeastModeHint, showBeastModeHint = !preferences.shownBeastModeHint,
@ -156,11 +157,6 @@ class TaskEditViewModel @Inject constructor(
backButtonSavesTask = preferences.backButtonSavesTask(), backButtonSavesTask = preferences.backButtonSavesTask(),
isReadOnly = task.readOnly, isReadOnly = task.readOnly,
linkify = preferences.linkify, linkify = preferences.linkify,
list = savedStateHandle[TaskEditFragment.EXTRA_LIST]!!,
location = savedStateHandle[TaskEditFragment.EXTRA_LOCATION],
tags = savedStateHandle.get<ArrayList<TagData>>(TaskEditFragment.EXTRA_TAGS)
?.toPersistentSet()
?: persistentSetOf(),
calendar = if (task.isNew && permissionChecker.canAccessCalendars()) { calendar = if (task.isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar preferences.defaultCalendar
} else { } else {
@ -198,11 +194,17 @@ class TaskEditViewModel @Inject constructor(
} }
} }
} else { } else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!! emptyList()
}.toPersistentSet(), }.toPersistentSet(),
multilineTitle = preferences.multilineTitle, multilineTitle = preferences.multilineTitle,
location = null,
tags = persistentSetOf(),
list = CaldavFilter(calendar = CaldavCalendar(), account = CaldavAccount()),
) )
) )
val originalState: StateFlow<ViewState> = _originalState
private val _viewState = MutableStateFlow(originalState.value)
val viewState: StateFlow<ViewState> = _viewState val viewState: StateFlow<ViewState> = _viewState
var eventUri = MutableStateFlow(task.calendarURI) var eventUri = MutableStateFlow(task.calendarURI)
@ -269,7 +271,7 @@ class TaskEditViewModel @Inject constructor(
fun hasChanges(): Boolean { fun hasChanges(): Boolean {
val viewState = _viewState.value val viewState = _viewState.value
return originalState != viewState || return originalState.value != viewState ||
(viewState.isNew && viewState.task.title?.isNotBlank() == true) || // text shared to tasks (viewState.isNew && viewState.task.title?.isNotBlank() == true) || // text shared to tasks
task.dueDate != dueDate.value || task.dueDate != dueDate.value ||
task.hideUntil != startDate.value || task.hideUntil != startDate.value ||
@ -315,8 +317,8 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(task) taskDao.createNew(task)
} }
val selectedLocation = _viewState.value.location val selectedLocation = _viewState.value.location
if ((isNew && selectedLocation != null) || originalState.location != selectedLocation) { if ((isNew && selectedLocation != null) || originalState.value.location != selectedLocation) {
originalState.location?.let { location -> originalState.value.location?.let { location ->
if (location.geofence.id > 0) { if (location.geofence.id > 0) {
locationDao.delete(location.geofence) locationDao.delete(location.geofence)
geofenceApi.update(location.place) geofenceApi.update(location.place)
@ -337,7 +339,7 @@ class TaskEditViewModel @Inject constructor(
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
} }
val selectedTags = _viewState.value.tags val selectedTags = _viewState.value.tags
if ((isNew && selectedTags.isNotEmpty()) || originalState.tags.toHashSet() != selectedTags.toHashSet()) { if ((isNew && selectedTags.isNotEmpty()) || originalState.value.tags.toHashSet() != selectedTags.toHashSet()) {
tagDao.applyTags(task, tagDataDao, selectedTags) tagDao.applyTags(task, tagDataDao, selectedTags)
task.putTransitory(FORCE_CALDAV_SYNC, true) task.putTransitory(FORCE_CALDAV_SYNC, true)
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
@ -360,7 +362,7 @@ class TaskEditViewModel @Inject constructor(
if ( if (
(isNew && _viewState.value.alarms.isNotEmpty()) || (isNew && _viewState.value.alarms.isNotEmpty()) ||
originalState.alarms != _viewState.value.alarms originalState.value.alarms != _viewState.value.alarms
) { ) {
alarmService.synchronizeAlarms(task.id, _viewState.value.alarms.toMutableSet()) alarmService.synchronizeAlarms(task.id, _viewState.value.alarms.toMutableSet())
task.putTransitory(FORCE_CALDAV_SYNC, true) task.putTransitory(FORCE_CALDAV_SYNC, true)
@ -369,7 +371,7 @@ class TaskEditViewModel @Inject constructor(
taskDao.save(task, null) taskDao.save(task, null)
val selectedList = _viewState.value.list val selectedList = _viewState.value.list
if (isNew || originalState.list != selectedList) { if (isNew || originalState.value.list != selectedList) {
task.parent = 0 task.parent = 0
taskMover.move(listOf(task.id), selectedList) taskMover.move(listOf(task.id), selectedList)
} }
@ -417,13 +419,13 @@ class TaskEditViewModel @Inject constructor(
} }
} }
if (originalState.attachments != _viewState.value.attachments) { if (originalState.value.attachments != _viewState.value.attachments) {
originalState.attachments originalState.value.attachments
.minus(_viewState.value.attachments) .minus(_viewState.value.attachments)
.map { it.remoteId } .map { it.remoteId }
.let { taskAttachmentDao.delete(task.id, it) } .let { taskAttachmentDao.delete(task.id, it) }
_viewState.value.attachments _viewState.value.attachments
.minus(originalState.attachments) .minus(originalState.value.attachments)
.map { .map {
Attachment( Attachment(
task = task.id, task = task.id,
@ -484,7 +486,7 @@ class TaskEditViewModel @Inject constructor(
suspend fun discard(remove: Boolean = true) { suspend fun discard(remove: Boolean = true) {
if (_viewState.value.isNew) { if (_viewState.value.isNew) {
timerPlugin.stopTimer(task) timerPlugin.stopTimer(task)
(originalState.attachments + _viewState.value.attachments) (originalState.value.attachments + _viewState.value.attachments)
.onEach { attachment -> FileHelper.delete(context, attachment.uri.toUri()) } .onEach { attachment -> FileHelper.delete(context, attachment.uri.toUri()) }
.let { taskAttachmentDao.delete(it.toList()) } .let { taskAttachmentDao.delete(it.toList()) }
} }
@ -618,11 +620,36 @@ class TaskEditViewModel @Inject constructor(
} }
init { init {
_originalState = _viewState.value.copy()
viewModelScope.launch { viewModelScope.launch {
taskAttachmentDao.getAttachments(task.id).toPersistentSet().let { attachments -> taskAttachmentDao.getAttachments(task.id).toPersistentSet().let { attachments ->
_originalState = _originalState.copy(attachments = attachments) _originalState.update { it.copy(attachments = attachments) }
_viewState.value = _viewState.value.copy(attachments = attachments) _viewState.update { it.copy(attachments = attachments) }
}
}
if (!task.isNew) {
viewModelScope.launch {
alarmDao.getAlarms(task.id).toPersistentSet().let { alarms ->
_originalState.update { it.copy(alarms = alarms) }
_viewState.update { it.copy(alarms = alarms) }
}
}
}
viewModelScope.launch {
defaultFilterProvider.getList(task).let { list ->
_originalState.update { it.copy(list = list) }
_viewState.update { it.copy(list = list) }
}
}
viewModelScope.launch {
locationDao.getLocation(task, preferences)?.let { location ->
_originalState.update { it.copy(location = location) }
_viewState.update { it.copy(location = location) }
}
}
viewModelScope.launch {
tagDataDao.getTags(task).toPersistentSet().let { tags ->
_originalState.update { it.copy(tags = tags) }
_viewState.update { it.copy(tags = tags) }
} }
} }
} }

@ -10,6 +10,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import com.todoroo.astrid.activity.MainActivity.Companion.FINISH_AFFINITY
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -190,6 +191,7 @@ class TasksWidget : AppWidgetProvider() {
private fun getNewTaskIntent(context: Context, filter: Filter, widgetId: Int): PendingIntent { private fun getNewTaskIntent(context: Context, filter: Filter, widgetId: Int): PendingIntent {
val intent = TaskIntents.getNewTaskIntent(context, filter, "widget") val intent = TaskIntents.getNewTaskIntent(context, filter, "widget")
.putExtra(FINISH_AFFINITY, true)
intent.action = "new_task" intent.action = "new_task"
return PendingIntent.getActivity( return PendingIntent.getActivity(
context, context,

@ -3,6 +3,7 @@ package org.tasks.widget
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.activity.MainActivity.Companion.FINISH_AFFINITY
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -49,7 +50,11 @@ class WidgetClickActivity : AppCompatActivity(), OnDismissHandler {
val filter = intent.getParcelableExtra<Filter?>(EXTRA_FILTER) val filter = intent.getParcelableExtra<Filter?>(EXTRA_FILTER)
val task = task val task = task
Timber.tag("$action task=$task filter=$filter") Timber.tag("$action task=$task filter=$filter")
startActivity(TaskIntents.getEditTaskIntent(this, filter, task)) startActivity(
TaskIntents
.getEditTaskIntent(this, filter, task)
.putExtra(FINISH_AFFINITY, true)
)
finish() finish()
} }
TOGGLE_SUBTASKS -> { TOGGLE_SUBTASKS -> {

@ -1,38 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<!-- Task List -->
<FrameLayout
android:id="@+id/master"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="60"
tools:ignore="InconsistentLayout"/>
<!-- Task Edit -->
<FrameLayout
android:id="@+id/detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="40"
tools:ignore="InconsistentLayout"/>
</LinearLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

@ -1,27 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/master"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="InconsistentLayout" />
<FrameLayout
android:id="@+id/detail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:ignore="InconsistentLayout" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="two_pane_layout">true</bool>
</resources>

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<bool name="two_pane_layout">false</bool>
<bool name="default_bundle_notifications">true</bool> <bool name="default_bundle_notifications">true</bool>
<bool name="is_dark">false</bool> <bool name="is_dark">false</bool>
<bool name="light_status_bar">true</bool> <bool name="light_status_bar">true</bool>

@ -68,9 +68,11 @@ wearCompose = "1.4.0"
[libraries] [libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
androidx-adaptive-navigation-android = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation-android", version = "1.0.0" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-compose = { module = "androidx.compose:compose-bom", version.ref = "compose" } androidx-compose = { module = "androidx.compose:compose-bom", version.ref = "compose" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout-android", version = "1.0.0" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.1.2" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.1.2" }

Loading…
Cancel
Save