Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1590e028c |
@@ -15,10 +15,8 @@ import de.jeanlucmakiola.agendula.data.sync.AccountCreator
|
|||||||
import de.jeanlucmakiola.agendula.data.sync.AccountRepository
|
import de.jeanlucmakiola.agendula.data.sync.AccountRepository
|
||||||
import de.jeanlucmakiola.agendula.data.sync.CalDavGateway
|
import de.jeanlucmakiola.agendula.data.sync.CalDavGateway
|
||||||
import de.jeanlucmakiola.agendula.data.sync.LoginFlowRecord
|
import de.jeanlucmakiola.agendula.data.sync.LoginFlowRecord
|
||||||
import de.jeanlucmakiola.agendula.data.sync.ManualSync
|
|
||||||
import de.jeanlucmakiola.agendula.data.sync.OkHttpCalDavGateway
|
import de.jeanlucmakiola.agendula.data.sync.OkHttpCalDavGateway
|
||||||
import de.jeanlucmakiola.agendula.data.sync.PendingLoginFlowStore
|
import de.jeanlucmakiola.agendula.data.sync.PendingLoginFlowStore
|
||||||
import de.jeanlucmakiola.agendula.data.sync.PullSync
|
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteListRepository
|
import de.jeanlucmakiola.agendula.data.sync.RemoteListRepository
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
||||||
import de.jeanlucmakiola.agendula.data.sync.SyncNoticeNotifier
|
import de.jeanlucmakiola.agendula.data.sync.SyncNoticeNotifier
|
||||||
@@ -83,10 +81,6 @@ abstract class SyncBindModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindSyncRequests(impl: SyncTrigger): SyncRequests
|
abstract fun bindSyncRequests(impl: SyncTrigger): SyncRequests
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Singleton
|
|
||||||
abstract fun bindManualSync(impl: PullSync): ManualSync
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindSyncNoticeChannel(impl: SyncNoticeNotifier): ChannelRefresher
|
abstract fun bindSyncNoticeChannel(impl: SyncNoticeNotifier): ChannelRefresher
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.sync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pull to sync over [SyncTrigger]: an expedited sync per account, and
|
|
||||||
* [syncing] held until they finish.
|
|
||||||
*
|
|
||||||
* A sync waiting on its network constraint never starts, so if none is running
|
|
||||||
* after [START_GRACE] the indicator lets go; the work stays queued and runs
|
|
||||||
* once the device is back online.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
class PullSync @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val accounts: AccountRepository,
|
|
||||||
private val trigger: SyncTrigger,
|
|
||||||
availability: SyncAvailability,
|
|
||||||
@ApplicationScope private val scope: CoroutineScope,
|
|
||||||
) : ManualSync {
|
|
||||||
|
|
||||||
override val available: Flow<Boolean> =
|
|
||||||
combine(availability.observe(), accounts.observeAll()) { usable, all -> usable && all.isNotEmpty() }
|
|
||||||
|
|
||||||
private val _syncing = MutableStateFlow(false)
|
|
||||||
override val syncing: StateFlow<Boolean> = _syncing
|
|
||||||
|
|
||||||
override fun syncAll() {
|
|
||||||
if (_syncing.value) return
|
|
||||||
_syncing.value = true
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
val names = accounts.syncable().map { trigger.enqueue(it.displayName, expedited = true) }
|
|
||||||
if (names.isNotEmpty()) awaitSyncs(names)
|
|
||||||
} finally {
|
|
||||||
_syncing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun awaitSyncs(names: List<String>) {
|
|
||||||
val workManager = WorkManager.getInstance(context)
|
|
||||||
val states = combine(names.map { workManager.getWorkInfosForUniqueWorkFlow(it) }) { infos ->
|
|
||||||
infos.flatMap { it }.map { it.state }
|
|
||||||
}
|
|
||||||
withTimeoutOrNull(MAX_WAIT) {
|
|
||||||
val started = withTimeoutOrNull(START_GRACE) {
|
|
||||||
states.first { all -> WorkInfo.State.RUNNING in all || all.all { it.isFinished } }
|
|
||||||
} ?: return@withTimeoutOrNull
|
|
||||||
if (started.all { it.isFinished }) return@withTimeoutOrNull
|
|
||||||
states.map { all -> WorkInfo.State.RUNNING in all }.first { running -> !running }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val START_GRACE = 3.seconds
|
|
||||||
val MAX_WAIT = 2.minutes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -65,8 +65,10 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- MAIN/LAUNCHER lives on the two aliases below, so the launcher
|
<intent-filter>
|
||||||
name can be switched at runtime (#104). -->
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
<!-- Shared text becomes a new task's title. -->
|
<!-- Shared text becomes a new task's title. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -87,32 +89,6 @@
|
|||||||
<data android:mimeType="text/calendar" />
|
<data android:mimeType="text/calendar" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity-alias
|
|
||||||
android:name=".DefaultNameAlias"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:targetActivity=".MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity-alias>
|
|
||||||
<activity-alias
|
|
||||||
android:name=".TasksNameAlias"
|
|
||||||
android:enabled="false"
|
|
||||||
android:exported="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name_tasks_alias"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:targetActivity=".MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity-alias>
|
|
||||||
|
|
||||||
<!-- Quick Settings "New task" tile. Only the system QS host can bind it. -->
|
<!-- Quick Settings "New task" tile. Only the system QS host can bind it. -->
|
||||||
<service
|
<service
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import de.jeanlucmakiola.agendula.data.di.LaunchHook
|
|||||||
import de.jeanlucmakiola.agendula.domain.SmartList
|
import de.jeanlucmakiola.agendula.domain.SmartList
|
||||||
import de.jeanlucmakiola.agendula.ui.imports.importIntentUri
|
import de.jeanlucmakiola.agendula.ui.imports.importIntentUri
|
||||||
import de.jeanlucmakiola.agendula.ui.navigation.AppShortcuts
|
import de.jeanlucmakiola.agendula.ui.navigation.AppShortcuts
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.agendula.ui.RootScreen
|
import de.jeanlucmakiola.agendula.ui.RootScreen
|
||||||
import de.jeanlucmakiola.agendula.ui.crash.CrashReportActivity
|
import de.jeanlucmakiola.agendula.ui.crash.CrashReportActivity
|
||||||
import de.jeanlucmakiola.agendula.ui.navigation.NavRequest
|
import de.jeanlucmakiola.agendula.ui.navigation.NavRequest
|
||||||
@@ -87,15 +88,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val ui by settingsViewModel.state.collectAsStateWithLifecycle()
|
val ui by settingsViewModel.state.collectAsStateWithLifecycle()
|
||||||
val darkTheme = ui.settings.themeMode.isDark(isSystemInDarkTheme())
|
val darkTheme = when (ui.settings.themeMode) {
|
||||||
|
ThemeMode.SYSTEM -> isSystemInDarkTheme()
|
||||||
|
ThemeMode.LIGHT -> false
|
||||||
|
ThemeMode.DARK -> true
|
||||||
|
}
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val use24Hour = ui.settings.timeFormat.is24Hour(DateFormat.is24HourFormat(context))
|
val use24Hour = ui.settings.timeFormat.is24Hour(DateFormat.is24HourFormat(context))
|
||||||
val firstDayOfWeek = ui.settings.weekStart ?: localeFirstDayOfWeek()
|
val firstDayOfWeek = ui.settings.weekStart ?: localeFirstDayOfWeek()
|
||||||
AgendulaTheme(
|
AgendulaTheme(darkTheme = darkTheme, dynamicColor = ui.settings.dynamicColor) {
|
||||||
darkTheme = darkTheme,
|
|
||||||
dynamicColor = ui.settings.dynamicColor,
|
|
||||||
pureBlack = ui.settings.themeMode.isPureBlack,
|
|
||||||
) {
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalUse24HourFormat provides use24Hour,
|
LocalUse24HourFormat provides use24Hour,
|
||||||
LocalFirstDayOfWeek provides firstDayOfWeek,
|
LocalFirstDayOfWeek provides firstDayOfWeek,
|
||||||
@@ -104,7 +105,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
navRequest = navRequest,
|
navRequest = navRequest,
|
||||||
onNavRequestConsumed = { navRequest = null },
|
onNavRequestConsumed = { navRequest = null },
|
||||||
onReturnHome = ::finish,
|
|
||||||
)
|
)
|
||||||
pendingCrashReport?.let { report ->
|
pendingCrashReport?.let { report ->
|
||||||
CrashReportDialog(
|
CrashReportDialog(
|
||||||
@@ -153,7 +153,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
private const val EXTRA_FILTER_ID = "de.jeanlucmakiola.agendula.extra.FILTER_ID"
|
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_SMART_LIST = "de.jeanlucmakiola.agendula.extra.SMART_LIST"
|
||||||
private const val EXTRA_LIST_ID = "de.jeanlucmakiola.agendula.extra.LIST_ID"
|
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 EXTRA_PRESET_LIST_ID = "de.jeanlucmakiola.agendula.extra.PRESET_LIST_ID"
|
||||||
private const val SHARED_TITLE_LIMIT = 500
|
private const val SHARED_TITLE_LIMIT = 500
|
||||||
private const val NO_OCCURRENCE = -1L
|
private const val NO_OCCURRENCE = -1L
|
||||||
@@ -162,16 +161,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
* Opens a task's detail (reminder taps). [occurrenceStart] (epoch millis)
|
* Opens a task's detail (reminder taps). [occurrenceStart] (epoch millis)
|
||||||
* picks the occurrence of a recurring task.
|
* picks the occurrence of a recurring task.
|
||||||
*/
|
*/
|
||||||
fun taskIntent(
|
fun taskIntent(context: Context, taskId: Long, occurrenceStart: Long? = null): Intent =
|
||||||
context: Context,
|
|
||||||
taskId: Long,
|
|
||||||
occurrenceStart: Long? = null,
|
|
||||||
returnHome: Boolean = false,
|
|
||||||
): Intent =
|
|
||||||
Intent(context, MainActivity::class.java).apply {
|
Intent(context, MainActivity::class.java).apply {
|
||||||
putExtra(EXTRA_TASK_ID, taskId)
|
putExtra(EXTRA_TASK_ID, taskId)
|
||||||
putExtra(EXTRA_OCCURRENCE_START, occurrenceStart ?: NO_OCCURRENCE)
|
putExtra(EXTRA_OCCURRENCE_START, occurrenceStart ?: NO_OCCURRENCE)
|
||||||
putExtra(EXTRA_RETURN_HOME, returnHome)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +216,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_NEW_TASK -> return NavRequest.NewTask(
|
ACTION_NEW_TASK -> return NavRequest.NewTask(
|
||||||
listId = intent.getLongExtra(EXTRA_PRESET_LIST_ID, -1L).takeIf { it > 0L },
|
listId = intent.getLongExtra(EXTRA_PRESET_LIST_ID, -1L).takeIf { it > 0L },
|
||||||
returnHome = true,
|
|
||||||
)
|
)
|
||||||
ACTION_TODAY -> return NavRequest.OpenSmart(SmartList.TODAY)
|
ACTION_TODAY -> return NavRequest.OpenSmart(SmartList.TODAY)
|
||||||
ACTION_OPEN_SMART -> intent.getStringExtra(EXTRA_SMART_LIST)
|
ACTION_OPEN_SMART -> intent.getStringExtra(EXTRA_SMART_LIST)
|
||||||
@@ -238,12 +230,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (intent.action == Intent.ACTION_SEND && intent.type?.startsWith("text/plain") == true) {
|
if (intent.action == Intent.ACTION_SEND && intent.type?.startsWith("text/plain") == true) {
|
||||||
val shared = intent.getStringExtra(Intent.EXTRA_SUBJECT)?.takeIf { it.isNotBlank() }
|
val shared = intent.getStringExtra(Intent.EXTRA_SUBJECT)?.takeIf { it.isNotBlank() }
|
||||||
?: intent.getStringExtra(Intent.EXTRA_TEXT)
|
?: 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 taskId = intent.getLongExtra(EXTRA_TASK_ID, -1L).takeIf { it > 0L } ?: return null
|
||||||
val occurrence = intent.getLongExtra(EXTRA_OCCURRENCE_START, NO_OCCURRENCE)
|
val occurrence = intent.getLongExtra(EXTRA_OCCURRENCE_START, NO_OCCURRENCE)
|
||||||
.takeIf { it != NO_OCCURRENCE }
|
.takeIf { it != NO_OCCURRENCE }
|
||||||
return NavRequest.OpenTask(taskId, occurrence, intent.getBooleanExtra(EXTRA_RETURN_HOME, false))
|
return NavRequest.OpenTask(taskId, occurrence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.appname
|
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import de.jeanlucmakiola.agendula.ui.navigation.AppShortcuts
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/** The label the launcher shows for the app (#104). */
|
|
||||||
enum class LauncherName { AGENDULA, TASKS }
|
|
||||||
|
|
||||||
/** The two launcher aliases declared in the manifest. */
|
|
||||||
enum class LauncherAlias { DEFAULT, TASKS }
|
|
||||||
|
|
||||||
/** One component-enable change in an [aliasWritePlan]. */
|
|
||||||
data class AliasStateChange(val alias: LauncherAlias, val enabled: Boolean)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the `TasksNameAlias` enabled state as a [LauncherName]. Only an explicit
|
|
||||||
* enable means "Tasks"; never toggled and disabled both mean the manifest default.
|
|
||||||
*/
|
|
||||||
fun launcherNameFor(tasksAliasState: Int): LauncherName =
|
|
||||||
if (tasksAliasState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
|
||||||
LauncherName.TASKS
|
|
||||||
} else {
|
|
||||||
LauncherName.AGENDULA
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The steps to switch to [target]: enable the target alias first, then disable
|
|
||||||
* the other, so the launcher can briefly see two entries but never none.
|
|
||||||
*/
|
|
||||||
fun aliasWritePlan(target: LauncherName): List<AliasStateChange> = when (target) {
|
|
||||||
LauncherName.TASKS -> listOf(
|
|
||||||
AliasStateChange(LauncherAlias.TASKS, enabled = true),
|
|
||||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = false),
|
|
||||||
)
|
|
||||||
LauncherName.AGENDULA -> listOf(
|
|
||||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = true),
|
|
||||||
AliasStateChange(LauncherAlias.TASKS, enabled = false),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches the launcher label by enabling one of the two `<activity-alias>`
|
|
||||||
* components. The component state is the only source of truth; nothing is
|
|
||||||
* stored in prefs.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
class LauncherNameManager @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
) {
|
|
||||||
private val packageManager: PackageManager get() = context.packageManager
|
|
||||||
|
|
||||||
// The package is the (suffixed) applicationId, the class is namespace-qualified.
|
|
||||||
private fun component(alias: LauncherAlias): ComponentName {
|
|
||||||
val simpleName = when (alias) {
|
|
||||||
LauncherAlias.DEFAULT -> "DefaultNameAlias"
|
|
||||||
LauncherAlias.TASKS -> "TasksNameAlias"
|
|
||||||
}
|
|
||||||
return ComponentName(context.packageName, "$NAMESPACE.$simpleName")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun current(): LauncherName =
|
|
||||||
launcherNameFor(packageManager.getComponentEnabledSetting(component(LauncherAlias.TASKS)))
|
|
||||||
|
|
||||||
fun set(name: LauncherName) {
|
|
||||||
for (change in aliasWritePlan(name)) {
|
|
||||||
val state = if (change.enabled) {
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
||||||
} else {
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
||||||
}
|
|
||||||
packageManager.setComponentEnabledSetting(component(change.alias), state, PackageManager.DONT_KILL_APP)
|
|
||||||
}
|
|
||||||
// Dynamic shortcuts hang off the launcher activity, which just changed.
|
|
||||||
AppShortcuts.publish(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
/** `namespace` in app/build.gradle.kts, not the suffixed applicationId. */
|
|
||||||
const val NAMESPACE = "de.jeanlucmakiola.agendula"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import de.jeanlucmakiola.agendula.domain.Task
|
|||||||
import de.jeanlucmakiola.agendula.domain.TaskFilter
|
import de.jeanlucmakiola.agendula.domain.TaskFilter
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskFormField
|
import de.jeanlucmakiola.agendula.domain.TaskFormField
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskReminder
|
import de.jeanlucmakiola.agendula.domain.TaskReminder
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
|
||||||
import de.jeanlucmakiola.agendula.domain.asTaskReminder
|
import de.jeanlucmakiola.agendula.domain.asTaskReminder
|
||||||
import de.jeanlucmakiola.agendula.domain.asReminderLead
|
import de.jeanlucmakiola.agendula.domain.asReminderLead
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
||||||
@@ -32,18 +31,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/** [BLACK] is dark with pure-black surfaces, for AMOLED screens. */
|
enum class ThemeMode { SYSTEM, LIGHT, DARK }
|
||||||
enum class ThemeMode {
|
|
||||||
SYSTEM, LIGHT, DARK, BLACK;
|
|
||||||
|
|
||||||
fun isDark(systemDark: Boolean): Boolean = when (this) {
|
|
||||||
SYSTEM -> systemDark
|
|
||||||
LIGHT -> false
|
|
||||||
DARK, BLACK -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
val isPureBlack: Boolean get() = this == BLACK
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Clock convention: AUTO follows the device's 24-hour switch. */
|
/** Clock convention: AUTO follows the device's 24-hour switch. */
|
||||||
enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR }
|
enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR }
|
||||||
@@ -55,7 +43,6 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val DEFAULT_SNOOZE_MINUTES = 10
|
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. */
|
/** The snooze lengths Settings offers. */
|
||||||
val SNOOZE_PRESETS = listOf(5, 10, 15, 30, 60)
|
val SNOOZE_PRESETS = listOf(5, 10, 15, 30, 60)
|
||||||
@@ -116,8 +103,6 @@ data class Settings(
|
|||||||
val hiddenFromSmartLists: Set<Long> = emptySet(),
|
val hiddenFromSmartLists: Set<Long> = emptySet(),
|
||||||
/** Pre-fill a new task's start with today's date. */
|
/** Pre-fill a new task's start with today's date. */
|
||||||
val defaultStartToday: Boolean = false,
|
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. */
|
/** [tasks] as [filter] shows them: a smart list drops the lists kept out of it. */
|
||||||
fun visibleIn(filter: TaskFilter, tasks: List<Task>): List<Task> =
|
fun visibleIn(filter: TaskFilter, tasks: List<Task>): List<Task> =
|
||||||
@@ -183,18 +168,9 @@ class SettingsPrefs @Inject constructor(
|
|||||||
.toSet()
|
.toSet()
|
||||||
},
|
},
|
||||||
defaultStartToday = p[DEFAULT_START_TODAY] ?: false,
|
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 setSortOrder(order: TaskSortOrder) = dataStore.edit { it[SORT_ORDER] = order.name }
|
||||||
|
|
||||||
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
|
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
|
||||||
@@ -352,7 +328,6 @@ class SettingsPrefs @Inject constructor(
|
|||||||
val PUSH_ENABLED = booleanPreferencesKey("push_enabled")
|
val PUSH_ENABLED = booleanPreferencesKey("push_enabled")
|
||||||
val SMART_LIST_HIDDEN = stringSetPreferencesKey("smart_list_hidden")
|
val SMART_LIST_HIDDEN = stringSetPreferencesKey("smart_list_hidden")
|
||||||
val DEFAULT_START_TODAY = booleanPreferencesKey("default_start_today")
|
val DEFAULT_START_TODAY = booleanPreferencesKey("default_start_today")
|
||||||
val COLLAPSED_SECTIONS = stringSetPreferencesKey("collapsed_sections")
|
|
||||||
val SAVED_FILTERS = stringPreferencesKey("saved_filters")
|
val SAVED_FILTERS = stringPreferencesKey("saved_filters")
|
||||||
val SAVED_FILTER_NEXT_ID = longPreferencesKey("saved_filter_next_id")
|
val SAVED_FILTER_NEXT_ID = longPreferencesKey("saved_filter_next_id")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.core.content.edit
|
|||||||
import de.jeanlucmakiola.agendula.MainActivity
|
import de.jeanlucmakiola.agendula.MainActivity
|
||||||
import de.jeanlucmakiola.agendula.R
|
import de.jeanlucmakiola.agendula.R
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||||
import de.jeanlucmakiola.agendula.domain.readableDescription
|
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
||||||
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.ProviderResolver
|
import de.jeanlucmakiola.agendula.data.tasks.ProviderResolver
|
||||||
@@ -85,7 +84,6 @@ class TaskNotifier @Inject constructor(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val description = task.description?.readableDescription()?.trim()?.takeIf { it.isNotBlank() }
|
|
||||||
val tapIntent = PendingIntent.getActivity(
|
val tapIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
task.taskId.toInt(),
|
task.taskId.toInt(),
|
||||||
@@ -95,10 +93,7 @@ class TaskNotifier @Inject constructor(
|
|||||||
val notification = NotificationCompat.Builder(context, channelId)
|
val notification = NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.apply { (text ?: description)?.let(::setContentText) }
|
.apply { if (text != null) setContentText(text) }
|
||||||
.apply {
|
|
||||||
expandedText(text, description)?.let { setStyle(NotificationCompat.BigTextStyle().bigText(it)) }
|
|
||||||
}
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
@@ -245,10 +240,3 @@ private fun relativeDay(date: LocalDate, today: LocalDate, words: RelativeDays,
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The expanded body of a due reminder: the due line, then the task's description. */
|
|
||||||
internal fun expandedText(dueLine: String?, description: String?): String? = when {
|
|
||||||
description == null -> null
|
|
||||||
dueLine == null -> description
|
|
||||||
else -> "$dueLine\n\n$description"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.sync
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
|
|
||||||
/** Pull to sync (#73): syncs every account at once. Inert in the `offline` flavor. */
|
|
||||||
interface ManualSync {
|
|
||||||
/** Whether there is anything to sync: our own store, with at least one account. */
|
|
||||||
val available: Flow<Boolean>
|
|
||||||
|
|
||||||
/** True from [syncAll] until the syncs it started have finished. */
|
|
||||||
val syncing: StateFlow<Boolean>
|
|
||||||
|
|
||||||
fun syncAll()
|
|
||||||
|
|
||||||
object None : ManualSync {
|
|
||||||
override val available: Flow<Boolean> = flowOf(false)
|
|
||||||
override val syncing: StateFlow<Boolean> = MutableStateFlow(false)
|
|
||||||
override fun syncAll() = Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,6 @@ fun RootScreen(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navRequest: NavRequest? = null,
|
navRequest: NavRequest? = null,
|
||||||
onNavRequestConsumed: () -> Unit = {},
|
onNavRequestConsumed: () -> Unit = {},
|
||||||
onReturnHome: () -> Unit = {},
|
|
||||||
permissionViewModel: PermissionViewModel = hiltViewModel(),
|
permissionViewModel: PermissionViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val permission by permissionViewModel.state.collectAsStateWithLifecycle()
|
val permission by permissionViewModel.state.collectAsStateWithLifecycle()
|
||||||
@@ -87,7 +86,6 @@ fun RootScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navRequest = navRequest,
|
navRequest = navRequest,
|
||||||
onNavRequestConsumed = onNavRequestConsumed,
|
onNavRequestConsumed = onNavRequestConsumed,
|
||||||
onReturnHome = onReturnHome,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +101,6 @@ private fun ReadyGate(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navRequest: NavRequest? = null,
|
navRequest: NavRequest? = null,
|
||||||
onNavRequestConsumed: () -> Unit = {},
|
onNavRequestConsumed: () -> Unit = {},
|
||||||
onReturnHome: () -> Unit = {},
|
|
||||||
onboardingViewModel: OnboardingViewModel = hiltViewModel(),
|
onboardingViewModel: OnboardingViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val done by onboardingViewModel.done.collectAsStateWithLifecycle()
|
val done by onboardingViewModel.done.collectAsStateWithLifecycle()
|
||||||
@@ -116,7 +113,6 @@ private fun ReadyGate(
|
|||||||
modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface),
|
modifier = modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface),
|
||||||
navRequest = navRequest,
|
navRequest = navRequest,
|
||||||
onNavRequestConsumed = onNavRequestConsumed,
|
onNavRequestConsumed = onNavRequestConsumed,
|
||||||
onReturnHome = onReturnHome,
|
|
||||||
)
|
)
|
||||||
false -> ReadableWidth(modifier) {
|
false -> ReadableWidth(modifier) {
|
||||||
OnboardingFlow(modifier = Modifier.fillMaxSize(), viewModel = onboardingViewModel)
|
OnboardingFlow(modifier = Modifier.fillMaxSize(), viewModel = onboardingViewModel)
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.systemGestures
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.positionInWindow
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/** How far past the system back-gesture zone a horizontal swipe is still ignored. */
|
|
||||||
private val EDGE_MARGIN = 24.dp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swallows horizontal swipes that start near the screen's left or right edge,
|
|
||||||
* before a swipe box below it sees them (#90). A back gesture started just
|
|
||||||
* inside the system's gesture zone otherwise reaches the app as a row swipe.
|
|
||||||
* Taps and vertical scrolls are left alone.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun Modifier.ignoringEdgeSwipes(): Modifier {
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
|
||||||
val windowWidth = LocalWindowInfo.current.containerSize.width
|
|
||||||
val insets = WindowInsets.systemGestures
|
|
||||||
val margin = with(density) { EDGE_MARGIN.roundToPx() }
|
|
||||||
val leftEdge = insets.getLeft(density, layoutDirection) + margin
|
|
||||||
val rightEdge = windowWidth - insets.getRight(density, layoutDirection) - margin
|
|
||||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
|
||||||
return this
|
|
||||||
.onGloballyPositioned { offsetX = it.positionInWindow().x }
|
|
||||||
.pointerInput(leftEdge, rightEdge) {
|
|
||||||
val slop = viewConfiguration.touchSlop
|
|
||||||
awaitEachGesture {
|
|
||||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
|
||||||
val x = offsetX + down.position.x
|
|
||||||
if (x > leftEdge && x < rightEdge) return@awaitEachGesture
|
|
||||||
var horizontal = false
|
|
||||||
while (true) {
|
|
||||||
val change = awaitPointerEvent(PointerEventPass.Initial).changes
|
|
||||||
.firstOrNull { it.id == down.id } ?: break
|
|
||||||
if (!horizontal) {
|
|
||||||
val delta = change.position - down.position
|
|
||||||
when {
|
|
||||||
abs(delta.x) > slop -> horizontal = true
|
|
||||||
abs(delta.y) > slop -> break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (horizontal) change.consume()
|
|
||||||
if (!change.pressed) break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pull down to sync every account (#73). Without an account there is nothing
|
|
||||||
* to sync, so the gesture is off. [topPadding] keeps the indicator below a top
|
|
||||||
* bar the content scrolls under.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
|
||||||
fun SyncPullBox(
|
|
||||||
available: Boolean,
|
|
||||||
syncing: Boolean,
|
|
||||||
onSync: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
topPadding: Dp = 0.dp,
|
|
||||||
content: @Composable BoxScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
if (!available) {
|
|
||||||
Box(modifier, content = content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val state = rememberPullToRefreshState()
|
|
||||||
PullToRefreshBox(
|
|
||||||
isRefreshing = syncing,
|
|
||||||
onRefresh = onSync,
|
|
||||||
modifier = modifier,
|
|
||||||
state = state,
|
|
||||||
indicator = {
|
|
||||||
PullToRefreshDefaults.LoadingIndicator(
|
|
||||||
state = state,
|
|
||||||
isRefreshing = syncing,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter).padding(top = topPadding),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -97,7 +97,6 @@ import kotlinx.coroutines.delay
|
|||||||
import de.jeanlucmakiola.agendula.ui.common.ListColorChip
|
import de.jeanlucmakiola.agendula.ui.common.ListColorChip
|
||||||
import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.agendula.ui.common.ShapedActionButton
|
import de.jeanlucmakiola.agendula.ui.common.ShapedActionButton
|
||||||
import de.jeanlucmakiola.agendula.ui.common.SyncPullBox
|
|
||||||
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
|
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
|
||||||
import de.jeanlucmakiola.agendula.ui.tasklist.priorityLabel
|
import de.jeanlucmakiola.agendula.ui.tasklist.priorityLabel
|
||||||
import de.jeanlucmakiola.floret.components.positionOf
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
@@ -123,8 +122,6 @@ fun ListsScreen(
|
|||||||
viewModel: ListsViewModel = hiltViewModel(),
|
viewModel: ListsViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val syncAvailable by viewModel.syncAvailable.collectAsStateWithLifecycle()
|
|
||||||
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
|
||||||
var query by rememberSaveable { mutableStateOf("") }
|
var query by rememberSaveable { mutableStateOf("") }
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
var newList by rememberSaveable { mutableStateOf(false) }
|
var newList by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -176,12 +173,7 @@ fun ListsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { inner ->
|
) { inner ->
|
||||||
SyncPullBox(
|
Box(Modifier.fillMaxSize().padding(top = inner.calculateTopPadding())) {
|
||||||
available = syncAvailable && !searchActive,
|
|
||||||
syncing = syncing,
|
|
||||||
onSync = viewModel::syncAll,
|
|
||||||
modifier = Modifier.fillMaxSize().padding(top = inner.calculateTopPadding()),
|
|
||||||
) {
|
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
ListsUiState.Loading -> Unit // brief; avoids a flash before first emission
|
ListsUiState.Loading -> Unit // brief; avoids a flash before first emission
|
||||||
ListsUiState.Failure ->
|
ListsUiState.Failure ->
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.agendula.data.sync.ManualSync
|
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.recoveringFromProviderFailure
|
import de.jeanlucmakiola.agendula.data.tasks.recoveringFromProviderFailure
|
||||||
@@ -106,17 +105,9 @@ private const val UPCOMING_PREVIEW = 3
|
|||||||
class ListsViewModel @Inject constructor(
|
class ListsViewModel @Inject constructor(
|
||||||
private val repository: TasksRepository,
|
private val repository: TasksRepository,
|
||||||
private val remoteLists: RemoteLists,
|
private val remoteLists: RemoteLists,
|
||||||
private val manualSync: ManualSync,
|
|
||||||
settingsPrefs: SettingsPrefs,
|
settingsPrefs: SettingsPrefs,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
/** Pull to sync (#73); off without an account. */
|
|
||||||
val syncAvailable: StateFlow<Boolean> =
|
|
||||||
manualSync.available.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
|
||||||
val syncing: StateFlow<Boolean> = manualSync.syncing
|
|
||||||
|
|
||||||
fun syncAll() = manualSync.syncAll()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a new list may go: the device, then every account whose server said
|
* Where a new list may go: the device, then every account whose server said
|
||||||
* it would take one.
|
* it would take one.
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -51,19 +46,8 @@ fun AgendulaNavHost(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navRequest: NavRequest? = null,
|
navRequest: NavRequest? = null,
|
||||||
onNavRequestConsumed: () -> Unit = {},
|
onNavRequestConsumed: () -> Unit = {},
|
||||||
onReturnHome: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val nav = rememberNavController()
|
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) {
|
LaunchedEffect(navRequest) {
|
||||||
when (navRequest) {
|
when (navRequest) {
|
||||||
@@ -87,9 +71,6 @@ fun AgendulaNavHost(
|
|||||||
}
|
}
|
||||||
is NavRequest.Import -> nav.navigate(Dest.Import.build(navRequest.uri))
|
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()
|
onNavRequestConsumed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,8 @@ import de.jeanlucmakiola.agendula.domain.SmartList
|
|||||||
* [AgendulaNavHost], which pushes it over whatever is showing.
|
* [AgendulaNavHost], which pushes it over whatever is showing.
|
||||||
*/
|
*/
|
||||||
sealed interface NavRequest {
|
sealed interface NavRequest {
|
||||||
/**
|
/** One task's detail; [occurrenceStart] (epoch millis) picks the occurrence of a series. */
|
||||||
* One task's detail; [occurrenceStart] (epoch millis) picks the occurrence of a series.
|
data class OpenTask(val taskId: Long, val occurrenceStart: Long? = null) : NavRequest
|
||||||
* [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
|
|
||||||
|
|
||||||
/** Settings → Accounts, where a sync report's detail lives. */
|
/** Settings → Accounts, where a sync report's detail lives. */
|
||||||
data object OpenAccounts : NavRequest
|
data object OpenAccounts : NavRequest
|
||||||
@@ -28,13 +21,8 @@ sealed interface NavRequest {
|
|||||||
/**
|
/**
|
||||||
* A new task, optionally titled with text shared from another app, or
|
* 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.
|
* 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(
|
data class NewTask(val title: String? = null, val listId: Long? = null) : NavRequest
|
||||||
val title: String? = null,
|
|
||||||
val listId: Long? = null,
|
|
||||||
val returnHome: Boolean = false,
|
|
||||||
) : NavRequest
|
|
||||||
|
|
||||||
/** One of the smart lists — the launcher's "Today" shortcut, a widget header. */
|
/** One of the smart lists — the launcher's "Today" shortcut, a widget header. */
|
||||||
data class OpenSmart(val list: SmartList) : NavRequest
|
data class OpenSmart(val list: SmartList) : NavRequest
|
||||||
|
|||||||
@@ -14,16 +14,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import de.jeanlucmakiola.agendula.R
|
import de.jeanlucmakiola.agendula.R
|
||||||
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.TimeFormatPref
|
import de.jeanlucmakiola.agendula.data.prefs.TimeFormatPref
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
||||||
import de.jeanlucmakiola.agendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.agendula.ui.common.formatMinuteOfDay
|
||||||
import de.jeanlucmakiola.agendula.ui.common.localeFirstDayOfWeek
|
import de.jeanlucmakiola.agendula.ui.common.localeFirstDayOfWeek
|
||||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.floret.components.GroupedHint
|
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.OptionPicker
|
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
@@ -41,8 +38,6 @@ internal fun AppearanceScreen(
|
|||||||
) {
|
) {
|
||||||
val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
var showTheme by remember { mutableStateOf(false) }
|
var showTheme by remember { mutableStateOf(false) }
|
||||||
var showAppName by remember { mutableStateOf(false) }
|
|
||||||
val launcherName by viewModel.launcherName.collectAsStateWithLifecycle()
|
|
||||||
var showTimeFormat by remember { mutableStateOf(false) }
|
var showTimeFormat by remember { mutableStateOf(false) }
|
||||||
var showWeekStart by remember { mutableStateOf(false) }
|
var showWeekStart by remember { mutableStateOf(false) }
|
||||||
val systemIs24Hour = DateFormat.is24HourFormat(LocalContext.current)
|
val systemIs24Hour = DateFormat.is24HourFormat(LocalContext.current)
|
||||||
@@ -95,26 +90,6 @@ internal fun AppearanceScreen(
|
|||||||
position = Position.Bottom,
|
position = Position.Bottom,
|
||||||
onClick = { showWeekStart = true },
|
onClick = { showWeekStart = true },
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
|
||||||
GroupedRow(
|
|
||||||
title = stringResource(R.string.settings_app_name),
|
|
||||||
summary = launcherNameLabel(launcherName),
|
|
||||||
position = Position.Alone,
|
|
||||||
onClick = { showAppName = true },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showAppName) {
|
|
||||||
OptionPicker(
|
|
||||||
title = stringResource(R.string.settings_app_name),
|
|
||||||
options = LauncherName.entries,
|
|
||||||
selected = launcherName,
|
|
||||||
label = { launcherNameLabel(it) },
|
|
||||||
header = { GroupedHint(stringResource(R.string.settings_app_name_hint)) },
|
|
||||||
onSelect = viewModel::setLauncherName,
|
|
||||||
onDismiss = { showAppName = false },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTimeFormat) {
|
if (showTimeFormat) {
|
||||||
@@ -165,7 +140,6 @@ internal fun AppearanceScreen(
|
|||||||
options = ThemeMode.entries,
|
options = ThemeMode.entries,
|
||||||
selected = state.settings.themeMode,
|
selected = state.settings.themeMode,
|
||||||
label = { stringResource(themeLabel(it)) },
|
label = { stringResource(themeLabel(it)) },
|
||||||
summary = { if (it == ThemeMode.BLACK) stringResource(R.string.settings_theme_black_summary) else null },
|
|
||||||
onSelect = viewModel::setThemeMode,
|
onSelect = viewModel::setThemeMode,
|
||||||
onDismiss = { showTheme = false },
|
onDismiss = { showTheme = false },
|
||||||
)
|
)
|
||||||
@@ -189,17 +163,8 @@ private fun weekStartLabel(day: DayOfWeek?): String =
|
|||||||
/** 14:00, the sample time "Automatic" is shown with. */
|
/** 14:00, the sample time "Automatic" is shown with. */
|
||||||
private const val SAMPLE_MINUTE_OF_DAY = 14 * 60
|
private const val SAMPLE_MINUTE_OF_DAY = 14 * 60
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun launcherNameLabel(name: LauncherName): String = stringResource(
|
|
||||||
when (name) {
|
|
||||||
LauncherName.AGENDULA -> R.string.app_name
|
|
||||||
LauncherName.TASKS -> R.string.app_name_tasks_alias
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun themeLabel(mode: ThemeMode): Int = when (mode) {
|
private fun themeLabel(mode: ThemeMode): Int = when (mode) {
|
||||||
ThemeMode.SYSTEM -> R.string.settings_theme_system
|
ThemeMode.SYSTEM -> R.string.settings_theme_system
|
||||||
ThemeMode.LIGHT -> R.string.settings_theme_light
|
ThemeMode.LIGHT -> R.string.settings_theme_light
|
||||||
ThemeMode.DARK -> R.string.settings_theme_dark
|
ThemeMode.DARK -> R.string.settings_theme_dark
|
||||||
ThemeMode.BLACK -> R.string.settings_theme_black
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package de.jeanlucmakiola.agendula.ui.settings
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
|
||||||
import de.jeanlucmakiola.agendula.data.appname.LauncherNameManager
|
|
||||||
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
|
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||||
@@ -35,7 +33,6 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -105,8 +102,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private val oneShotImport: OneShotImport,
|
private val oneShotImport: OneShotImport,
|
||||||
private val reminderScheduler: ReminderScheduler,
|
private val reminderScheduler: ReminderScheduler,
|
||||||
private val taskNotifier: TaskNotifier,
|
private val taskNotifier: TaskNotifier,
|
||||||
private val launcherNameManager: LauncherNameManager,
|
@IoDispatcher io: CoroutineDispatcher,
|
||||||
@IoDispatcher private val io: CoroutineDispatcher,
|
|
||||||
repository: TasksRepository,
|
repository: TasksRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -221,23 +217,6 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
val legacyImportRetrying: StateFlow<Boolean> = retryingLegacyImport
|
val legacyImportRetrying: StateFlow<Boolean> = retryingLegacyImport
|
||||||
|
|
||||||
private val _launcherName = MutableStateFlow(LauncherName.AGENDULA)
|
|
||||||
val launcherName: StateFlow<LauncherName> = _launcherName
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch { _launcherName.value = withContext(io) { launcherNameManager.current() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLauncherName(name: LauncherName) {
|
|
||||||
_launcherName.value = name
|
|
||||||
viewModelScope.launch {
|
|
||||||
_launcherName.value = withContext(io) {
|
|
||||||
launcherNameManager.set(name)
|
|
||||||
launcherNameManager.current()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setThemeMode(mode: ThemeMode) = viewModelScope.launch { prefs.setThemeMode(mode) }
|
fun setThemeMode(mode: ThemeMode) = viewModelScope.launch { prefs.setThemeMode(mode) }
|
||||||
fun setDynamicColor(enabled: Boolean) = viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
fun setDynamicColor(enabled: Boolean) = viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
||||||
fun setDefaultList(id: Long?) = viewModelScope.launch { prefs.setDefaultListId(id) }
|
fun setDefaultList(id: Long?) = viewModelScope.launch { prefs.setDefaultListId(id) }
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ import de.jeanlucmakiola.agendula.domain.TaskFilter
|
|||||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
import de.jeanlucmakiola.agendula.domain.TaskSection
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskSections
|
import de.jeanlucmakiola.agendula.domain.TaskSections
|
||||||
import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.agendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.agendula.ui.common.SyncPullBox
|
|
||||||
import de.jeanlucmakiola.agendula.ui.common.ignoringEdgeSwipes
|
|
||||||
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
|
import de.jeanlucmakiola.agendula.ui.common.priorityAccent
|
||||||
import de.jeanlucmakiola.agendula.ui.lists.ListEditorSheet
|
import de.jeanlucmakiola.agendula.ui.lists.ListEditorSheet
|
||||||
import de.jeanlucmakiola.agendula.ui.lists.listWriteFailureMessage
|
import de.jeanlucmakiola.agendula.ui.lists.listWriteFailureMessage
|
||||||
@@ -146,8 +144,6 @@ fun TaskListScreen(
|
|||||||
viewModel: TaskListViewModel = hiltViewModel(),
|
viewModel: TaskListViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val syncAvailable by viewModel.syncAvailable.collectAsStateWithLifecycle()
|
|
||||||
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
val content = state as? TaskListUiState.Content
|
val content = state as? TaskListUiState.Content
|
||||||
val list = content?.list
|
val list = content?.list
|
||||||
@@ -251,21 +247,13 @@ fun TaskListScreen(
|
|||||||
},
|
},
|
||||||
) { inner ->
|
) { inner ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
SyncPullBox(
|
when (val s = state) {
|
||||||
available = syncAvailable,
|
TaskListUiState.Loading -> Unit // brief; avoids a flash before first emission
|
||||||
syncing = syncing,
|
TaskListUiState.Failure -> CenteredMessage(stringResource(R.string.task_list_failure), inner)
|
||||||
onSync = viewModel::syncAll,
|
is TaskListUiState.Content -> TaskListBody(
|
||||||
modifier = Modifier.fillMaxSize(),
|
s, filter, inner, onOpenTask, onRequestDelete, viewModel,
|
||||||
topPadding = inner.calculateTopPadding(),
|
onClearCompleted = if (listId != null && list?.isReadOnly != true) ({ confirmClear = true }) else null,
|
||||||
) {
|
)
|
||||||
when (val s = state) {
|
|
||||||
TaskListUiState.Loading -> Unit // brief; avoids a flash before first emission
|
|
||||||
TaskListUiState.Failure -> CenteredMessage(stringResource(R.string.task_list_failure), inner)
|
|
||||||
is TaskListUiState.Content -> TaskListBody(
|
|
||||||
s, filter, inner, onOpenTask, onRequestDelete, viewModel,
|
|
||||||
onClearCompleted = if (listId != null && list?.isReadOnly != true) ({ confirmClear = true }) else null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// A FAB-height band anchored at the bottom-start with the FAB's own
|
// A FAB-height band anchored at the bottom-start with the FAB's own
|
||||||
// margin; centring the chip in it lines the chip up beside the bottom-end
|
// margin; centring the chip in it lines the chip up beside the bottom-end
|
||||||
@@ -383,6 +371,8 @@ private fun TaskListBody(
|
|||||||
// rows; inside a single real list both are redundant.
|
// rows; inside a single real list both are redundant.
|
||||||
val showListName = filter is TaskFilter.Smart || filter is TaskFilter.Tagged ||
|
val showListName = filter is TaskFilter.Smart || filter is TaskFilter.Tagged ||
|
||||||
(filter is TaskFilter.Saved && state.savedFilter?.listIds?.size != 1)
|
(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
|
// Parents the user expanded to reveal their subtasks inline. Children are
|
||||||
// grouped from the same flat task set the list already holds.
|
// grouped from the same flat task set the list already holds.
|
||||||
var expandedParents by rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
var expandedParents by rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||||
@@ -430,20 +420,23 @@ private fun TaskListBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sections.forEach { section ->
|
sections.forEach { section ->
|
||||||
// Every header folds its section away (#91); Done starts folded.
|
val collapsible = showHeaders && section.section == TaskSection.COMPLETED
|
||||||
val expanded = !showHeaders || section.section !in state.collapsedSections
|
|
||||||
if (showHeaders) {
|
if (showHeaders) {
|
||||||
stickyHeader(key = "hdr-${section.section}") {
|
stickyHeader(key = "hdr-${section.section}") {
|
||||||
CollapsibleSectionHeader(
|
if (collapsible) {
|
||||||
label = sectionLabel(section.section),
|
CollapsibleSectionHeader(
|
||||||
count = section.tasks.size,
|
label = sectionLabel(section.section),
|
||||||
expanded = expanded,
|
count = section.tasks.size,
|
||||||
onToggle = { viewModel.setSectionCollapsed(section.section, expanded) },
|
expanded = completedExpanded,
|
||||||
onClear = onClearCompleted.takeIf { section.section == TaskSection.COMPLETED },
|
onToggle = { completedExpanded = !completedExpanded },
|
||||||
)
|
onClear = onClearCompleted,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SectionHeader(sectionLabel(section.section))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (expanded) {
|
if (!collapsible || completedExpanded) {
|
||||||
// Flatten the section into one grouped run: each parent, followed
|
// Flatten the section into one grouped run: each parent, followed
|
||||||
// by its subtasks when expanded. Each row's corners then come from
|
// by its subtasks when expanded. Each row's corners then come from
|
||||||
// its place in the whole run (positionOf below) — so the children
|
// its place in the whole run (positionOf below) — so the children
|
||||||
@@ -662,7 +655,7 @@ private fun TaskRow(
|
|||||||
|
|
||||||
SwipeToDismissBox(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
modifier = modifier.ignoringEdgeSwipes().fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||||
// dismissDirection (not targetValue) tracks the live drag, so the colour +
|
// dismissDirection (not targetValue) tracks the live drag, so the colour +
|
||||||
// icon reveal as you swipe rather than only once the threshold is crossed.
|
// icon reveal as you swipe rather than only once the threshold is crossed.
|
||||||
backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) },
|
backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) },
|
||||||
@@ -1044,7 +1037,7 @@ private fun SubtaskRow(
|
|||||||
val gap = if (lastInRun) Modifier else Modifier.padding(bottom = 2.dp)
|
val gap = if (lastInRun) Modifier else Modifier.padding(bottom = 2.dp)
|
||||||
SwipeToDismissBox(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
modifier = modifier.ignoringEdgeSwipes().fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp).then(gap),
|
||||||
backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) },
|
backgroundContent = { SwipeBackground(dismissState.dismissDirection, task.isCompleted) },
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
@@ -1134,6 +1127,19 @@ private fun cardShape(position: Position, full: androidx.compose.ui.unit.Dp, sma
|
|||||||
Position.Bottom -> RoundedCornerShape(topStart = small, topEnd = small, bottomStart = full, bottomEnd = full)
|
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. */
|
/** A tappable section header with a count and a chevron that rotates on expand. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun CollapsibleSectionHeader(
|
private fun CollapsibleSectionHeader(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package de.jeanlucmakiola.agendula.ui.tasklist
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
||||||
@@ -15,13 +14,11 @@ import de.jeanlucmakiola.agendula.domain.TaskFiltering
|
|||||||
import de.jeanlucmakiola.agendula.domain.tagsInUse
|
import de.jeanlucmakiola.agendula.domain.tagsInUse
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskForm
|
import de.jeanlucmakiola.agendula.domain.TaskForm
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskList
|
import de.jeanlucmakiola.agendula.domain.TaskList
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskSection
|
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
import de.jeanlucmakiola.agendula.domain.TaskSortOrder
|
||||||
import de.jeanlucmakiola.agendula.domain.comparator
|
import de.jeanlucmakiola.agendula.domain.comparator
|
||||||
import de.jeanlucmakiola.agendula.ui.lists.ListWriteFailure
|
import de.jeanlucmakiola.agendula.ui.lists.ListWriteFailure
|
||||||
import de.jeanlucmakiola.agendula.ui.lists.asFailure
|
import de.jeanlucmakiola.agendula.ui.lists.asFailure
|
||||||
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
||||||
import de.jeanlucmakiola.agendula.data.sync.ManualSync
|
|
||||||
import de.jeanlucmakiola.agendula.domain.recurrence.RecurringScope
|
import de.jeanlucmakiola.agendula.domain.recurrence.RecurringScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -59,8 +56,6 @@ sealed interface TaskListUiState {
|
|||||||
/** Whether a real list uses the bottom quick-add bar instead of the FAB. */
|
/** Whether a real list uses the bottom quick-add bar instead of the FAB. */
|
||||||
val bottomAddBar: Boolean = false,
|
val bottomAddBar: Boolean = false,
|
||||||
val sortOrder: TaskSortOrder = TaskSortOrder.DUE,
|
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. */
|
/** This real list is kept out of the smart lists. */
|
||||||
val hiddenFromSmartLists: Boolean = false,
|
val hiddenFromSmartLists: Boolean = false,
|
||||||
/** Tags the unnarrowed view carries, offered as filter chips. */
|
/** Tags the unnarrowed view carries, offered as filter chips. */
|
||||||
@@ -94,17 +89,9 @@ class TaskListViewModel @Inject constructor(
|
|||||||
private val repository: TasksRepository,
|
private val repository: TasksRepository,
|
||||||
private val settingsPrefs: SettingsPrefs,
|
private val settingsPrefs: SettingsPrefs,
|
||||||
private val remoteLists: RemoteLists,
|
private val remoteLists: RemoteLists,
|
||||||
private val manualSync: ManualSync,
|
|
||||||
@ApplicationScope private val appScope: CoroutineScope,
|
@ApplicationScope private val appScope: CoroutineScope,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
/** Pull to sync (#73); off without an account. */
|
|
||||||
val syncAvailable: StateFlow<Boolean> =
|
|
||||||
manualSync.available.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
|
||||||
val syncing: StateFlow<Boolean> = manualSync.syncing
|
|
||||||
|
|
||||||
fun syncAll() = manualSync.syncAll()
|
|
||||||
|
|
||||||
private val filter = MutableStateFlow<TaskFilter?>(null)
|
private val filter = MutableStateFlow<TaskFilter?>(null)
|
||||||
|
|
||||||
private val tagFilter = MutableStateFlow<Set<String>>(emptySet())
|
private val tagFilter = MutableStateFlow<Set<String>>(emptySet())
|
||||||
@@ -157,7 +144,6 @@ class TaskListViewModel @Inject constructor(
|
|||||||
showAddSubtaskRow = settings.showAddSubtaskRow,
|
showAddSubtaskRow = settings.showAddSubtaskRow,
|
||||||
bottomAddBar = settings.bottomAddBar,
|
bottomAddBar = settings.bottomAddBar,
|
||||||
sortOrder = settings.sortOrder,
|
sortOrder = settings.sortOrder,
|
||||||
collapsedSections = settings.collapsedSections,
|
|
||||||
hiddenFromSmartLists = st.list?.id?.let { it in settings.hiddenFromSmartLists } ?: false,
|
hiddenFromSmartLists = st.list?.id?.let { it in settings.hiddenFromSmartLists } ?: false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -291,9 +277,6 @@ class TaskListViewModel @Inject constructor(
|
|||||||
fun setHiddenFromSmartLists(listId: Long, hidden: Boolean) =
|
fun setHiddenFromSmartLists(listId: Long, hidden: Boolean) =
|
||||||
viewModelScope.launch { settingsPrefs.setHiddenFromSmartLists(listId, hidden) }
|
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) }
|
fun setSortOrder(order: TaskSortOrder) = viewModelScope.launch { settingsPrefs.setSortOrder(order) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import de.jeanlucmakiola.floret.identity.FloretExpressiveTheme
|
|||||||
fun AgendulaTheme(
|
fun AgendulaTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
pureBlack: Boolean = false,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
FloretExpressiveTheme(
|
FloretExpressiveTheme(
|
||||||
@@ -23,7 +22,6 @@ fun AgendulaTheme(
|
|||||||
darkScheme = AgendulaDarkFallback,
|
darkScheme = AgendulaDarkFallback,
|
||||||
darkTheme = darkTheme,
|
darkTheme = darkTheme,
|
||||||
dynamicColor = dynamicColor,
|
dynamicColor = dynamicColor,
|
||||||
pureBlack = pureBlack,
|
|
||||||
typography = AgendulaTypography,
|
typography = AgendulaTypography,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ internal fun AgendaTaskRow(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val title = row.title.ifBlank { context.getString(R.string.task_untitled) }
|
val title = row.title.ifBlank { context.getString(R.string.task_untitled) }
|
||||||
val open = actionStartActivity(
|
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 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
|
val onColorVariant = if (colored) GlanceColorProvider(Color.White.copy(alpha = 0.8f)) else GlanceTheme.colors.onSurfaceVariant
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import de.jeanlucmakiola.agendula.R
|
import de.jeanlucmakiola.agendula.R
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
import de.jeanlucmakiola.agendula.data.tasks.TasksRepository
|
||||||
import de.jeanlucmakiola.agendula.domain.SavedFilter
|
import de.jeanlucmakiola.agendula.domain.SavedFilter
|
||||||
import de.jeanlucmakiola.agendula.domain.SmartList
|
import de.jeanlucmakiola.agendula.domain.SmartList
|
||||||
@@ -134,12 +135,12 @@ class TaskWidgetConfigActivity : ComponentActivity() {
|
|||||||
rowColor = parseWidgetRowColor(state[WIDGET_ROW_COLOR_KEY])
|
rowColor = parseWidgetRowColor(state[WIDGET_ROW_COLOR_KEY])
|
||||||
selectedListIds = parseWidgetListIds(state[WIDGET_LIST_IDS_KEY])
|
selectedListIds = parseWidgetListIds(state[WIDGET_LIST_IDS_KEY])
|
||||||
}
|
}
|
||||||
val dark = settings.themeMode.isDark(isSystemInDarkTheme())
|
val dark = when (settings.themeMode) {
|
||||||
AgendulaTheme(
|
ThemeMode.SYSTEM -> isSystemInDarkTheme()
|
||||||
darkTheme = dark,
|
ThemeMode.LIGHT -> false
|
||||||
dynamicColor = settings.dynamicColor,
|
ThemeMode.DARK -> true
|
||||||
pureBlack = settings.themeMode.isPureBlack,
|
}
|
||||||
) {
|
AgendulaTheme(darkTheme = dark, dynamicColor = settings.dynamicColor) {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {}
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {}
|
||||||
WidgetFilterPicker(
|
WidgetFilterPicker(
|
||||||
kind = kind,
|
kind = kind,
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ private fun ListDot(color: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openTask(context: Context, row: WidgetTaskRow): Action =
|
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 =
|
private fun completeTask(row: WidgetTaskRow): Action =
|
||||||
actionRunCallback<CompleteTaskAction>(
|
actionRunCallback<CompleteTaskAction>(
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ internal fun effectiveThemeMode(appThemeMode: ThemeMode, override: WidgetThemeMo
|
|||||||
override ?: when (appThemeMode) {
|
override ?: when (appThemeMode) {
|
||||||
ThemeMode.SYSTEM -> WidgetThemeMode.SYSTEM
|
ThemeMode.SYSTEM -> WidgetThemeMode.SYSTEM
|
||||||
ThemeMode.LIGHT -> WidgetThemeMode.LIGHT
|
ThemeMode.LIGHT -> WidgetThemeMode.LIGHT
|
||||||
ThemeMode.DARK, ThemeMode.BLACK -> WidgetThemeMode.DARK
|
ThemeMode.DARK -> WidgetThemeMode.DARK
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The smart lists a widget can show, in picker order. */
|
/** The smart lists a widget can show, in picker order. */
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Agendula</string>
|
<string name="app_name">Agendula</string>
|
||||||
<string name="app_name_tasks_alias">Tasks</string>
|
|
||||||
|
|
||||||
<!-- Generic -->
|
<!-- Generic -->
|
||||||
<string name="task_untitled">Untitled task</string>
|
<string name="task_untitled">Untitled task</string>
|
||||||
@@ -449,10 +448,6 @@
|
|||||||
<string name="settings_theme_system">Follow system</string>
|
<string name="settings_theme_system">Follow system</string>
|
||||||
<string name="settings_theme_light">Light</string>
|
<string name="settings_theme_light">Light</string>
|
||||||
<string name="settings_theme_dark">Dark</string>
|
<string name="settings_theme_dark">Dark</string>
|
||||||
<string name="settings_theme_black">Black</string>
|
|
||||||
<string name="settings_theme_black_summary">Dark with pure black backgrounds, for AMOLED screens</string>
|
|
||||||
<string name="settings_app_name">App name</string>
|
|
||||||
<string name="settings_app_name_hint">Show Agendula as “Tasks” in your launcher. Only the launcher name changes; the icon may move to a new spot. Agendula closes when you switch, so reopen it from the launcher.</string>
|
|
||||||
<string name="settings_dynamic_color">Dynamic colour</string>
|
<string name="settings_dynamic_color">Dynamic colour</string>
|
||||||
<string name="settings_dynamic_color_hint">Use colours from your wallpaper</string>
|
<string name="settings_dynamic_color_hint">Use colours from your wallpaper</string>
|
||||||
<string name="settings_section_reminders">Notifications</string>
|
<string name="settings_section_reminders">Notifications</string>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import de.jeanlucmakiola.agendula.data.sync.ManualSync
|
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
import de.jeanlucmakiola.agendula.data.sync.RemoteLists
|
||||||
import de.jeanlucmakiola.agendula.data.sync.RemoteLists.Outcome
|
import de.jeanlucmakiola.agendula.data.sync.RemoteLists.Outcome
|
||||||
import de.jeanlucmakiola.agendula.data.tasks.room.AccountEntity
|
import de.jeanlucmakiola.agendula.data.tasks.room.AccountEntity
|
||||||
@@ -27,9 +26,6 @@ object SyncModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRemoteLists(): RemoteLists = NoRemoteLists
|
fun provideRemoteLists(): RemoteLists = NoRemoteLists
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun provideManualSync(): ManualSync = ManualSync.None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object NoRemoteLists : RemoteLists {
|
private object NoRemoteLists : RemoteLists {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.appname
|
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class LauncherNameManagerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `an enabled Tasks alias reads as Tasks`() {
|
|
||||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)).isEqualTo(LauncherName.TASKS)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a default or disabled Tasks alias reads as Agendula`() {
|
|
||||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)).isEqualTo(LauncherName.AGENDULA)
|
|
||||||
assertThat(launcherNameFor(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)).isEqualTo(LauncherName.AGENDULA)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `switching always enables the target before disabling the other`() {
|
|
||||||
assertThat(aliasWritePlan(LauncherName.TASKS)).containsExactly(
|
|
||||||
AliasStateChange(LauncherAlias.TASKS, enabled = true),
|
|
||||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = false),
|
|
||||||
).inOrder()
|
|
||||||
assertThat(aliasWritePlan(LauncherName.AGENDULA)).containsExactly(
|
|
||||||
AliasStateChange(LauncherAlias.DEFAULT, enabled = true),
|
|
||||||
AliasStateChange(LauncherAlias.TASKS, enabled = false),
|
|
||||||
).inOrder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.reminders
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
class ExpandedTextTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `no description means no expanded body`() {
|
|
||||||
assertThat(expandedText("Due today", null)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `description alone when there is no due line`() {
|
|
||||||
assertThat(expandedText(null, "Buy oat milk")).isEqualTo("Buy oat milk")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `due line comes first, separated by a blank line`() {
|
|
||||||
assertThat(expandedText("Due today", "Buy oat milk")).isEqualTo("Due today\n\nBuy oat milk")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -210,7 +210,6 @@ class WidgetDataTest {
|
|||||||
fun `no override resolves to whatever the app's own theme means`() {
|
fun `no override resolves to whatever the app's own theme means`() {
|
||||||
assertThat(readyWithTheme(ThemeMode.DARK).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.DARK)
|
assertThat(readyWithTheme(ThemeMode.DARK).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.DARK)
|
||||||
assertThat(readyWithTheme(ThemeMode.SYSTEM).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.SYSTEM)
|
assertThat(readyWithTheme(ThemeMode.SYSTEM).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.SYSTEM)
|
||||||
assertThat(readyWithTheme(ThemeMode.BLACK).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.DARK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ xpp3 = "1.1.6"
|
|||||||
# Parses the raw SRV/TXT answers for RFC 6764 discovery; the lookup itself goes
|
# Parses the raw SRV/TXT answers for RFC 6764 discovery; the lookup itself goes
|
||||||
# through Android's DnsResolver, so it follows the network and Private DNS.
|
# through Android's DnsResolver, so it follows the network and Private DNS.
|
||||||
# BSD-3 — it goes on the attribution screen with the rest.
|
# BSD-3 — it goes on the attribution screen with the rest.
|
||||||
dnsjava = "3.6.3"
|
dnsjava = "3.6.5"
|
||||||
# WebDAV-Push delivery. The connector only: DAVx5 also ships the embedded FCM
|
# WebDAV-Push delivery. The connector only: DAVx5 also ships the embedded FCM
|
||||||
# distributor, which pulls in Play Services and would get us flagged on F-Droid.
|
# distributor, which pulls in Play Services and would get us flagged on F-Droid.
|
||||||
unifiedpush = "3.3.5"
|
unifiedpush = "3.3.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user