Initial commit

This commit is contained in:
2025-06-12 23:20:21 +02:00
parent 1656e706a0
commit 714cdb6795
122 changed files with 3335 additions and 916 deletions

View File

@ -0,0 +1,23 @@
package dev.adriankuta.flights
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import dev.adriankuta.flights.ui.home.navigation.HomeRoute
import dev.adriankuta.flights.ui.home.navigation.homeScreen
@Composable
fun FlightsNavGraph(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
) {
NavHost(
navController = navController,
startDestination = HomeRoute,
modifier = modifier,
) {
homeScreen()
}
}

View File

@ -0,0 +1,85 @@
package dev.adriankuta.flights
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.adriankuta.flights.ui.FlightsApp
import dev.adriankuta.flights.ui.designsystem.theme.FlightsTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
var isLoading: Boolean by mutableStateOf(true)
lifecycleScope.launch {
@Suppress("MagicNumber")
delay(500)
isLoading = false
}
splashScreen.setKeepOnScreenCondition {
isLoading
}
enableEdgeToEdge()
setContent {
val darkTheme = shouldUseDarkTheme()
// Update the edge to edge configuration to match the theme
// This is the same parameters as the default enableEdgeToEdge call, but we manually
// resolve whether or not to show dark theme using uiState, since it can be different
// than the configuration's dark theme value based on the user preference.
DisposableEffect(darkTheme) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
) { darkTheme },
navigationBarStyle = SystemBarStyle.auto(
lightScrim,
darkScrim,
) { darkTheme },
)
onDispose {}
}
FlightsTheme {
FlightsApp()
}
}
}
}
/**
* Returns `true` if dark theme should be used, as a function of the [uiState] and the
* current system context.
*/
@Composable
private fun shouldUseDarkTheme(): Boolean = isSystemInDarkTheme()
/**
* The default light scrim, as defined by androidx and the platform:
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
*/
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
/**
* The default dark scrim, as defined by androidx and the platform:
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
*/
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)

View File

@ -0,0 +1,25 @@
package dev.adriankuta.flights.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.adriankuta.flights.FlightsNavGraph
import dev.adriankuta.flights.ui.designsystem.theme.Elevation
@Composable
fun FlightsApp(
modifier: Modifier = Modifier,
) {
Surface(
tonalElevation = Elevation.Surface,
modifier = modifier,
) {
Scaffold(
snackbarHost = { InAppUpdates() },
) { paddingValues ->
FlightsNavGraph(Modifier.padding(paddingValues))
}
}
}

View File

@ -0,0 +1,93 @@
package dev.adriankuta.flights.ui
import androidx.activity.compose.LocalActivity
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallException
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.coroutines.suspendCoroutine
@Composable
fun InAppUpdates(
modifier: Modifier = Modifier,
@AppUpdateType updateType: Int = AppUpdateType.FLEXIBLE,
) {
val activity = LocalActivity.current ?: return
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val appUpdateManager = remember { AppUpdateManagerFactory.create(requireNotNull(activity)) }
val flexibleUpdateListener = remember {
InstallStateUpdatedListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
// After the update is downloaded, show a notification
// and request user confirmation to restart the app.
scope.launch {
val result = snackbarHostState.showSnackbar(
message = "An update has just been downloaded",
actionLabel = "Reload",
)
when (result) {
SnackbarResult.Dismissed -> Unit
SnackbarResult.ActionPerformed -> {
appUpdateManager.completeUpdate()
}
}
}
}
}
}
LaunchedEffect(Unit) {
try {
val appUpdateInfo = appUpdateManager.checkUpdateInfo()
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
appUpdateInfo.isUpdateTypeAllowed(updateType)
) {
appUpdateManager.startUpdateFlow(
appUpdateInfo,
activity,
AppUpdateOptions.newBuilder(updateType).build(),
)
}
} catch (e: InstallException) {
Timber.w(e)
}
}
DisposableEffect(appUpdateManager) {
appUpdateManager.registerListener(flexibleUpdateListener)
onDispose {
appUpdateManager.unregisterListener(flexibleUpdateListener)
}
}
SnackbarHost(
hostState = snackbarHostState,
modifier = modifier,
)
}
private suspend fun AppUpdateManager.checkUpdateInfo() =
suspendCoroutine<AppUpdateInfo> { continuation ->
appUpdateInfo.addOnSuccessListener {
continuation.resumeWith(Result.success(it))
}.addOnFailureListener {
continuation.resumeWith(Result.failure(it))
}
}