fix(android): detekt — package naming, file rename, length caps

Twelve detekt findings from CI:

* PackageNaming — `shared/widgets/track_actions/` → `trackactions/`
  (underscore violates [a-z]+(\.[a-z][A-Za-z0-9]*)*); 5 file moves +
  import updates across 7 caller files.
* MatchingDeclarationName — `RadioWire.kt` → `RadioResponseWire.kt`
  to match its single top-level declaration.
* TooManyFunctions on PlayerController (14/11) — @Suppress at class
  with rationale: transport + queue + radio + lifecycle are one
  cohesive controller; fragmenting would scatter related state.
* TooManyFunctions in SearchScreen.kt (file 12/11) — @file:Suppress
  with rationale: legitimate per-screen section/row helpers.
* LongMethod NowPlayingScreen (71/60) — extracted BottomActionsRow.
* LongMethod TrackActionsSheet (77/60) — extracted
  TrackActionMenuItems + TrackActionSubSheets.
* LongMethod HideTrackSheet (61/60) — extracted HideSheetButtons.

No behavior change; all suppressions carry rationale comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 14:18:15 -04:00
parent 4580f950e3
commit dc68156d98
15 changed files with 158 additions and 92 deletions
@@ -30,7 +30,7 @@ import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -56,8 +56,8 @@ import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
@OptIn(ExperimentalMaterial3Api::class)
@@ -44,7 +44,7 @@ import com.fabledsword.minstrel.nav.Library
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.ErrorRetry
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
/**
* Library tab. Five-tab TabBar (Artists / Albums / History / Liked /
@@ -41,7 +41,7 @@ import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -38,6 +38,13 @@ import kotlin.coroutines.resumeWithException
* the StateFlow projection means each ViewModel doesn't have to attach
* its own Player.Listener.
*/
// PlayerController is the UI-side facade over the full Media3 surface
// (transport + queue + radio + lifecycle). All the public methods are
// legitimately part of one cohesive controller — splitting into
// "TransportController", "QueueController", etc. would scatter related
// state for no gain. Suppress the per-class function-count cap rather
// than fragment.
@Suppress("TooManyFunctions")
@Singleton
class PlayerController @Inject constructor(
@ApplicationContext private val context: Context,
@@ -49,8 +49,8 @@ import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Queue
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
import com.fabledsword.minstrel.theme.LocalActionColors
@@ -121,28 +121,36 @@ fun NowPlayingScreen(
onNext = viewModel::skipToNext,
)
Spacer(Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ViewQueueButton(onClick = { navController.navigate(Queue) })
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = { id ->
navController.popBackStack()
navController.navigate(AlbumDetail(id))
},
onNavigateToArtist = { id ->
navController.popBackStack()
navController.navigate(ArtistDetail(id))
},
)
}
BottomActionsRow(navController = navController, track = track)
}
}
}
@Composable
private fun BottomActionsRow(
navController: NavHostController,
track: com.fabledsword.minstrel.models.TrackRef,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ViewQueueButton(onClick = { navController.navigate(Queue) })
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = { id ->
navController.popBackStack()
navController.navigate(AlbumDetail(id))
},
onNavigateToArtist = { id ->
navController.popBackStack()
navController.navigate(ArtistDetail(id))
},
)
}
}
@Composable
private fun ViewQueueButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
@@ -67,8 +67,8 @@ import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -1,3 +1,9 @@
@file:Suppress("TooManyFunctions")
// SearchScreen legitimately owns the screen-shell composable + a handful
// of small section/row helpers (ResultsPane, ResultsList, three section
// builders, two row variants, hint, loader). Splitting into a widgets/
// subdirectory for a single screen would be over-engineering.
package com.fabledsword.minstrel.search.ui
import androidx.compose.foundation.clickable
@@ -51,8 +57,8 @@ import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -93,17 +93,23 @@ fun HideTrackSheet(
maxLines = 2,
)
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = onDismiss) { Text("Cancel") }
Spacer(Modifier.padding(horizontal = 4.dp))
Button(onClick = { onSubmit(reason, notes.trim()) }) {
Text("Hide")
}
}
HideSheetButtons(
onCancel = onDismiss,
onSubmit = { onSubmit(reason, notes.trim()) },
)
Spacer(Modifier.height(8.dp))
}
}
}
@Composable
private fun HideSheetButtons(onCancel: () -> Unit, onSubmit: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = onCancel) { Text("Cancel") }
Spacer(Modifier.padding(horizontal = 4.dp))
Button(onClick = onSubmit) { Text("Hide") }
}
}
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -63,7 +63,6 @@ fun TrackActionsSheet(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val isLiked by viewModel.isLikedFlow(track.id).collectAsStateWithLifecycle(initialValue = false)
val isHidden by viewModel.isHiddenFlow(track.id).collectAsStateWithLifecycle(initialValue = false)
var showAddToPlaylist by remember { mutableStateOf(false) }
var showHideSheet by remember { mutableStateOf(false) }
@@ -71,71 +70,111 @@ fun TrackActionsSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
if (!hideQueueActions) {
MenuItem(Lucide.ListVideo, "Play next") {
viewModel.playNext(track)
onDismiss()
}
MenuItem(Lucide.ListMusic, "Add to queue") {
viewModel.enqueue(track)
onDismiss()
}
HorizontalDivider()
TrackActionMenuItems(
track = track,
hideQueueActions = hideQueueActions,
isLiked = isLiked,
isHidden = isHidden,
viewModel = viewModel,
onDismiss = onDismiss,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
onShowAddToPlaylist = { showAddToPlaylist = true },
onShowHideSheet = { showHideSheet = true },
)
}
TrackActionSubSheets(
track = track,
showAddToPlaylist = showAddToPlaylist,
showHideSheet = showHideSheet,
viewModel = viewModel,
onDismissParent = onDismiss,
onCloseAddToPlaylist = { showAddToPlaylist = false },
onCloseHideSheet = { showHideSheet = false },
)
}
@Composable
private fun TrackActionMenuItems(
track: TrackRef,
hideQueueActions: Boolean,
isLiked: Boolean,
isHidden: Boolean,
viewModel: TrackActionsViewModel,
onDismiss: () -> Unit,
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
onShowAddToPlaylist: () -> Unit,
onShowHideSheet: () -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
if (!hideQueueActions) {
MenuItem(Lucide.ListVideo, "Play next") {
viewModel.playNext(track); onDismiss()
}
MenuItem(Lucide.Heart, if (isLiked) "Unlike" else "Like") {
viewModel.toggleLike(track.id, isLiked)
onDismiss()
}
MenuItem(Lucide.ListPlus, "Add to playlist…") {
showAddToPlaylist = true
}
MenuItem(Lucide.Radio, "Start radio") {
viewModel.startRadio(track.id)
onDismiss()
MenuItem(Lucide.ListMusic, "Add to queue") {
viewModel.enqueue(track); onDismiss()
}
HorizontalDivider()
MenuItem(Lucide.Disc3, "Go to album") {
onDismiss()
if (track.albumId.isNotEmpty()) onNavigateToAlbum(track.albumId)
}
MenuItem(Lucide.User, "Go to artist") {
onDismiss()
if (track.artistId.isNotEmpty()) onNavigateToArtist(track.artistId)
}
HorizontalDivider()
MenuItem(
icon = if (isHidden) Lucide.Eye else Lucide.EyeOff,
label = if (isHidden) "Unhide" else "Hide",
) {
if (isHidden) {
viewModel.unflag(track.id)
onDismiss()
} else {
showHideSheet = true
}
}
MenuItem(Lucide.Heart, if (isLiked) "Unlike" else "Like") {
viewModel.toggleLike(track.id, isLiked); onDismiss()
}
MenuItem(Lucide.ListPlus, "Add to playlist", onShowAddToPlaylist)
MenuItem(Lucide.Radio, "Start radio") {
viewModel.startRadio(track.id); onDismiss()
}
HorizontalDivider()
MenuItem(Lucide.Disc3, "Go to album") {
onDismiss()
if (track.albumId.isNotEmpty()) onNavigateToAlbum(track.albumId)
}
MenuItem(Lucide.User, "Go to artist") {
onDismiss()
if (track.artistId.isNotEmpty()) onNavigateToArtist(track.artistId)
}
HorizontalDivider()
MenuItem(
icon = if (isHidden) Lucide.Eye else Lucide.EyeOff,
label = if (isHidden) "Unhide" else "Hide",
) {
if (isHidden) {
viewModel.unflag(track.id); onDismiss()
} else {
onShowHideSheet()
}
}
}
}
@Composable
private fun TrackActionSubSheets(
track: TrackRef,
showAddToPlaylist: Boolean,
showHideSheet: Boolean,
viewModel: TrackActionsViewModel,
onDismissParent: () -> Unit,
onCloseAddToPlaylist: () -> Unit,
onCloseHideSheet: () -> Unit,
) {
if (showAddToPlaylist) {
AddToPlaylistSheet(
onPick = { playlistId ->
viewModel.appendToPlaylist(playlistId, track.id)
showAddToPlaylist = false
onDismiss()
onCloseAddToPlaylist()
onDismissParent()
},
onDismiss = { showAddToPlaylist = false },
onDismiss = onCloseAddToPlaylist,
)
}
if (showHideSheet) {
HideTrackSheet(
onSubmit = { reason, notes ->
viewModel.flag(track, reason, notes)
showHideSheet = false
onDismiss()
onCloseHideSheet()
onDismissParent()
},
onDismiss = { showHideSheet = false },
onDismiss = onCloseHideSheet,
)
}
}
@@ -1,4 +1,4 @@
package com.fabledsword.minstrel.shared.widgets.track_actions
package com.fabledsword.minstrel.shared.widgets.trackactions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope