diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b99ee44..c329368 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,24 +28,41 @@ jobs: # Ref: https://github.com/actions/setup-java - uses: actions/setup-java@v5 with: - distribution: 'temurin' # See 'Supported distributions' for available options + distribution: 'oracle' java-version: '21' - name: Make gradlew executable run: chmod +x ./gradlew - - name: Setup Gradle Cache - uses: gradle/gradle-build-action@v2 - with: - gradle-home-cache-cleanup: true + # Ref: https://github.com/gradle/actions/blob/main/docs/setup-gradle.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 - name: Generate google-services.json run: | echo "${{ secrets.GOOGLE_SERVICES_JSON }}" | base64 --decode > app/google-services.json - - name: Build the debug apk + - name: Append API keys to local.properties + run: | + echo "" >> local.properties + echo "WEBSOCKET_URL=${{ secrets.WEBSOCKET_URL }}" >> local.properties + + - name: Generate keystore.properties run: | - ./gradlew assembleDebug + echo "${{ secrets.KEYSTORE_FILE }}" | base64 --decode > signing_key.jks + echo "KEYSTORE_FILE=$(realpath signing_key.jks)" >> keystore.properties + echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> keystore.properties + echo "KEYSTORE_PASS=${{ secrets.KEYSTORE_PASS }}" >> keystore.properties + echo "KEY_PASS=${{ secrets.KEY_PASS }}" >> keystore.properties + + - name: Build the debug apk + run: ./gradlew assembleDebug + continue-on-error: false + + - name: Build the release apk + if: github.event_name == 'workflow_dispatch' + run: ./gradlew assembleRelease + continue-on-error: false # Ref: https://github.com/actions/upload-artifact # Will only upload artifacts when manually called @@ -53,5 +70,7 @@ jobs: if: github.event_name == 'workflow_dispatch' uses: actions/upload-artifact@v4 with: - name: artifacts-bundle - path: app/build/outputs/apk/debug/*.apk \ No newline at end of file + name: apk-bundle + path: | + app/build/outputs/apk/debug/*.apk + app/build/outputs/apk/release/*.apk diff --git a/.gitignore b/.gitignore index 5086959..594a834 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .gradle /.idea /local.properties +/keystore.properties /.idea/caches /.idea/libraries /.idea/modules.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9664626..e2ada36 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,5 @@ -import java.util.Properties import java.io.FileInputStream +import java.util.Properties plugins { alias(libs.plugins.android.application) @@ -13,19 +13,52 @@ plugins { id("kotlin-kapt") } -// Load secrets from local.properties val localProperties = Properties() -val localPropertiesFile = rootProject.file("local.properties") -if (localPropertiesFile.exists()) { - localPropertiesFile.inputStream().use { stream: FileInputStream -> - localProperties.load(stream) +val localPropertiesFile = File(rootDir, "local.properties") +if (localPropertiesFile.exists() && localPropertiesFile.isFile) { + localPropertiesFile.inputStream().let { + localProperties.load(it) } +} else { + throw IllegalStateException( + "Missing configuration file: 'local.properties'.\n" + + "Please create this file in the project root and define required keys.n" + ) +} + +val localPropertiesRequiredKeys = listOf("WEBSOCKET_URL") + +val missingKeys = localPropertiesRequiredKeys.filterNot { localProperties.containsKey(it) } +if (missingKeys.isNotEmpty()) { + throw IllegalStateException( + "Missing required key(s) in local.properties: ${missingKeys.joinToString(", ")}" + ) } +// https://developer.android.com/studio/publish/app-signing#secure-shared-keystore +var keystoreProperties: Properties? = null +val keystorePropertiesFile = File(rootDir, "keystore.properties") +if (keystorePropertiesFile.exists() && keystorePropertiesFile.isFile) { + keystoreProperties = Properties() + keystoreProperties?.load(FileInputStream(keystorePropertiesFile)) +} + + android { namespace = "com.github.zzorgg.beezle" compileSdk = 36 + keystoreProperties?.let { keystore -> + signingConfigs { + create("beezle-config") { + keyAlias = keystore["KEY_ALIAS"] as String + keyPassword = keystore["KEY_PASS"] as String + storeFile = file(keystore["KEYSTORE_FILE"] as String) + storePassword = keystore["KEYSTORE_PASS"] as String + } + } + } + defaultConfig { applicationId = "com.github.zzorgg.beezle" minSdk = 26 @@ -34,11 +67,13 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - // Add secrets as BuildConfig fields - buildConfigField("String", "FIREBASE_DATABASE_URL", "\"${localProperties.getProperty("FIREBASE_DATABASE_URL", "")}\"") - buildConfigField("String", "FIREBASE_API_KEY", "\"${localProperties.getProperty("FIREBASE_API_KEY", "")}\"") - buildConfigField("String", "FIREBASE_PROJECT_ID", "\"${localProperties.getProperty("FIREBASE_PROJECT_ID", "")}\"") + localPropertiesRequiredKeys.onEach { + buildConfigField( + "String", + it, + "\"${localProperties[it] as String}\"" + ) + } } buildTypes { @@ -49,22 +84,18 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - // Read from local.properties - buildConfigField( - "String", - "WEBSOCKET_URL", - "\"${localProperties.getProperty("WEBSOCKET_URL", "wss://octopus-app-4x8aa.ondigitalocean.app/ws")}\"" - ) + isDebuggable = false + if (keystoreProperties != null) { + signingConfig = signingConfigs["beezle-config"] + } } debug { isMinifyEnabled = false isShrinkResources = false - // Read from local.properties - buildConfigField( - "String", - "WEBSOCKET_URL", - "\"${localProperties.getProperty("WEBSOCKET_URL", "wss://octopus-app-4x8aa.ondigitalocean.app/ws")}\"" - ) + // Having this commonly in defaultConfig doesn't work for debug somehow + if (keystoreProperties != null) { + signingConfig = signingConfigs["beezle-config"] + } } } compileOptions { @@ -147,7 +178,7 @@ dependencies { implementation(platform("com.google.firebase:firebase-bom:34.3.0")) implementation("com.google.firebase:firebase-auth") implementation("com.google.firebase:firebase-firestore") - implementation("com.google.firebase:firebase-database") // Add Firebase Realtime Database + implementation("com.google.firebase:firebase-database") // Also add the dependencies for the Credential Manager libraries and specify their versions implementation(libs.androidx.credentials) diff --git a/app/src/main/java/com/github/zzorgg/beezle/MainActivity.kt b/app/src/main/java/com/github/zzorgg/beezle/MainActivity.kt index 1c94ce2..f3ab151 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/MainActivity.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/MainActivity.kt @@ -4,6 +4,12 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel @@ -41,6 +47,28 @@ class MainActivity : ComponentActivity() { NavHost( navController = navController, startDestination = "splash", + enterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioLowBouncy, + ) + ) { it / 3 } + }, + exitTransition = { + slideOutHorizontally(animationSpec = tween()) { -it } + }, + popEnterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioLowBouncy, + ) + ) { -it / 3 } + }, + popExitTransition = { + slideOutHorizontally(animationSpec = tween()) { it } + } ) { composable("splash") { SplashScreen(onFinished = { @@ -84,7 +112,8 @@ class MainActivity : ComponentActivity() { ) } composable("duel/{mode}") { backStackEntry -> - val modeStr = backStackEntry.arguments?.getString("mode")?.uppercase() ?: "MATH" + val modeStr = + backStackEntry.arguments?.getString("mode")?.uppercase() ?: "MATH" val mode = when (modeStr) { "CS" -> DuelMode.CS "MATH" -> DuelMode.MATH @@ -98,13 +127,20 @@ class MainActivity : ComponentActivity() { ) } composable("practice/{subject}") { backStackEntry -> - val subject = backStackEntry.arguments?.getString("subject")?.uppercase() ?: "MATH" + val subject = + backStackEntry.arguments?.getString("subject")?.uppercase() ?: "MATH" val cat = if (subject == "CS") Category.CS else Category.MATH - DuelsPracticeScreenRoot(navController = navController, initialCategory = cat) + DuelsPracticeScreenRoot( + navController = navController, + initialCategory = cat + ) } // New: default practice route for bottom bar composable("practice") { - DuelsPracticeScreenRoot(navController = navController, initialCategory = Category.MATH) + DuelsPracticeScreenRoot( + navController = navController, + initialCategory = Category.MATH + ) } // New: leaderboards route for bottom bar composable("leaderboards") { diff --git a/app/src/main/java/com/github/zzorgg/beezle/di/AppModule.kt b/app/src/main/java/com/github/zzorgg/beezle/di/AppModule.kt index 94e29e8..969e6d5 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/di/AppModule.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/di/AppModule.kt @@ -49,16 +49,7 @@ class AppModule { @Provides @Singleton - fun provideFirebaseDatabase(): FirebaseDatabase { - // Read FIREBASE_DATABASE_URL from BuildConfig if present; otherwise fall back to default - val url: String? = try { - val field = com.github.zzorgg.beezle.BuildConfig::class.java.getField("FIREBASE_DATABASE_URL") - (field.get(null) as? String)?.trim()?.trimEnd('/') - } catch (t: Throwable) { - null - } - return if (url.isNullOrBlank()) FirebaseDatabase.getInstance() else FirebaseDatabase.getInstance(url) - } + fun provideFirebaseDatabase(): FirebaseDatabase = FirebaseDatabase.getInstance() @Provides @Singleton diff --git a/app/src/main/java/com/github/zzorgg/beezle/ui/components/AppBottomBar.kt b/app/src/main/java/com/github/zzorgg/beezle/ui/components/AppBottomBar.kt index 8954af9..ad76d3a 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/ui/components/AppBottomBar.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/ui/components/AppBottomBar.kt @@ -1,22 +1,31 @@ package com.github.zzorgg.beezle.ui.components +import android.view.HapticFeedbackConstants import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.EmojiEvents import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.School -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp @Composable @@ -27,22 +36,27 @@ private fun NavIcon( currentRoute: String, onNavigate: (String) -> Unit ) { + val view = LocalView.current val isSelected = currentRoute == route val bgColor by animateColorAsState( - if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) else MaterialTheme.colorScheme.surface.copy(alpha = 0f), + if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) else MaterialTheme.colorScheme.surface.copy( + alpha = 0f + ), label = "iconBg" ) val tint by animateColorAsState( if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, label = "iconTint" ) - Box( + IconButton( + onClick = { + view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + onNavigate(route) + }, modifier = Modifier .size(42.dp) .clip(CircleShape) .background(bgColor) - .clickable { onNavigate(route) }, - contentAlignment = Alignment.Center ) { Icon( imageVector = icon, @@ -58,33 +72,25 @@ fun AppBottomBar( currentRoute: String, onNavigate: (String) -> Unit, ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp) // further reduced bottom spacing + Card( + modifier = Modifier.clip(CircleShape), + shape = CircleShape, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.90f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp) ) { - Card( + Row( modifier = Modifier - .align(Alignment.Center) - .clip(CircleShape), - shape = CircleShape, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.90f) - ), - elevation = CardDefaults.cardElevation(defaultElevation = 10.dp) + .padding(horizontal = 14.dp, vertical = 8.dp) + .heightIn(min = 54.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .padding(horizontal = 14.dp, vertical = 8.dp) - .heightIn(min = 54.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - NavIcon("main", Icons.Default.Home, "Home", currentRoute, onNavigate) - NavIcon("profile", Icons.Default.Person, "Profile", currentRoute, onNavigate) - NavIcon("leaderboards", Icons.Default.EmojiEvents, "Leaderboards", currentRoute, onNavigate) - NavIcon("practice", Icons.Default.School, "Practice", currentRoute, onNavigate) - } + NavIcon("main", Icons.Default.Home, "Home", currentRoute, onNavigate) + NavIcon("profile", Icons.Default.Person, "Profile", currentRoute, onNavigate) + NavIcon("leaderboards", Icons.Default.EmojiEvents, "Leaderboards", currentRoute, onNavigate) + NavIcon("practice", Icons.Default.School, "Practice", currentRoute, onNavigate) } } } diff --git a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/duel/components/DuelsPracticeScreen.kt b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/duel/components/DuelsPracticeScreen.kt index c6a78b0..c8b12d9 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/duel/components/DuelsPracticeScreen.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/duel/components/DuelsPracticeScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -133,6 +135,8 @@ fun DuelsPracticeScreen( onNavigate: (String) -> Unit, modifier: Modifier = Modifier, ) { + val density = LocalDensity.current + Scaffold( topBar = { TopAppBar( @@ -159,9 +163,14 @@ fun DuelsPracticeScreen( } ) }, - bottomBar = { - AppBottomBar(currentRoute = "practice", onNavigate = onNavigate) - } + floatingActionButton = { AppBottomBar(currentRoute = "practice", onNavigate = onNavigate) }, + floatingActionButtonPosition = FabPosition.Center, + contentWindowInsets = WindowInsets( + top = WindowInsets.systemBars.getTop(density), + left = WindowInsets.systemBars.getLeft(density, LocalLayoutDirection.current), + right = WindowInsets.systemBars.getRight(density, LocalLayoutDirection.current), + bottom = WindowInsets.systemBars.getBottom(density) / 3 + ) ) { innerPadding -> Column( modifier = modifier diff --git a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/leaderboards/LeaderboardsScreen.kt b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/leaderboards/LeaderboardsScreen.kt index 38caabb..43d0661 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/leaderboards/LeaderboardsScreen.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/leaderboards/LeaderboardsScreen.kt @@ -5,6 +5,8 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -13,6 +15,8 @@ import com.github.zzorgg.beezle.ui.components.AppBottomBar @OptIn(ExperimentalMaterial3Api::class) @Composable fun LeaderboardsScreen(onNavigate: (String) -> Unit) { + val density = LocalDensity.current + Scaffold( topBar = { TopAppBar( @@ -22,9 +26,14 @@ fun LeaderboardsScreen(onNavigate: (String) -> Unit) { ) ) }, - bottomBar = { - AppBottomBar(currentRoute = "leaderboards", onNavigate = onNavigate) - } + floatingActionButton = { AppBottomBar(currentRoute = "leaderboards", onNavigate = onNavigate) }, + floatingActionButtonPosition = FabPosition.Center, + contentWindowInsets = WindowInsets( + top = WindowInsets.systemBars.getTop(density), + left = WindowInsets.systemBars.getLeft(density, LocalLayoutDirection.current), + right = WindowInsets.systemBars.getRight(density, LocalLayoutDirection.current), + bottom = WindowInsets.systemBars.getBottom(density) / 3 + ) ) { innerPadding -> Surface(modifier = Modifier.fillMaxSize().padding(innerPadding)) { Column( diff --git a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/main/MainAppScreen.kt b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/main/MainAppScreen.kt index 0bb672f..e78b938 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/main/MainAppScreen.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/main/MainAppScreen.kt @@ -2,39 +2,62 @@ package com.github.zzorgg.beezle.ui.screens.main import android.content.res.Configuration import android.os.Build -import android.view.HapticFeedbackConstants import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBalanceWallet -import androidx.compose.material.icons.filled.EmojiEvents import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.SportsMartialArts -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel import androidx.compose.material3.carousel.rememberCarouselState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.graphics.vector.ImageVector import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -43,7 +66,11 @@ import coil.compose.AsyncImage import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.request.ImageRequest -import com.airbnb.lottie.compose.* +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.github.zzorgg.beezle.R import com.github.zzorgg.beezle.data.wallet.SolanaWalletManager import com.github.zzorgg.beezle.data.wallet.WalletState import com.github.zzorgg.beezle.ui.components.AppBottomBar @@ -52,9 +79,8 @@ import com.github.zzorgg.beezle.ui.components.BannerVideoPlayer import com.github.zzorgg.beezle.ui.components.MonochromeAsyncImage import com.github.zzorgg.beezle.ui.screens.profile.ProfileViewModel import com.github.zzorgg.beezle.ui.screens.profile.components.LevelBadge -import com.google.firebase.auth.FirebaseAuth -import com.github.zzorgg.beezle.R import com.github.zzorgg.beezle.ui.theme.BeezleTheme +import com.google.firebase.auth.FirebaseAuth private enum class Subject { MATH, CS } @@ -84,17 +110,13 @@ fun MainAppScreenRoot( BannerMedia.AssetGif(R.drawable.maths_banner), BannerMedia.AssetGif(R.drawable.cs_banner), ) - val view = LocalView.current MainAppScreen( walletState = walletState, bannerItems = bannerItems, aggregatedLevel = aggregatedLevel, avatarUrl = FirebaseAuth.getInstance().currentUser?.photoUrl?.toString(), - navigateToCallback = { - view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) - navController.navigate(it) - } + navigateToCallback = { navController.navigate(it) } ) } @@ -113,6 +135,8 @@ fun MainAppScreen( // Remove preferredWidth shrink; use full width banners like duel card val pagerState = rememberPagerState(pageCount = { bannerItems.size }) + val density = LocalDensity.current + Scaffold( topBar = { TopAppBar( @@ -173,7 +197,11 @@ fun MainAppScreen( modifier = Modifier.size(16.dp) ) Spacer(Modifier.width(6.dp)) - Text("Wallet", color = MaterialTheme.colorScheme.tertiary, fontSize = 12.sp) + Text( + "Wallet", + color = MaterialTheme.colorScheme.tertiary, + fontSize = 12.sp + ) } } else { val chipBg = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f) @@ -192,15 +220,24 @@ fun MainAppScreen( modifier = Modifier.size(16.dp) ) Spacer(Modifier.width(6.dp)) - Text("Connect", color = MaterialTheme.colorScheme.primary, fontSize = 12.sp) + Text( + "Connect", + color = MaterialTheme.colorScheme.primary, + fontSize = 12.sp + ) } } } ) }, - bottomBar = { - AppBottomBar(currentRoute = "main", onNavigate = navigateToCallback) - } + floatingActionButton = { AppBottomBar( currentRoute = "main", onNavigate = navigateToCallback ) }, + floatingActionButtonPosition = FabPosition.Center, + contentWindowInsets = WindowInsets( + top = WindowInsets.systemBars.getTop(density), + left = WindowInsets.systemBars.getLeft(density, LocalLayoutDirection.current), + right = WindowInsets.systemBars.getRight(density, LocalLayoutDirection.current), + bottom = WindowInsets.systemBars.getBottom(density) / 3 + ) ) { innerPadding -> Column( modifier = modifier @@ -299,7 +336,8 @@ fun MainAppScreen( ) { Subject.entries.forEach { subject -> val selected = subject == selectedSubject - val baseColor = if (subject == Subject.MATH) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary + val baseColor = + if (subject == Subject.MATH) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary val bgColor by animateColorAsState( if (selected) baseColor.copy(alpha = 0.25f) else MaterialTheme.colorScheme.surfaceContainerLow, label = "subjectBg" @@ -358,33 +396,8 @@ fun MainAppScreen( ) } Spacer(modifier = Modifier.height(6.dp)) - Text("Real-time competitive play", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) - } - } - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { navigateToCallback("practice/${selectedSubject.name.lowercase()}") }, - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow) - ) { - Column(Modifier.padding(16.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Person, - contentDescription = null, - tint = if (selectedSubject == Subject.MATH) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(24.dp) - ) - Spacer(Modifier.width(8.dp)) - Text( - "${subjectLabels[selectedSubject]} Practice Mode", - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold - ) - } - Spacer(modifier = Modifier.height(6.dp)) Text( - "Single-player training & streaks", + "Real-time competitive play", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp ) diff --git a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/profile/ProfileScreenRoot.kt b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/profile/ProfileScreenRoot.kt index bbd45b2..c4f47fd 100644 --- a/app/src/main/java/com/github/zzorgg/beezle/ui/screens/profile/ProfileScreenRoot.kt +++ b/app/src/main/java/com/github/zzorgg/beezle/ui/screens/profile/ProfileScreenRoot.kt @@ -45,6 +45,8 @@ import kotlinx.coroutines.launch import android.content.ClipboardManager import android.content.ClipData import android.content.res.Configuration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import com.github.zzorgg.beezle.ui.components.AppBottomBar import com.solana.mobilewalletadapter.clientlib.ActivityResultSender @@ -171,6 +173,8 @@ fun ProfileScreen( onNavigate: (String) -> Unit, modifier: Modifier = Modifier, ) { + val density = LocalDensity.current + Scaffold( topBar = { TopAppBar( @@ -191,9 +195,14 @@ fun ProfileScreen( actions = {} ) }, - bottomBar = { - AppBottomBar(currentRoute = "profile", onNavigate = onNavigate) - }, + floatingActionButton = { AppBottomBar(currentRoute = "profile", onNavigate = onNavigate) }, + floatingActionButtonPosition = FabPosition.Center, + contentWindowInsets = WindowInsets( + top = WindowInsets.systemBars.getTop(density), + left = WindowInsets.systemBars.getLeft(density, LocalLayoutDirection.current), + right = WindowInsets.systemBars.getRight(density, LocalLayoutDirection.current), + bottom = WindowInsets.systemBars.getBottom(density) / 3 + ) ) { innerPadding -> Box( modifier = modifier