Adaptive layout

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

@ -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)

@ -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<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(
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 }
}
}
}

@ -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<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) =
if (disabled) {
pointerInput(Unit) {

@ -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

@ -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(

@ -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<String>(GoogleTask.KEY)!!

@ -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) {

@ -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)

@ -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<ArrayList<TagData>>(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<ViewState> = _originalState
private val _viewState = MutableStateFlow(originalState.value)
val viewState: StateFlow<ViewState> = _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) }
}
}
}

@ -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,

@ -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<Filter?>(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 -> {

@ -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"?>
<resources>
<bool name="two_pane_layout">false</bool>
<bool name="default_bundle_notifications">true</bool>
<bool name="is_dark">false</bool>
<bool name="light_status_bar">true</bool>

@ -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" }

Loading…
Cancel
Save