rewrite the app in native kotlin with compose

This commit is contained in:
2024-01-19 20:47:36 +02:00
committed by Illusion
parent a40cf85925
commit 0069f62115
92 changed files with 3391 additions and 2008 deletions
@@ -0,0 +1,170 @@
package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.adamratzman.spotify.SpotifyClientApi
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.NotificationEvent
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.data.HomeRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): Uri {
return Uri.parse(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}
}
data class SearchResult(
val title: String,
val artist: String,
val url: Uri,
val artistUrl: Uri,
val thumbnail: Uri,
val id: String,
)
@Serializable
data class Resources(
@Serializable(with = UriSerializer::class)
val thumbnail: Uri,
)
@Serializable
data class Artist(
val name: String,
@Serializable(with = UriSerializer::class)
val url: Uri,
)
@Serializable
data class Meta(
val title: String,
val primaryArtist: Artist,
)
@Serializable
data class IntermediateResult(
@Serializable(with = UriSerializer::class)
val url: Uri,
val meta: Meta,
val resources: Resources,
val id: Int,
)
@Serializable
data class SearchResultsResponse(
val all: List<IntermediateResult>
)
data class HomeState(
val isLoading: Boolean = false,
val results: List<SearchResult> = emptyList(),
val notification: NotificationEvent? = null,
val error: String? = null,
)
class HomeViewModel(private val context: Context, private val homeRepository: HomeRepository): ViewModel() {
private val _uiState = MutableStateFlow(HomeState())
val uiState = _uiState.asStateFlow()
private val json = Json {
ignoreUnknownKeys = true
}
fun getListeningPermission(): Boolean {
return homeRepository.isPermissionGranted()
}
fun setSong(song: NotificationEvent) {
_uiState.value = _uiState.value.copy(isLoading = true, notification = song, error = null)
}
fun makeRequestToSpotify(spotifyApi: SpotifyClientApi) {
viewModelScope.launch {
val track = spotifyApi.player.getCurrentlyPlaying()?.item
if (track?.id == null) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = context.resources.getString(R.string.no_spotify_song_playing)
)
} else {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = null,
results = listOf(SearchResult(
title = _uiState.value.notification!!.title,
artist = _uiState.value.notification!!.artist,
url = track.asTrack?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
artistUrl = track.asTrack?.artists?.get(0)?.externalUrls?.spotify?.toUri() ?: Uri.EMPTY,
thumbnail = track.asTrack?.album?.images?.get(0)?.url?.toUri() ?: Uri.EMPTY,
id = track.id!!,
))
)
}
}
}
fun makeRequestToGenius(song: String, artist: String) {
viewModelScope.launch {
_uiState.update { HomeState(true, uiState.value.results, uiState.value.notification, null) }
withContext(Dispatchers.IO) {
val (res, isSuccess) = homeRepository.makeSearchRequest(song, artist)
if (isSuccess) {
val results = json.decodeFromString(SearchResultsResponse.serializer(), res!!).all.map {
SearchResult(
title = it.meta.title,
artist = it.meta.primaryArtist.name,
url = it.url,
artistUrl = it.meta.primaryArtist.url,
thumbnail = it.resources.thumbnail,
id = it.id.toString()
)
}
_uiState.update { HomeState(false, results, uiState.value.notification, null) }
} else {
_uiState.update { HomeState(
false,
emptyList(),
uiState.value.notification,
res
) }
}
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as LGApp)
HomeViewModel(application.applicationContext, application.homeRepository)
}
}
}
}
@@ -0,0 +1,78 @@
package com.illusionman1212.lyricsgrabbr.viewmodels
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.data.LyricsRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class LyricsState(
// set by Home screen
val song: String = "",
val artist: String = "",
val url: Uri = Uri.EMPTY,
val trackId: String = "",
val useSpotifyApi: Boolean = false,
// set by Lyrics screen
val lyrics: String = "",
val isLoading: Boolean = true,
val error: String? = null,
)
class LyricsViewModel(private val lyricsRepository: LyricsRepository): ViewModel() {
private val _uiState = MutableStateFlow(LyricsState())
val uiState = _uiState.asStateFlow()
fun onSongChange(trackId: String, title: String, subtitle: String, url: Uri, useSpotifyApi: Boolean) {
_uiState.update {
it.copy(song = title, artist = subtitle, url = url, useSpotifyApi = useSpotifyApi, trackId = trackId)
}
}
fun fetchLyricsFromSpotify() {
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
viewModelScope.launch {
withContext(Dispatchers.IO) {
val (lyrics, isSuccess) = lyricsRepository.fetchLyricsFromSpotify(uiState.value.trackId)
if (isSuccess) {
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
} else {
_uiState.value = _uiState.value.copy(isLoading = false, error = lyrics, lyrics = "")
}
}
}
}
fun fetchLyricsFromGenius() {
_uiState.value = _uiState.value.copy(isLoading = true, lyrics = "")
viewModelScope.launch {
withContext(Dispatchers.IO) {
val lyrics = lyricsRepository.fetchLyricsFromGenius(uiState.value.url)
_uiState.value = _uiState.value.copy(isLoading = false, lyrics = lyrics)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as LGApp)
LyricsViewModel(application.lyricsRepository)
}
}
}
}
@@ -0,0 +1,60 @@
package com.illusionman1212.lyricsgrabbr.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionman1212.lyricsgrabbr.LGApp
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
import com.illusionman1212.lyricsgrabbr.data.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal,
val isSpotifyAuthed: Boolean = false,
)
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
private val _uiState = MutableStateFlow(SettingsState())
val uiState = _uiState.asStateFlow()
suspend fun getAppTheme(): Int {
return settingsPrefsRepo.preferences.first().appTheme
}
fun setTheme(theme: Theme) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setTheme(theme)
}
}
fun updateSpotifyAuthStatus(isAuthed: Boolean) {
_uiState.value = _uiState.value.copy(isSpotifyAuthed = isAuthed)
}
init {
viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest {
_uiState.value = _uiState.value.copy(
appTheme = it.appTheme,
)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as LGApp)
SettingsViewModel(application.settingsPreferencesRepository)
}
}
}
}