Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf9222ab0d | ||
|
|
d0c5ce86fd | ||
|
|
80b0300141 | ||
|
|
31d47f4cb5 | ||
|
|
4c63421400 | ||
|
|
598556dbdd | ||
|
|
90afcf3b6a | ||
|
|
838809ec0c |
|
|
@ -15,8 +15,8 @@ android {
|
||||||
applicationId = "com.rada"
|
applicationId = "com.rada"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 7
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.5"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.rada.data.local.dao
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.rada.data.local.entity.PlaylistEntity
|
import com.rada.data.local.entity.PlaylistEntity
|
||||||
import com.rada.data.local.entity.PlaylistSongEntity
|
import com.rada.data.local.entity.PlaylistSongEntity
|
||||||
|
|
@ -30,7 +31,9 @@ interface PlaylistDao {
|
||||||
@Query("DELETE FROM playlists WHERE id = :playlistId")
|
@Query("DELETE FROM playlists WHERE id = :playlistId")
|
||||||
suspend fun deletePlaylistById(playlistId: Long)
|
suspend fun deletePlaylistById(playlistId: Long)
|
||||||
|
|
||||||
@Insert
|
// 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)
|
||||||
suspend fun addSongToPlaylist(playlistSong: PlaylistSongEntity)
|
suspend fun addSongToPlaylist(playlistSong: PlaylistSongEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM playlist_songs WHERE playlistId = :playlistId AND songId = :songId")
|
@Query("DELETE FROM playlist_songs WHERE playlistId = :playlistId AND songId = :songId")
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,16 @@ class RadioBrowserRepository @Inject constructor() {
|
||||||
mergeResults(byName, byTag, limit)
|
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<Station> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
fetch(mapOf("tag" to tag.trim().lowercase()), limit)
|
||||||
|
}
|
||||||
|
|
||||||
/** Name hits keep their order; tag hits append, de-duplicated by id. */
|
/** Name hits keep their order; tag hits append, de-duplicated by id. */
|
||||||
internal fun mergeResults(
|
internal fun mergeResults(
|
||||||
byName: List<Station>,
|
byName: List<Station>,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ fun AppHomeButton(
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onClick, modifier = modifier) {
|
IconButton(onClick = onClick, modifier = modifier) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.ic_launcher),
|
painter = painterResource(R.drawable.ic_home_mark),
|
||||||
contentDescription = "Home",
|
contentDescription = "Home",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ fun DiscoverScreen(
|
||||||
val rankingsGenre by viewModel.rankingsGenre.collectAsState()
|
val rankingsGenre by viewModel.rankingsGenre.collectAsState()
|
||||||
val rankingsLoading by viewModel.rankingsLoading.collectAsState()
|
val rankingsLoading by viewModel.rankingsLoading.collectAsState()
|
||||||
val rankingsError by viewModel.rankingsError.collectAsState()
|
val rankingsError by viewModel.rankingsError.collectAsState()
|
||||||
|
val browseStations by viewModel.browseStations.collectAsState()
|
||||||
val addresses by viewModel.addresses.collectAsState()
|
val addresses by viewModel.addresses.collectAsState()
|
||||||
val selectedArtistId by viewModel.selectedArtistId.collectAsState()
|
val selectedArtistId by viewModel.selectedArtistId.collectAsState()
|
||||||
val artistDetail by viewModel.artistDetail.collectAsState()
|
val artistDetail by viewModel.artistDetail.collectAsState()
|
||||||
|
|
@ -232,6 +233,15 @@ fun DiscoverScreen(
|
||||||
selected = rankingsGenre,
|
selected = rankingsGenre,
|
||||||
onSelect = { viewModel.refreshRankings(it) },
|
onSelect = { viewModel.refreshRankings(it) },
|
||||||
)
|
)
|
||||||
|
val radio = browseStations
|
||||||
|
if (radio != null) {
|
||||||
|
RadioGenreList(
|
||||||
|
genre = rankingsGenre ?: "",
|
||||||
|
stations = radio,
|
||||||
|
loading = rankingsLoading,
|
||||||
|
onPlay = { viewModel.playFoundStation(it) },
|
||||||
|
)
|
||||||
|
} else
|
||||||
RankingsList(
|
RankingsList(
|
||||||
title = if (rankingsGenre != null) "Top in ${rankingsGenre}" else "Top on Wavlake",
|
title = if (rankingsGenre != null) "Top in ${rankingsGenre}" else "Top on Wavlake",
|
||||||
tracks = rankings,
|
tracks = rankings,
|
||||||
|
|
@ -535,6 +545,108 @@ private fun GenreChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RadioGenreList(
|
||||||
|
genre: String,
|
||||||
|
stations: List<com.rada.domain.model.Station>,
|
||||||
|
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
|
@Composable
|
||||||
private fun RankingsList(
|
private fun RankingsList(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,33 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/** Curated genre shortcuts — exact Wavlake catalog names only. */
|
/** Genres the Wavlake rankings endpoint accepts (probed against the live API). */
|
||||||
val DISCOVER_GENRES = listOf("Dance/EDM", "Electronic", "Hip-Hop/Rap", "Pop", "Rock")
|
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
|
||||||
|
|
||||||
/** What the Discover search looks for; user-toggled chips. */
|
/** What the Discover search looks for; user-toggled chips. */
|
||||||
enum class DiscoverType(val label: String) {
|
enum class DiscoverType(val label: String) {
|
||||||
|
|
@ -57,6 +82,7 @@ class WavlakeViewModel @Inject constructor(
|
||||||
private val playbackManager: PlaybackManager,
|
private val playbackManager: PlaybackManager,
|
||||||
private val playlists: PlaylistRepository,
|
private val playlists: PlaylistRepository,
|
||||||
private val stations: com.rada.data.repository.StationRepository,
|
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,
|
@dagger.hilt.android.qualifiers.ApplicationContext private val appContext: android.content.Context,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
@ -104,6 +130,14 @@ class WavlakeViewModel @Inject constructor(
|
||||||
private val _rankingsError = MutableStateFlow<String?>(null)
|
private val _rankingsError = MutableStateFlow<String?>(null)
|
||||||
val rankingsError: StateFlow<String?> = _rankingsError.asStateFlow()
|
val rankingsError: StateFlow<String?> = _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<List<com.rada.domain.model.Station>?>(null)
|
||||||
|
val browseStations: StateFlow<List<com.rada.domain.model.Station>?> = _browseStations.asStateFlow()
|
||||||
|
|
||||||
/** Resolved Lightning Addresses by artist id — populated best-effort. */
|
/** Resolved Lightning Addresses by artist id — populated best-effort. */
|
||||||
private val _addresses = MutableStateFlow<Map<String, String>>(emptyMap())
|
private val _addresses = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val addresses: StateFlow<Map<String, String>> = _addresses.asStateFlow()
|
val addresses: StateFlow<Map<String, String>> = _addresses.asStateFlow()
|
||||||
|
|
@ -177,14 +211,19 @@ class WavlakeViewModel @Inject constructor(
|
||||||
refreshRankings(null)
|
refreshRankings(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Genres matching the query: curated discover genres + every station genre. */
|
/** Genres matching the query: curated genres (incl. sub-genres) + saved-station genres. */
|
||||||
private fun genreMatches(query: String): List<String> {
|
private fun genreMatches(query: String): List<String> {
|
||||||
val q = query.lowercase()
|
val norm = { s: String -> s.lowercase().replace(" ", "") }
|
||||||
return (DISCOVER_GENRES + stations.getAll().map { it.genre }.filter { it.isNotBlank() })
|
val q = norm(query)
|
||||||
.map { it to it.lowercase() }
|
val pool = DISCOVER_GENRES + SUBGENRE_TAGS +
|
||||||
.filter { (_, low) -> low.contains(q) || q.contains(low.substringBefore('/')) }
|
stations.getAll().map { it.genre }.filter { it.isNotBlank() }
|
||||||
.map { it.first }
|
return pool
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.filter { low ->
|
||||||
|
val l = norm(low)
|
||||||
|
l.contains(q) || q.contains(l) ||
|
||||||
|
l.substringBefore('/').let { it.isNotEmpty() && q.contains(it) }
|
||||||
|
}
|
||||||
.take(8)
|
.take(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +233,11 @@ class WavlakeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun refreshRankings(genre: String?) {
|
fun refreshRankings(genre: String?) {
|
||||||
_rankingsGenre.value = genre
|
_rankingsGenre.value = genre
|
||||||
|
if (genre != null && !isWavlakeGenre(genre)) {
|
||||||
|
browseRadioGenre(genre)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_browseStations.value = null
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_rankingsLoading.value = true
|
_rankingsLoading.value = true
|
||||||
_rankingsError.value = null
|
_rankingsError.value = null
|
||||||
|
|
@ -313,22 +357,24 @@ class WavlakeViewModel @Inject constructor(
|
||||||
playbackManager.playStation(station.url, station.name)
|
playbackManager.playStation(station.url, station.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Jump the Stations tab filtered to this genre (Discover genre hit). */
|
/** Browse a genre: Wavlake rankings when indexed, radio-tag search otherwise. */
|
||||||
fun openGenre(genre: String) {
|
fun openGenre(genre: String) {
|
||||||
_rankingsGenre.value = genre
|
// 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()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_rankingsLoading.value = true
|
_rankingsLoading.value = true
|
||||||
_rankingsError.value = null
|
_rankingsError.value = null
|
||||||
when (val r = repository.rankings(days = 7, genre = genre, limit = 25)) {
|
_browseStations.value = runCatching { radioBrowser.byTag(genre) }
|
||||||
is WavlakeResult.Ok -> {
|
.getOrDefault(emptyList())
|
||||||
_rankings.value = r.value
|
|
||||||
resolveAddresses(r.value)
|
|
||||||
}
|
|
||||||
is WavlakeResult.Failed -> {
|
|
||||||
_rankings.value = emptyList()
|
|
||||||
_rankingsError.value = r.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_rankingsLoading.value = false
|
_rankingsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
app/src/main/res/drawable-hdpi/ic_home_mark.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_home_mark.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 334 B |
BIN
app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 739 B |
BIN
app/src/main/res/drawable-xhdpi/ic_home_mark.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 684 B |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_home_mark.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 1,022 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_home_mark.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
|
</adaptive-icon>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 33 KiB |