From 2fa3fba8bf502753dd40ced5291b3c760cbe5627 Mon Sep 17 00:00:00 2001 From: Tsoi Anatoli Date: Sat, 8 Apr 2023 15:48:06 +0200 Subject: [PATCH 1/9] created the activities --- .idea/inspectionProfiles/Project_Default.xml | 41 +++++ app/src/main/AndroidManifest.xml | 5 + .../com/example/toptracer/MainActivity.kt | 151 ++++++++++++------ .../ui/theme/TopTracerWelcomeActivity.kt | 61 +++++++ .../toptracer/ui/theme/ui/theme/Color.kt | 11 ++ .../toptracer/ui/theme/ui/theme/Theme.kt | 68 ++++++++ .../toptracer/ui/theme/ui/theme/Type.kt | 34 ++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 321 insertions(+), 51 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt create mode 100644 app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Color.kt create mode 100644 app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Type.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75414aa..422f30d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,11 @@ android:supportsRtl="true" android:theme="@style/Theme.TopTracer" tools:targetApi="31"> + + username = usernameInput + isUsernameEmpty = usernameInput.isEmpty() + } + PasswordRow(password = password) { passwordInput -> + password = passwordInput + isPasswordValid = passwordInput == "password" } Row( modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Password", - modifier = Modifier.weight(1f) - ) - TextField( - value = password, - onValueChange = { password = it }, - visualTransformation = TopTracerPasswordTransformation(), - singleLine = true, - colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), - modifier = Modifier - .weight(3f) - ) - } - Row( + .padding(24.dp) - modifier = Modifier - .fillMaxWidth(), - - ) { + ) { Text( text = "Forgot Password", modifier = Modifier @@ -103,17 +81,88 @@ fun LoginScreen() { modifier = Modifier .weight(1f) .wrapContentSize(Alignment.Center) - .clickable {} + .clickable { + if (isUsernameEmpty) { + showDialog = true + dialogText = "It looks like you forgot to provide a username." + } else if (isPasswordValid) { + context.startActivity(navigate) + } else { + showDialog = true + dialogText = "The password you provided doesn't match our records." + } + } ) } } + ValidationAlert(showDialog, dialogText) { + showDialog = false + } } +@Composable +fun UsernameRow(username: String, onValueChange: (String) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Username", + modifier = Modifier.weight(1f) + ) + TextField( + value = username, + onValueChange = onValueChange, + colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), + modifier = Modifier.weight(3f), + ) + } +} -@Preview(showBackground = true) @Composable -fun DefaultPreview() { - TopTracerTheme { - LoginScreen() +fun PasswordRow(password: String, onValueChange: (String) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Password", + modifier = Modifier.weight(1f) + ) + TextField( + value = password, + onValueChange = onValueChange, + visualTransformation = TopTracerPasswordTransformation(), + singleLine = true, + colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), + modifier = Modifier + .weight(3f) + ) + } +} + +@Composable +fun ValidationAlert(showDialog: Boolean, dialogText: String, onDismiss: () -> Unit) { + if (showDialog) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = "Oops!", + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) + ) + }, + text = { Text(dialogText) }, + confirmButton = { + Button( + onClick = onDismiss, + content = { Text("OK") } + ) + } + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt b/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt new file mode 100644 index 0000000..424b461 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt @@ -0,0 +1,61 @@ +package com.example.toptracer + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.example.toptracer.ui.theme.TopTracerTheme + +class TopTracerWelcomeActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TopTracerTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + WelcomeScreen() + } + } + } + } +} + +@Composable +fun WelcomeScreen() { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Welcome, toptracer!" + ) + Text( + "GifTitle by GifAuthor!" + ) + Text( + text = "Logout", + modifier = Modifier + .clickable {} + ) + } +} + + +@Preview +@Composable +fun WelcomeScreenPreview() { + WelcomeScreen() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Color.kt b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Color.kt new file mode 100644 index 0000000..403de6a --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.toptracer.ui.theme.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Theme.kt b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Theme.kt new file mode 100644 index 0000000..ead4475 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Theme.kt @@ -0,0 +1,68 @@ +package com.example.toptracer.ui.theme.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun TopTracerTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + 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) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Type.kt b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Type.kt new file mode 100644 index 0000000..ddf23e0 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ui/theme/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.toptracer.ui.theme.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4bb0a91..afcc777 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Top Tracer + TopTracerWelcomeActivity \ No newline at end of file From 24ca87a0324419ba2fb3611b6d4112c536c704fe Mon Sep 17 00:00:00 2001 From: Tsoi Anatoli Date: Sat, 8 Apr 2023 16:20:16 +0200 Subject: [PATCH 2/9] implemented gif fetching --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 13 ++- .../toptracer/TopTracerWelcomeActivity.kt | 98 +++++++++++++++++++ .../toptracer/helpers/GiphyApiService.kt | 56 +++++++++++ .../ui/theme/TopTracerWelcomeActivity.kt | 61 ------------ 5 files changed, 169 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt create mode 100644 app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt delete mode 100644 app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt diff --git a/app/build.gradle b/app/build.gradle index 75a9dac..02c2fcf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,4 +60,9 @@ dependencies { androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation("io.coil-kt:coil-compose:2.2.2") + implementation("io.coil-kt:coil-gif:2.2.2") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 422f30d..a2a774d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.example.toptracer"> + + + + + - + + - \ No newline at end of file + diff --git a/app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt b/app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt new file mode 100644 index 0000000..28692fd --- /dev/null +++ b/app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt @@ -0,0 +1,98 @@ +package com.example.toptracer + +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import com.example.toptracer.helpers.GiphyImageDetails +import com.example.toptracer.helpers.fetchRandomGif +import com.example.toptracer.ui.theme.TopTracerTheme + +class TopTracerWelcomeActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TopTracerTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + WelcomeScreen() + } + } + } + } +} + +@Composable +fun WelcomeScreen() { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Welcome, toptracer!" + ) + RandomGif( + ) + Text( + text = "GifTitle by GifAuthor!" + ) + Text( + text = "Logout", + modifier = Modifier + .clickable {} + ) + } +} + +@Composable +fun RandomGif() { + val apiKey = "vYbi41ARNKCZHJvrZ7IlDdqfCGSb8ZZy" + val imageDetails = remember { mutableStateOf(null) } + + LaunchedEffect(apiKey) { + imageDetails.value = fetchRandomGif(apiKey) + } + + imageDetails.value?.let {gif -> + val imageLoader = ImageLoader.Builder(LocalContext.current) + .components { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + + + Image( + painter = rememberAsyncImagePainter(gif.url, imageLoader), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(16.dp) + ) + } +} diff --git a/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt b/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt new file mode 100644 index 0000000..a942bb8 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt @@ -0,0 +1,56 @@ +package com.example.toptracer.helpers + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Query + +data class GiphyRandomResponse( + val data: GiphyData +) + +data class GiphyData( + val id: String, + val url: String, + val images: GiphyImages +) + +data class GiphyImages( + val fixed_height: GiphyImageDetails +) + +data class GiphyImageDetails( + val url: String +) + +interface GiphyApi { + @GET("v1/gifs/random") + suspend fun getRandomGif( + @Query("api_key") apiKey: String, + @Query("tag") tag: String, + @Query("rating") rating: String + ): GiphyRandomResponse +} + +fun createGiphyApiService(): GiphyApi { + return Retrofit.Builder() + .baseUrl("https://api.giphy.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(GiphyApi::class.java) +} + + +suspend fun fetchRandomGif(apiKey: String, tag: String = "", rating: String = "g"): GiphyImageDetails? { + val giphyApi = createGiphyApiService() + return try { + val response = withContext(Dispatchers.IO) { + giphyApi.getRandomGif(apiKey, tag, rating) + } + response.data.images.fixed_height + } catch (e: Exception) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt b/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt deleted file mode 100644 index 424b461..0000000 --- a/app/src/main/java/com/example/toptracer/ui/theme/TopTracerWelcomeActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.toptracer - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.example.toptracer.ui.theme.TopTracerTheme - -class TopTracerWelcomeActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - TopTracerTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - WelcomeScreen() - } - } - } - } -} - -@Composable -fun WelcomeScreen() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - "Welcome, toptracer!" - ) - Text( - "GifTitle by GifAuthor!" - ) - Text( - text = "Logout", - modifier = Modifier - .clickable {} - ) - } -} - - -@Preview -@Composable -fun WelcomeScreenPreview() { - WelcomeScreen() -} \ No newline at end of file From 20524fd834275621cc915ce84ea444e3db558377 Mon Sep 17 00:00:00 2001 From: Tsoi Anatoli Date: Sun, 9 Apr 2023 21:49:47 +0200 Subject: [PATCH 3/9] Implemented navigation and viewModel --- app/build.gradle | 1 + .../java/com/example/toptracer/Application.kt | 43 ++++++ .../com/example/toptracer/MainActivity.kt | 141 +---------------- .../java/com/example/toptracer/ViewModel.kt | 33 ++++ .../toptracer/helpers/GiphyApiService.kt | 2 + .../com/example/toptracer/ui/LoginScreen.kt | 144 ++++++++++++++++++ .../WelcomeScreen.kt} | 61 +++----- 7 files changed, 248 insertions(+), 177 deletions(-) create mode 100644 app/src/main/java/com/example/toptracer/Application.kt create mode 100644 app/src/main/java/com/example/toptracer/ViewModel.kt create mode 100644 app/src/main/java/com/example/toptracer/ui/LoginScreen.kt rename app/src/main/java/com/example/toptracer/{TopTracerWelcomeActivity.kt => ui/WelcomeScreen.kt} (56%) diff --git a/app/build.gradle b/app/build.gradle index 02c2fcf..11d4d9e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,6 +60,7 @@ dependencies { androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + implementation "androidx.navigation:navigation-compose:2.5.3" implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' diff --git a/app/src/main/java/com/example/toptracer/Application.kt b/app/src/main/java/com/example/toptracer/Application.kt new file mode 100644 index 0000000..b681f3e --- /dev/null +++ b/app/src/main/java/com/example/toptracer/Application.kt @@ -0,0 +1,43 @@ +package com.example.toptracer + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.toptracer.ui.WelcomeScreen + +enum class AppSection() { + Login, Welcome +} + +@Composable +fun Application(modifier: Modifier = Modifier) { + val navController = rememberNavController() + + Surface( + modifier.fillMaxWidth() + ) { + NavHost( + navController = navController, + startDestination = AppSection.Login.name + ) { + composable(route = AppSection.Login.name) { + LoginScreen( + onLoginSuccess = { + navController.navigate(AppSection.Welcome.name) + } + ) + } + composable(route = AppSection.Welcome.name) { + WelcomeScreen( + onLogoutClicked = { + navController.navigate(AppSection.Login.name) + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/MainActivity.kt b/app/src/main/java/com/example/toptracer/MainActivity.kt index 9513086..cab5735 100644 --- a/app/src/main/java/com/example/toptracer/MainActivity.kt +++ b/app/src/main/java/com/example/toptracer/MainActivity.kt @@ -22,147 +22,8 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { TopTracerTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - LoginScreen() - } + Application() } } } } - -@Composable -fun LoginScreen() { - var username by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var isPasswordValid by remember { mutableStateOf(false) } - var isUsernameEmpty by remember { mutableStateOf(true) } - var showDialog by remember { mutableStateOf(false) } - var dialogText by remember { mutableStateOf("") } - val context = LocalContext.current - val navigate = Intent(context, TopTracerWelcomeActivity::class.java) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - - UsernameRow(username = username) { usernameInput -> - username = usernameInput - isUsernameEmpty = usernameInput.isEmpty() - } - PasswordRow(password = password) { passwordInput -> - password = passwordInput - isPasswordValid = passwordInput == "password" - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - - ) { - Text( - text = "Forgot Password", - modifier = Modifier - .weight(1f) - .wrapContentSize(Alignment.Center) - .clickable {} - ) - - Text( - text = "Login", - modifier = Modifier - .weight(1f) - .wrapContentSize(Alignment.Center) - .clickable { - if (isUsernameEmpty) { - showDialog = true - dialogText = "It looks like you forgot to provide a username." - } else if (isPasswordValid) { - context.startActivity(navigate) - } else { - showDialog = true - dialogText = "The password you provided doesn't match our records." - } - } - ) - } - } - ValidationAlert(showDialog, dialogText) { - showDialog = false - } -} - -@Composable -fun UsernameRow(username: String, onValueChange: (String) -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Username", - modifier = Modifier.weight(1f) - ) - TextField( - value = username, - onValueChange = onValueChange, - colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), - modifier = Modifier.weight(3f), - ) - } -} - -@Composable -fun PasswordRow(password: String, onValueChange: (String) -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Password", - modifier = Modifier.weight(1f) - ) - TextField( - value = password, - onValueChange = onValueChange, - visualTransformation = TopTracerPasswordTransformation(), - singleLine = true, - colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), - modifier = Modifier - .weight(3f) - ) - } -} - -@Composable -fun ValidationAlert(showDialog: Boolean, dialogText: String, onDismiss: () -> Unit) { - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = "Oops!", - style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) - ) - }, - text = { Text(dialogText) }, - confirmButton = { - Button( - onClick = onDismiss, - content = { Text("OK") } - ) - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/ViewModel.kt b/app/src/main/java/com/example/toptracer/ViewModel.kt new file mode 100644 index 0000000..aa06876 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ViewModel.kt @@ -0,0 +1,33 @@ +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class LoginViewModel : ViewModel() { + private val _username = mutableStateOf("") + val username: State get() = _username + + private val _password = mutableStateOf("") + val password: State get() = _password + + fun onUsernameChanged(newUsername: String) { + _username.value = newUsername + } + + fun onPasswordChanged(newPassword: String) { + _password.value = newPassword + } + + fun login(onSuccess: () -> Unit, onError: (String) -> Unit) { + viewModelScope.launch { + if (_username.value.isEmpty()) { + onError("It looks like you forgot to provide a username.") + } else if (_password.value == "password") { + onSuccess() + } else { + onError("The password you provided doesn't match our records.") + } + } + } +} diff --git a/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt b/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt index a942bb8..8f9cf0f 100644 --- a/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt +++ b/app/src/main/java/com/example/toptracer/helpers/GiphyApiService.kt @@ -14,6 +14,8 @@ data class GiphyRandomResponse( data class GiphyData( val id: String, val url: String, + val title: String, + val username: String, val images: GiphyImages ) diff --git a/app/src/main/java/com/example/toptracer/ui/LoginScreen.kt b/app/src/main/java/com/example/toptracer/ui/LoginScreen.kt new file mode 100644 index 0000000..a0069a1 --- /dev/null +++ b/app/src/main/java/com/example/toptracer/ui/LoginScreen.kt @@ -0,0 +1,144 @@ +package com.example.toptracer + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.toptracer.helpers.TopTracerPasswordTransformation + +@Composable +fun LoginScreen( + onLoginSuccess: () -> Unit, + modifier: Modifier = Modifier +) { + var username by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var isPasswordValid by remember { mutableStateOf(false) } + var isUsernameEmpty by remember { mutableStateOf(true) } + var showDialog by remember { mutableStateOf(false) } + var dialogText by remember { mutableStateOf("") } + + Column( + modifier = modifier .padding(16.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + UsernameRow(username = username) { usernameInput -> + username = usernameInput + isUsernameEmpty = usernameInput.isEmpty() + } + PasswordRow(password = password) { passwordInput -> + password = passwordInput + isPasswordValid = passwordInput == "password" + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + + ) { + Text( + text = "Forgot Password", + modifier = Modifier + .weight(1f) + .wrapContentSize(Alignment.Center) + .clickable {} + ) + + Text( + text = "Login", + modifier = Modifier + .weight(1f) + .wrapContentSize(Alignment.Center) + .clickable { + if (isUsernameEmpty) { + showDialog = true + dialogText = "It looks like you forgot to provide a username." + } else if (isPasswordValid) { + onLoginSuccess() + } else { + showDialog = true + dialogText = "The password you provided doesn't match our records." + } + } + ) + } + } + ValidationAlert(showDialog, dialogText) { + showDialog = false + } +} + +@Composable +fun UsernameRow(username: String, onValueChange: (String) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Username", + modifier = Modifier.weight(1f) + ) + TextField( + value = username, + onValueChange = onValueChange, + colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), + modifier = Modifier.weight(3f), + ) + } +} + +@Composable +fun PasswordRow(password: String, onValueChange: (String) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Password", + modifier = Modifier.weight(1f) + ) + TextField( + value = password, + onValueChange = onValueChange, + visualTransformation = TopTracerPasswordTransformation(), + singleLine = true, + colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent), + modifier = Modifier + .weight(3f) + ) + } +} + +@Composable +fun ValidationAlert(showDialog: Boolean, dialogText: String, onDismiss: () -> Unit) { + if (showDialog) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = "Oops!", + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) + ) + }, + text = { Text(dialogText) }, + confirmButton = { + Button( + onClick = onDismiss, + content = { Text("OK") } + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt b/app/src/main/java/com/example/toptracer/ui/WelcomeScreen.kt similarity index 56% rename from app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt rename to app/src/main/java/com/example/toptracer/ui/WelcomeScreen.kt index 28692fd..e34b15e 100644 --- a/app/src/main/java/com/example/toptracer/TopTracerWelcomeActivity.kt +++ b/app/src/main/java/com/example/toptracer/ui/WelcomeScreen.kt @@ -1,21 +1,21 @@ -package com.example.toptracer +package com.example.toptracer.ui -import android.os.Build.VERSION.SDK_INT -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent +import android.os.Build import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.ImageLoader @@ -24,28 +24,14 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import com.example.toptracer.helpers.GiphyImageDetails import com.example.toptracer.helpers.fetchRandomGif -import com.example.toptracer.ui.theme.TopTracerTheme - -class TopTracerWelcomeActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - TopTracerTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - WelcomeScreen() - } - } - } - } -} @Composable -fun WelcomeScreen() { +fun WelcomeScreen( + modifier: Modifier = Modifier, + onLogoutClicked: () -> Unit +) { Column( + modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -60,24 +46,26 @@ fun WelcomeScreen() { Text( text = "Logout", modifier = Modifier - .clickable {} + .clickable { + onLogoutClicked() + } ) } } @Composable -fun RandomGif() { +fun RandomGif(modifier: Modifier = Modifier) { val apiKey = "vYbi41ARNKCZHJvrZ7IlDdqfCGSb8ZZy" - val imageDetails = remember { mutableStateOf(null) } + val imageDetails = rememberSaveable { mutableStateOf(null) } LaunchedEffect(apiKey) { imageDetails.value = fetchRandomGif(apiKey) } - imageDetails.value?.let {gif -> + imageDetails.value?.let { gif -> val imageLoader = ImageLoader.Builder(LocalContext.current) .components { - if (SDK_INT >= 28) { + if (Build.VERSION.SDK_INT >= 28) { add(ImageDecoderDecoder.Factory()) } else { add(GifDecoder.Factory()) @@ -89,10 +77,9 @@ fun RandomGif() { Image( painter = rememberAsyncImagePainter(gif.url, imageLoader), contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) - .padding(16.dp) + modifier = modifier + .size(200.dp) + .clip(RectangleShape) ) } } From b67699e03660c82b764549630d69babad32172cb Mon Sep 17 00:00:00 2001 From: Tsoi Anatoli Date: Mon, 10 Apr 2023 22:41:30 +0200 Subject: [PATCH 4/9] Implemented navigation and viewModel --- .idea/gradle.xml | 1 + app/build.gradle | 15 +++++- .../java/com/example/toptracer/Application.kt | 4 ++ .../com/example/toptracer/MainActivity.kt | 12 ----- .../com/example/toptracer/ui/LoginScreen.kt | 51 +++++++++---------- 5 files changed, 44 insertions(+), 39 deletions(-) diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a9f4e52..a2d7c21 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ +