diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 94559f5..0e2d0fd 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -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) } } @@ -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 { diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 0015074..998241a 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -21,7 +21,7 @@ () + // 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 + } + } } diff --git a/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MyApplication.kt b/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MyApplication.kt deleted file mode 100644 index 7acda81..0000000 --- a/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MyApplication.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.apimorlabs.reluct - -import android.app.Application -import com.apimorlabs.reluct.common.di.KoinMain -import org.koin.android.ext.koin.androidContext - -class MyApplication : Application() { - - override fun onCreate() { - super.onCreate() - KoinMain.init { - androidContext(this@MyApplication) - } - } -} diff --git a/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/ReluctApplication.kt b/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/ReluctApplication.kt new file mode 100644 index 0000000..2d4d95d --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/ReluctApplication.kt @@ -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() + if (settings.getOnBoardingShow()) { + val screenTimeServices = get() + screenTimeServices.startLimitsService() + } + }.invokeOnCompletion { scope.cancel() } + + // Setup Resume Apps worker + ResumeAppsWork.run { + WorkManager.getInstance(this@ReluctApplication).scheduleWork() + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt index a51842d..61d40ae 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt @@ -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() - 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() + 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(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 + ) + } diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt new file mode 100644 index 0000000..3cef2a4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt @@ -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 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 + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/Transitions.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/Transitions.kt new file mode 100644 index 0000000..2856079 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/Transitions.kt @@ -0,0 +1,47 @@ +package com.apimorlabs.reluct.navigation + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut + +private const val AnimDurationLong = 500 +private const val AnimDurationShort = 300 + +// Enter transition when you navigate to a screen +@ExperimentalAnimationApi +fun scaleInEnterTransition() = scaleIn( + initialScale = .9f, + animationSpec = tween(AnimDurationLong) +) + fadeIn( + animationSpec = tween(AnimDurationShort) +) + +// Exit transition when you navigate to a screen +@ExperimentalAnimationApi +fun scaleOutExitTransition() = scaleOut( + targetScale = 1.1f, + animationSpec = tween(AnimDurationShort) +) + fadeOut( + animationSpec = tween(AnimDurationShort) +) + +// Enter transition of a screen when you pop to it +@ExperimentalAnimationApi +fun scaleInPopEnterTransition() = scaleIn( + initialScale = 1.1f, + animationSpec = tween(AnimDurationLong) +) + fadeIn( + animationSpec = tween(AnimDurationShort) +) + +// Exit transition of a screen you are popping from +@ExperimentalAnimationApi +fun scaleOutPopExitTransition() = scaleOut( + targetScale = .9f, + animationSpec = tween(AnimDurationShort) +) + fadeOut( + animationSpec = tween(AnimDurationShort) +) diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt new file mode 100644 index 0000000..bee9211 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt @@ -0,0 +1,9 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +data object DashboardOverviewDestination + +@Serializable +data object DashboardStatsDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt new file mode 100644 index 0000000..1686c0a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt @@ -0,0 +1,27 @@ +package com.apimorlabs.reluct.navigation.destinations + +import com.apimorlabs.reluct.common.models.util.AppURI +import kotlinx.serialization.Serializable + +@Serializable +data object InactiveGoalsDestination + +@Serializable +data object ActiveGoalsDestination + +@Serializable +data class AddEditGoalDestination(val goalId: String?, val defaultGoalIndex: Int? = -1) + +object AddEditGoalLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/add_edit_goal" + fun deepLink(goalId: String, defaultGoalIndex: Int = -1) = + "$DEEP_LINK?goalId=$goalId?defaultGoalIndex=$defaultGoalIndex" +} + +@Serializable +data class GoalDetailsDestination(val goalId: String?) + +object GoalDetailsLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/goals" + fun deepLink(goalId: String) = "$DEEP_LINK?goalId=$goalId" +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoardingDestination.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoardingDestination.kt new file mode 100644 index 0000000..819ebc5 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoardingDestination.kt @@ -0,0 +1,6 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +data object OnBoardingDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherGraphDestination.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherGraphDestination.kt new file mode 100644 index 0000000..d394bcf --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherGraphDestination.kt @@ -0,0 +1,15 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +data object OtherGraphDestination + +@Serializable +data object GoalDataGraphDestination + +@Serializable +data object TasksGraphDestination + +@Serializable +data object GoalsGraphDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt new file mode 100644 index 0000000..122823a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt @@ -0,0 +1,18 @@ +package com.apimorlabs.reluct.navigation.destinations + +import com.apimorlabs.reluct.common.models.util.AppURI +import kotlinx.serialization.Serializable + +@Serializable +object ScreenTimeStatsDestination + +@Serializable +object ScreenTimeLimitsDestination + +@Serializable +data class AppScreenTimeStatsDestination(val packageName: String?) + +object AppScreenTimeStatsLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/screen_time_stats" + fun deepLink(packageName: String) = "$DEEP_LINK?packageName=$packageName" +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt new file mode 100644 index 0000000..824c0a3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt @@ -0,0 +1,13 @@ +package com.apimorlabs.reluct.navigation.destinations + +data class SettingsCheck( + val isOnBoardingDone: Boolean, + val showChangeLog: Boolean, + // val accountCheck: AccountCheck?, + val loginSkipped: Boolean +) +/* +data class AccountCheck( + val isEmailVerified: Boolean, + val email: String +)*/ diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsDestination.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsDestination.kt new file mode 100644 index 0000000..2d3d196 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsDestination.kt @@ -0,0 +1,6 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +data object SettingsDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt new file mode 100644 index 0000000..2d1e155 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt @@ -0,0 +1,32 @@ +package com.apimorlabs.reluct.navigation.destinations + +import com.apimorlabs.reluct.common.models.util.AppURI +import kotlinx.serialization.Serializable + +@Serializable +data object TasksStatisticsDestination + +@Serializable +data class TaskDetailsDestination(val taskId: String?) + +@Serializable +data class AddEditTaskDestination(val taskId: String?) + +@Serializable +data object CompletedTasksDestination + +@Serializable +data object PendingTasksDestination + +@Serializable +data object SearchTasksDestination + +// Deep-links +object TaskDetailsLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/tasks" + fun deepLink(taskId: String) = "$DEEP_LINK?taskId=$taskId" +} + +object PendingTasksLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/pending_tasks" +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt new file mode 100644 index 0000000..e9b2ddd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt @@ -0,0 +1,30 @@ +package com.apimorlabs.reluct.navigation.navbar + +import kotlinx.serialization.Serializable + +@Serializable +object DashBoardDestinations + +@Serializable +object TasksDestinations + +@Serializable +object ScreenTimeDestinations + +@Serializable +object GoalsDestinations + +@Serializable +sealed class NavBarDestinations(val route: T) { + @Serializable + data object Dashboard : NavBarDestinations(DashBoardDestinations) + + @Serializable + data object Tasks : NavBarDestinations(TasksDestinations) + + @Serializable + data object ScreenTime : NavBarDestinations(ScreenTimeDestinations) + + @Serializable + data object Goals : NavBarDestinations(GoalsDestinations) +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarElevation.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarElevation.kt new file mode 100644 index 0000000..7a1970f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarElevation.kt @@ -0,0 +1,12 @@ +package com.apimorlabs.reluct.navigation.navbar + +import androidx.compose.ui.unit.dp + +internal object NavBarElevation { + val Level0 = 0.0.dp + val Level1 = 1.0.dp + val Level2 = 3.0.dp + val Level3 = 6.0.dp + val Level4 = 8.0.dp + val Level5 = 12.0.dp +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt new file mode 100644 index 0000000..6bfffa7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt @@ -0,0 +1,173 @@ +package com.apimorlabs.reluct.navigation.navbar + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.FactCheck +import androidx.compose.material.icons.automirrored.rounded.FactCheck +import androidx.compose.material.icons.outlined.Aod +import androidx.compose.material.icons.outlined.GridView +import androidx.compose.material.icons.outlined.TaskAlt +import androidx.compose.material.icons.rounded.Aod +import androidx.compose.material.icons.rounded.GridView +import androidx.compose.material.icons.rounded.TaskAlt +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.dashboard_destination_text +import com.apimorlabs.reluct.compose.ui.goals_destination_text +import com.apimorlabs.reluct.compose.ui.screen_time_destination_text +import com.apimorlabs.reluct.compose.ui.tasks_destination_text +import com.apimorlabs.reluct.navigation.NavHelpers.navigateNavBarElements +import kotlinx.collections.immutable.persistentListOf +import org.jetbrains.compose.resources.stringResource + +@Stable +private data class NavDestination( + val destination: NavBarDestinations, + val iconActive: ImageVector, + val iconInactive: ImageVector, + val label: String +) + +@Composable +fun ReluctBottomNavBar( + navController: NavHostController, + startDestination: NavBarDestinations, + modifier: Modifier = Modifier, +) { + // Title Texts + val dashboardTitle = stringResource(Res.string.dashboard_destination_text) + val tasksTitle = stringResource(Res.string.tasks_destination_text) + val screenTimeTitle = stringResource(Res.string.screen_time_destination_text) + val goalsTitle = stringResource(Res.string.goals_destination_text) + + val items = remember { + persistentListOf( + NavDestination( + destination = NavBarDestinations.Dashboard, + iconActive = Icons.Rounded.GridView, + iconInactive = Icons.Outlined.GridView, + label = dashboardTitle + ), + NavDestination( + destination = NavBarDestinations.Tasks, + iconActive = Icons.AutoMirrored.Rounded.FactCheck, + iconInactive = Icons.AutoMirrored.Outlined.FactCheck, + label = tasksTitle + ), + NavDestination( + destination = NavBarDestinations.ScreenTime, + iconActive = Icons.Rounded.Aod, + iconInactive = Icons.Outlined.Aod, + label = screenTimeTitle + ), + NavDestination( + destination = NavBarDestinations.Goals, + iconActive = Icons.Rounded.TaskAlt, + iconInactive = Icons.Outlined.TaskAlt, + label = goalsTitle + ) + ) + } + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + FloatingNavigationBar( + modifier = modifier.navigationBarsPadding(), + containerColor = Color.Transparent + ) { + items.forEach { item -> + val selected = currentDestination?.hierarchy?.any { + it.route == item.destination.route::class.qualifiedName + } ?: false + + NavigationBarItem( + label = { + Text( + text = item.label, + style = MaterialTheme.typography.labelMedium + ) + }, + selected = selected, + // colors = , + icon = { + Icon( + imageVector = if (selected) item.iconActive else item.iconInactive, + contentDescription = item.label + ) + }, + onClick = { + navController.navigateNavBarElements( + route = item.destination.route, + startDestination.route + ) + } + ) + } + } +} + +@Composable +fun FloatingNavigationBar( + modifier: Modifier = Modifier, + shape: Shape = MaterialTheme.shapes.extraLarge, + containerColor: Color = NavigationBarDefaults.containerColor, + contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), + tonalElevation: Dp = NavigationBarDefaults.Elevation, + content: @Composable RowScope.() -> Unit, +) { + Surface( + color = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + shape = shape, + border = BorderStroke( + width = 0.5.dp, + brush = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surfaceVariant, + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + ), + ), + ), + modifier = modifier, + ) { + Row( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + .height(80.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt new file mode 100644 index 0000000..a9a193f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt @@ -0,0 +1,268 @@ +package com.apimorlabs.reluct.navigation.navhost + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation.NavDeepLink +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.apimorlabs.reluct.compose.ui.components.animations.slideInVerticallyFadeReversed +import com.apimorlabs.reluct.compose.ui.components.animations.slideOutVerticallyFadeReversed +import com.apimorlabs.reluct.compose.ui.theme.Dimens +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.compose.ui.util.rememberBarVisibility +import com.apimorlabs.reluct.navigation.destinations.OnBoardingDestination +import com.apimorlabs.reluct.navigation.destinations.PendingTasksLink +import com.apimorlabs.reluct.navigation.destinations.SettingsCheck +import com.apimorlabs.reluct.navigation.destinations.SettingsDestination +import com.apimorlabs.reluct.navigation.navbar.DashBoardDestinations +import com.apimorlabs.reluct.navigation.navbar.GoalsDestinations +import com.apimorlabs.reluct.navigation.navbar.NavBarDestinations +import com.apimorlabs.reluct.navigation.navbar.ReluctBottomNavBar +import com.apimorlabs.reluct.navigation.navbar.ScreenTimeDestinations +import com.apimorlabs.reluct.navigation.navbar.TasksDestinations +import com.apimorlabs.reluct.navigation.navhost.graphs.DashboardNavHost +import com.apimorlabs.reluct.navigation.navhost.graphs.GoalsNavHost +import com.apimorlabs.reluct.navigation.navhost.graphs.ScreenTimeNavHost +import com.apimorlabs.reluct.navigation.navhost.graphs.TasksNavHost +import com.apimorlabs.reluct.navigation.navhost.graphs.otherScreenNavGraph +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.screens.onboarding.OnBoardingScreen +import com.apimorlabs.reluct.screens.settings.SettingsScreen +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials + +// Route used specifically for checking if the On Boarding flow was shown and User Signed In +private const val CHECKING_ROUTE = "dummy_checking_route" + +@OptIn(ExperimentalAnimationApi::class, ExperimentalHazeMaterialsApi::class) +@Composable +fun AppNavHost(settingsCheck: State, modifier: Modifier = Modifier) { + val navController = rememberNavController() + val barsVisibility = rememberBarVisibility(defaultBottomBar = false) + + val mainPadding = PaddingValues( + bottom = Dimens.ExtraLargePadding.size + Dimens.MediumPadding.size + + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + ) + + // Haze blur effect + val hazeState = remember { HazeState() } + + /** + * Don't use the bottomBar param of Scaffold for the AppBottomBar + * It will cause the whole screen to recompose when you hide/show the bottomBar + * with [BarsVisibility] since the innerPadding changes during this transition + * The AppBottomBar is provided to Box inside of this Scaffold and the padding needed by inner + * items is mainPadding from above + */ + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + NavHost( + modifier = Modifier, + navController = navController, + startDestination = CHECKING_ROUTE, + route = "root" + ) { + // Checking Route + composable( + route = CHECKING_ROUTE, + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() }, + popEnterTransition = { fadeIn() }, + popExitTransition = { fadeOut() } + ) { + HandleRouteChecks(settingsCheck, navController) + + Box( + modifier = Modifier + .statusBarsPadding() + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // OnBoarding + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + OnBoardingScreen() + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // Dashboard + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + DashboardNavHost( + mainNavController = navController, + barsVisibility = barsVisibility, + mainScaffoldPadding = mainPadding + ) + } + + // Tasks + composable( + deepLinks = listOf(NavDeepLink(PendingTasksLink.DEEP_LINK)), + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + TasksNavHost( + mainNavController = navController, + mainScaffoldPadding = mainPadding, + barsVisibility = barsVisibility + ) + } + + // Screen Time + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + ScreenTimeNavHost( + mainNavController = navController, + barsVisibility = barsVisibility, + mainScaffoldPadding = mainPadding + ) + } + + // Goals + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + GoalsNavHost( + mainNavController = navController, + barsVisibility = barsVisibility, + mainScaffoldPadding = mainPadding + ) + } + + // Settings + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + barsVisibility.bottomBar.hide() + + SettingsScreen( + onNavigateToManageAcc = { }, + goBack = navController::popBackStack + ) + } + + // All Other screens that don't share Scaffolds + otherScreenNavGraph( + navController = navController, + barsVisibility = barsVisibility + ) + } + + // Bottom Bar + AppBottomBar( + modifier = Modifier.hazeChild( + state = hazeState, + style = HazeMaterials.regular() + ), + navController = navController, + barsVisibility = barsVisibility + ) + } + } +} + +@Composable +private fun BoxScope.AppBottomBar( + navController: NavHostController, + barsVisibility: BarsVisibility, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + modifier = modifier.align(Alignment.BottomCenter), + visible = barsVisibility.bottomBar.isVisible, + enter = slideInVerticallyFadeReversed(), + exit = slideOutVerticallyFadeReversed() + ) { + ReluctBottomNavBar( + navController = navController, + startDestination = NavBarDestinations.Dashboard + ) + } +} + +@Composable +private fun HandleRouteChecks( + settingsCheck: State, + navController: NavHostController +) { + LaunchedEffect(settingsCheck.value) { + settingsCheck.value?.let { check -> + if (check.isOnBoardingDone && (!check.showChangeLog || check.loginSkipped)) { + // Everything is ok. Go to Dashboard + navController.navigate(DashBoardDestinations) { + popUpTo(CHECKING_ROUTE) { inclusive = true } + } + } else if (!check.isOnBoardingDone) { + // Show On boarding flow + navController.navigate(OnBoardingDestination) { + popUpTo(CHECKING_ROUTE) { inclusive = true } + } + } else { + // Show Changelog Screen + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/DashboardNavHost.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/DashboardNavHost.kt new file mode 100644 index 0000000..dc040e9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/DashboardNavHost.kt @@ -0,0 +1,205 @@ +package com.apimorlabs.reluct.navigation.navhost.graphs + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.topBar.ReluctPageHeading +import com.apimorlabs.reluct.compose.ui.dashboard_destination_text +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.navigation.NavHelpers.navigateNavBarElements +import com.apimorlabs.reluct.navigation.destinations.AppScreenTimeStatsDestination +import com.apimorlabs.reluct.navigation.destinations.DashboardOverviewDestination +import com.apimorlabs.reluct.navigation.destinations.DashboardStatsDestination +import com.apimorlabs.reluct.navigation.destinations.GoalDetailsDestination +import com.apimorlabs.reluct.navigation.destinations.SettingsDestination +import com.apimorlabs.reluct.navigation.destinations.TaskDetailsDestination +import com.apimorlabs.reluct.navigation.navbar.NavBarDestinations +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.navigation.toptabs.TabBar +import com.apimorlabs.reluct.navigation.toptabs.TabDestinations +import com.apimorlabs.reluct.navigation.toptabs.getCurrentTab +import com.apimorlabs.reluct.navigation.toptabs.rememberDashboardTabs +import com.apimorlabs.reluct.screens.dashboard.overview.DashboardOverviewScreen +import com.apimorlabs.reluct.screens.dashboard.statistics.DashboardStatsScreen +import kotlinx.collections.immutable.ImmutableList +import org.jetbrains.compose.resources.stringResource + +@ExperimentalAnimationApi +@Composable +internal fun DashboardNavHost( + mainNavController: NavHostController, + barsVisibility: BarsVisibility, + mainScaffoldPadding: PaddingValues, + navController: NavHostController = rememberNavController(), +) { + val destinations = rememberDashboardTabs() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val tabPage = remember(currentDestination) { + derivedStateOf { + getCurrentTab(currentDestination, destinations) + } + } + val startDestination by remember(destinations) { + derivedStateOf { destinations.first().destination.route } + } + + Scaffold( + topBar = { + DashboardScreenTopBar( + tabPage = tabPage, + tabs = destinations, + // profilePicUrl = "https://pbs.twimg.com/profile_images/1451052243067805698/LIEt076e_400x400.jpg", + updateTabPage = { + navController.navigateNavBarElements(it.route, startDestination) + }, + onSettingsClicked = { + mainNavController.navigate(SettingsDestination) + } + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { innerPadding -> + NavHost( + modifier = Modifier.padding(innerPadding), + navController = navController, + startDestination = startDestination + ) { + // Overview + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + DashboardOverviewScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToScreenTime = { + mainNavController.navigateNavBarElements( + route = NavBarDestinations.ScreenTime.route, + startDestination = null + ) + }, + onNavigateToTaskDetails = { + mainNavController.navigate(TaskDetailsDestination(it)) + }, + onNavigateToGoalDetails = { goalId: String? -> + mainNavController.navigate(GoalDetailsDestination(goalId)) + } + ) + } + + // Dashboard Stats + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + DashboardStatsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAppUsageInfo = { packageName -> + mainNavController.navigate(AppScreenTimeStatsDestination(packageName)) + }, + onNavigateToScreenTimeStats = { + mainNavController.navigateNavBarElements( + route = NavBarDestinations.ScreenTime.route, + startDestination = null + ) + } + ) + } + } + } +} + +@Composable +private fun DashboardScreenTopBar( + tabPage: State>, + tabs: ImmutableList>, + // profilePicUrl: String?, + updateTabPage: (TabDestinations) -> Unit, + onSettingsClicked: () -> Unit +) { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .statusBarsPadding() + .fillMaxWidth(), + verticalArrangement = Arrangement + .spacedBy(16.dp) + ) { + ReluctPageHeading( + modifier = Modifier.padding(horizontal = 16.dp), + title = stringResource(Res.string.dashboard_destination_text), + extraItems = { + IconButton( + modifier = Modifier.background( + MaterialTheme.colorScheme.surfaceVariant, + CircleShape + ), + onClick = onSettingsClicked, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = "Settings" + ) + } + /*ProfilePicture( + modifier = Modifier,//.padding(4.dp), + pictureUrl = profilePicUrl, + size = 36.dp + )*/ + } + ) + + LazyRow { + item { + TabBar( + modifier = Modifier.padding(horizontal = 16.dp), + selectedTabPage = tabPage, + tabs = tabs, + onTabSelected = { updateTabPage(it) } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/GoalsNavHost.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/GoalsNavHost.kt new file mode 100644 index 0000000..de37058 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/GoalsNavHost.kt @@ -0,0 +1,126 @@ +package com.apimorlabs.reluct.navigation.navhost.graphs + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.goals_destination_text +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.navigation.NavHelpers.navigateNavBarElements +import com.apimorlabs.reluct.navigation.destinations.ActiveGoalsDestination +import com.apimorlabs.reluct.navigation.destinations.AddEditGoalDestination +import com.apimorlabs.reluct.navigation.destinations.GoalDetailsDestination +import com.apimorlabs.reluct.navigation.destinations.InactiveGoalsDestination +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.navigation.toptabs.TopTabBar +import com.apimorlabs.reluct.navigation.toptabs.getCurrentTab +import com.apimorlabs.reluct.navigation.toptabs.rememberGoalsTabs +import com.apimorlabs.reluct.screens.goals.active.ActiveGoalsScreen +import com.apimorlabs.reluct.screens.goals.inactive.InactiveGoalsScreen +import org.jetbrains.compose.resources.stringResource + +@ExperimentalAnimationApi +@Composable +internal fun GoalsNavHost( + mainNavController: NavHostController, + barsVisibility: BarsVisibility, + mainScaffoldPadding: PaddingValues, + navController: NavHostController = rememberNavController(), +) { + val destinations = rememberGoalsTabs() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val tabPage = remember(currentDestination) { + derivedStateOf { + getCurrentTab(currentDestination, destinations) + } + } + val startDestination by remember(destinations) { + derivedStateOf { destinations.first().destination.route } + } + + Scaffold( + topBar = { + TopTabBar( + title = stringResource(Res.string.goals_destination_text), + tabPage = tabPage, + tabs = destinations, + updateTabPage = { + navController.navigateNavBarElements(it.route, startDestination) + } + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { innerPadding -> + NavHost( + modifier = Modifier.padding(innerPadding), + navController = navController, + startDestination = startDestination + ) { + // Active + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + ActiveGoalsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAddGoal = { defaultGoalIndex: Int? -> + mainNavController.navigate( + AddEditGoalDestination( + goalId = null, + defaultGoalIndex = defaultGoalIndex + ) + ) + }, + onNavigateToGoalDetails = { goalId: String? -> + mainNavController.navigate(GoalDetailsDestination(goalId)) + } + ) + } + + // Inactive + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + InactiveGoalsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAddGoal = { defaultGoalIndex: Int? -> + mainNavController.navigate( + AddEditGoalDestination( + goalId = null, + defaultGoalIndex = defaultGoalIndex + ) + ) + }, + onNavigateToGoalDetails = { goalId: String? -> + mainNavController.navigate(GoalDetailsDestination(goalId)) + } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/OtherScreenNavGraph.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/OtherScreenNavGraph.kt new file mode 100644 index 0000000..5f9d414 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/OtherScreenNavGraph.kt @@ -0,0 +1,159 @@ +package com.apimorlabs.reluct.navigation.navhost.graphs + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.SideEffect +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import androidx.navigation.toRoute +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.navigation.destinations.AddEditGoalDestination +import com.apimorlabs.reluct.navigation.destinations.AddEditGoalLink +import com.apimorlabs.reluct.navigation.destinations.AddEditTaskDestination +import com.apimorlabs.reluct.navigation.destinations.AppScreenTimeStatsDestination +import com.apimorlabs.reluct.navigation.destinations.AppScreenTimeStatsLink +import com.apimorlabs.reluct.navigation.destinations.GoalDetailsDestination +import com.apimorlabs.reluct.navigation.destinations.GoalDetailsLink +import com.apimorlabs.reluct.navigation.destinations.OtherGraphDestination +import com.apimorlabs.reluct.navigation.destinations.SearchTasksDestination +import com.apimorlabs.reluct.navigation.destinations.TaskDetailsDestination +import com.apimorlabs.reluct.navigation.destinations.TaskDetailsLink +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.screens.goals.addEdit.AddEditGoalScreen +import com.apimorlabs.reluct.screens.goals.details.GoalDetailsScreen +import com.apimorlabs.reluct.screens.screentime.appStatsDetails.AppScreenTimeStatsScreen +import com.apimorlabs.reluct.screens.tasks.addEdit.AddEditTaskScreen +import com.apimorlabs.reluct.screens.tasks.details.TaskDetailsScreen +import com.apimorlabs.reluct.screens.tasks.search.TasksSearchScreen + +@ExperimentalAnimationApi +fun NavGraphBuilder.otherScreenNavGraph( + navController: NavHostController, + barsVisibility: BarsVisibility +) { + navigation( + startDestination = TaskDetailsDestination(null) + ) { + // Task Details + composable( + deepLinks = listOf(NavDeepLink(TaskDetailsLink.DEEP_LINK)), + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { navBackStackEntry -> + + val detailDest = navBackStackEntry.toRoute() + TaskDetailsScreen( + taskId = detailDest.taskId, + onNavigateToEditTask = { + navController.navigate( + AddEditTaskDestination(it) + ) + }, + onBackClicked = { navController.popBackStack() } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // Add Task + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { navBackStackEntry -> + // Its safe to cast since this composable will always be inside an activity + val addEditTaskDest = navBackStackEntry.toRoute() + AddEditTaskScreen( + taskId = addEditTaskDest.taskId, + onBackClicked = { navController.popBackStack() } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // Task Search + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + TasksSearchScreen( + onNavigateToTaskDetails = { + navController.navigate(TaskDetailsDestination(it)) + }, + onBackClicked = { navController.popBackStack() } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // Goal Details + composable( + deepLinks = listOf(NavDeepLink(GoalDetailsLink.DEEP_LINK)), + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { backStackEntry -> + + val goalDest = backStackEntry.toRoute() + GoalDetailsScreen( + goalId = goalDest.goalId, + onExit = { navController.popBackStack() }, + onNavigateToEditGoal = { goalId -> + navController.navigate( + AddEditGoalDestination(goalId) + ) + } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // Add or Edit Goal + composable( + deepLinks = listOf(NavDeepLink(AddEditGoalLink.DEEP_LINK)), + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { backStackEntry -> + + val goalIDest = backStackEntry.toRoute() + AddEditGoalScreen( + goalId = goalIDest.goalId, + defaultGoalIndex = goalIDest.defaultGoalIndex, + onExit = { navController.popBackStack() } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + + // App Screen Time Stats + composable( + deepLinks = listOf(NavDeepLink(AppScreenTimeStatsLink.DEEP_LINK)), + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { backStackEntry -> + + val stats = backStackEntry.toRoute() + AppScreenTimeStatsScreen( + packageName = stats.packageName ?: "", + onBackClicked = { navController.popBackStack() } + ) + + SideEffect { barsVisibility.bottomBar.hide() } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/ScreenTimeNavHost.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/ScreenTimeNavHost.kt new file mode 100644 index 0000000..2890567 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/ScreenTimeNavHost.kt @@ -0,0 +1,108 @@ +package com.apimorlabs.reluct.navigation.navhost.graphs + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.screen_time_destination_text +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.navigation.NavHelpers.navigateNavBarElements +import com.apimorlabs.reluct.navigation.destinations.AppScreenTimeStatsDestination +import com.apimorlabs.reluct.navigation.destinations.ScreenTimeLimitsDestination +import com.apimorlabs.reluct.navigation.destinations.ScreenTimeStatsDestination +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.navigation.toptabs.TopTabBar +import com.apimorlabs.reluct.navigation.toptabs.rememberScreenTimeTabs +import com.apimorlabs.reluct.screens.screentime.limits.ScreenTimeLimitsScreen +import com.apimorlabs.reluct.screens.screentime.statistics.ScreenTimeStatisticsScreen +import org.jetbrains.compose.resources.stringResource + +@ExperimentalAnimationApi +@Composable +internal fun ScreenTimeNavHost( + mainNavController: NavHostController, + barsVisibility: BarsVisibility, + mainScaffoldPadding: PaddingValues, + navController: NavHostController = rememberNavController(), +) { + val destinations = rememberScreenTimeTabs() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val tabPage = remember(currentDestination) { + derivedStateOf { + com.apimorlabs.reluct.navigation.toptabs.getCurrentTab(currentDestination, destinations) + } + } + val startDestination by remember(destinations) { + derivedStateOf { destinations.first().destination.route } + } + + Scaffold( + topBar = { + TopTabBar( + title = stringResource(Res.string.screen_time_destination_text), + tabPage = tabPage, + tabs = destinations, + updateTabPage = { + navController.navigateNavBarElements(it.route, startDestination) + } + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { innerPadding -> + NavHost( + modifier = Modifier.padding(innerPadding), + navController = navController, + startDestination = ScreenTimeStatsDestination + ) { + // Statistics + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + ScreenTimeStatisticsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAppUsageInfo = { packageName -> + mainNavController.navigate(AppScreenTimeStatsDestination(packageName)) + } + ) + } + + // Limits + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + ScreenTimeLimitsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAppUsageInfo = { packageName -> + mainNavController.navigate(AppScreenTimeStatsDestination(packageName)) + } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/TasksNavHost.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/TasksNavHost.kt new file mode 100644 index 0000000..302a96d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/TasksNavHost.kt @@ -0,0 +1,174 @@ +package com.apimorlabs.reluct.navigation.navhost.graphs + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.textFields.search.PlaceholderMaterialSearchBar +import com.apimorlabs.reluct.compose.ui.search_tasks_hint_text +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.navigation.NavHelpers.navigateNavBarElements +import com.apimorlabs.reluct.navigation.destinations.AddEditTaskDestination +import com.apimorlabs.reluct.navigation.destinations.CompletedTasksDestination +import com.apimorlabs.reluct.navigation.destinations.PendingTasksDestination +import com.apimorlabs.reluct.navigation.destinations.SearchTasksDestination +import com.apimorlabs.reluct.navigation.destinations.TaskDetailsDestination +import com.apimorlabs.reluct.navigation.destinations.TasksStatisticsDestination +import com.apimorlabs.reluct.navigation.scaleInEnterTransition +import com.apimorlabs.reluct.navigation.scaleInPopEnterTransition +import com.apimorlabs.reluct.navigation.scaleOutExitTransition +import com.apimorlabs.reluct.navigation.scaleOutPopExitTransition +import com.apimorlabs.reluct.navigation.toptabs.TabBar +import com.apimorlabs.reluct.navigation.toptabs.TabDestinations +import com.apimorlabs.reluct.navigation.toptabs.rememberTasksTabs +import com.apimorlabs.reluct.screens.tasks.completed.CompletedTasksScreen +import com.apimorlabs.reluct.screens.tasks.pending.PendingTasksScreen +import com.apimorlabs.reluct.screens.tasks.statistics.TasksStatisticsScreen +import kotlinx.collections.immutable.ImmutableList +import org.jetbrains.compose.resources.stringResource + +@ExperimentalAnimationApi +@Composable +internal fun TasksNavHost( + mainNavController: NavHostController, + barsVisibility: BarsVisibility, + mainScaffoldPadding: PaddingValues, + navController: NavHostController = rememberNavController(), +) { + val destinations = rememberTasksTabs() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + val tabPage = remember(currentDestination) { + derivedStateOf { + com.apimorlabs.reluct.navigation.toptabs.getCurrentTab(currentDestination, destinations) + } + } + val startDestination by remember(destinations) { + derivedStateOf { destinations.first().destination.route } + } + Scaffold( + topBar = { + TasksScreenTopBar( + tabPage = tabPage, + tabs = destinations, + navigateToSearch = { mainNavController.navigate(SearchTasksDestination) }, + updateTabPage = { + navController.navigateNavBarElements(it.route, startDestination) + }, + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { innerPapping -> + NavHost( + modifier = Modifier.padding(innerPapping), + navController = navController, + startDestination = PendingTasksDestination + ) { + // Tasks + composable( + // Transition animations + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + PendingTasksScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAddTask = { + mainNavController.navigate(AddEditTaskDestination(it)) + }, + onNavigateToTaskDetails = { + mainNavController.navigate(TaskDetailsDestination(it)) + } + ) + } + + // Done - Completed Tasks + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + CompletedTasksScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToAddTask = { + mainNavController.navigate(AddEditTaskDestination(it)) + }, + onNavigateToTaskDetails = { + mainNavController.navigate(TaskDetailsDestination(it)) + } + ) + } + // Statistics + composable( + enterTransition = { scaleInEnterTransition() }, + exitTransition = { scaleOutExitTransition() }, + popEnterTransition = { scaleInPopEnterTransition() }, + popExitTransition = { scaleOutPopExitTransition() } + ) { + TasksStatisticsScreen( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + onNavigateToTaskDetails = { + mainNavController.navigate(TaskDetailsDestination(it)) + } + ) + } + } + } +} + +@Composable +private fun TasksScreenTopBar( + tabPage: State>, + tabs: ImmutableList>, + navigateToSearch: () -> Unit, + updateTabPage: (TabDestinations) -> Unit, +) { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .statusBarsPadding() + .fillMaxWidth(), + verticalArrangement = Arrangement + .spacedBy(16.dp) + ) { + PlaceholderMaterialSearchBar( + modifier = Modifier.padding(horizontal = 16.dp), + hint = stringResource(Res.string.search_tasks_hint_text), + onClick = navigateToSearch + ) + LazyRow { + item { + TabBar( + modifier = Modifier.padding(horizontal = 16.dp), + selectedTabPage = tabPage, + tabs = tabs, + onTabSelected = { updateTabPage(it) } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ListsOfTabs.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ListsOfTabs.kt new file mode 100644 index 0000000..6840ea7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ListsOfTabs.kt @@ -0,0 +1,71 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.dashboard_overview +import com.apimorlabs.reluct.compose.ui.dashboard_statistics +import com.apimorlabs.reluct.compose.ui.goals_active +import com.apimorlabs.reluct.compose.ui.goals_inactive +import com.apimorlabs.reluct.compose.ui.screen_time_limit +import com.apimorlabs.reluct.compose.ui.screen_time_stats +import com.apimorlabs.reluct.compose.ui.tasks_done +import com.apimorlabs.reluct.compose.ui.tasks_pending +import com.apimorlabs.reluct.compose.ui.tasks_stats +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun rememberDashboardTabs(): ImmutableList> { + val dashboardOverviewText = stringResource(Res.string.dashboard_overview) + val dashboardStatsText = stringResource(Res.string.dashboard_statistics) + + return remember { + persistentListOf( + TabBar(TabDestinations.DashboardOverview, dashboardOverviewText), + TabBar(TabDestinations.DashboardStats, dashboardStatsText) + ) + } +} + +@Composable +internal fun rememberTasksTabs(): ImmutableList> { + val pendingTasksText = stringResource(Res.string.tasks_pending) + val completedTasksText = stringResource(Res.string.tasks_stats) + val tasksStatsText = stringResource(Res.string.tasks_done) + + return remember { + persistentListOf( + TabBar(TabDestinations.TasksPending, pendingTasksText), + TabBar(TabDestinations.TasksCompleted, completedTasksText), + TabBar(TabDestinations.TasksStats, tasksStatsText) + ) + } +} + +@Composable +internal fun rememberGoalsTabs(): ImmutableList> { + val goalsActiveText = stringResource(Res.string.goals_active) + val goalsInactiveText = stringResource(Res.string.goals_inactive) + + return remember { + persistentListOf( + TabBar(TabDestinations.TasksPending, goalsActiveText), + TabBar(TabDestinations.TasksCompleted, goalsInactiveText) + ) + } +} + +@Composable +internal fun rememberScreenTimeTabs(): ImmutableList> { + val screenTimeStatsText = stringResource(Res.string.screen_time_stats) + val screenTimeLimitsText = stringResource(Res.string.screen_time_limit) + + return remember { + persistentListOf( + TabBar(TabDestinations.ScreenTimeStats, screenTimeStatsText), + TabBar(TabDestinations.ScreenTimeLimits, screenTimeLimitsText) + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabBar.kt new file mode 100644 index 0000000..e11525e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabBar.kt @@ -0,0 +1,69 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.TabRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator +import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun TabBar( + selectedTabPage: State>, + tabs: ImmutableList>, + onTabSelected: (tabPage: TabDestinations) -> Unit, + modifier: Modifier = Modifier, +) { + val selectedIndex by remember(selectedTabPage, tabs) { + derivedStateOf { + tabs.indexOfFirst { it.destination == selectedTabPage.value }.takeIf { it >= 0 } ?: 0 + } + } + + TabRow( + modifier = modifier.width(250.dp), + selectedTabIndex = selectedIndex, + containerColor = Color.Transparent, + indicator = { tabPositions -> + ReluctTabIndicator(currentTabPosition = tabPositions[selectedIndex]) + }, + divider = { } + ) { + tabs.forEachIndexed { _, tabPage -> + TabEntry( + title = tabPage.label, + textColor = GetTabTextColor( + tabPage = tabPage.destination, + selectedTabPage = selectedTabPage.value + ), + onClick = { onTabSelected(tabPage.destination) } + ) + } + } +} + +@Stable +data class TabBar(val destination: TabDestinations, val label: String) + +internal fun getCurrentTab( + currentDestination: NavDestination?, + allTabs: ImmutableList> +): TabDestinations { + allTabs.forEach { item -> + val isSelected = currentDestination?.hierarchy?.any { + it.route == item.destination.route::class.qualifiedName + } ?: false + if (isSelected) return item.destination + } + return allTabs.first().destination +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt new file mode 100644 index 0000000..d002169 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt @@ -0,0 +1,67 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.apimorlabs.reluct.navigation.destinations.ActiveGoalsDestination +import com.apimorlabs.reluct.navigation.destinations.CompletedTasksDestination +import com.apimorlabs.reluct.navigation.destinations.DashboardOverviewDestination +import com.apimorlabs.reluct.navigation.destinations.DashboardStatsDestination +import com.apimorlabs.reluct.navigation.destinations.InactiveGoalsDestination +import com.apimorlabs.reluct.navigation.destinations.PendingTasksDestination +import com.apimorlabs.reluct.navigation.destinations.ScreenTimeLimitsDestination +import com.apimorlabs.reluct.navigation.destinations.ScreenTimeStatsDestination +import com.apimorlabs.reluct.navigation.destinations.TasksStatisticsDestination +import kotlinx.serialization.Serializable + +@Serializable +sealed class TabDestinations(val route: T) { + + // Dashboard + @Serializable + data object DashboardOverview : + TabDestinations(DashboardOverviewDestination) + + @Serializable + data object DashboardStats : + TabDestinations(DashboardStatsDestination) + + // Tasks + @Serializable + data object TasksCompleted : + TabDestinations(CompletedTasksDestination) + + @Serializable + data object TasksStats : TabDestinations(TasksStatisticsDestination) + + @Serializable + data object TasksPending : TabDestinations(PendingTasksDestination) + + // Goals + @Serializable + data object GoalsActive : TabDestinations(ActiveGoalsDestination) + + @Serializable + data object GoalsInactive : TabDestinations(InactiveGoalsDestination) + + // Screen Time + @Serializable + data object ScreenTimeStats : + TabDestinations(ScreenTimeStatsDestination) + + @Serializable + data object ScreenTimeLimits : + TabDestinations(ScreenTimeLimitsDestination) +} + +@Composable +internal fun GetTabTextColor( + tabPage: TabDestinations, + selectedTabPage: TabDestinations, +): Color = + if (tabPage.route == selectedTabPage.route) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + .copy(alpha = .5f) + } diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TopTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TopTabBar.kt new file mode 100644 index 0000000..df75071 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TopTabBar.kt @@ -0,0 +1,50 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.apimorlabs.reluct.compose.ui.components.topBar.ReluctPageHeading +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun TopTabBar( + title: String, + tabPage: State>, + tabs: ImmutableList>, + updateTabPage: (TabDestinations) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .padding(vertical = 8.dp) + .statusBarsPadding() + .fillMaxWidth(), + verticalArrangement = Arrangement + .spacedBy(16.dp) + ) { + ReluctPageHeading( + modifier = Modifier.padding(horizontal = 16.dp), + title = title, + extraItems = {} + ) + + LazyRow { + item { + TabBar( + modifier = Modifier + .padding(horizontal = 16.dp), + selectedTabPage = tabPage, + tabs = tabs, + onTabSelected = { updateTabPage(it) } + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/dashboard/statistics/DashboardStatsScreen.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/dashboard/statistics/DashboardStatsScreen.kt new file mode 100644 index 0000000..c840c98 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/dashboard/statistics/DashboardStatsScreen.kt @@ -0,0 +1,38 @@ +package com.apimorlabs.reluct.screens.dashboard.statistics + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.features.dashboard.DashboardStatisticsViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun DashboardStatsScreen( + mainScaffoldPadding: PaddingValues, + barsVisibility: BarsVisibility, + onNavigateToAppUsageInfo: (packageName: String) -> Unit, + onNavigateToScreenTimeStats: () -> Unit +) { + val snackbarState = remember { SnackbarHostState() } + + val viewModel: DashboardStatisticsViewModel = koinViewModel() + val screenTimeUiState = viewModel.screenTimeUiState.collectAsStateWithLifecycle() + val tasksStatsUiState = viewModel.tasksStatsUiState.collectAsStateWithLifecycle() + + DashboardStatsUI( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + snackbarState = snackbarState, + screenTimeUiState = screenTimeUiState, + tasksStatsUiState = tasksStatsUiState, + onTasksSelectDay = { viewModel.tasksSelectDay(it) }, + onScreenTimeSelectDay = { viewModel.screenTimeSelectDay(it) }, + onSelectAppTimeLimit = { viewModel.selectAppTimeLimit(it) }, + onSaveTimeLimit = { hours, minutes -> viewModel.saveAppTimeLimit(hours, minutes) }, + onAppUsageInfoClick = { onNavigateToAppUsageInfo(it.packageName) }, + onViewAllScreenTimeStats = onNavigateToScreenTimeStats + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/goals/active/ActiveGoalsScreen.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/goals/active/ActiveGoalsScreen.kt new file mode 100644 index 0000000..d81d99e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/goals/active/ActiveGoalsScreen.kt @@ -0,0 +1,74 @@ +package com.apimorlabs.reluct.screens.goals.active + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.goal_marked_active +import com.apimorlabs.reluct.compose.ui.goal_marked_inactive +import com.apimorlabs.reluct.compose.ui.util.BarsVisibility +import com.apimorlabs.reluct.features.goals.active.ActiveGoalsViewModel +import com.apimorlabs.reluct.features.goals.states.events.GoalsEvents +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun ActiveGoalsScreen( + mainScaffoldPadding: PaddingValues, + barsVisibility: BarsVisibility, + onNavigateToAddGoal: (defaultGoalIndex: Int?) -> Unit, + onNavigateToGoalDetails: (goalId: String) -> Unit +) { + val snackbarState = remember { SnackbarHostState() } + + val viewModel = koinViewModel() + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + val events = viewModel.event.collectAsStateWithLifecycle(initialValue = GoalsEvents.Nothing) + + HandleEvents( + eventsState = events, + snackbarState = snackbarState + ) + + ActiveGoalsUI( + mainScaffoldPadding = mainScaffoldPadding, + barsVisibility = barsVisibility, + snackbarState = snackbarState, + uiState = uiState, + fetchMoreData = viewModel::fetchMoreData, + onAddGoal = onNavigateToAddGoal, + onGoalClicked = { onNavigateToGoalDetails(it.id) } + ) +} + +@Composable +private fun HandleEvents( + eventsState: State, + snackbarState: SnackbarHostState, +) { + LaunchedEffect(eventsState.value) { + when (val events = eventsState.value) { + is GoalsEvents.ChangedGoalState -> { + val msg = if (events.isActive) { + getString(Res.string.goal_marked_active) + } else { + getString(Res.string.goal_marked_inactive) + } + launch { + snackbarState.showSnackbar( + message = msg, + duration = SnackbarDuration.Short + ) + } + } + + else -> {} + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/onboarding/OnBoardingScreen.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/onboarding/OnBoardingScreen.kt index 161a398..53f42fd 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/onboarding/OnBoardingScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/onboarding/OnBoardingScreen.kt @@ -6,9 +6,7 @@ import com.apimorlabs.reluct.features.onboarding.OnBoardingViewModel import org.koin.compose.viewmodel.koinViewModel @Composable -fun OnBoardingScreen( - onNavigateToAuth: () -> Unit -) { +fun OnBoardingScreen() { val viewModel: OnBoardingViewModel = koinViewModel() val uiState = viewModel.uiState.collectAsStateWithLifecycle() @@ -20,7 +18,7 @@ fun OnBoardingScreen( onToggleAppBlocking = viewModel::toggleAppBlocking, onBoardingComplete = { viewModel.onBoardingComplete() - onNavigateToAuth() + // Navigate to auth: Implement onNavigateToAuth() } ) } diff --git a/composeApp/src/desktopMain/kotlin/com/apimorlabs/reluct/Main.kt b/composeApp/src/desktopMain/kotlin/com/apimorlabs/reluct/Main.kt index d6b1ab7..100348a 100644 --- a/composeApp/src/desktopMain/kotlin/com/apimorlabs/reluct/Main.kt +++ b/composeApp/src/desktopMain/kotlin/com/apimorlabs/reluct/Main.kt @@ -1,17 +1,41 @@ package com.apimorlabs.reluct +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import com.apimorlabs.reluct.common.di.KoinMain +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.app_name +import org.jetbrains.compose.resources.stringResource +import java.awt.Dimension fun main() { KoinMain.init() application { + val windowState = rememberWindowState( + size = DpSize(width = 1100.dp, height = 600.dp), + placement = WindowPlacement.Floating, + position = WindowPosition.Aligned(Alignment.Center) + ) + Window( onCloseRequest = ::exitApplication, - title = "KMP Hello World", + state = windowState, + title = stringResource(Res.string.app_name), ) { + setWindowMinSize(800, 500) + App() } } } + +fun FrameWindowScope.setWindowMinSize(width: Int, height: Int) { + window.minimumSize = Dimension(width, height) +} diff --git a/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettings.kt b/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettings.kt index 3c65814..6cb21b4 100644 --- a/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettings.kt +++ b/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettings.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow interface MultiplatformSettings { fun saveThemeSettings(value: Int): Boolean val theme: Flow + fun getTheme(): Int fun saveFocusMode(value: Boolean): Boolean val focusMode: Flow fun saveDoNotDisturb(value: Boolean): Boolean @@ -13,6 +14,7 @@ interface MultiplatformSettings { val savedVersionCode: Flow fun saveOnBoardingShown(value: Boolean): Boolean val onBoardingShown: Flow + fun getOnBoardingShown(): Boolean fun saveAppBlocking(value: Boolean): Boolean val appBlockingEnabled: Flow diff --git a/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettingsImpl.kt b/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettingsImpl.kt index 85f9f16..3e7187b 100644 --- a/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettingsImpl.kt +++ b/data-source/src/commonMain/kotlin/com/apimorlabs/reluct/data/source/settings/MultiplatformSettingsImpl.kt @@ -39,6 +39,9 @@ internal class MultiplatformSettingsImpl( _loginSkipped.tryEmit(readLoginSkipped()) } + override fun getTheme(): Int = readThemeSettings() + override fun getOnBoardingShown(): Boolean = readOnBoardingShown() + override fun saveThemeSettings(value: Int): Boolean { settings.putInt(key = Keys.THEME_OPTION, value = value) return _themeFlow.tryEmit(readThemeSettings()) diff --git a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/AndroidScreenTimeServices.kt b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/AndroidScreenTimeServices.kt index 6bb201c..b32d2b3 100644 --- a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/AndroidScreenTimeServices.kt +++ b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/AndroidScreenTimeServices.kt @@ -4,6 +4,7 @@ import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri import com.apimorlabs.reluct.common.models.domain.usageStats.AppUsageStats @@ -51,10 +52,15 @@ internal class AndroidScreenTimeServices( .getCurrentAppStats(getAppUsageInfo, context).mapLatest { stats -> // Update Our Notification val notification = appStatsNotification(context, stats) - notificationManager.notify( - ScreenTimeServiceNotification.NOTIFICATION_ID, - notification - ) + if (notificationManager.areNotificationsEnabled()) { + notificationManager.notify( + ScreenTimeServiceNotification.NOTIFICATION_ID, + notification + ) + } else { + Toast.makeText(context, "Please enable notifications!", Toast.LENGTH_SHORT) + .show() + } val currentDuration = stats.appUsageInfo.timeInForeground val isFocusModeOn = manageFocusMode.isFocusModeOn.first() @@ -62,7 +68,6 @@ internal class AndroidScreenTimeServices( val appPastLimit = (currentDuration >= appLimits.timeLimit && appLimits.timeLimit != 0L) - // Check if the app doesn't violate limits // Check if the app doesn't violate limits val limitParamChecks = (isFocusModeOn && appLimits.isADistractingAp) || appLimits.isPaused || appPastLimit @@ -76,7 +81,7 @@ internal class AndroidScreenTimeServices( } private fun appStatsNotification(context: Context, app: AppUsageStats): Notification { - val uriString = ScreenTimeLink.appScreenTimeDeepLink(app.appUsageInfo.packageName) + val uriString = ScreenTimeLink.deepLink(app.appUsageInfo.packageName) val intent = Intent( Intent.ACTION_VIEW, uriString.toUri() diff --git a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLimitService.kt b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLimitService.kt index 54277bc..251bcad 100644 --- a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLimitService.kt +++ b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLimitService.kt @@ -14,8 +14,15 @@ import com.apimorlabs.reluct.features.screenTime.statistics.AppScreenTimeStatsVi import com.apimorlabs.reluct.features.screenTime.ui.overlay.AppLimitedOverlayView import com.apimorlabs.reluct.features.screenTime.ui.overlay.LimitsOverlayParams import com.apimorlabs.reluct.features.screenTime.ui.overlay.OverlayLifecycleOwner -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koin.core.component.inject @@ -88,11 +95,11 @@ internal class ScreenTimeLimitService : Service(), KoinComponent { overlayView = viewModel?.let { vm -> AppLimitedOverlayView( applicationContext, - /*viewModel = vm, + viewModel = vm, exit = { goHome() removeOverlayView() - }*/ + } ).getView() } overlayLifecycleOwner.attachToView(overlayView) diff --git a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLink.kt b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLink.kt index bde52fd..0239a62 100644 --- a/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLink.kt +++ b/features/src/androidMain/kotlin/com/apimorlabs/reluct/features/screenTime/services/ScreenTimeLink.kt @@ -3,6 +3,6 @@ package com.apimorlabs.reluct.features.screenTime.services import com.apimorlabs.reluct.common.models.util.AppURI object ScreenTimeLink { - const val DEEP_LINK = "${AppURI.BASE_URI}/screen-time" - fun appScreenTimeDeepLink(packageName: String) = "$DEEP_LINK?packageName=$packageName" + const val DEEP_LINK = "${AppURI.BASE_URI}/screen_time" + fun deepLink(packageName: String) = "$DEEP_LINK?packageName=$packageName" } diff --git a/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/di/Features.kt b/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/di/Features.kt index 287e0ae..5299533 100644 --- a/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/di/Features.kt +++ b/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/di/Features.kt @@ -7,8 +7,10 @@ import com.apimorlabs.reluct.features.screenTime.limits.ScreenTimeLimitsViewMode import com.apimorlabs.reluct.features.screenTime.statistics.AppScreenTimeStatsViewModel import com.apimorlabs.reluct.features.screenTime.statistics.ScreenTimeStatsViewModel import com.apimorlabs.reluct.features.settings.AppSettingsViewModel +import com.apimorlabs.reluct.features.settings.GetSettings import org.koin.core.KoinApplication import org.koin.core.module.Module +import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module @@ -28,6 +30,7 @@ object Features { // Settings viewModelOf(::AppSettingsViewModel) + singleOf(::GetSettings) // Screen Time viewModelOf(::ScreenTimeLimitsViewModel) diff --git a/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/settings/GetSettings.kt b/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/settings/GetSettings.kt new file mode 100644 index 0000000..c7677ea --- /dev/null +++ b/features/src/commonMain/kotlin/com/apimorlabs/reluct/features/settings/GetSettings.kt @@ -0,0 +1,14 @@ +package com.apimorlabs.reluct.features.settings + +import com.apimorlabs.reluct.data.source.settings.MultiplatformSettings +import kotlinx.coroutines.flow.Flow + +class GetSettings(private val settings: MultiplatformSettings) { + val theme: Flow = settings.theme + val savedVersionCode: Flow = settings.savedVersionCode + val loginSkipped: Flow = settings.loginSkipped + val onBoardingShown: Flow = settings.onBoardingShown + fun saveVersionCode(versionCode: Int) = settings.saveVersionCode(versionCode) + fun getTheme(): Int = settings.getTheme() + fun getOnBoardingShow(): Boolean = settings.getOnBoardingShown() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 796b454..2e99b8c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] #Important kotlin = "2.0.0" -compose-plugin = "1.7.0-alpha03" +compose-plugin = "1.7.0-rc01" progurad-gradle = "7.3.0" agp = "8.3.0" moko-resources = "0.24.2" @@ -29,14 +29,14 @@ androidx-splash-screen = "1.0.0" junit = "4.13.2" # Lifecycle -androidx-lifecycle = "2.8.3" +androidx-lifecycle = "2.8.4" androidx-lifecycle-viewmodel-compose = "2.8.3" androidx-savedstate = "1.2.1" -jetbrains-androidx-lifecycle = "2.8.0" +jetbrains-androidx-lifecycle = "2.8.2" # Navigation androidx-navigation = "2.8.0-beta05" -jetbrains-androidx-navigation = "2.8.0-alpha09" +jetbrains-androidx-navigation = "2.8.0-alpha10" # DI koin = "4.0.0-RC1" @@ -52,6 +52,7 @@ sketch = "4.0.0-alpha08" coil-image = "2.1.0" accompanist = "0.27.0" androidx-workmanager = "2.9.0" +haze = "0.9.0-beta02" # Preference multiplatform-settings = "1.1.1" @@ -157,6 +158,8 @@ palette = { module = "androidx.palette:palette-ktx", version.ref = "palette" } androidx_workmanager = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-workmanager" } androidx_workmanager_multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "androidx-workmanager" } androidx_workmanager_testing = { module = "androidx.work:work-testing", version.ref = "androidx-workmanager" } +chrisbanes_haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } +chrisbanes_haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } # Network (KTOR) ktor_client_core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }