diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7e0393..957223e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.rada" minSdk = 26 targetSdk = 35 - versionCode = 7 - versionName = "0.1.5" + versionCode = 1 + versionName = "0.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/rada/data/local/dao/PlaylistDao.kt b/app/src/main/java/com/rada/data/local/dao/PlaylistDao.kt index 4fbc280..5d3ed84 100644 --- a/app/src/main/java/com/rada/data/local/dao/PlaylistDao.kt +++ b/app/src/main/java/com/rada/data/local/dao/PlaylistDao.kt @@ -3,7 +3,6 @@ package com.rada.data.local.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.Query import com.rada.data.local.entity.PlaylistEntity import com.rada.data.local.entity.PlaylistSongEntity @@ -31,9 +30,7 @@ interface PlaylistDao { @Query("DELETE FROM playlists WHERE id = :playlistId") suspend fun deletePlaylistById(playlistId: Long) - // IGNORE: (playlistId, songId) is the PK — re-adding a song that is - // already in the playlist must be a no-op, not a crash. - @Insert(onConflict = OnConflictStrategy.IGNORE) + @Insert suspend fun addSongToPlaylist(playlistSong: PlaylistSongEntity) @Query("DELETE FROM playlist_songs WHERE playlistId = :playlistId AND songId = :songId") diff --git a/app/src/main/java/com/rada/data/repository/RadioBrowserRepository.kt b/app/src/main/java/com/rada/data/repository/RadioBrowserRepository.kt index 1410f9b..fd701ae 100644 --- a/app/src/main/java/com/rada/data/repository/RadioBrowserRepository.kt +++ b/app/src/main/java/com/rada/data/repository/RadioBrowserRepository.kt @@ -49,16 +49,6 @@ class RadioBrowserRepository @Inject constructor() { mergeResults(byName, byTag, limit) } - /** - * Stations tagged with [tag] (genre browse), most-listened first. - * Tag match is case-SENSITIVE on radio-browser — the directory stores - * tags lowercase, so "Trance" returns zero hits while "trance" works. - */ - suspend fun byTag(tag: String, limit: Int = 40): List = - withContext(Dispatchers.IO) { - fetch(mapOf("tag" to tag.trim().lowercase()), limit) - } - /** Name hits keep their order; tag hits append, de-duplicated by id. */ internal fun mergeResults( byName: List, diff --git a/app/src/main/java/com/rada/ui/components/AppHomeButton.kt b/app/src/main/java/com/rada/ui/components/AppHomeButton.kt index 2214a0a..d55398d 100644 --- a/app/src/main/java/com/rada/ui/components/AppHomeButton.kt +++ b/app/src/main/java/com/rada/ui/components/AppHomeButton.kt @@ -24,7 +24,7 @@ fun AppHomeButton( ) { IconButton(onClick = onClick, modifier = modifier) { Image( - painter = painterResource(R.drawable.ic_home_mark), + painter = painterResource(R.mipmap.ic_launcher), contentDescription = "Home", modifier = Modifier .size(30.dp) diff --git a/app/src/main/java/com/rada/ui/discover/DiscoverScreen.kt b/app/src/main/java/com/rada/ui/discover/DiscoverScreen.kt index 66dba0c..6a5d856 100644 --- a/app/src/main/java/com/rada/ui/discover/DiscoverScreen.kt +++ b/app/src/main/java/com/rada/ui/discover/DiscoverScreen.kt @@ -88,7 +88,6 @@ fun DiscoverScreen( val rankingsGenre by viewModel.rankingsGenre.collectAsState() val rankingsLoading by viewModel.rankingsLoading.collectAsState() val rankingsError by viewModel.rankingsError.collectAsState() - val browseStations by viewModel.browseStations.collectAsState() val addresses by viewModel.addresses.collectAsState() val selectedArtistId by viewModel.selectedArtistId.collectAsState() val artistDetail by viewModel.artistDetail.collectAsState() @@ -233,15 +232,6 @@ fun DiscoverScreen( selected = rankingsGenre, onSelect = { viewModel.refreshRankings(it) }, ) - val radio = browseStations - if (radio != null) { - RadioGenreList( - genre = rankingsGenre ?: "", - stations = radio, - loading = rankingsLoading, - onPlay = { viewModel.playFoundStation(it) }, - ) - } else RankingsList( title = if (rankingsGenre != null) "Top in ${rankingsGenre}" else "Top on Wavlake", tracks = rankings, @@ -545,108 +535,6 @@ private fun GenreChip(label: String, selected: Boolean, onClick: () -> Unit) { ) } -@Composable -private fun RadioGenreList( - genre: String, - stations: List, - loading: Boolean, - onPlay: (com.rada.domain.model.Station) -> Unit, -) { - Column(modifier = Modifier.fillMaxSize()) { - Text( - text = "RADIO · $genre".uppercase(), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = LocalTokens.current.accentText, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) - when { - loading -> { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - horizontalArrangement = Arrangement.Center, - ) { - CircularProgressIndicator( - modifier = Modifier.size(22.dp), - strokeWidth = 2.dp, - color = LocalTokens.current.accentText, - ) - } - } - stations.isEmpty() -> { - Text( - text = "No stations found for “$genre”.", - style = MaterialTheme.typography.bodyMedium, - color = LocalTokens.current.textSecondary, - modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp), - ) - } - else -> { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - items(stations, key = { it.id }, contentType = { "rstation" }) { st -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background( - LocalTokens.current.accentFill.copy(alpha = 0.15f), - ), - contentAlignment = Alignment.Center, - ) { - Icon( - Icons.Default.Radio, - contentDescription = null, - tint = LocalTokens.current.accentText, - modifier = Modifier.size(20.dp), - ) - } - Spacer(modifier = Modifier.width(14.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = st.name, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold, - color = LocalTokens.current.text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = st.genre.ifBlank { "Radio" }, - style = MaterialTheme.typography.bodySmall, - color = LocalTokens.current.textSecondary, - maxLines = 1, - ) - } - IconButton( - onClick = { onPlay(st) }, - modifier = Modifier.size(48.dp), - ) { - Icon( - Icons.Default.PlayArrow, - contentDescription = "Play ${st.name}", - tint = LocalTokens.current.textSecondary, - modifier = Modifier.size(22.dp), - ) - } - } - } - } - } - } - } -} - @Composable private fun RankingsList( title: String, diff --git a/app/src/main/java/com/rada/ui/discover/WavlakeViewModel.kt b/app/src/main/java/com/rada/ui/discover/WavlakeViewModel.kt index 7a6d5f4..819afad 100644 --- a/app/src/main/java/com/rada/ui/discover/WavlakeViewModel.kt +++ b/app/src/main/java/com/rada/ui/discover/WavlakeViewModel.kt @@ -23,33 +23,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject -/** Genres the Wavlake rankings endpoint accepts (probed against the live API). */ -val WAVLAKE_GENRES = listOf( - "Dance/EDM", "Electronic", "Hip-Hop/Rap", "Pop", "Rock", "Jazz", "Blues", - "Country", "Reggae", "Classical", "Lofi", "Soul", "Alternative", "R&B", - "Latin", "Gospel", "World", "Soundtrack", -) - -/** - * Sub-genres Wavlake doesn't index (rankings 400 on them). Browsing one - * searches live radio stations by tag instead — trance, classic rock, and - * friends have rich station coverage even where Wavlake is silent. - */ -val SUBGENRE_TAGS = listOf( - "Trance", "Techno", "Hard House", "Hardstyle", "Classic Rock", "Oldies", - "Old School Hip Hop", "Metal", "Punk", "Ambient", "Drum and Bass", - "Dubstep", "Disco", "Funk", "Synthwave", -) - -/** Curated genre shortcuts shown as Discover chips. */ -val DISCOVER_GENRES = listOf( - "Dance/EDM", "Electronic", "Hip-Hop/Rap", "Pop", "Rock", "Trance", "Techno", - "Hard House", "Classic Rock", "Oldies", "Old School Hip Hop", "Metal", - "Jazz", "Lofi", "Ambient", "Reggae", -) - -/** True when a genre browses Wavlake rankings; false → radio-tag browse. */ -fun isWavlakeGenre(genre: String): Boolean = genre in WAVLAKE_GENRES +/** Curated genre shortcuts — exact Wavlake catalog names only. */ +val DISCOVER_GENRES = listOf("Dance/EDM", "Electronic", "Hip-Hop/Rap", "Pop", "Rock") /** What the Discover search looks for; user-toggled chips. */ enum class DiscoverType(val label: String) { @@ -82,7 +57,6 @@ class WavlakeViewModel @Inject constructor( private val playbackManager: PlaybackManager, private val playlists: PlaylistRepository, private val stations: com.rada.data.repository.StationRepository, - private val radioBrowser: com.rada.data.repository.RadioBrowserRepository, @dagger.hilt.android.qualifiers.ApplicationContext private val appContext: android.content.Context, ) : ViewModel() { @@ -130,14 +104,6 @@ class WavlakeViewModel @Inject constructor( private val _rankingsError = MutableStateFlow(null) val rankingsError: StateFlow = _rankingsError.asStateFlow() - /** - * Radio stations while browsing a sub-genre Wavlake doesn't index - * (trance, classic rock…). Non-null means the rankings view shows - * stations instead of Wavlake tracks. - */ - private val _browseStations = MutableStateFlow?>(null) - val browseStations: StateFlow?> = _browseStations.asStateFlow() - /** Resolved Lightning Addresses by artist id — populated best-effort. */ private val _addresses = MutableStateFlow>(emptyMap()) val addresses: StateFlow> = _addresses.asStateFlow() @@ -211,19 +177,14 @@ class WavlakeViewModel @Inject constructor( refreshRankings(null) } - /** Genres matching the query: curated genres (incl. sub-genres) + saved-station genres. */ + /** Genres matching the query: curated discover genres + every station genre. */ private fun genreMatches(query: String): List { - val norm = { s: String -> s.lowercase().replace(" ", "") } - val q = norm(query) - val pool = DISCOVER_GENRES + SUBGENRE_TAGS + - stations.getAll().map { it.genre }.filter { it.isNotBlank() } - return pool + val q = query.lowercase() + return (DISCOVER_GENRES + stations.getAll().map { it.genre }.filter { it.isNotBlank() }) + .map { it to it.lowercase() } + .filter { (_, low) -> low.contains(q) || q.contains(low.substringBefore('/')) } + .map { it.first } .distinct() - .filter { low -> - val l = norm(low) - l.contains(q) || q.contains(l) || - l.substringBefore('/').let { it.isNotEmpty() && q.contains(it) } - } .take(8) } @@ -233,11 +194,6 @@ class WavlakeViewModel @Inject constructor( fun refreshRankings(genre: String?) { _rankingsGenre.value = genre - if (genre != null && !isWavlakeGenre(genre)) { - browseRadioGenre(genre) - return - } - _browseStations.value = null viewModelScope.launch { _rankingsLoading.value = true _rankingsError.value = null @@ -357,24 +313,22 @@ class WavlakeViewModel @Inject constructor( playbackManager.playStation(station.url, station.name) } - /** Browse a genre: Wavlake rankings when indexed, radio-tag search otherwise. */ + /** Jump the Stations tab filtered to this genre (Discover genre hit). */ fun openGenre(genre: String) { - // Reached from a search-result genre card: the browse view lives in - // the Idle branch, so drop the query or the update would stay invisible. - _searchQuery.value = "" - _searchState.value = WavlakeSearchState.Idle - refreshRankings(genre) - } - - /** Sub-genre browse: stations tagged [genre] on radio-browser, most-listened first. */ - private fun browseRadioGenre(genre: String) { - _rankings.value = emptyList() - _browseStations.value = emptyList() + _rankingsGenre.value = genre viewModelScope.launch { _rankingsLoading.value = true _rankingsError.value = null - _browseStations.value = runCatching { radioBrowser.byTag(genre) } - .getOrDefault(emptyList()) + when (val r = repository.rankings(days = 7, genre = genre, limit = 25)) { + is WavlakeResult.Ok -> { + _rankings.value = r.value + resolveAddresses(r.value) + } + is WavlakeResult.Failed -> { + _rankings.value = emptyList() + _rankingsError.value = r.message + } + } _rankingsLoading.value = false } } diff --git a/app/src/main/res/drawable-hdpi/ic_home_mark.png b/app/src/main/res/drawable-hdpi/ic_home_mark.png deleted file mode 100644 index 36edc50..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_home_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/app/src/main/res/drawable-hdpi/ic_launcher_background.png deleted file mode 100644 index 0a1acf1..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 32fca30..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png deleted file mode 100644 index 0deb89c..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_home_mark.png b/app/src/main/res/drawable-mdpi/ic_home_mark.png deleted file mode 100644 index a7b124e..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_home_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_background.png b/app/src/main/res/drawable-mdpi/ic_launcher_background.png deleted file mode 100644 index 3f1c041..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png deleted file mode 100644 index a8dcb2f..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png deleted file mode 100644 index 18c9084..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_home_mark.png b/app/src/main/res/drawable-xhdpi/ic_home_mark.png deleted file mode 100644 index 1b05a6a..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_home_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xhdpi/ic_launcher_background.png deleted file mode 100644 index 352eec7..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index eb5976d..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png deleted file mode 100644 index c2befb9..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_home_mark.png b/app/src/main/res/drawable-xxhdpi/ic_home_mark.png deleted file mode 100644 index 676fe09..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_home_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png deleted file mode 100644 index d1361a6..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index b627a98..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png deleted file mode 100644 index cd8fd6a..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_home_mark.png b/app/src/main/res/drawable-xxxhdpi/ic_home_mark.png deleted file mode 100644 index f98298a..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_home_mark.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index 22dec82..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 984534d..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png deleted file mode 100644 index 0e7c3e5..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png and /dev/null differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 8fde456..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 8fde456..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 36edc50..7f63d63 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index ce943ba..7f63d63 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index a7b124e..5d2061f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 330732b..5d2061f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 1b05a6a..5b9375b 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index ca02b0f..5b9375b 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 676fe09..dd56295 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index be3f625..dd56295 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index f98298a..98c0ee5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 2218c2d..98c0ee5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ