Add detail screen.
This commit is contained in:
parent
de64718e10
commit
0f2b50f42e
18 changed files with 621 additions and 280 deletions
|
|
@ -89,7 +89,7 @@ dependencies {
|
|||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
|
||||
// Android Compose
|
||||
implementation "androidx.compose.ui:ui:1.2.0-alpha03"
|
||||
implementation "androidx.compose.ui:ui:1.2.0-alpha08"
|
||||
implementation "androidx.compose.material:material:1.1.1"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@ import androidx.activity.viewModels
|
|||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.systemuicontroller.SystemUiController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.pixelized.biblib.ui.composable.SystemThemeColor
|
||||
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
|
||||
import com.pixelized.biblib.ui.navigation.screen.ScreenNavHost
|
||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
|
|
@ -35,7 +33,7 @@ class MainActivity : ComponentActivity() {
|
|||
// splashscreen management
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition {
|
||||
launcherViewModel.isLoading.value
|
||||
launcherViewModel.isLoading
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,18 +42,18 @@ class MainActivity : ComponentActivity() {
|
|||
BibLibTheme {
|
||||
ProvideWindowInsets {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
// handle the system colors
|
||||
// Handle the system colors
|
||||
val systemUiController: SystemUiController = rememberSystemUiController()
|
||||
SystemThemeColor(
|
||||
systemUiController = systemUiController,
|
||||
statusDarkIcons = isSystemInDarkTheme().not(),
|
||||
navigationDarkIcons = isSystemInDarkTheme().not(),
|
||||
)
|
||||
// handle the main composition
|
||||
val loading by launcherViewModel.isLoading
|
||||
if (loading.not()) {
|
||||
|
||||
// Handle the main Navigation
|
||||
if (launcherViewModel.isLoading.not()) {
|
||||
ScreenNavHost(
|
||||
startDestination = Screen.Authentication
|
||||
startDestination = launcherViewModel.startDestination
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable
|
|||
|
||||
@Composable
|
||||
fun AnimatedDelayer(
|
||||
delay: Delay = Delay(),
|
||||
content: @Composable AnimatedDelayerScope.() -> Unit
|
||||
) {
|
||||
val scope = AnimatedDelayerScope()
|
||||
val scope = AnimatedDelayerScope(delay = delay)
|
||||
scope.content()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package com.pixelized.biblib.ui.navigation.screen
|
||||
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
) {
|
||||
|
|
@ -12,12 +16,16 @@ sealed class Screen(
|
|||
)
|
||||
|
||||
class BookDetail(id: Int) : Screen(
|
||||
route = "$ROOT/$id"
|
||||
route = "$ROUTE/$id"
|
||||
) {
|
||||
companion object {
|
||||
const val ROOT = "detail"
|
||||
private const val ROUTE = "detail"
|
||||
const val ARG_BOOK_ID = "id"
|
||||
const val route = "$ROOT/{$ARG_BOOK_ID}"
|
||||
|
||||
const val route = "$ROUTE/{$ARG_BOOK_ID}"
|
||||
val arguments: List<NamedNavArgument> = listOf(
|
||||
navArgument(ARG_BOOK_ID) { type = NavType.IntType }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,9 +34,23 @@ fun ScreenNavHost(
|
|||
composable(Screen.Home.route) {
|
||||
HomeScreen()
|
||||
}
|
||||
composable(Screen.BookDetail.route) {
|
||||
composable(
|
||||
route = Screen.BookDetail.route,
|
||||
arguments = Screen.BookDetail.arguments,
|
||||
) {
|
||||
DetailScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavHostController.navigateToHome() {
|
||||
navigate(Screen.Home.route) {
|
||||
launchSingleTop = true
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
fun NavHostController.navigateToBookDetail(bookId: Int) {
|
||||
navigate(Screen.BookDetail(id = bookId).route)
|
||||
}
|
||||
|
|
@ -32,40 +32,46 @@ import androidx.navigation.NavHostController
|
|||
import com.google.accompanist.insets.systemBarsPadding
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.composable.StateUioHandler
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||
import com.pixelized.biblib.ui.navigation.screen.navigateToHome
|
||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
|
||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
||||
@Composable
|
||||
fun AuthenticationScreen(
|
||||
viewModel: AuthenticationViewModel = hiltViewModel(),
|
||||
authenticationViewModel: AuthenticationViewModel = hiltViewModel(),
|
||||
formViewModel: AuthenticationFormViewModel = hiltViewModel(),
|
||||
) {
|
||||
val navHostController = LocalScreenNavHostController.current
|
||||
|
||||
AuthenticationScreenContent(
|
||||
login = viewModel.form.login,
|
||||
password = viewModel.form.password,
|
||||
rememberPassword = viewModel.form.remember,
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
rememberPassword = formViewModel.form.remember,
|
||||
onLoginChange = {
|
||||
viewModel.onLoginChange(it)
|
||||
formViewModel.onLoginChange(it)
|
||||
},
|
||||
onPasswordChange = {
|
||||
viewModel.onPasswordChange(it)
|
||||
formViewModel.onPasswordChange(it)
|
||||
},
|
||||
onRememberPasswordChange = {
|
||||
viewModel.onRememberChange(it)
|
||||
formViewModel.onRememberChange(it)
|
||||
},
|
||||
onGoogleSignIn = {
|
||||
viewModel.loginWithGoogle()
|
||||
authenticationViewModel.loginWithGoogle()
|
||||
},
|
||||
onSignIn = {
|
||||
viewModel.login()
|
||||
authenticationViewModel.login(
|
||||
login = formViewModel.form.login,
|
||||
password = formViewModel.form.password,
|
||||
)
|
||||
},
|
||||
onRegister = {
|
||||
navHostController.navigateToRegister()
|
||||
|
|
@ -74,7 +80,7 @@ fun AuthenticationScreen(
|
|||
|
||||
AuthenticationHandler(
|
||||
onDismissRequest = {
|
||||
if (it is StateUio.Failure) viewModel.dismissError()
|
||||
if (it is StateUio.Failure) authenticationViewModel.dismissError()
|
||||
},
|
||||
onSuccess = {
|
||||
navHostController.navigateToHome()
|
||||
|
|
@ -308,10 +314,6 @@ private fun CredentialRemember(
|
|||
//////////////////////////////////////
|
||||
// region: Navigation Helper
|
||||
|
||||
private fun NavHostController.navigateToHome() {
|
||||
navigate(Screen.Home.route) { popUpTo(0) { inclusive = true } }
|
||||
}
|
||||
|
||||
private fun NavHostController.navigateToRegister() {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.REGISTER_URL)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,220 +0,0 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
|
||||
import com.pixelized.biblib.utils.exception.MissingTokenException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthenticationViewModel @Inject constructor(
|
||||
private val credentialRepository: ICredentialRepository,
|
||||
private val googleSignIn: IGoogleSingInRepository,
|
||||
private val client: IBibLibClient,
|
||||
) : ViewModel() {
|
||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
private var authenticationJob: Job? = null
|
||||
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
|
||||
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
|
||||
|
||||
private val _login: MutableState<String>
|
||||
private val _password: MutableState<String>
|
||||
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
|
||||
|
||||
val form: AuthenticationFormUIO
|
||||
get() = AuthenticationFormUIO(
|
||||
login = _login,
|
||||
password = _password,
|
||||
remember = _remember,
|
||||
)
|
||||
|
||||
init {
|
||||
if (credentialRepository.rememberCredential) {
|
||||
_login = mutableStateOf(credentialRepository.login ?: "")
|
||||
_password = mutableStateOf(credentialRepository.password ?: "")
|
||||
} else {
|
||||
_login = mutableStateOf("")
|
||||
_password = mutableStateOf("")
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// region: Login with BibLibClient
|
||||
|
||||
fun login(
|
||||
login: String = _login.value,
|
||||
password: String = _password.value,
|
||||
) {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val query = AuthLoginQuery(username = login, password = password)
|
||||
_authenticationProcess.value = StateUio.Progress()
|
||||
try {
|
||||
val response = client.service.login(query)
|
||||
val idToken = response.token ?: throw MissingTokenException()
|
||||
client.token = idToken
|
||||
_authenticationProcess.value = StateUio.Success(Unit)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_authenticationProcess.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: Login with Google
|
||||
|
||||
@Composable
|
||||
fun PrepareLoginWithGoogle() {
|
||||
launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
|
||||
val account = task.getResult(ApiException::class.java)
|
||||
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
|
||||
val response = client.service.loginWithGoogle(googleToken)
|
||||
val token = response.token ?: throw MissingTokenException()
|
||||
client.token = token
|
||||
_authenticationProcess.value = StateUio.Success(Unit)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_authenticationProcess.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithGoogle() {
|
||||
_authenticationProcess.value = StateUio.Progress()
|
||||
launcher?.launch(googleSignIn.client.signInIntent)
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: AutoLogin
|
||||
|
||||
@Composable
|
||||
fun AutoLogin() {
|
||||
LaunchedEffect(key1 = "AuthenticationViewModel AutoLogin") {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = launch(Dispatchers.IO) {
|
||||
_authenticationProcess.value = StateUio.Progress()
|
||||
try {
|
||||
autoLoginWithGoogle() || autologinWithCredential()
|
||||
_authenticationProcess.value = StateUio.Success(Unit)
|
||||
} catch (exception: Exception) {
|
||||
_authenticationProcess.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autoLoginWithGoogle(): Boolean {
|
||||
val googleToken = googleSignIn.lastGoogleToken
|
||||
return if (googleToken != null) {
|
||||
try {
|
||||
client.service.loginWithGoogle(googleToken).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autologinWithCredential(): Boolean {
|
||||
val login = credentialRepository.login
|
||||
val password = credentialRepository.password
|
||||
return if (login != null && password != null) {
|
||||
try {
|
||||
val query = AuthLoginQuery(login, password)
|
||||
client.service.login(query).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: OnDataChange callback.
|
||||
|
||||
fun onLoginChange(login: String) {
|
||||
// update login in the repository
|
||||
if (_remember.value) {
|
||||
credentialRepository.login = login
|
||||
}
|
||||
// update the UI State
|
||||
_login.value = login
|
||||
}
|
||||
|
||||
fun onPasswordChange(password: String) {
|
||||
// update password in the repository
|
||||
if (_remember.value) {
|
||||
credentialRepository.password = password
|
||||
}
|
||||
// update the UI State
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
fun onRememberChange(remember: Boolean) {
|
||||
// save the remember state in the repository
|
||||
credentialRepository.rememberCredential = remember
|
||||
// update login & password in the repository
|
||||
if (remember.not()) {
|
||||
credentialRepository.login = null
|
||||
credentialRepository.password = null
|
||||
} else {
|
||||
credentialRepository.login = _login.value
|
||||
credentialRepository.password = _password.value
|
||||
}
|
||||
// update the UI State
|
||||
_remember.value = remember
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: Dialog
|
||||
|
||||
fun dismissError() {
|
||||
_authenticationProcess.value = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.viewModel
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.ui.screen.authentication.AuthenticationFormUIO
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthenticationFormViewModel @Inject constructor(
|
||||
private val credentialRepository: ICredentialRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _login: MutableState<String>
|
||||
private val _password: MutableState<String>
|
||||
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
|
||||
|
||||
val form: AuthenticationFormUIO
|
||||
get() = AuthenticationFormUIO(
|
||||
login = _login,
|
||||
password = _password,
|
||||
remember = _remember,
|
||||
)
|
||||
|
||||
init {
|
||||
if (credentialRepository.rememberCredential) {
|
||||
_login = mutableStateOf(credentialRepository.login ?: "")
|
||||
_password = mutableStateOf(credentialRepository.password ?: "")
|
||||
} else {
|
||||
_login = mutableStateOf("")
|
||||
_password = mutableStateOf("")
|
||||
}
|
||||
}
|
||||
|
||||
fun onLoginChange(login: String) {
|
||||
// update login in the repository
|
||||
if (_remember.value) {
|
||||
credentialRepository.login = login
|
||||
}
|
||||
// update the UI State
|
||||
_login.value = login
|
||||
}
|
||||
|
||||
fun onPasswordChange(password: String) {
|
||||
// update password in the repository
|
||||
if (_remember.value) {
|
||||
credentialRepository.password = password
|
||||
}
|
||||
// update the UI State
|
||||
_password.value = password
|
||||
}
|
||||
|
||||
fun onRememberChange(remember: Boolean) {
|
||||
// save the remember state in the repository
|
||||
credentialRepository.rememberCredential = remember
|
||||
// update login & password in the repository
|
||||
if (remember.not()) {
|
||||
credentialRepository.login = null
|
||||
credentialRepository.password = null
|
||||
} else {
|
||||
credentialRepository.login = _login.value
|
||||
credentialRepository.password = _password.value
|
||||
}
|
||||
// update the UI State
|
||||
_remember.value = remember
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.pixelized.biblib.ui.screen.authentication.viewModel
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
|
||||
import com.pixelized.biblib.utils.exception.MissingTokenException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthenticationViewModel @Inject constructor(
|
||||
private val googleSignIn: IGoogleSingInRepository,
|
||||
private val client: IBibLibClient,
|
||||
) : ViewModel() {
|
||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
private var authenticationJob: Job? = null
|
||||
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
|
||||
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
|
||||
|
||||
//////////////////////////////////////
|
||||
// region: Login with BibLibClient
|
||||
|
||||
fun login(login: String, password: String) {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val query = AuthLoginQuery(username = login, password = password)
|
||||
_authenticationProcess.value = StateUio.Progress()
|
||||
try {
|
||||
val response = client.service.login(query)
|
||||
val idToken = response.token ?: throw MissingTokenException()
|
||||
client.token = idToken
|
||||
_authenticationProcess.value = StateUio.Success(Unit)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_authenticationProcess.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: Login with Google
|
||||
|
||||
@Composable
|
||||
fun PrepareLoginWithGoogle() {
|
||||
launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
authenticationJob?.cancel()
|
||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
|
||||
val account = task.getResult(ApiException::class.java)
|
||||
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
|
||||
val response = client.service.loginWithGoogle(googleToken)
|
||||
val token = response.token ?: throw MissingTokenException()
|
||||
client.token = token
|
||||
_authenticationProcess.value = StateUio.Success(Unit)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_authenticationProcess.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithGoogle() {
|
||||
_authenticationProcess.value = StateUio.Progress()
|
||||
launcher?.launch(googleSignIn.client.signInIntent)
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: Dialog
|
||||
|
||||
fun dismissError() {
|
||||
_authenticationProcess.value = null
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.pixelized.biblib.ui.screen.detail
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.biblib.model.book.Book
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||
import com.pixelized.biblib.utils.extention.capitalize
|
||||
import com.pixelized.biblib.utils.extention.shortDate
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BookDetailViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val client: IBibLibClient,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
|
||||
val state: State<StateUio<BookUio>> get() = _state
|
||||
|
||||
val book: State<BookUio?> = derivedStateOf {
|
||||
state.value.let { if (it is StateUio.Success<BookUio>) it.value else null }
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val id = savedStateHandle.bookId
|
||||
val book = getBookDetail(id = id)
|
||||
_state.value = StateUio.Success(book)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("BookDetailViewModel", exception.message, exception)
|
||||
_state.value = StateUio.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getBookDetail(id: Int): BookUio {
|
||||
val factory = BookFactory()
|
||||
val response = client.service.detail(id)
|
||||
val book = factory.fromDetailResponseToBook(response)
|
||||
return book.toUio()
|
||||
}
|
||||
|
||||
private fun Book.toUio() = BookUio(
|
||||
id = id,
|
||||
title = title,
|
||||
author = author.joinToString { it.name },
|
||||
rating = rating?.toFloat() ?: 0.0f,
|
||||
language = language?.displayLanguage?.capitalize() ?: "",
|
||||
date = releaseDate.shortDate(),
|
||||
series = series?.name,
|
||||
description = synopsis ?: "",
|
||||
)
|
||||
|
||||
private val SavedStateHandle.bookId: Int
|
||||
get() = get<Int>(Screen.BookDetail.ARG_BOOK_ID) ?: error("")
|
||||
}
|
||||
|
|
@ -2,22 +2,197 @@ package com.pixelized.biblib.ui.screen.detail
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
|
||||
import com.google.accompanist.insets.systemBarsPadding
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||
import com.pixelized.biblib.ui.composable.animation.Delay
|
||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
|
||||
@Composable
|
||||
fun DetailScreen(
|
||||
booksViewModel: BooksViewModel = hiltViewModel()
|
||||
viewModel: BookDetailViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
Box {
|
||||
val book by viewModel.book
|
||||
book?.let {
|
||||
DetailScreenContent(
|
||||
book = it,
|
||||
onSendClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailScreenContent() {
|
||||
private fun DetailScreenContent(
|
||||
book: BookUio,
|
||||
onSendClick: () -> Unit,
|
||||
) {
|
||||
AnimatedDelayer(delay = Delay(300)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(200.dp, 320.dp)
|
||||
.background(MaterialTheme.colors.surface),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Image(
|
||||
modifier = Modifier.size(64.dp),
|
||||
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Button(
|
||||
onClick = onSendClick,
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Send, contentDescription = "")
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(text = stringResource(id = R.string.action_send))
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(bottom = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.h5,
|
||||
text = book.title,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedOffset(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterHorizontally)
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = book.author,
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(bottom = 8.dp)) {
|
||||
AnimatedOffset(modifier = Modifier.weight(1f)) {
|
||||
TitleLabel(
|
||||
title = stringResource(id = R.string.detail_rating),
|
||||
label = book.rating.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedOffset(modifier = Modifier.weight(1f)) {
|
||||
TitleLabel(
|
||||
title = stringResource(id = R.string.detail_language),
|
||||
label = book.language,
|
||||
)
|
||||
}
|
||||
|
||||
book.date?.let {
|
||||
AnimatedOffset(modifier = Modifier.weight(1f)) {
|
||||
TitleLabel(
|
||||
title = stringResource(id = R.string.detail_release),
|
||||
label = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedOffset {
|
||||
Row(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||
TitleLabel(
|
||||
title = stringResource(id = R.string.detail_series),
|
||||
label = book.series ?: "-",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedOffset {
|
||||
HtmlText(
|
||||
html = book.description,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleLabel(
|
||||
title: String,
|
||||
label: String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.body2,
|
||||
fontWeight = FontWeight.Bold,
|
||||
text = title,
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.body1,
|
||||
textAlign = TextAlign.Center,
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HtmlText(
|
||||
html: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context -> TextView(context) },
|
||||
update = { it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -25,6 +200,8 @@ private fun DetailScreenContent() {
|
|||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||
private fun DetailScreenContentPreview() {
|
||||
BibLibTheme {
|
||||
DetailScreenContent()
|
||||
// DetailScreenContent(
|
||||
//
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -93,14 +94,14 @@ private fun Cover(
|
|||
) {
|
||||
val cover by image
|
||||
AnimatedContent(
|
||||
targetState = cover,
|
||||
targetState = cover.painter,
|
||||
transitionSpec = { fadeIn() with fadeOut() }
|
||||
) {
|
||||
Image(
|
||||
modifier = modifier,
|
||||
alignment = Alignment.Center,
|
||||
contentScale = it.contentScale,
|
||||
painter = it.painter,
|
||||
contentScale = cover.contentScale,
|
||||
painter = cover.painter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,24 +28,30 @@ import kotlinx.coroutines.flow.flowOf
|
|||
@Composable
|
||||
fun LazyBookThumbnailColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.medium),
|
||||
contentPadding: PaddingValues = PaddingValues(all = MaterialTheme.bibLib.dimen.medium),
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
books: LazyPagingItems<BookThumbnailUio>,
|
||||
onItemClick: (BookThumbnailUio) -> Unit = {},
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.small),
|
||||
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.small),
|
||||
verticalArrangement = verticalArrangement,
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
) {
|
||||
items(books) { thumbnail ->
|
||||
BookThumbnail(thumbnail)
|
||||
BookThumbnail(
|
||||
thumbnail = thumbnail,
|
||||
onClick = onItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun LazyBookThumbnailColumnPreview() {
|
||||
private fun LazyBookThumbnailColumnPreview() {
|
||||
BibLibTheme {
|
||||
LazyBookThumbnailColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
@ -55,7 +61,7 @@ fun LazyBookThumbnailColumnPreview() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun previewResources(): LazyPagingItems<BookThumbnailUio> {
|
||||
private fun previewResources(): LazyPagingItems<BookThumbnailUio> {
|
||||
val cover = CoverUio(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
||||
contentScale = ContentScale.None,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,31 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.books
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
|
||||
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.screen.navigateToBookDetail
|
||||
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
|
||||
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
||||
@Composable
|
||||
fun BooksPage(
|
||||
booksViewModel: BooksViewModel = hiltViewModel()
|
||||
) {
|
||||
val navHostController = LocalScreenNavHostController.current
|
||||
LazyBookThumbnailColumn(
|
||||
books = booksViewModel.books
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.thumbnail.arrangement),
|
||||
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
|
||||
books = booksViewModel.books,
|
||||
onItemClick = {
|
||||
navHostController.navigateToBookDetail(bookId = it.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,30 @@
|
|||
package com.pixelized.biblib.ui.screen.home.page.news
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.screen.navigateToBookDetail
|
||||
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
||||
@Composable
|
||||
fun NewsPage(
|
||||
booksViewModel: NewsBookViewModel = hiltViewModel()
|
||||
) {
|
||||
val navHostController = LocalScreenNavHostController.current
|
||||
LazyBookThumbnailColumn(
|
||||
books = booksViewModel.news
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.thumbnail.arrangement),
|
||||
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
|
||||
books = booksViewModel.news,
|
||||
onItemClick = {
|
||||
navHostController.navigateToBookDetail(bookId = it.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,123 @@
|
|||
package com.pixelized.biblib.ui.screen.launch
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LauncherViewModel @Inject constructor() : ViewModel() {
|
||||
class LauncherViewModel @Inject constructor(
|
||||
private val credentialRepository: ICredentialRepository,
|
||||
private val googleSignIn: IGoogleSingInRepository,
|
||||
private val client: IBibLibClient,
|
||||
private val bookRepository: IBookRepository,
|
||||
private val apiCache: IAPICacheRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _isLoading = mutableStateOf(true)
|
||||
val isLoading: State<Boolean> get() = _isLoading
|
||||
val isLoading: Boolean by _isLoading
|
||||
|
||||
var startDestination: Screen = Screen.Authentication
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(1500)
|
||||
// Try to Authenticate
|
||||
if (autoLoginWithGoogle() || autologinWithCredential()) {
|
||||
startDestination = Screen.Home
|
||||
// Update book
|
||||
if (loadNewBooks()) {
|
||||
loadAllBooks()
|
||||
}
|
||||
}
|
||||
//
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// region: Authentication
|
||||
|
||||
private suspend fun autoLoginWithGoogle(): Boolean {
|
||||
val googleToken = googleSignIn.lastGoogleToken
|
||||
return if (googleToken != null) {
|
||||
try {
|
||||
client.service.loginWithGoogle(googleToken).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autologinWithCredential(): Boolean {
|
||||
val login = credentialRepository.login
|
||||
val password = credentialRepository.password
|
||||
return if (login != null && password != null) {
|
||||
try {
|
||||
val query = AuthLoginQuery(login, password)
|
||||
client.service.login(query).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
//////////////////////////////////////
|
||||
// region: Books update
|
||||
|
||||
private suspend fun loadNewBooks(): Boolean {
|
||||
val cached = apiCache.new
|
||||
val updated = client.service.new()
|
||||
return if (cached != updated) {
|
||||
apiCache.new = updated
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAllBooks(): Boolean {
|
||||
client.service.list().let { response ->
|
||||
val newIds = apiCache.new?.data?.map { it.book_id } ?: listOf()
|
||||
val factory = BookFactory()
|
||||
val books = response.data?.map { dto ->
|
||||
val isNew = newIds.contains(dto.book_id)
|
||||
val index = newIds.indexOf(dto.book_id)
|
||||
factory.fromListResponseToBook(dto, isNew, index)
|
||||
}
|
||||
books?.let { data -> bookRepository.update(data) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@ data class BibLibDimen(
|
|||
val extraLarge: Dp = 64.dp,
|
||||
val dialog: Dialog = Dialog(),
|
||||
val thumbnail: BookThumbnail = BookThumbnail(),
|
||||
) {
|
||||
|
||||
) {
|
||||
@Stable
|
||||
@Immutable
|
||||
data class Dialog(
|
||||
|
|
@ -29,6 +30,8 @@ data class BibLibDimen(
|
|||
@Stable
|
||||
@Immutable
|
||||
data class BookThumbnail(
|
||||
val padding: Dp = 16.dp,
|
||||
val arrangement: Dp = 8.dp,
|
||||
val cover: DpSize = DpSize(60.dp, 96.dp),
|
||||
val corner: Dp = 8.dp,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,6 @@ import androidx.compose.ui.graphics.painter.Painter
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
|
||||
@Composable
|
||||
fun painterResource(@DrawableRes res: Int): Painter {
|
||||
val context = LocalContext.current
|
||||
val drawable = AppCompatResources.getDrawable(context, res)
|
||||
return DrawablePainter(drawable = drawable ?: error(""))
|
||||
}
|
||||
|
||||
fun painterResource(context: Context, @DrawableRes res: Int): Painter {
|
||||
val drawable = AppCompatResources.getDrawable(context, res)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue