select a server on tap and refuse to delete the active one

This commit is contained in:
austinried
2026-10-04 00:30:03 +00:00
parent a77ce6984e
commit 46dc6bda0e
6 changed files with 23 additions and 29 deletions
@@ -20,9 +20,6 @@ interface SourcesDao {
@Query("SELECT * FROM sources WHERE id = :id") @Query("SELECT * FROM sources WHERE id = :id")
suspend fun sourceOnce(id: Long): Source? suspend fun sourceOnce(id: Long): Source?
@Query("SELECT COUNT(*) FROM sources")
suspend fun sourceCount(): Int
@Query("SELECT id FROM sources WHERE isActive = 1 LIMIT 1") @Query("SELECT id FROM sources WHERE isActive = 1 LIMIT 1")
fun activeSourceId(): Flow<Long?> fun activeSourceId(): Flow<Long?>
@@ -32,9 +29,6 @@ interface SourcesDao {
@Query("SELECT id FROM sources WHERE isActive = 1 LIMIT 1") @Query("SELECT id FROM sources WHERE isActive = 1 LIMIT 1")
suspend fun activeSourceIdOnce(): Long? suspend fun activeSourceIdOnce(): Long?
@Query("SELECT id FROM sources ORDER BY createdAt LIMIT 1")
suspend fun firstSourceId(): Long?
@Insert @Insert
suspend fun insertSource(source: Source): Long suspend fun insertSource(source: Source): Long
@@ -175,13 +175,8 @@ class SourceRepository(
} }
suspend fun deleteSource(id: Long): Boolean { suspend fun deleteSource(id: Long): Boolean {
val dao = db.sourcesDao() if (db.sourcesDao().activeSourceIdOnce() == id) return false
if (dao.sourceCount() <= 1) return false db.sourcesDao().deleteSource(id)
val wasActive = dao.activeSourceIdOnce() == id
dao.deleteSource(id)
if (wasActive) {
dao.firstSourceId()?.let { dao.setActiveSource(it) }
}
return true return true
} }
@@ -173,7 +173,7 @@ fun AddSourceScreen(
) )
if (state.isEditing && !state.canDelete) { if (state.isEditing && !state.canDelete) {
Text( Text(
"This is the only server. Add another before you can remove this one.", "This is the active server. Switch to another server before deleting it.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -59,7 +59,8 @@ class AddSourceViewModel(
} }
viewModelScope.launch { viewModelScope.launch {
sourceRepository.sources().collect { sources -> sourceRepository.sources().collect { sources ->
_state.update { it.copy(canDelete = sources.size > 1) } val active = sources.any { it.id == sourceId && it.isActive }
_state.update { it.copy(canDelete = !active) }
} }
} }
} }
@@ -145,7 +146,7 @@ class AddSourceViewModel(
viewModelScope.launch { viewModelScope.launch {
if (!sourceRepository.deleteSource(id)) { if (!sourceRepository.deleteSource(id)) {
_state.update { _state.update {
it.copy(busy = false, message = "At least one server is required", isError = true) it.copy(busy = false, message = "The active server can't be deleted; switch to another server first", isError = true)
} }
return@launch return@launch
} }
@@ -19,8 +19,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -140,9 +140,11 @@ fun SettingsScreen(
) )
}, },
trailingContent = { trailingContent = {
Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, contentDescription = null) IconButton(onClick = { onEditServer(source.id) }) {
Icon(Icons.Rounded.Edit, contentDescription = "Edit ${source.name}")
}
}, },
modifier = Modifier.clickable(onClickLabel = "Edit ${source.name}") { onEditServer(source.id) }, modifier = Modifier.clickable(onClickLabel = "Use ${source.name}") { onSelectSource(source.id) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
) )
} }
@@ -105,19 +105,21 @@ class SourceRepositoryTest {
} }
@Test @Test
fun theLastSourceCannotBeDeleted() = fun theActiveSourceCannotBeDeleted() =
runTest { runTest {
repository.addSource("only", "http://a.example", "u", "p", true) repository.addSource("first", "http://a.example", "u", "p", true)
val id = repository.activeSourceId().first()!! val firstId = repository.activeSourceId().first()!!
assertFalse("the last source must survive deletion", repository.deleteSource(id))
assertEquals(1, repository.sources().first().size)
repository.addSource("second", "http://b.example", "u", "p", true) repository.addSource("second", "http://b.example", "u", "p", true)
val secondId = repository.activeSourceId().first()!!
assertTrue(repository.deleteSource(id)) assertFalse("the active source must survive deletion", repository.deleteSource(secondId))
assertEquals(listOf("second"), repository.sources().first().map { it.name }) assertEquals(2, repository.sources().first().size)
assertEquals("second", repository.activeConfig().first()?.name)
repository.selectSource(firstId)
assertTrue(repository.deleteSource(secondId))
assertEquals(listOf("first"), repository.sources().first().map { it.name })
assertEquals("first", repository.activeConfig().first()?.name)
} }
@Test @Test