Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.github.feelbeatapp.androidclient.di

import androidx.compose.material3.SnackbarHostState
import com.github.feelbeatapp.androidclient.BuildConfig
import com.github.feelbeatapp.androidclient.auth.AuthManager
import com.github.feelbeatapp.androidclient.auth.OauthConfig
import com.github.feelbeatapp.androidclient.auth.spotify.SpotifyAuthManager
import com.github.feelbeatapp.androidclient.auth.storage.AuthStorage
import com.github.feelbeatapp.androidclient.auth.storage.PreferencesAuthStorage
import com.github.feelbeatapp.androidclient.error.ErrorEmitter
import com.github.feelbeatapp.androidclient.error.ErrorHandler
import com.github.feelbeatapp.androidclient.error.ErrorReceiver
import com.github.feelbeatapp.androidclient.network.api.FeelBeatApi
import com.github.feelbeatapp.androidclient.network.api.KtorFeelBeatApi
import com.github.feelbeatapp.androidclient.network.fullduplex.NetworkAgent
Expand Down Expand Up @@ -37,6 +41,8 @@ abstract class AppModule {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
}

private val errorHandler = ErrorHandler()

@Provides
@Singleton
fun provideHttpClient(): HttpClient {
Expand Down Expand Up @@ -68,6 +74,18 @@ abstract class AppModule {
fun provideApiUrl(): String {
return BuildConfig.API_URL
}

@Provides
@Singleton
fun provideErrorEmitter(): ErrorEmitter {
return errorHandler
}

@Provides
@Singleton
fun provideErrorReceiver(): ErrorReceiver {
return errorHandler
}
}

@Singleton @Binds abstract fun bindAuthManager(authManager: SpotifyAuthManager): AuthManager
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.feelbeatapp.androidclient.error

import com.github.feelbeatapp.androidclient.R

enum class ErrorCode {
AUTHORIZATION_SERVER_UNREACHABLE,
AUTHORIZATION_ACCESS_TOKEN_ERROR,
Expand All @@ -8,4 +10,15 @@ enum class ErrorCode {
FEELBEAT_SERVER_UNREACHABLE,
FEELBEAT_SERVER_ERROR,
FEELBEAT_SERVER_INCORRECT_RESPONSE_FORMAT,
INCORRECT_PLAYLIST_LINK;

fun toStringId(): Int {
return when (this) {
AUTHORIZATION_SERVER_UNREACHABLE -> R.string.authorization_server_unreachable
FEELBEAT_SERVER_UNREACHABLE -> R.string.feelbeat_server_unreachable
INCORRECT_PLAYLIST_LINK -> R.string.incorrect_playlist_link
FEELBEAT_SERVER_ERROR -> R.string.feelbeat_server_error
else -> R.string.unexpected_error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.feelbeatapp.androidclient.error

import kotlinx.coroutines.flow.SharedFlow

interface ErrorEmitter {
val errors: SharedFlow<FeelBeatException>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.feelbeatapp.androidclient.error

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject

class ErrorHandler @Inject constructor() : ErrorEmitter, ErrorReceiver {
private val _errors = MutableSharedFlow<FeelBeatException>()
override val errors = _errors.asSharedFlow()

override suspend fun submitError(exception: FeelBeatException) {
_errors.emit(exception)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.feelbeatapp.androidclient.error

interface ErrorReceiver {
suspend fun submitError(exception: FeelBeatException)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.github.feelbeatapp.androidclient.error

class FeelBeatException(val code: ErrorCode, debugMessage: String?, cause: Throwable?) :
open class FeelBeatException(val code: ErrorCode, debugMessage: String?, cause: Throwable?) :
Exception(debugMessage, cause) {
constructor(code: ErrorCode, debugMessage: String) : this(code, debugMessage, null)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.github.feelbeatapp.androidclient.error

class FeelBeatServerException(val serverMessage: String) :
FeelBeatException(ErrorCode.FEELBEAT_SERVER_ERROR)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.feelbeatapp.androidclient.network.api
import com.github.feelbeatapp.androidclient.auth.AuthManager
import com.github.feelbeatapp.androidclient.error.ErrorCode
import com.github.feelbeatapp.androidclient.error.FeelBeatException
import com.github.feelbeatapp.androidclient.error.FeelBeatServerException
import com.github.feelbeatapp.androidclient.network.api.payloads.CreateRoomPayload
import com.github.feelbeatapp.androidclient.network.api.responses.CreateRoomResponse
import com.github.feelbeatapp.androidclient.ui.app.model.RoomSettings
Expand Down Expand Up @@ -48,7 +49,7 @@ constructor(
}

if (res.status != HttpStatusCode.OK) {
throw FeelBeatException(ErrorCode.FEELBEAT_SERVER_ERROR, res.bodyAsText())
throw FeelBeatServerException(res.bodyAsText())
}

val (roomId) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -78,6 +79,7 @@ fun AppScreen(
onNavigateBack = onNavigateBack,
)
},
snackbarHost = { SnackbarHost(hostState = appViewModel.snackBarHost) },
bottomBar = { bottomBar() },
modifier = Modifier.fillMaxSize(),
) { padding ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
package com.github.feelbeatapp.androidclient.ui.app

import android.content.Context
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.feelbeatapp.androidclient.auth.AuthManager
import com.github.feelbeatapp.androidclient.error.ErrorEmitter
import com.github.feelbeatapp.androidclient.error.FeelBeatException
import com.github.feelbeatapp.androidclient.error.FeelBeatServerException
import com.github.feelbeatapp.androidclient.network.spotify.SpotifyAPI
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

data class PlayerIdentity(val name: String, val imageUrl: String)

@HiltViewModel
class AppViewModel
@Inject
constructor(private val authManager: AuthManager, private val spotifyAPI: SpotifyAPI) :
ViewModel() {
constructor(
@ApplicationContext private val ctx: Context,
private val authManager: AuthManager,
private val spotifyAPI: SpotifyAPI,
private val errorEmitter: ErrorEmitter,
) : ViewModel() {
private val _playerIdentity = MutableStateFlow<PlayerIdentity?>(null)
val playerIdentity = _playerIdentity.asStateFlow()

val snackBarHost = SnackbarHostState()

init {
loadPlayerIdentity()
handleErrors()
}

private fun handleErrors() {
viewModelScope.launch(Dispatchers.Default) {
errorEmitter.errors.collect {
snackBarHost.showSnackbar(formatException(it), withDismissAction = true)
}
}
}

private fun formatException(exception: FeelBeatException): String {
return when (exception) {
is FeelBeatServerException -> ctx.getString(exception.code.toStringId(), exception.serverMessage)
else -> ctx.getString(exception.code.toStringId())
}
}

private fun loadPlayerIdentity() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ fun NewRoomSettingsScreen(
newRoomSettingsViewModel: NewRoomSettingsViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
) {
val roomCreationState by newRoomSettingsViewModel.roomCreationState.collectAsState()
val createdRoomId by newRoomSettingsViewModel.roomCreated.collectAsState(null)
val loading by newRoomSettingsViewModel.loading.collectAsState()

LaunchedEffect(createdRoomId) {
val roomId = createdRoomId
Expand All @@ -47,9 +47,7 @@ fun NewRoomSettingsScreen(
) {
SettingsControls(viewModel = newRoomSettingsViewModel)

Text(roomCreationState.errorMessage ?: "")

if (roomCreationState.loading) {
if (loading) {
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.secondary,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
package com.github.feelbeatapp.androidclient.ui.app.roomsettings.viewmodels

import androidx.lifecycle.viewModelScope
import com.github.feelbeatapp.androidclient.error.ErrorCode
import com.github.feelbeatapp.androidclient.error.ErrorReceiver
import com.github.feelbeatapp.androidclient.error.FeelBeatException
import com.github.feelbeatapp.androidclient.network.api.FeelBeatApi
import dagger.hilt.android.lifecycle.HiltViewModel
import io.ktor.http.Url
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class RoomCreationState(val errorMessage: String?, val loading: Boolean = false)
const val SPOTIFY_PLAYLIST_HOST = "open.spotify.com"

@HiltViewModel
class NewRoomSettingsViewModel @Inject constructor(private val feelBeatApi: FeelBeatApi) :
class NewRoomSettingsViewModel
@Inject
constructor(private val feelBeatApi: FeelBeatApi, private val errorReceiver: ErrorReceiver) :
RoomSettingsViewModel() {
private val _roomCreationState = MutableStateFlow<RoomCreationState>(RoomCreationState(null))
val roomCreationState = _roomCreationState.asStateFlow()
private val _loading = MutableStateFlow(false)
val loading = _loading.asStateFlow()

private val _roomCreated = MutableSharedFlow<String?>()
val roomCreated = _roomCreated.asSharedFlow()

fun createRoom() {
viewModelScope.launch(Dispatchers.IO) {
_roomCreationState.update { it.copy(loading = true) }
val url = Url(roomSettings.value.playlistLink)
if (url.host != SPOTIFY_PLAYLIST_HOST || url.segments.isEmpty()) {
errorReceiver.submitError(FeelBeatException(ErrorCode.INCORRECT_PLAYLIST_LINK))
return@launch
}

_loading.value = true
try {
val roomId = feelBeatApi.createRoom(roomSettings.value)
_roomCreated.emit(roomId)
_roomCreationState.update { it.copy(loading = false) }
} catch (e: Throwable) {
_roomCreationState.update { it.copy(errorMessage = e.message, loading = false) }
} catch (e: FeelBeatException) {
errorReceiver.submitError(e)
} finally {
_loading.value = false
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
<string name="ups_that_s_not_correct_answer">Ups, that\'s not correct answer</string>
<string name="time_left">time left</string>
<string name="authorization_server_unreachable">Authorization server is unreachable</string>
<string name="feelbeat_server_unreachable">FeelBeat server is unreachable</string>
<string name="feelbeat_server_error">Server error: %1$s</string>
<string name="unexpected_error">Unexpected error occurred</string>
<string name="incorrect_playlist_link">Make sure playlist link is valid</string>
<string name="base_points">With how many points you starting to guess</string>
<string name="incorrectGuessPenalty">How many points you will loose after incorrect guess</string>
<string name="get_ready">Get ready</string>
Expand Down
Loading