diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62a1285f7..3e6210717 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,6 +154,7 @@ dependencies { implementation(projects.data) implementation(projects.kmp) implementation(projects.icons) + implementation(libs.androidx.adaptive.navigation.android) coreLibraryDesugaring(libs.desugar.jdk.libs) implementation(libs.bitfire.dav4jvm) { exclude(group = "junit") @@ -205,6 +206,7 @@ dependencies { implementation(libs.persistent.cookiejar) implementation(libs.material) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.preference) 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 93695e66b..6aa248c04 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -8,46 +8,65 @@ package com.todoroo.astrid.activity import android.content.Intent import android.graphics.Color import android.os.Bundle -import android.view.View import androidx.activity.SystemBarStyle +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box 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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme 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.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration 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.fragment.compose.AndroidFragment +import androidx.fragment.compose.rememberFragmentState import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.todoroo.andlib.utility.AndroidUtilities 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.dao.TaskDao import com.todoroo.astrid.service.TaskCreator import dagger.hilt.android.AndroidEntryPoint 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.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.runBlocking import org.tasks.BuildConfig import org.tasks.R import org.tasks.TasksApplication @@ -66,9 +85,7 @@ import org.tasks.data.dao.LocationDao import org.tasks.data.dao.TagDataDao import org.tasks.data.entity.Place import org.tasks.data.entity.Task -import org.tasks.data.getLocation import org.tasks.data.listSettingsClass -import org.tasks.databinding.TaskListActivityBinding import org.tasks.dialogs.NewFilterDialog import org.tasks.dialogs.WhatsNewDialog import org.tasks.extensions.Context.findActivity @@ -91,12 +108,12 @@ import org.tasks.preferences.Preferences import org.tasks.themes.ColorProvider import org.tasks.themes.TasksTheme import org.tasks.themes.Theme -import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment import org.tasks.ui.MainActivityEvent import org.tasks.ui.MainActivityEventBus import timber.log.Timber import javax.inject.Inject +@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalSharedTransitionApi::class) @AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var preferences: Preferences @@ -117,7 +134,6 @@ class MainActivity : AppCompatActivity() { private var currentNightMode = 0 private var currentPro = false private var actionMode: ActionMode? = null - private lateinit var binding: TaskListActivityBinding /** @see android.app.Activity.onCreate */ @@ -127,10 +143,6 @@ class MainActivity : AppCompatActivity() { theme.applyTheme(this) currentNightMode = nightMode currentPro = inventory.hasPro - binding = TaskListActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - logIntent("onCreate") - handleIntent() enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( @@ -143,9 +155,112 @@ class MainActivity : AppCompatActivity() { ) ) - binding.composeView.setContent { - if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) { - TasksTheme(theme = theme.themeBase.index) { + setContent { + 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( + 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( + 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( skipPartiallyExpanded = true, confirmValueChange = { true }, @@ -164,7 +279,6 @@ class MainActivity : AppCompatActivity() { ) }, ) { - val state = viewModel.state.collectAsStateWithLifecycle().value val context = LocalContext.current val settingsRequest = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() @@ -227,10 +341,12 @@ class MainActivity : AppCompatActivity() { REQUEST_NEW_LIST -> { val account = - caldavDao.getAccount(it.header.id.toLong()) ?: return@launch + caldavDao.getAccount(it.header.id.toLong()) + ?: return@launch when (it.header.subheaderType) { NavigationDrawerSubheader.SubheaderType.CALDAV, - NavigationDrawerSubheader.SubheaderType.TASKS -> + NavigationDrawerSubheader.SubheaderType.TASKS, + -> startActivityForResult( Intent( this@MainActivity, @@ -297,6 +413,8 @@ class MainActivity : AppCompatActivity() { } } } + logIntent("onCreate") + handleIntent() eventBus .onEach(this::process) @@ -307,77 +425,6 @@ class MainActivity : AppCompatActivity() { 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) { @@ -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) { super.onSupportActionModeStarted(mode) actionMode = mode @@ -590,4 +613,4 @@ class MainActivity : AppCompatActivity() { .filter { flags or it.getInt(null) == flags } .joinToString(" | ") { it.name } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index 3b924e9eb..90fa63371 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.compose.BackHandler import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable 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.TaskEditScreen import org.tasks.compose.edit.TitleRow -import org.tasks.data.Location 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.date.DateTimeUtils.newDateTime import org.tasks.dialogs.DateTimePicker @@ -55,7 +51,6 @@ import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.Linkify import org.tasks.extensions.Context.is24HourFormat 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.getRelativeDateTime import org.tasks.markdown.MarkdownProvider @@ -118,15 +113,6 @@ class TaskEditFragment : Fragment() { } TasksTheme(theme = theme.themeBase.index,) { val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value - BackHandler { - if (viewState.backButtonSavesTask) { - lifecycleScope.launch { - save() - } - } else { - discardButtonClick() - } - } LaunchedEffect(viewState.isNew) { if (!viewState.isNew) { notificationManager.cancel(viewState.task.id) @@ -140,7 +126,15 @@ class TaskEditFragment : Fragment() { .value, save = { lifecycleScope.launch { save() } }, discard = { discardButtonClick() }, - onBackPressed = { activity?.onBackPressed() }, + onBackPressed = { + if (viewState.backButtonSavesTask) { + lifecycleScope.launch { + save() + } + } else { + discardButtonClick() + } + }, delete = { deleteButtonClick() }, openBeastModeSettings = { editViewModel.hideBeastModeHint(click = true) @@ -351,10 +345,6 @@ class TaskEditFragment : Fragment() { companion object { 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" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" @@ -368,24 +358,6 @@ class TaskEditFragment : Fragment() { newDateTime(this).endOfDay().isBeforeNow } - fun newTaskEditFragment( - task: Task, - list: Filter, - location: Location?, - tags: ArrayList, - alarms: ArrayList, - ): 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) = if (disabled) { pointerInput(Unit) { 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 bccfcebe0..1f8a01f1b 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -73,7 +73,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.tasks.LocalBroadcastManager import org.tasks.R @@ -278,13 +277,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) { if (search.isActionViewExpanded) { 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) @@ -1103,7 +1095,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL const val ACTION_DELETED = "action_deleted" private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids" 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_PRIORITY_PICKER = "frag_tag_priority_picker" private const val REQUEST_TAG_TASKS = 10106 diff --git a/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt b/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt index e053568f0..33315434a 100644 --- a/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt +++ b/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt @@ -1,6 +1,7 @@ package org.tasks.compose.edit 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -50,6 +51,9 @@ fun TaskEditScreen( deleteComment: (UserActivity) -> Unit, content: @Composable (Int) -> Unit, ) { + BackHandler { + onBackPressed() + } Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt index 2f4ff5575..bc72bbc80 100644 --- a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt +++ b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt @@ -163,8 +163,8 @@ class DefaultFilterProvider @Inject constructor( } } - suspend fun getList(task: Task): Filter { - var originalList: Filter? = null + suspend fun getList(task: Task): CaldavFilter { + var originalList: CaldavFilter? = null if (task.isNew) { if (task.hasTransitory(GoogleTask.KEY)) { val listId = task.getTransitory(GoogleTask.KEY)!! diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index f232c1790..bda67fc9e 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -53,8 +53,9 @@ class SubtaskControlSet : TaskEditControlFragment() { listViewModel = ViewModelProvider(requireParentFragment())[TaskListViewModel::class.java] setContent { val viewState = viewModel.viewState.collectAsStateWithLifecycle().value + val originalState = viewModel.originalState.collectAsStateWithLifecycle().value SubtaskRow( - originalFilter = viewModel.originalState.list, + originalFilter = originalState.list, filter = viewState.list, hasParent = viewState.hasParent, existingSubtasks = if (viewModel.viewState.collectAsStateWithLifecycle().value.isNew) { diff --git a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt index 4d4ec2065..96277a283 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -18,7 +17,6 @@ abstract class TaskEditControlFragment : Fragment() { savedInstanceState: Bundle? ): View? { val composeView = ComposeView(requireActivity()) - composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) viewModel = ViewModelProvider(requireParentFragment())[TaskEditViewModel::class.java] bind(composeView) createView(savedInstanceState) diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index a0bbacb1e..5b013a06b 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -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.whenStarted 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.FORCE_CALDAV_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.TaskAttachment import org.tasks.data.entity.UserActivity +import org.tasks.data.getLocation import org.tasks.data.setPicture import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.files.FileHelper import org.tasks.filters.CaldavFilter import org.tasks.location.GeofenceApi +import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.PermissionChecker import org.tasks.preferences.Preferences import org.tasks.time.DateTimeUtils2.currentTimeMillis @@ -107,6 +111,7 @@ class TaskEditViewModel @Inject constructor( private val userActivityDao: UserActivityDao, private val alarmDao: AlarmDao, private val taskAttachmentDao: TaskAttachmentDao, + private val defaultFilterProvider: DefaultFilterProvider, ) : ViewModel() { data class ViewState( val task: Task, @@ -143,11 +148,7 @@ class TaskEditViewModel @Inject constructor( ?.apply { notes = notes?.stripCarriageReturns() } // copying here broke tests 🙄 ?: throw IllegalArgumentException("task is null") - private var _originalState: ViewState - val originalState: ViewState - get() = _originalState - - private val _viewState = MutableStateFlow( + private val _originalState = MutableStateFlow( ViewState( task = task, showBeastModeHint = !preferences.shownBeastModeHint, @@ -156,11 +157,6 @@ class TaskEditViewModel @Inject constructor( backButtonSavesTask = preferences.backButtonSavesTask(), isReadOnly = task.readOnly, linkify = preferences.linkify, - list = savedStateHandle[TaskEditFragment.EXTRA_LIST]!!, - location = savedStateHandle[TaskEditFragment.EXTRA_LOCATION], - tags = savedStateHandle.get>(TaskEditFragment.EXTRA_TAGS) - ?.toPersistentSet() - ?: persistentSetOf(), calendar = if (task.isNew && permissionChecker.canAccessCalendars()) { preferences.defaultCalendar } else { @@ -198,11 +194,17 @@ class TaskEditViewModel @Inject constructor( } } } else { - savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!! + emptyList() }.toPersistentSet(), multilineTitle = preferences.multilineTitle, + location = null, + tags = persistentSetOf(), + list = CaldavFilter(calendar = CaldavCalendar(), account = CaldavAccount()), ) ) + val originalState: StateFlow = _originalState + + private val _viewState = MutableStateFlow(originalState.value) val viewState: StateFlow = _viewState var eventUri = MutableStateFlow(task.calendarURI) @@ -269,7 +271,7 @@ class TaskEditViewModel @Inject constructor( fun hasChanges(): Boolean { val viewState = _viewState.value - return originalState != viewState || + return originalState.value != viewState || (viewState.isNew && viewState.task.title?.isNotBlank() == true) || // text shared to tasks task.dueDate != dueDate.value || task.hideUntil != startDate.value || @@ -315,8 +317,8 @@ class TaskEditViewModel @Inject constructor( taskDao.createNew(task) } val selectedLocation = _viewState.value.location - if ((isNew && selectedLocation != null) || originalState.location != selectedLocation) { - originalState.location?.let { location -> + if ((isNew && selectedLocation != null) || originalState.value.location != selectedLocation) { + originalState.value.location?.let { location -> if (location.geofence.id > 0) { locationDao.delete(location.geofence) geofenceApi.update(location.place) @@ -337,7 +339,7 @@ class TaskEditViewModel @Inject constructor( task.modificationDate = currentTimeMillis() } 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) task.putTransitory(FORCE_CALDAV_SYNC, true) task.modificationDate = currentTimeMillis() @@ -360,7 +362,7 @@ class TaskEditViewModel @Inject constructor( if ( (isNew && _viewState.value.alarms.isNotEmpty()) || - originalState.alarms != _viewState.value.alarms + originalState.value.alarms != _viewState.value.alarms ) { alarmService.synchronizeAlarms(task.id, _viewState.value.alarms.toMutableSet()) task.putTransitory(FORCE_CALDAV_SYNC, true) @@ -369,7 +371,7 @@ class TaskEditViewModel @Inject constructor( taskDao.save(task, null) val selectedList = _viewState.value.list - if (isNew || originalState.list != selectedList) { + if (isNew || originalState.value.list != selectedList) { task.parent = 0 taskMover.move(listOf(task.id), selectedList) } @@ -417,13 +419,13 @@ class TaskEditViewModel @Inject constructor( } } - if (originalState.attachments != _viewState.value.attachments) { - originalState.attachments + if (originalState.value.attachments != _viewState.value.attachments) { + originalState.value.attachments .minus(_viewState.value.attachments) .map { it.remoteId } .let { taskAttachmentDao.delete(task.id, it) } _viewState.value.attachments - .minus(originalState.attachments) + .minus(originalState.value.attachments) .map { Attachment( task = task.id, @@ -484,7 +486,7 @@ class TaskEditViewModel @Inject constructor( suspend fun discard(remove: Boolean = true) { if (_viewState.value.isNew) { timerPlugin.stopTimer(task) - (originalState.attachments + _viewState.value.attachments) + (originalState.value.attachments + _viewState.value.attachments) .onEach { attachment -> FileHelper.delete(context, attachment.uri.toUri()) } .let { taskAttachmentDao.delete(it.toList()) } } @@ -618,11 +620,36 @@ class TaskEditViewModel @Inject constructor( } init { - _originalState = _viewState.value.copy() viewModelScope.launch { taskAttachmentDao.getAttachments(task.id).toPersistentSet().let { attachments -> - _originalState = _originalState.copy(attachments = attachments) - _viewState.value = _viewState.value.copy(attachments = attachments) + _originalState.update { it.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) } } } } diff --git a/app/src/main/java/org/tasks/widget/TasksWidget.kt b/app/src/main/java/org/tasks/widget/TasksWidget.kt index a43850ae3..ca846fb85 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidget.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidget.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.view.View import android.widget.RemoteViews 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.qualifiers.ApplicationContext import kotlinx.coroutines.runBlocking @@ -190,6 +191,7 @@ class TasksWidget : AppWidgetProvider() { private fun getNewTaskIntent(context: Context, filter: Filter, widgetId: Int): PendingIntent { val intent = TaskIntents.getNewTaskIntent(context, filter, "widget") + .putExtra(FINISH_AFFINITY, true) intent.action = "new_task" return PendingIntent.getActivity( context, diff --git a/app/src/main/java/org/tasks/widget/WidgetClickActivity.kt b/app/src/main/java/org/tasks/widget/WidgetClickActivity.kt index 590c5d31a..a26ab28e7 100644 --- a/app/src/main/java/org/tasks/widget/WidgetClickActivity.kt +++ b/app/src/main/java/org/tasks/widget/WidgetClickActivity.kt @@ -3,6 +3,7 @@ package org.tasks.widget import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope +import com.todoroo.astrid.activity.MainActivity.Companion.FINISH_AFFINITY import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.service.TaskCompleter import dagger.hilt.android.AndroidEntryPoint @@ -49,7 +50,11 @@ class WidgetClickActivity : AppCompatActivity(), OnDismissHandler { val filter = intent.getParcelableExtra(EXTRA_FILTER) val task = task Timber.tag("$action task=$task filter=$filter") - startActivity(TaskIntents.getEditTaskIntent(this, filter, task)) + startActivity( + TaskIntents + .getEditTaskIntent(this, filter, task) + .putExtra(FINISH_AFFINITY, true) + ) finish() } TOGGLE_SUBTASKS -> { diff --git a/app/src/main/res/layout-w820dp/task_list_activity.xml b/app/src/main/res/layout-w820dp/task_list_activity.xml deleted file mode 100644 index 130a1e463..000000000 --- a/app/src/main/res/layout-w820dp/task_list_activity.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/task_list_activity.xml b/app/src/main/res/layout/task_list_activity.xml deleted file mode 100644 index 2cd2d2480..000000000 --- a/app/src/main/res/layout/task_list_activity.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/values-w820dp/bools.xml b/app/src/main/res/values-w820dp/bools.xml deleted file mode 100644 index fcbdd015c..000000000 --- a/app/src/main/res/values-w820dp/bools.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - true - \ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml index baeac3b04..e0ac54130 100644 --- a/app/src/main/res/values/bools.xml +++ b/app/src/main/res/values/bools.xml @@ -1,6 +1,5 @@ - false true false true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e47d96329..1771dc00b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,13 +68,15 @@ wearCompose = "1.4.0" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } 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-compose = { module = "androidx.compose:compose-bom", version.ref = "compose" } 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-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" } androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.1.2" } -androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version = "1.8.5"} +androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version = "1.8.5" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt" } androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit" }