Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e50bd161 |
@@ -105,7 +105,6 @@ class MainActivity : ComponentActivity() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navRequest = navRequest,
|
||||
onNavRequestConsumed = { navRequest = null },
|
||||
onReturnHome = ::finish,
|
||||
)
|
||||
pendingCrashReport?.let { report ->
|
||||
CrashReportDialog(
|
||||
@@ -154,7 +153,6 @@ class MainActivity : ComponentActivity() {
|
||||
private const val EXTRA_FILTER_ID = "de.jeanlucmakiola.agendula.extra.FILTER_ID"
|
||||
private const val EXTRA_SMART_LIST = "de.jeanlucmakiola.agendula.extra.SMART_LIST"
|
||||
private const val EXTRA_LIST_ID = "de.jeanlucmakiola.agendula.extra.LIST_ID"
|
||||
private const val EXTRA_RETURN_HOME = "de.jeanlucmakiola.agendula.extra.RETURN_HOME"
|
||||
private const val EXTRA_PRESET_LIST_ID = "de.jeanlucmakiola.agendula.extra.PRESET_LIST_ID"
|
||||
private const val SHARED_TITLE_LIMIT = 500
|
||||
private const val NO_OCCURRENCE = -1L
|
||||
@@ -163,16 +161,10 @@ class MainActivity : ComponentActivity() {
|
||||
* Opens a task's detail (reminder taps). [occurrenceStart] (epoch millis)
|
||||
* picks the occurrence of a recurring task.
|
||||
*/
|
||||
fun taskIntent(
|
||||
context: Context,
|
||||
taskId: Long,
|
||||
occurrenceStart: Long? = null,
|
||||
returnHome: Boolean = false,
|
||||
): Intent =
|
||||
fun taskIntent(context: Context, taskId: Long, occurrenceStart: Long? = null): Intent =
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
putExtra(EXTRA_TASK_ID, taskId)
|
||||
putExtra(EXTRA_OCCURRENCE_START, occurrenceStart ?: NO_OCCURRENCE)
|
||||
putExtra(EXTRA_RETURN_HOME, returnHome)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
@@ -224,7 +216,6 @@ class MainActivity : ComponentActivity() {
|
||||
when (intent.action) {
|
||||
ACTION_NEW_TASK -> return NavRequest.NewTask(
|
||||
listId = intent.getLongExtra(EXTRA_PRESET_LIST_ID, -1L).takeIf { it > 0L },
|
||||
returnHome = true,
|
||||
)
|
||||
ACTION_TODAY -> return NavRequest.OpenSmart(SmartList.TODAY)
|
||||
ACTION_OPEN_SMART -> intent.getStringExtra(EXTRA_SMART_LIST)
|
||||
@@ -239,12 +230,12 @@ class MainActivity : ComponentActivity() {
|
||||
if (intent.action == Intent.ACTION_SEND && intent.type?.startsWith("text/plain") == true) {
|
||||
val shared = intent.getStringExtra(Intent.EXTRA_SUBJECT)?.takeIf { it.isNotBlank() }
|
||||
?: intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
return NavRequest.NewTask(shared?.trim()?.take(SHARED_TITLE_LIMIT), returnHome = true)
|
||||
return NavRequest.NewTask(shared?.trim()?.take(SHARED_TITLE_LIMIT))
|
||||
}
|
||||
val taskId = intent.getLongExtra(EXTRA_TASK_ID, -1L).takeIf { it > 0L } ?: return null
|
||||
val occurrence = intent.getLongExtra(EXTRA_OCCURRENCE_START, NO_OCCURRENCE)
|
||||
.takeIf { it != NO_OCCURRENCE }
|
||||
return NavRequest.OpenTask(taskId, occurrence, intent.getBooleanExtra(EXTRA_RETURN_HOME, false))
|
||||
return NavRequest.OpenTask(taskId, occurrence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import de.jeanlucmakiola.agendula.domain.Task
|
||||
import de.jeanlucmakiola.agendula.domain.TaskFilter
|
||||
import de.jeanlucmakiola.agendula.domain.TaskFormField
|
||||
import de.jeanlucmakiola.agendula.domain.TaskReminder
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
||||
import de.jeanlucmakiola.agendula.domain.asTaskReminder
|
||||
import de.jeanlucmakiola.agendula.domain.asReminderLead
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
||||
@@ -43,6 +44,7 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) {
|
||||
}
|
||||
|
||||
const val DEFAULT_SNOOZE_MINUTES = 10
|
||||
/** Done starts folded away, to keep the open tasks in focus. */ val DEFAULT_COLLAPSED_SECTIONS: Set<TaskSection> = setOf(TaskSection.COMPLETED)
|
||||
|
||||
/** The snooze lengths Settings offers. */
|
||||
val SNOOZE_PRESETS = listOf(5, 10, 15, 30, 60)
|
||||
@@ -103,6 +105,8 @@ data class Settings(
|
||||
val hiddenFromSmartLists: Set<Long> = emptySet(),
|
||||
/** Pre-fill a new task's start with today's date. */
|
||||
val defaultStartToday: Boolean = false,
|
||||
/** Task-list sections folded away; Done starts folded. */
|
||||
val collapsedSections: Set<TaskSection> = DEFAULT_COLLAPSED_SECTIONS,
|
||||
) {
|
||||
/** [tasks] as [filter] shows them: a smart list drops the lists kept out of it. */
|
||||
fun visibleIn(filter: TaskFilter, tasks: List<Task>): List<Task> =
|
||||
@@ -168,9 +172,18 @@ class SettingsPrefs @Inject constructor(
|
||||
.toSet()
|
||||
},
|
||||
defaultStartToday = p[DEFAULT_START_TODAY] ?: false,
|
||||
collapsedSections = p[COLLAPSED_SECTIONS]
|
||||
?.mapNotNull { name -> runCatching { TaskSection.valueOf(name) }.getOrNull() }
|
||||
?.toSet()
|
||||
?: DEFAULT_COLLAPSED_SECTIONS,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setSectionCollapsed(section: TaskSection, collapsed: Boolean) = dataStore.edit { p ->
|
||||
val current = p[COLLAPSED_SECTIONS] ?: DEFAULT_COLLAPSED_SECTIONS.mapTo(HashSet()) { it.name }
|
||||
p[COLLAPSED_SECTIONS] = if (collapsed) current + section.name else current - section.name
|
||||
}
|
||||
|
||||
suspend fun setSortOrder(order: TaskSortOrder) = dataStore.edit { it[SORT_ORDER] = order.name }
|
||||
|
||||
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
|
||||
@@ -328,6 +341,7 @@ class SettingsPrefs @Inject constructor(
|
||||
val PUSH_ENABLED = booleanPreferencesKey("push_enabled")
|
||||
val SMART_LIST_HIDDEN = stringSetPreferencesKey("smart_list_hidden")
|
||||
val DEFAULT_START_TODAY = booleanPreferencesKey("default_start_today")
|
||||
val COLLAPSED_SECTIONS = stringSetPreferencesKey("collapsed_sections")
|
||||
val SAVED_FILTERS = stringPreferencesKey("saved_filters")
|
||||
val SAVED_FILTER_NEXT_ID = longPreferencesKey("saved_filter_next_id")
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ fun RootScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
navRequest: NavRequest? = null,
|
||||
onNavRequestConsumed: () -> Unit = {},
|
||||
onReturnHome: () -> Unit = {},
|
||||
permissionViewModel: PermissionViewModel = hiltViewModel(),
|
||||
) {
|
||||
val permission by permissionViewModel.state.collectAsStateWithLifecycle()
|
||||
@@ -87,7 +86,6 @@ fun RootScreen(
|
||||
modifier = modifier,
|
||||
navRequest = navRequest,
|
||||
onNavRequestConsumed = onNavRequestConsumed,
|
||||
onReturnHome = onReturnHome,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -103,7 +101,6 @@ private fun ReadyGate(
|
||||
modifier: Modifier = Modifier,
|
||||
navRequest: NavRequest? = null,
|
||||
onNavRequestConsumed: () -> Unit = {},
|
||||
onReturnHome: () -> Unit = {},
|
||||
onboardingViewModel: OnboardingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val done by onboardingViewModel.done.collectAsStateWithLifecycle()
|
||||
@@ -116,7 +113,6 @@ private fun ReadyGate(
|
||||
modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface),
|
||||
navRequest = navRequest,
|
||||
onNavRequestConsumed = onNavRequestConsumed,
|
||||
onReturnHome = onReturnHome,
|
||||
)
|
||||
false -> ReadableWidth(modifier) {
|
||||
OnboardingFlow(modifier = Modifier.fillMaxSize(), viewModel = onboardingViewModel)
|
||||
|
||||
@@ -7,11 +7,6 @@ import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -51,19 +46,8 @@ fun AgendulaNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navRequest: NavRequest? = null,
|
||||
onNavRequestConsumed: () -> Unit = {},
|
||||
onReturnHome: () -> Unit = {},
|
||||
) {
|
||||
val nav = rememberNavController()
|
||||
// The entry a widget, tile, shortcut or share opened; closing it leaves the app (#93).
|
||||
var returnHomeEntry by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val backStack by nav.currentBackStack.collectAsState()
|
||||
LaunchedEffect(backStack, returnHomeEntry) {
|
||||
val id = returnHomeEntry ?: return@LaunchedEffect
|
||||
if (backStack.none { it.id == id }) {
|
||||
returnHomeEntry = null
|
||||
onReturnHome()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(navRequest) {
|
||||
when (navRequest) {
|
||||
@@ -87,9 +71,6 @@ fun AgendulaNavHost(
|
||||
}
|
||||
is NavRequest.Import -> nav.navigate(Dest.Import.build(navRequest.uri))
|
||||
}
|
||||
val returnHome = (navRequest as? NavRequest.NewTask)?.returnHome == true ||
|
||||
(navRequest as? NavRequest.OpenTask)?.returnHome == true
|
||||
returnHomeEntry = if (returnHome) nav.currentBackStackEntry?.id else null
|
||||
onNavRequestConsumed()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,8 @@ import de.jeanlucmakiola.agendula.domain.SmartList
|
||||
* [AgendulaNavHost], which pushes it over whatever is showing.
|
||||
*/
|
||||
sealed interface NavRequest {
|
||||
/**
|
||||
* One task's detail; [occurrenceStart] (epoch millis) picks the occurrence of a series.
|
||||
* [returnHome] leaves the app once the detail is closed (a widget row, #93).
|
||||
*/
|
||||
data class OpenTask(
|
||||
val taskId: Long,
|
||||
val occurrenceStart: Long? = null,
|
||||
val returnHome: Boolean = false,
|
||||
) : NavRequest
|
||||
/** One task's detail; [occurrenceStart] (epoch millis) picks the occurrence of a series. */
|
||||
data class OpenTask(val taskId: Long, val occurrenceStart: Long? = null) : NavRequest
|
||||
|
||||
/** Settings → Accounts, where a sync report's detail lives. */
|
||||
data object OpenAccounts : NavRequest
|
||||
@@ -28,13 +21,8 @@ sealed interface NavRequest {
|
||||
/**
|
||||
* A new task, optionally titled with text shared from another app, or
|
||||
* preset to a list — a widget's own add button, when it shows one list.
|
||||
* [returnHome] leaves the app once the form is closed (#93).
|
||||
*/
|
||||
data class NewTask(
|
||||
val title: String? = null,
|
||||
val listId: Long? = null,
|
||||
val returnHome: Boolean = false,
|
||||
) : NavRequest
|
||||
data class NewTask(val title: String? = null, val listId: Long? = null) : NavRequest
|
||||
|
||||
/** One of the smart lists — the launcher's "Today" shortcut, a widget header. */
|
||||
data class OpenSmart(val list: SmartList) : NavRequest
|
||||
|
||||
@@ -371,8 +371,6 @@ private fun TaskListBody(
|
||||
// rows; inside a single real list both are redundant.
|
||||
val showListName = filter is TaskFilter.Smart || filter is TaskFilter.Tagged ||
|
||||
(filter is TaskFilter.Saved && state.savedFilter?.listIds?.size != 1)
|
||||
// Completed tasks collapse away by default to keep the active list in focus.
|
||||
var completedExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
// Parents the user expanded to reveal their subtasks inline. Children are
|
||||
// grouped from the same flat task set the list already holds.
|
||||
var expandedParents by rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||
@@ -420,23 +418,20 @@ private fun TaskListBody(
|
||||
}
|
||||
|
||||
sections.forEach { section ->
|
||||
val collapsible = showHeaders && section.section == TaskSection.COMPLETED
|
||||
// Every header folds its section away (#91); Done starts folded.
|
||||
val expanded = !showHeaders || section.section !in state.collapsedSections
|
||||
if (showHeaders) {
|
||||
stickyHeader(key = "hdr-${section.section}") {
|
||||
if (collapsible) {
|
||||
CollapsibleSectionHeader(
|
||||
label = sectionLabel(section.section),
|
||||
count = section.tasks.size,
|
||||
expanded = completedExpanded,
|
||||
onToggle = { completedExpanded = !completedExpanded },
|
||||
onClear = onClearCompleted,
|
||||
)
|
||||
} else {
|
||||
SectionHeader(sectionLabel(section.section))
|
||||
}
|
||||
CollapsibleSectionHeader(
|
||||
label = sectionLabel(section.section),
|
||||
count = section.tasks.size,
|
||||
expanded = expanded,
|
||||
onToggle = { viewModel.setSectionCollapsed(section.section, expanded) },
|
||||
onClear = onClearCompleted.takeIf { section.section == TaskSection.COMPLETED },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!collapsible || completedExpanded) {
|
||||
if (expanded) {
|
||||
// Flatten the section into one grouped run: each parent, followed
|
||||
// by its subtasks when expanded. Each row's corners then come from
|
||||
// its place in the whole run (positionOf below) — so the children
|
||||
@@ -1127,19 +1122,6 @@ private fun cardShape(position: Position, full: androidx.compose.ui.unit.Dp, sma
|
||||
Position.Bottom -> RoundedCornerShape(topStart = small, topEnd = small, bottomStart = full, bottomEnd = full)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(text: String) {
|
||||
// Opaque surface so rows scroll cleanly under the pinned header.
|
||||
Surface(color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 28.dp, end = 28.dp, top = 16.dp, bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A tappable section header with a count and a chevron that rotates on expand. */
|
||||
@Composable
|
||||
private fun CollapsibleSectionHeader(
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.jeanlucmakiola.agendula.ui.tasklist
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.jeanlucmakiola.agendula.data.prefs.DEFAULT_COLLAPSED_SECTIONS
|
||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
||||
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
||||
@@ -14,6 +15,7 @@ import de.jeanlucmakiola.agendula.domain.TaskFiltering
|
||||
import de.jeanlucmakiola.agendula.domain.tagsInUse
|
||||
import de.jeanlucmakiola.agendula.domain.TaskForm
|
||||
import de.jeanlucmakiola.agendula.domain.TaskList
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
||||
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
||||
import de.jeanlucmakiola.agendula.domain.comparator
|
||||
import de.jeanlucmakiola.agendula.ui.lists.ListWriteFailure
|
||||
@@ -56,6 +58,8 @@ sealed interface TaskListUiState {
|
||||
/** Whether a real list uses the bottom quick-add bar instead of the FAB. */
|
||||
val bottomAddBar: Boolean = false,
|
||||
val sortOrder: TaskSortOrder = TaskSortOrder.DUE,
|
||||
/** Sections whose rows are folded under their header. */
|
||||
val collapsedSections: Set<TaskSection> = DEFAULT_COLLAPSED_SECTIONS,
|
||||
/** This real list is kept out of the smart lists. */
|
||||
val hiddenFromSmartLists: Boolean = false,
|
||||
/** Tags the unnarrowed view carries, offered as filter chips. */
|
||||
@@ -144,6 +148,7 @@ class TaskListViewModel @Inject constructor(
|
||||
showAddSubtaskRow = settings.showAddSubtaskRow,
|
||||
bottomAddBar = settings.bottomAddBar,
|
||||
sortOrder = settings.sortOrder,
|
||||
collapsedSections = settings.collapsedSections,
|
||||
hiddenFromSmartLists = st.list?.id?.let { it in settings.hiddenFromSmartLists } ?: false,
|
||||
)
|
||||
} else {
|
||||
@@ -277,6 +282,9 @@ class TaskListViewModel @Inject constructor(
|
||||
fun setHiddenFromSmartLists(listId: Long, hidden: Boolean) =
|
||||
viewModelScope.launch { settingsPrefs.setHiddenFromSmartLists(listId, hidden) }
|
||||
|
||||
fun setSectionCollapsed(section: TaskSection, collapsed: Boolean) =
|
||||
viewModelScope.launch { settingsPrefs.setSectionCollapsed(section, collapsed) }
|
||||
|
||||
fun setSortOrder(order: TaskSortOrder) = viewModelScope.launch { settingsPrefs.setSortOrder(order) }
|
||||
|
||||
/**
|
||||
|
||||
@@ -328,7 +328,7 @@ internal fun AgendaTaskRow(
|
||||
val context = LocalContext.current
|
||||
val title = row.title.ifBlank { context.getString(R.string.task_untitled) }
|
||||
val open = actionStartActivity(
|
||||
MainActivity.taskIntent(context, row.taskId, row.occurrenceStart?.toEpochMilliseconds(), returnHome = true),
|
||||
MainActivity.taskIntent(context, row.taskId, row.occurrenceStart?.toEpochMilliseconds()),
|
||||
)
|
||||
val onColor = if (colored) GlanceColorProvider(Color.White) else GlanceTheme.colors.onSurface
|
||||
val onColorVariant = if (colored) GlanceColorProvider(Color.White.copy(alpha = 0.8f)) else GlanceTheme.colors.onSurfaceVariant
|
||||
|
||||
@@ -339,7 +339,7 @@ private fun ListDot(color: Int) {
|
||||
}
|
||||
|
||||
private fun openTask(context: Context, row: WidgetTaskRow): Action =
|
||||
actionStartActivity(MainActivity.taskIntent(context, row.taskId, row.occurrenceStart?.toEpochMilliseconds(), returnHome = true))
|
||||
actionStartActivity(MainActivity.taskIntent(context, row.taskId, row.occurrenceStart?.toEpochMilliseconds()))
|
||||
|
||||
private fun completeTask(row: WidgetTaskRow): Action =
|
||||
actionRunCallback<CompleteTaskAction>(
|
||||
|
||||
Reference in New Issue
Block a user