Skip to content
Draft
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
10 changes: 10 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ kotlin {
implementation(project(":features"))
implementation(compose.materialIconsExtended)
implementation(libs.kotlinx.serialization.core)
implementation(libs.chrisbanes.haze)
implementation(libs.chrisbanes.haze.materials)
}
}

Expand All @@ -20,6 +22,14 @@ kotlin {
implementation(libs.coroutines.swing)
}
}

val androidMain by getting {
dependencies {
implementation(libs.splash.screen.core)
implementation(libs.androidx.workmanager)
implementation(libs.koin.androidx.workmanager)
}
}
}
}
android {
Expand Down
2 changes: 1 addition & 1 deletion composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</queries>

<application
android:name="com.apimorlabs.reluct.MyApplication"
android:name="com.apimorlabs.reluct.ReluctApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,93 @@
package com.apimorlabs.reluct

import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.view.WindowManager.LayoutParams
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.apimorlabs.reluct.compose.ui.theme.Theme
import com.apimorlabs.reluct.features.settings.GetSettings
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Get Settings
val settings = get<GetSettings>()
// Enable support for Splash Screen API for
// proper Android 12+ support
installSplashScreen()

// Enable edge-to-edge experience and ProvideWindowInsets to the composable
enableEdgeToEdgeForTheme(themeValue = settings.getTheme())

// Fix MIUI issue
triggerBackgroundChangeIfNeeded()

setContent {
App()
App(settings = settings)
}
}

/**
* Function to enable edge to edge mode for Android
*/
private fun ComponentActivity.enableEdgeToEdgeForTheme(themeValue: Int) {
val style = when (themeValue) {
Theme.LIGHT_THEME.themeValue -> SystemBarStyle.light(
Color.TRANSPARENT,
Color.TRANSPARENT
)

Theme.DARK_THEME.themeValue -> SystemBarStyle.dark(Color.TRANSPARENT)
else -> SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)
}
enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
// Fix for three-button nav not properly going edge-to-edge.
// Issue: https://issuetracker.google.com/issues/298296168
window.setFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS, LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}

/**
* Function to tackle https://issuetracker.google.com/issues/227926002.
* This will trigger the compose NavHost to load the startDestination without having a user input.
*/
private fun triggerBackgroundChangeIfNeeded() {
if (isMiui()) {
lifecycleScope.launch {
delay(400)
window.setBackgroundDrawableResource(android.R.color.transparent)
}
}
}
}

@Preview
@Composable
fun AppAndroidPreview() {
App()
/**
* Function to determine if the device runs on MIUI by Xiaomi.
*/
@SuppressLint("PrivateApi")
private fun isMiui(): Boolean {
return try {
Class
.forName("android.os.SystemProperties")
.getMethod("get", String::class.java)
.let { propertyClass ->
propertyClass
.invoke(propertyClass, "ro.miui.ui.version.name")
?.toString()
?.isNotEmpty()
?: false
}
} catch (e: Exception) {
// e.printStackTrace()
println("Property not found: ${e.message}")
false
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.apimorlabs.reluct

import android.app.Application
import androidx.work.WorkManager
import com.apimorlabs.reluct.common.di.KoinMain
import com.apimorlabs.reluct.features.screenTime.services.ScreenTimeServices
import com.apimorlabs.reluct.features.screenTime.work.ResumeAppsWork
import com.apimorlabs.reluct.features.settings.GetSettings
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.logger.Level

class ReluctApplication : Application() {

override fun onCreate() {
super.onCreate()
KoinMain.init {
androidLogger(if (BuildConfig.DEBUG) Level.ERROR else Level.NONE)
androidContext(this@ReluctApplication)
workManagerFactory()
}

// Enable once Services have been fixed and Work Manager is set up
val scope = MainScope()
scope.launch {
val settings = get<GetSettings>()
if (settings.getOnBoardingShow()) {
val screenTimeServices = get<ScreenTimeServices>()
screenTimeServices.startLimitsService()
}
}.invokeOnCompletion { scope.cancel() }

// Setup Resume Apps worker
ResumeAppsWork.run {
WorkManager.getInstance(this@ReluctApplication).scheduleWork()
}
}
}
123 changes: 48 additions & 75 deletions composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt
Original file line number Diff line number Diff line change
@@ -1,90 +1,63 @@
package com.apimorlabs.reluct

import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.apimorlabs.reluct.common.sources.MyViewModel
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import com.apimorlabs.reluct.compose.ui.theme.ReluctAppTheme
import com.apimorlabs.reluct.compose.ui.theme.Theme
import com.apimorlabs.reluct.features.settings.GetSettings
import com.apimorlabs.reluct.navigation.destinations.SettingsCheck
import com.apimorlabs.reluct.navigation.navhost.AppNavHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.KoinContext
import org.koin.compose.viewmodel.koinViewModel
import org.koin.compose.koinInject

@OptIn(ExperimentalAnimationApi::class)
@Composable
@Preview
fun App() {
fun App(settings: GetSettings? = null) {
ReluctAppTheme {
KoinContext {
val controller = rememberNavController()
NavHost(
navController = controller,
startDestination = "home"
) {
composable(
route = "home",
enterTransition = { scaleInEnterTransition() },
exitTransition = { scaleOutExitTransition() },
popEnterTransition = { scaleInPopEnterTransition() },
popExitTransition = { scaleOutPopExitTransition() }
) {
val viewModel = koinViewModel<MyViewModel>()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = viewModel.getHelloWorldString()
)
Spacer(Modifier.padding(16.dp))
Button(onClick = { controller.navigate("details") }) {
Text(text = "Details")
}
Spacer(Modifier.padding(16.dp))
Button(onClick = { controller.navigate("charts") }) {
Text(text = "Charts")
}
}
}
}
val localSettings = settings ?: koinInject<GetSettings>()
ReluctMainCompose(localSettings)
}
}
}

composable(
route = "details",
enterTransition = { scaleInEnterTransition() },
exitTransition = { scaleOutExitTransition() },
popEnterTransition = { scaleInPopEnterTransition() },
popExitTransition = { scaleOutPopExitTransition() }
) {
DetailsScreen(
modifier = Modifier.fillMaxSize().padding(16.dp),
onGoBack = { controller.popBackStack() }
)
}
@Composable
private fun ReluctMainCompose(settings: GetSettings) {
// Theming Stuff
val themeValue by settings.theme.collectAsState(
Theme.FOLLOW_SYSTEM.themeValue,
Dispatchers.Main.immediate
)

composable(
route = "charts",
enterTransition = { scaleInEnterTransition() },
exitTransition = { scaleOutExitTransition() },
popEnterTransition = { scaleInPopEnterTransition() },
popExitTransition = { scaleOutPopExitTransition() }
) {
ChartsScreen(
onGoBack = { controller.popBackStack() }
)
}
}
}
// Settings for determining start destinations
val settingsCheck = produceState<SettingsCheck?>(initialValue = null) {
value = getSettingsCheck(settings)
}

ReluctAppTheme(theme = themeValue) {
AppNavHost(settingsCheck = settingsCheck)
}
}

/**
* Provide Settings check
*/
private suspend fun getSettingsCheck(settings: GetSettings): SettingsCheck =
withContext(Dispatchers.IO) {
val appVersionCode = 1 // ?: (BuildConfig.VERSION_CODE)
val loginSkipped = settings.loginSkipped.firstOrNull()
val onBoardingShown = settings.onBoardingShown.firstOrNull()
val savedVersionCode = settings.savedVersionCode.firstOrNull() ?: appVersionCode
settings.saveVersionCode(appVersionCode)
SettingsCheck(
isOnBoardingDone = onBoardingShown ?: false,
showChangeLog = appVersionCode > savedVersionCode,
loginSkipped = loginSkipped ?: false
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.apimorlabs.reluct.navigation

import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController

internal object NavHelpers {
fun getStringArgs(backStackEntry: NavBackStackEntry, key: String?): String? {
val arg = backStackEntry.arguments?.getString(key)
return if (arg == "null") null else arg
}

/** Useful when destination is launched from a deeplink and no backstack is present **/
// This is Android only
/*fun NavHostController.popBackStackOrExitActivity(activity: Activity) {
if (!popBackStack()) {
activity.finish()
}
}*/

/**
* Navigates Nav bar elements that can be the Top or Bottom Navbar while making sure only one
* destination instance is on the stack and pop unwanted destinations such that it always
* returns to start destination when you pop the current destination
* Make [startDestination] null if you don't want to have a start destination
*/
fun <T : Any, V : Any> NavHostController.navigateNavBarElements(route: T, startDestination: V?) {
navigate(route) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
navigate(route = route) {
val popDestination = startDestination ?: graph.findStartDestination().route
popDestination?.let { dest ->
popUpTo(dest) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
}
Loading