fix(android): extract Discover tiles to DiscoverTiles.kt (detekt TooManyFunctions)

DiscoverScreen.kt still tripped TooManyFunctions (13/11) after the VM
split because the file kept all the per-row composables + helpers.
Moved SuggestionTile + ResultTile + Avatar + AvatarShape into a
sibling DiscoverTiles.kt and dropped the now-unused imports
(background, CircleShape, RoundedCornerShape, AssistChip, Disc3,
User, AsyncImage, size, clip, ImageVector, TextOverflow).

DiscoverScreen.kt now hosts 10 functions: the screen + 6 layout-only
helpers (SearchBar, KindChips, SuggestionsPane, SuggestionsList,
SuggestionsHeader, ResultsList) + LoadingCentered + CenteredMessage
+ snackbarFor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 00:21:42 -04:00
parent f573512940
commit 786a73dad7
2 changed files with 136 additions and 119 deletions
@@ -1,6 +1,5 @@
package com.fabledsword.minstrel.discover.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -9,15 +8,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
@@ -38,18 +32,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Disc3
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Search as LucideSearch
import com.composables.icons.lucide.User
import com.fabledsword.minstrel.discover.data.RequestOutcome
import com.fabledsword.minstrel.models.ArtistSuggestionRef
import com.fabledsword.minstrel.models.LidarrRequestKind
@@ -247,114 +236,6 @@ private fun ResultsList(
}
}
@Composable
private fun SuggestionTile(s: ArtistSuggestionRef, onRequest: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(imageUrl = s.imageUrl, fallback = Lucide.User, shape = AvatarShape.CIRCLE)
Column(modifier = Modifier.weight(1f)) {
Text(
text = s.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (s.attributionText.isNotEmpty()) {
Text(
text = s.attributionText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
Button(onClick = onRequest) { Text("Request") }
}
}
@Composable
private fun ResultTile(
row: LidarrSearchResultRef,
locallyRequested: Boolean,
onRequest: () -> Unit,
) {
val effectivelyRequested = row.requested || locallyRequested
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(imageUrl = row.imageUrl, fallback = Lucide.Disc3, shape = AvatarShape.ROUNDED)
Column(modifier = Modifier.weight(1f)) {
Text(
text = row.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (row.secondaryText.isNotEmpty()) {
Text(
text = row.secondaryText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
when {
row.inLibrary -> AssistChip(onClick = {}, enabled = false, label = { Text("In library") })
effectivelyRequested -> AssistChip(onClick = {}, enabled = false, label = { Text("Requested") })
else -> Button(onClick = onRequest) { Text("Request") }
}
}
}
private enum class AvatarShape { CIRCLE, ROUNDED }
@Composable
private fun Avatar(
imageUrl: String,
fallback: androidx.compose.ui.graphics.vector.ImageVector,
shape: AvatarShape,
) {
val clip = when (shape) {
AvatarShape.CIRCLE -> CircleShape
AvatarShape.ROUNDED -> RoundedCornerShape(6.dp)
}
Box(
modifier = Modifier
.size(56.dp)
.clip(clip)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
if (imageUrl.isEmpty()) {
Icon(
imageVector = fallback,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
)
}
}
}
@Composable
private fun LoadingCentered() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -0,0 +1,136 @@
package com.fabledsword.minstrel.discover.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.composables.icons.lucide.Disc3
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.User
import com.fabledsword.minstrel.models.ArtistSuggestionRef
import com.fabledsword.minstrel.models.LidarrSearchResultRef
internal enum class AvatarShape { CIRCLE, ROUNDED }
@Composable
internal fun SuggestionTile(s: ArtistSuggestionRef, onRequest: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(imageUrl = s.imageUrl, fallback = Lucide.User, shape = AvatarShape.CIRCLE)
Column(modifier = Modifier.weight(1f)) {
Text(
text = s.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (s.attributionText.isNotEmpty()) {
Text(
text = s.attributionText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
Button(onClick = onRequest) { Text("Request") }
}
}
@Composable
internal fun ResultTile(
row: LidarrSearchResultRef,
locallyRequested: Boolean,
onRequest: () -> Unit,
) {
val effectivelyRequested = row.requested || locallyRequested
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Avatar(imageUrl = row.imageUrl, fallback = Lucide.Disc3, shape = AvatarShape.ROUNDED)
Column(modifier = Modifier.weight(1f)) {
Text(
text = row.name,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (row.secondaryText.isNotEmpty()) {
Text(
text = row.secondaryText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
when {
row.inLibrary -> AssistChip(onClick = {}, enabled = false, label = { Text("In library") })
effectivelyRequested ->
AssistChip(onClick = {}, enabled = false, label = { Text("Requested") })
else -> Button(onClick = onRequest) { Text("Request") }
}
}
}
@Composable
private fun Avatar(imageUrl: String, fallback: ImageVector, shape: AvatarShape) {
val clip = when (shape) {
AvatarShape.CIRCLE -> CircleShape
AvatarShape.ROUNDED -> RoundedCornerShape(6.dp)
}
Box(
modifier = Modifier
.size(56.dp)
.clip(clip)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
if (imageUrl.isEmpty()) {
Icon(
imageVector = fallback,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
)
}
}
}