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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,49 @@ 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
- name: Upload apk as artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: artifacts-bundle
path: app/build/outputs/apk/debug/*.apk
name: apk-bundle
path: |
app/build/outputs/apk/debug/*.apk
app/build/outputs/apk/release/*.apk
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.gradle
/.idea
/local.properties
/keystore.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
Expand Down
79 changes: 55 additions & 24 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import java.util.Properties
import java.io.FileInputStream
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
44 changes: 40 additions & 4 deletions app/src/main/java/com/github/zzorgg/beezle/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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") {
Expand Down
11 changes: 1 addition & 10 deletions app/src/main/java/com/github/zzorgg/beezle/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading