Skip to content

Commit

Permalink
feat: allow individual reminders to be deactivated
Browse files Browse the repository at this point in the history
  • Loading branch information
JanMalch committed Apr 21, 2024
1 parent 10d8152 commit 29c198c
Show file tree
Hide file tree
Showing 12 changed files with 705 additions and 6 deletions.
600 changes: 600 additions & 0 deletions app/schemas/io.github.janmalch.woroboro.data.AppDatabase/8.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ fun Reminder.asEntities(): Pair<ReminderEntity, List<String>> {
filterDuration = filter.durationFilter,
filterRoutineId = (query as? RoutineQuery.Single)?.routineId,
routinesOrder = filter.routinesOrder,
isActive = isActive,
) to filter.selectedTags.map { it.label }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.github.janmalch.woroboro.data.model.ReminderEntityWithFilterTags
import io.github.janmalch.woroboro.data.model.asModel
import io.github.janmalch.woroboro.models.Reminder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.util.UUID
import javax.inject.Inject
Expand All @@ -22,6 +23,21 @@ interface ReminderRepository {
suspend fun update(reminder: Reminder): UUID

suspend fun delete(reminderId: UUID)

}

suspend fun ReminderRepository.activate(reminderId: UUID) {
val reminder = requireNotNull(findOne(reminderId).first()) {
"'$reminderId' is not an ID for an existing reminder"
}
update(reminder.copy(isActive = true))
}

suspend fun ReminderRepository.deactivate(reminderId: UUID) {
val reminder = requireNotNull(findOne(reminderId).first()) {
"'$reminderId' is not an ID for an existing reminder"
}
update(reminder.copy(isActive = false))
}

class ReminderRepositoryImpl @Inject constructor(
Expand All @@ -41,7 +57,9 @@ class ReminderRepositoryImpl @Inject constructor(
val model = reminder.copy(id = UUID.randomUUID())
val (entity, filterTags) = model.asEntities()
reminderDao.upsert(entity, filterTags)
reminderScheduler.schedule(model)
if (reminder.isActive) {
reminderScheduler.schedule(model)
}
return model.id
}

Expand All @@ -58,5 +76,4 @@ class ReminderRepositoryImpl @Inject constructor(
reminderScheduler.cancel(reminderId)
reminderDao.delete(reminderId)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class AndroidReminderScheduler @Inject constructor(

override fun cancel(reminderId: UUID) {
alarmManager.cancel(createPendingIntent(reminderId.hashCode()))
Log.d("AndroidReminderScheduler", "Cancelled reminder with ID $reminderId")
}

private fun createPendingIntent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ import java.util.UUID
ReminderEntity::class,
ReminderFilterTagCrossRefEntity::class,
],
version = 7,
version = 8,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
AutoMigration(from = 7, to = 8),
]
)
@TypeConverters(StandardConverters::class, DomainConverters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ data class ReminderEntity(
val filterDuration: DurationFilter,
@ColumnInfo("routines_order", defaultValue = "NameAsc")
val routinesOrder: RoutinesOrder,
@ColumnInfo("is_active", defaultValue = "1")
val isActive: Boolean,
)

@Entity(
Expand Down Expand Up @@ -85,6 +87,7 @@ fun ReminderEntityWithFilterTags.asModel() = Reminder(
name = reminder.name,
remindAt = reminder.remindAt,
weekdays = reminder.weekdays,
isActive = reminder.isActive,
repeat = if (reminder.repeatEvery != null && reminder.repeatUntil != null) {
Reminder.Repeat(
every = reminder.repeatEvery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data class Reminder(
val remindAt: LocalTime,
val repeat: Repeat?,
val query: RoutineQuery,
val isActive: Boolean,
) {

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.NotificationsActive
import androidx.compose.material.icons.rounded.NotificationsOff
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand All @@ -38,6 +42,7 @@ fun ReminderListScreen(
reminders: ImmutableList<Reminder>,
onNewReminder: () -> Unit,
onGoToReminder: (UUID) -> Unit,
onToggleReminderActive: (reminderId: UUID, shouldActivate: Boolean) -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
Expand All @@ -55,6 +60,7 @@ fun ReminderListScreen(
ReminderList(
reminders = reminders,
onGoToReminder = onGoToReminder,
onToggleReminderActive = onToggleReminderActive,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
Expand All @@ -67,6 +73,7 @@ fun ReminderListScreen(
fun ReminderList(
reminders: ImmutableList<Reminder>,
onGoToReminder: (UUID) -> Unit,
onToggleReminderActive: (reminderId: UUID, shouldActivate: Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val timeFormat = stringResource(id = R.string.time_format)
Expand All @@ -75,7 +82,9 @@ fun ReminderList(
LazyColumn(
contentPadding = PaddingValues(bottom = 80.dp),
) {
items(reminders, key = { it.id }) { reminder ->
// So reminder IDs are not stable (update generates a new ID) so we can't use that key,
// but indices can't change (currently), so it is a suitable key.
itemsIndexed(reminders, key = { idx, _ -> idx }) { _, reminder ->
ListItem(
headlineContent = { Text(reminder.name) },
supportingContent = {
Expand All @@ -89,6 +98,27 @@ fun ReminderList(
})

},
trailingContent = {
Switch(
checked = reminder.isActive,
onCheckedChange = { onToggleReminderActive(reminder.id, it) },
thumbContent = {
if (reminder.isActive) {
Icon(
Icons.Rounded.NotificationsActive,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
} else {
Icon(
Icons.Rounded.NotificationsOff,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
}
}
)
},
modifier = Modifier.clickable { onGoToReminder(reminder.id) }
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package io.github.janmalch.woroboro.ui.reminder

import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import io.github.janmalch.woroboro.R
import io.github.janmalch.woroboro.ui.CollectAsEvents
import io.github.janmalch.woroboro.ui.Outcome
import java.util.UUID


Expand All @@ -19,17 +23,26 @@ fun NavController.navigateToReminderList(navOptions: NavOptions? = null) {
fun NavGraphBuilder.reminderListScreen(
onNewReminder: () -> Unit,
onGoToReminder: (UUID) -> Unit,
onShowSnackbar: (String) -> Unit,
) {
composable(
route = REMINDER_LIST_ROUTE,
) {
val context = LocalContext.current
val viewModel = hiltViewModel<ReminderListViewModel>()
val reminders by viewModel.reminders.collectAsState()

CollectAsEvents(viewModel.onToggleReminder) {
if (it == Outcome.Failure) {
onShowSnackbar(context.getString(R.string.unknown_error_message))
}
}

ReminderListScreen(
reminders = reminders,
onNewReminder = onNewReminder,
onGoToReminder = onGoToReminder,
onToggleReminderActive = viewModel::toggleReminderActive,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
package io.github.janmalch.woroboro.ui.reminder

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.janmalch.woroboro.business.ReminderRepository
import io.github.janmalch.woroboro.business.activate
import io.github.janmalch.woroboro.business.deactivate
import io.github.janmalch.woroboro.models.Reminder
import io.github.janmalch.woroboro.ui.Outcome
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject

@HiltViewModel
class ReminderListViewModel @Inject constructor(
reminderRepository: ReminderRepository,
private val reminderRepository: ReminderRepository,
) : ViewModel() {


private val _onToggleReminder = Channel<Outcome>()
val onToggleReminder = _onToggleReminder.receiveAsFlow()

private val toggleReminderExceptionHandler = CoroutineExceptionHandler { _, exception ->
Log.e("ReminderListViewModel", "Error while toggling reminder active state.", exception)
viewModelScope.launch {
_onToggleReminder.send(Outcome.Failure)
}
}

val reminders = reminderRepository.findAll()
.map(List<Reminder>::toImmutableList)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = persistentListOf(),
)

fun toggleReminderActive(reminderId: UUID, shouldActivate: Boolean) {
viewModelScope.launch(toggleReminderExceptionHandler) {
if (shouldActivate) {
reminderRepository.activate(reminderId)
} else {
reminderRepository.deactivate(reminderId)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fun NavGraphBuilder.remindersGraph(
reminderListScreen(
onNewReminder = onNewReminder,
onGoToReminder = onGoToReminder,
onShowSnackbar = onShowSnackbar,
)
reminderEditorScreen(
onBackClick = onBackClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ fun ReminderEditorScreen(
repeat = if (repeatEverySnapshot != null && repeatUntilSnapshot != null)
Reminder.Repeat(repeatEverySnapshot, repeatUntilSnapshot)
else null,
isActive = reminder?.isActive ?: true,
query = if (routineIdFilterSnapshot != null) {
RoutineQuery.Single(routineIdFilterSnapshot)
} else {
Expand Down

0 comments on commit 29c198c

Please sign in to comment.