Add google signin & navigation.

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-15 22:00:36 +02:00
parent 6876ad7052
commit f2357c6151
31 changed files with 764 additions and 114 deletions

View file

@ -1,3 +1,5 @@
import com.android.build.gradle.internal.dsl.SigningConfig
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
@ -6,15 +8,24 @@ plugins {
}
android {
namespace = "com.pixelized.lexique"
namespace = "com.pixelized.rplexicon"
compileSdk = 33
signingConfigs {
create("pixelized") {
storeFile = file(project.properties["PIXELIZED_RELEASE_STORE_FILE"] as String)
storePassword = project.properties["PIXELIZED_RELEASE_STORE_PASSWORD"] as String
keyAlias = project.properties["PIXELIZED_RELEASE_KEY_ALIAS"] as String
keyPassword = project.properties["PIXELIZED_RELEASE_KEY_PASSWORD"] as String
}
}
defaultConfig {
applicationId = "com.pixelized.lexique"
applicationId = "com.pixelized.rplexicon"
minSdk = 26
targetSdk = 33
versionCode = 1
versionName = "1.0"
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -24,10 +35,20 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isDebuggable = false
isMinifyEnabled = true
signingConfig = signingConfigs.pixelized
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
debug {
applicationIdSuffix = ".dev"
isDebuggable = true
isMinifyEnabled = false
signingConfig = signingConfigs.pixelized
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
@ -67,15 +88,24 @@ dependencies {
implementation("androidx.compose.material3:material3:1.1.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
// Accompanist
implementation("com.google.accompanist:accompanist-navigation-animation:0.30.1")
implementation("com.google.accompanist:accompanist-placeholder:0.30.1")
// Hilt: Dependency injection
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation("com.google.dagger:hilt-android:2.45")
kapt("com.google.dagger:hilt-compiler:2.45")
// Google service
implementation("com.google.android.gms:play-services-auth:20.6.0")
// Image
implementation("com.github.skydoves:landscapist-glide:2.1.11")
kapt("com.github.bumptech.glide:compiler:4.14.2") // this have to be align with landscapist-glide
// Retrofit : Network
implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
}
private val NamedDomainObjectContainer<SigningConfig>.pixelized get() = this.getByName("pixelized")

View file

@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<application
android:name=".MainApplication"

View file

@ -1,32 +0,0 @@
package com.pixelized.lexique
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.pixelized.lexique.ui.screens.lexicon.LexiconScreen
import com.pixelized.lexique.ui.theme.LexiconTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LexiconTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
LexiconScreen()
}
}
}
}
}

View file

@ -1,9 +0,0 @@
package com.pixelized.lexique.ui.screens.detail
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class CharacterDetailViewModel @Inject constructor() : ViewModel() {
}

View file

@ -0,0 +1,48 @@
package com.pixelized.rplexicon
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import com.pixelized.rplexicon.ui.navigation.ScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import dagger.hilt.android.AndroidEntryPoint
val LocalActivity = staticCompositionLocalOf<Activity> { error("Activity not available") }
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LexiconTheme {
CompositionLocalProvider(
LocalActivity provides this
) {
Scaffold(
content = { padding ->
Surface(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = padding),
color = MaterialTheme.colorScheme.background
) {
ScreenNavHost()
}
}
)
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique
package com.pixelized.rplexicon
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.model
package com.pixelized.rplexicon.model
import android.net.Uri

View file

@ -1,6 +1,6 @@
package com.pixelized.lexique.module
package com.pixelized.rplexicon.module
import com.pixelized.lexique.network.IGoogleSpreadSheet
import com.pixelized.rplexicon.network.IGoogleSpreadSheet
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.network
package com.pixelized.rplexicon.network
import retrofit2.http.GET

View file

@ -1,8 +1,8 @@
package com.pixelized.lexique.repository
package com.pixelized.rplexicon.repository
import android.net.Uri
import com.pixelized.lexique.model.Lexicon
import com.pixelized.lexique.network.IGoogleSpreadSheet
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.network.IGoogleSpreadSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow

View file

@ -0,0 +1,25 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.screens.authentication.AuthenticationScreen
private const val ROUTE = "authentication"
const val AUTHENTICATION_ROUTE = ROUTE
fun NavGraphBuilder.composableAuthentication() {
animatedComposable(
route = AUTHENTICATION_ROUTE,
animation = NavigationAnimation.Fade,
) {
AuthenticationScreen()
}
}
fun NavHostController.navigateToAuthentication(
option: NavOptionsBuilder.() -> Unit = {},
) {
navigate(route = ROUTE, builder = option)
}

View file

@ -0,0 +1,49 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.pixelized.rplexicon.ui.screens.detail.CharacterDetailScreen
import com.pixelized.rplexicon.utilitary.extentions.ARG
private const val ROUTE = "CharacterDetail"
private const val ARG_ID = "id"
val CHARACTER_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}"
@Stable
@Immutable
data class CharacterDetailArgument(
val id: String,
)
val SavedStateHandle.characterDetailArgument: CharacterDetailArgument
get() = CharacterDetailArgument(
id = get(ARG_ID) ?: error("CharacterDetailArgument argument: id")
)
fun NavGraphBuilder.composableCharacterDetail() {
animatedComposable(
route = CHARACTER_DETAIL_ROUTE,
arguments = listOf(
navArgument(name = ARG_ID) {
type = NavType.StringType
}
),
animation = NavigationAnimation.Push,
) {
CharacterDetailScreen()
}
}
fun NavHostController.navigateToCharacterDetail(
id: String,
option: NavOptionsBuilder.() -> Unit = {},
) {
val route = "$ROUTE?$ARG_ID=$id"
navigate(route = route, builder = option)
}

View file

@ -0,0 +1,25 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconScreen
private const val ROUTE = "lexicon"
const val LEXICON_ROUTE = ROUTE
fun NavGraphBuilder.composableLexicon() {
animatedComposable(
route = LEXICON_ROUTE,
animation = NavigationAnimation.Fade,
) {
LexiconScreen()
}
}
fun NavHostController.navigateToLexicon(
option: NavOptionsBuilder.() -> Unit = {},
) {
navigate(route = ROUTE, builder = option)
}

View file

@ -0,0 +1,45 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
error("LocalScreenNavHost not ready")
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ScreenNavHost(
navHostController: NavHostController = rememberAnimatedNavController(),
startDestination: String = AUTHENTICATION_ROUTE,
) {
CompositionLocalProvider(
LocalScreenNavHost provides navHostController,
) {
AnimatedNavHost(
navController = navHostController,
startDestination = startDestination,
) {
composableAuthentication()
composableLexicon()
composableCharacterDetail()
}
}
}
fun defaultOption(): NavOptionsBuilder.() -> Unit = { }
fun rootOption(): NavOptionsBuilder.() -> Unit = {
launchSingleTop = true
restoreState = true
popUpTo(0) {
saveState = true
inclusive = true
}
}

View file

@ -0,0 +1,107 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import com.google.accompanist.navigation.animation.composable
@OptIn(ExperimentalAnimationApi::class)
fun NavGraphBuilder.animatedComposable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
animation: NavigationAnimation = NavigationAnimation.NONE,
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
) {
composable(
route = route,
arguments = arguments,
enterTransition = animation.enterTransition,
exitTransition = animation.exitTransition,
popEnterTransition = animation.popEnterTransition,
popExitTransition = animation.popExitTransition,
content = content,
)
}
@OptIn(ExperimentalAnimationApi::class)
sealed class NavigationAnimation constructor(
val enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?,
val exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?,
val popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?,
val popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?,
) {
object Push : NavigationAnimation(
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(DURATION),
)
},
exitTransition = {
null
},
popEnterTransition = {
null
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Right,
animationSpec = tween(DURATION),
)
},
)
object Modal : NavigationAnimation(
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Up,
animationSpec = tween(DURATION),
)
},
exitTransition = {
null
},
popEnterTransition = {
null
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Down,
animationSpec = tween(DURATION),
)
},
)
object Fade : NavigationAnimation(
enterTransition = {
fadeIn(
animationSpec = tween(DURATION),
)
},
exitTransition = {
null
},
popEnterTransition = {
null
},
popExitTransition = {
fadeOut(
animationSpec = tween(DURATION),
)
},
)
object NONE : NavigationAnimation(
enterTransition = null,
exitTransition = null,
popEnterTransition = null,
popExitTransition = null,
)
companion object {
const val DURATION = 300
}
}

View file

@ -0,0 +1,182 @@
package com.pixelized.rplexicon.ui.screens.authentication
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalActivity
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.navigateToLexicon
import com.pixelized.rplexicon.ui.navigation.rootOption
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.GoogleColorPalette
import kotlinx.coroutines.CoroutineScope
@Composable
fun AuthenticationScreen(
viewModel: AuthenticationViewModel = hiltViewModel()
) {
val screen = LocalScreenNavHost.current
val activity = LocalActivity.current
val state = viewModel.rememberAuthenticationState(activity = activity)
Surface {
AuthenticationScreenContent(
modifier = Modifier.fillMaxSize(),
onGoogleSignIn = {
viewModel.signIn(activity = activity)
},
)
HandleAuthenticationState(
state = state,
onSignIn = {
screen.navigateToLexicon(option = rootOption())
},
)
}
}
@Composable
fun HandleAuthenticationState(
state: State<AuthenticationViewModel.Authentication>,
onSignIn: suspend CoroutineScope.() -> Unit
) {
if (state.value == AuthenticationViewModel.Authentication.Success) {
LaunchedEffect(key1 = "Authentication.Success", block = onSignIn)
}
}
@Composable
private fun AuthenticationScreenContent(
modifier: Modifier = Modifier,
onGoogleSignIn: () -> Unit,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.BottomCenter,
) {
Button(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onGoogleSignIn,
) {
Text(text = rememeberGoogleStringResource())
}
}
}
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.2f),
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
)
}
}
@Composable
private fun rememeberGoogleStringResource(): AnnotatedString {
val default = LocalTextStyle.current.toSpanStyle()
val google = stringResource(id = R.string.action_google_sign_in)
return remember {
buildAnnotatedString {
withStyle(
style = default
) {
append(google)
append(" ")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.BLUE,
fontWeight = FontWeight.ExtraBold
),
) {
append("G")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.RED,
fontWeight = FontWeight.ExtraBold
),
) {
append("o")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.YELLOW,
fontWeight = FontWeight.ExtraBold
),
) {
append("o")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.BLUE,
fontWeight = FontWeight.ExtraBold
),
) {
append("g")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.GREEN,
fontWeight = FontWeight.ExtraBold
),
) {
append("l")
}
withStyle(
style = default.copy(
color = GoogleColorPalette.RED,
fontWeight = FontWeight.ExtraBold
),
) {
append("e")
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun AuthenticationScreenContentPreview() {
LexiconTheme {
AuthenticationScreenContent(
onGoogleSignIn = { },
)
}
}

View file

@ -0,0 +1,95 @@
package com.pixelized.rplexicon.ui.screens.authentication
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.IntentSender.SendIntentException
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.pixelized.rplexicon.R
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
application: Application,
) : AndroidViewModel(application) {
private val context: Context get() = getApplication()
private var launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>? = null
private val account: GoogleSignInAccount? by lazy {
GoogleSignIn.getLastSignedInAccount(application)
}
private val lastGoogleToken: String? by lazy {
GoogleSignIn.getLastSignedInAccount(application)?.idToken
}
private val state = mutableStateOf(
when (account) {
null -> Authentication.Initial
else -> Authentication.Success
}
)
@Composable
fun rememberAuthenticationState(activity: Activity): State<Authentication> {
launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
if (it.resultCode == Activity.RESULT_OK) {
val credential = Identity
.getSignInClient(activity)
.getSignInCredentialFromIntent(it.data)
state.value = Authentication.Success
} else {
state.value = Authentication.Failure
}
},
)
return state
}
fun signIn(activity: Activity) {
state.value = Authentication.Initial
val request: GetSignInIntentRequest = GetSignInIntentRequest.builder()
.setServerClientId(context.getString(R.string.google_sign_in_id))
.build()
Identity.getSignInClient(activity).getSignInIntent(request)
.addOnSuccessListener { result ->
try {
launcher?.launch(
IntentSenderRequest.Builder(result.intentSender).build()
)
} catch (e: SendIntentException) {
state.value = Authentication.Failure
}
}
.addOnFailureListener { e ->
state.value = Authentication.Failure
}
}
@Stable
sealed class Authentication {
object Initial : Authentication()
object Success : Authentication()
object Failure : Authentication()
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.screens.detail
package com.pixelized.rplexicon.ui.screens.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -32,6 +32,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -50,8 +52,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.lexique.R
import com.pixelized.lexique.ui.theme.LexiconTheme
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage
@ -70,7 +72,14 @@ data class CharacterDetailUio(
fun CharacterDetailScreen(
viewModel: CharacterDetailViewModel = hiltViewModel(),
) {
Surface {
CharacterDetailScreenContent(
modifier = Modifier.fillMaxSize(),
item = viewModel.character,
onBack = { },
onItem = { },
)
}
}
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@ -78,8 +87,9 @@ fun CharacterDetailScreen(
private fun CharacterDetailScreenContent(
modifier: Modifier = Modifier,
state: ScrollState = rememberScrollState(),
item: CharacterDetailUio,
item: State<CharacterDetailUio>,
onBack: () -> Unit,
onItem: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
@ -104,7 +114,7 @@ private fun CharacterDetailScreenContent(
Box(
modifier = Modifier.padding(paddingValues = paddingValues),
) {
item.portrait.firstOrNull()?.let { uri ->
item.value.portrait.firstOrNull()?.let { uri ->
Box(
modifier = Modifier
.fillMaxWidth()
@ -142,14 +152,14 @@ private fun CharacterDetailScreenContent(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item.name?.let {
item.value.name?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = typography.headlineSmall,
text = it,
)
}
item.diminutive?.let {
item.value.diminutive?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
@ -161,20 +171,20 @@ private fun CharacterDetailScreenContent(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item.race?.let {
item.value.race?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
item.gender?.let {
item.value.gender?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
}
item.description?.let {
item.value.description?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
@ -193,7 +203,7 @@ private fun CharacterDetailScreenContent(
text = it,
)
}
item.history?.let {
item.value.history?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
@ -205,7 +215,7 @@ private fun CharacterDetailScreenContent(
text = it,
)
}
if (item.portrait.isNotEmpty()) {
if (item.value.portrait.isNotEmpty()) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
@ -215,7 +225,7 @@ private fun CharacterDetailScreenContent(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = item.portrait) {
items(items = item.value.portrait) {
GlideImage(
modifier = Modifier.height(320.dp),
imageModel = { it },
@ -261,29 +271,35 @@ private fun Modifier.scrollOffset(
private fun CharacterDetailScreenContentPreview() {
LexiconTheme {
Surface {
val character = remember {
mutableStateOf(
CharacterDetailUio(
name = "Brulkhai",
diminutive = "./ Bru",
gender = "female",
race = "Demi-Orc",
portrait = listOf(
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/877/large/bayard-wu-0714.jpg?1468642665"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/887/large/bayard-wu-0715.jpg?1468642839"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/891/large/bayard-wu-0623-03.jpg?1468642872"),
Uri.parse("https://cdna.artstation.com/p/assets/images/images/002/869/868/large/bayard-wu-0622-03.jpg?1466664135"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/002/869/871/large/bayard-wu-0622-04.jpg?1466664153"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/347/181/large/bayard-wu-1217.jpg?1482770883"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/635/large/bayard-wu-1215.jpg?1482166826"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/631/large/bayard-wu-1209.jpg?1482166803"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/641/large/bayard-wu-1212.jpg?1482166838"),
),
description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history = null,
)
)
}
CharacterDetailScreenContent(
modifier = Modifier.fillMaxSize(),
item = CharacterDetailUio(
name = "Brulkhai",
diminutive = "./ Bru",
gender = "female",
race = "Demi-Orc",
portrait = listOf(
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/877/large/bayard-wu-0714.jpg?1468642665"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/887/large/bayard-wu-0715.jpg?1468642839"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/891/large/bayard-wu-0623-03.jpg?1468642872"),
Uri.parse("https://cdna.artstation.com/p/assets/images/images/002/869/868/large/bayard-wu-0622-03.jpg?1466664135"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/002/869/871/large/bayard-wu-0622-04.jpg?1466664153"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/347/181/large/bayard-wu-1217.jpg?1482770883"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/635/large/bayard-wu-1215.jpg?1482166826"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/631/large/bayard-wu-1209.jpg?1482166803"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/641/large/bayard-wu-1212.jpg?1482166838"),
),
description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history = null,
),
item = character,
onBack = { },
onItem = { },
)
}
}

View file

@ -0,0 +1,35 @@
package com.pixelized.rplexicon.ui.screens.detail
import android.net.Uri
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class CharacterDetailViewModel @Inject constructor() : ViewModel() {
val character: State<CharacterDetailUio> = mutableStateOf(
CharacterDetailUio(
name = "Brulkhai",
diminutive = "./ Bru",
gender = "female",
race = "Demi-Orc",
portrait = listOf(
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/877/large/bayard-wu-0714.jpg?1468642665"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/887/large/bayard-wu-0715.jpg?1468642839"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/891/large/bayard-wu-0623-03.jpg?1468642872"),
Uri.parse("https://cdna.artstation.com/p/assets/images/images/002/869/868/large/bayard-wu-0622-03.jpg?1466664135"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/002/869/871/large/bayard-wu-0622-04.jpg?1466664153"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/347/181/large/bayard-wu-1217.jpg?1482770883"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/635/large/bayard-wu-1215.jpg?1482166826"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/631/large/bayard-wu-1209.jpg?1482166803"),
Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/641/large/bayard-wu-1212.jpg?1482166838"),
),
description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history = null,
)
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.screens.lexicon
package com.pixelized.rplexicon.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -17,7 +17,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.lexique.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class LexiconItemUio(

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.screens.lexicon
package com.pixelized.rplexicon.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -18,16 +18,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.lexique.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
Surface {
LexiconScreenContent(
items = viewModel.items,
onItem = { },
onItem = {
screen.navigateToCharacterDetail(id = "")
},
)
}
}

View file

@ -1,11 +1,11 @@
package com.pixelized.lexique.ui.screens.lexicon
package com.pixelized.rplexicon.ui.screens.lexicon
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.lexique.model.Lexicon
import com.pixelized.lexique.repository.LexiconRepository
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.theme
package com.pixelized.rplexicon.ui.theme
import androidx.compose.ui.graphics.Color

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.theme
package com.pixelized.rplexicon.ui.theme
import android.app.Activity
import android.os.Build
@ -45,22 +45,22 @@ fun LexiconTheme(
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
// val context = LocalContext.current
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
// }
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
// val view = LocalView.current
// if (!view.isInEditMode) {
// SideEffect {
// val window = (view.context as Activity).window
// window.statusBarColor = colorScheme.primary.toArgb()
// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
// }
// }
MaterialTheme(
colorScheme = colorScheme,

View file

@ -1,4 +1,4 @@
package com.pixelized.lexique.ui.theme
package com.pixelized.rplexicon.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle

View file

@ -0,0 +1,12 @@
package com.pixelized.rplexicon.ui.theme.colors
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
object GoogleColorPalette {
val BLUE = Color(0xFF4285F4)
val RED = Color(0xFFEA4335)
val YELLOW = Color(0xFFFBBC05)
val GREEN = Color(0xFF34A853)
}

View file

@ -0,0 +1,3 @@
package com.pixelized.rplexicon.utilitary.extentions
val String.ARG: String get() = "$this={$this}"

View file

@ -0,0 +1,5 @@
<resources>
<string name="app_name">Lexique</string>
<string name="action_google_sign_in">Se connecter avec</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="google_sign_in_id" translatable="false">62913404482-ergqkjiuvint49q8lm555j21vvb6af7s.apps.googleusercontent.com</string>
</resources>

View file

@ -1,3 +1,5 @@
<resources>
<string name="app_name">Lexique</string>
<string name="action_google_sign_in">Sign in with</string>
</resources>

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.1.0-rc01" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("org.jetbrains.kotlin.kapt") version "1.8.22" apply false
id("com.google.dagger.hilt.android") version "2.44" apply false
}