From ef044afbb4b5a2baa04b6081f98ddc3834d40ed1 Mon Sep 17 00:00:00 2001 From: Racka Date: Thu, 3 Oct 2024 15:53:05 +0300 Subject: [PATCH 1/8] setup base navigation destinations Signed-off-by: Racka --- .../reluct/navigation/NavHelpers.kt | 38 +++++++++++++++++++ .../navigation/destinations/Dashboard.kt | 9 +++++ .../reluct/navigation/destinations/Goals.kt | 27 +++++++++++++ .../navigation/destinations/OnBoarding.kt | 6 +++ .../destinations/OtherDestination.kt | 6 +++ .../navigation/destinations/ScreenTime.kt | 18 +++++++++ .../navigation/destinations/Settings.kt | 6 +++ .../navigation/destinations/SettingsCheck.kt | 13 +++++++ .../reluct/navigation/destinations/Tasks.kt | 31 +++++++++++++++ .../services/AndroidScreenTimeServices.kt | 19 ++++++---- .../services/ScreenTimeLimitService.kt | 13 +++++-- .../screenTime/services/ScreenTimeLink.kt | 4 +- 12 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt 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..0a24734 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt @@ -0,0 +1,38 @@ +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 + */ + fun NavHostController.navigateNavBarElements(route: String, startDestination: String? = null) { + 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/destinations/Dashboard.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt new file mode 100644 index 0000000..f5e11e6 --- /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 +object DashboardOverviewDestination + +@Serializable +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..4cfb69d --- /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 +object InactiveGoalsDestination + +@Serializable +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/OnBoarding.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt new file mode 100644 index 0000000..cc4befa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt @@ -0,0 +1,6 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +object OnBoardingDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt new file mode 100644 index 0000000..7c9eb8e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt @@ -0,0 +1,6 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +object OtherDestination 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..aa8b329 --- /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 +object AppScreenTimeStatsDestination + +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/Settings.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt new file mode 100644 index 0000000..bdc047c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt @@ -0,0 +1,6 @@ +package com.apimorlabs.reluct.navigation.destinations + +import kotlinx.serialization.Serializable + +@Serializable +object SettingsDestination 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/Tasks.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt new file mode 100644 index 0000000..be57699 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt @@ -0,0 +1,31 @@ +package com.apimorlabs.reluct.navigation.destinations + +import com.apimorlabs.reluct.common.models.util.AppURI +import kotlinx.serialization.Serializable + +@Serializable +object TasksStatisticsDestination + +@Serializable +data class TaskDetailsDestination(val taskId: String?) + +@Serializable +data class AddEditTaskDestination(val taskId: String?) + +object TaskDetailsLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/tasks" + fun deepLink(taskId: String) = "$DEEP_LINK?taskId=$taskId" +} + +@Serializable +object CompletedTasksDestination + +@Serializable +object PendingTasksDestination + +object PendingTasksLink { + const val DEEP_LINK = "${AppURI.BASE_URI}/pending_tasks" +} + +@Serializable +object SearchTasksDestination 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..1831b3e 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,10 +68,9 @@ 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 + appLimits.isPaused || appPastLimit if (!appLimits.overridden && limitParamChecks) { ScreenTimeServices.BlockState.Blocked(appLimits.appInfo.packageName) } else { @@ -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" } From 007f1fe386e335c4e4dc6e9ea0919d8ff4b8aadc Mon Sep 17 00:00:00 2001 From: Racka Date: Thu, 3 Oct 2024 17:37:21 +0300 Subject: [PATCH 2/8] add top tab destinations Signed-off-by: Racka --- .../navigation/toptabs/DashboardTabBar.kt | 88 +++++++++++++++++++ .../reluct/navigation/toptabs/GoalsTabBar.kt | 70 +++++++++++++++ .../navigation/toptabs/ScreenTimeTabBar.kt | 71 +++++++++++++++ .../reluct/navigation/toptabs/TasksTabBar.kt | 81 +++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt new file mode 100644 index 0000000..c0313b0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt @@ -0,0 +1,88 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator +import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry +import com.apimorlabs.reluct.compose.ui.dashboard_overview +import com.apimorlabs.reluct.compose.ui.dashboard_statistics +import com.apimorlabs.reluct.compose.ui.theme.ReluctAppTheme +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun DashboardTabBar( + tabPage: State, + onTabSelected: (tabPage: DashboardTabDestination) -> Unit, + modifier: Modifier = Modifier, +) { + TabRow( + modifier = modifier.width(250.dp), + selectedTabIndex = tabPage.value.ordinal, + containerColor = Color.Transparent, + indicator = { tabPositions -> + ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) + }, + divider = { } + ) { + TabEntry( + title = stringResource(Res.string.dashboard_overview), + textColor = getDashboardTabTextColor( + tabPage = DashboardTabDestination.Overview, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(DashboardTabDestination.Overview) + } + ) + TabEntry( + title = stringResource(Res.string.dashboard_statistics), + textColor = getDashboardTabTextColor( + tabPage = DashboardTabDestination.Statistics, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(DashboardTabDestination.Statistics) + } + ) + } +} + +@Preview +@Composable +private fun DashTabPrev() { + ReluctAppTheme { + val page = remember { mutableStateOf(DashboardTabDestination.Overview) } + DashboardTabBar( + tabPage = page, + onTabSelected = {} + ) + } +} + +@Composable +private fun getDashboardTabTextColor( + tabPage: DashboardTabDestination, + selectedTabPage: DashboardTabDestination, +): Color = + if (tabPage == selectedTabPage) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + .copy(alpha = .5f) + } + +// Destinations +enum class DashboardTabDestination { + Overview, + Statistics; +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt new file mode 100644 index 0000000..9fa939e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt @@ -0,0 +1,70 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator +import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry +import com.apimorlabs.reluct.compose.ui.goals_active +import com.apimorlabs.reluct.compose.ui.goals_inactive +import org.jetbrains.compose.resources.stringResource + +@Composable +fun GoalsTabBar( + tabPage: State, + onTabSelected: (tabPage: GoalsTabDestination) -> Unit, + modifier: Modifier = Modifier, +) { + TabRow( + modifier = modifier.width(250.dp), + selectedTabIndex = tabPage.value.ordinal, + containerColor = Color.Transparent, + indicator = { tabPositions -> + ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) + }, + divider = { } + ) { + TabEntry( + title = stringResource(Res.string.goals_active), + textColor = getGoalsTabTextColor( + tabPage = GoalsTabDestination.Active, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(GoalsTabDestination.Active) + } + ) + TabEntry( + title = stringResource(Res.string.goals_inactive), + textColor = getGoalsTabTextColor( + tabPage = GoalsTabDestination.Inactive, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(GoalsTabDestination.Inactive) + } + ) + } +} + +@Composable +private fun getGoalsTabTextColor( + tabPage: GoalsTabDestination, + selectedTabPage: GoalsTabDestination, +): Color = + if (tabPage == selectedTabPage) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + .copy(alpha = .5f) + } + +enum class GoalsTabDestination { + Active, Inactive; +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt new file mode 100644 index 0000000..5470e25 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt @@ -0,0 +1,71 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator +import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry +import com.apimorlabs.reluct.compose.ui.screen_time_limit +import com.apimorlabs.reluct.compose.ui.screen_time_stats +import org.jetbrains.compose.resources.stringResource + +@Composable +fun ScreenTimeTabBar( + tabPage: State, + onTabSelected: (tabPage: ScreenTimeTabDestination) -> Unit, + modifier: Modifier = Modifier, +) { + TabRow( + modifier = modifier.width(250.dp), + selectedTabIndex = tabPage.value.ordinal, + containerColor = Color.Transparent, + indicator = { tabPositions -> + ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) + }, + divider = { } + ) { + TabEntry( + title = stringResource(Res.string.screen_time_stats), + textColor = getScreenTimeTabTextColor( + tabPage = ScreenTimeTabDestination.Statistics, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(ScreenTimeTabDestination.Statistics) + } + ) + + TabEntry( + title = stringResource(Res.string.screen_time_limit), + textColor = getScreenTimeTabTextColor( + tabPage = ScreenTimeTabDestination.Limits, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(ScreenTimeTabDestination.Limits) + } + ) + } +} + +@Composable +private fun getScreenTimeTabTextColor( + tabPage: ScreenTimeTabDestination, + selectedTabPage: ScreenTimeTabDestination, +): Color = + if (tabPage == selectedTabPage) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + .copy(alpha = .5f) + } + +enum class ScreenTimeTabDestination { + Statistics, Limits; +} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt new file mode 100644 index 0000000..efdc845 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt @@ -0,0 +1,81 @@ +package com.apimorlabs.reluct.navigation.toptabs + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabRow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.apimorlabs.reluct.compose.ui.Res +import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator +import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry +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 org.jetbrains.compose.resources.stringResource + +@Composable +fun TasksTabBar( + tabPage: State, + onTabSelected: (tabPage: TasksTabDestination) -> Unit, + modifier: Modifier = Modifier, +) { + TabRow( + modifier = modifier.width(300.dp), + selectedTabIndex = tabPage.value.ordinal, + containerColor = Color.Transparent, + indicator = { tabPositions -> + ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) + }, + divider = { } + ) { + TabEntry( + title = stringResource(Res.string.tasks_pending), + textColor = getTasksTabTextColor( + tabPage = TasksTabDestination.Tasks, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(TasksTabDestination.Tasks) + } + ) + TabEntry( + title = stringResource(Res.string.tasks_done), + textColor = getTasksTabTextColor( + tabPage = TasksTabDestination.Done, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(TasksTabDestination.Done) + } + ) + TabEntry( + title = stringResource(Res.string.tasks_stats), + textColor = getTasksTabTextColor( + tabPage = TasksTabDestination.Statistics, + selectedTabPage = tabPage.value + ), + onClick = { + onTabSelected(TasksTabDestination.Statistics) + } + ) + } +} + +@Composable +private fun getTasksTabTextColor( + tabPage: TasksTabDestination, + selectedTabPage: TasksTabDestination, +): Color = + if (tabPage == selectedTabPage) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + .copy(alpha = .5f) + } + +enum class TasksTabDestination { + Tasks, Done, Statistics; +} From b165703ae61e500a0c52de0245bf5bd852e2130d Mon Sep 17 00:00:00 2001 From: Racka Date: Thu, 3 Oct 2024 18:12:42 +0300 Subject: [PATCH 3/8] setup bottom navigation destinations Signed-off-by: Racka --- .../navigation/navbar/NavBarDestinations.kt | 53 ++++++++ .../navigation/navbar/NavBarElevation.kt | 12 ++ .../navigation/navbar/ReluctBottomNavBar.kt | 119 ++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarElevation.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt 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..0e171ba --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt @@ -0,0 +1,53 @@ +package com.apimorlabs.reluct.navigation.navbar + +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.ui.graphics.vector.ImageVector +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 org.jetbrains.compose.resources.StringResource + +enum class NavBarDestinations( + val route: String, + val iconActive: ImageVector, + val iconInactive: ImageVector, + val label: StringResource +) { + Dashboard( + route = "dashboard_destination", + iconActive = Icons.Rounded.GridView, + iconInactive = Icons.Outlined.GridView, + label = Res.string.dashboard_destination_text + ), + + Tasks( + route = "tasks_destination", + iconActive = Icons.AutoMirrored.Rounded.FactCheck, + iconInactive = Icons.AutoMirrored.Outlined.FactCheck, + label = Res.string.tasks_destination_text + ), + + ScreenTime( + route = "screen_time_destination", + iconActive = Icons.Rounded.Aod, + iconInactive = Icons.Outlined.Aod, + label = Res.string.screen_time_destination_text + ), + + Goals( + route = "goals_destination", + iconActive = Icons.Rounded.TaskAlt, + iconInactive = Icons.Outlined.TaskAlt, + label = Res.string.goals_destination_text + ); +} 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..94a3c0f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt @@ -0,0 +1,119 @@ +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.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +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.getValue +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.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.navigation.NavHelpers.navigateNavBarElements +import org.jetbrains.compose.resources.stringResource + +@Composable +fun ReluctBottomNavBar( + navController: NavHostController, + graphStartDestination: String, + modifier: Modifier = Modifier, +) { + val items = NavBarDestinations.entries + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + Surface( + color = MaterialTheme.colorScheme.surface, + tonalElevation = NavBarElevation.Level2 + ) { + FloatingNavigationBar( + modifier = modifier.navigationBarsPadding(), + containerColor = Color.Transparent + ) { + items.forEach { item -> + val selected = currentDestination?.hierarchy?.any { + it.route == item.route + } ?: false + + NavigationBarItem( + label = { + Text( + text = stringResource(item.label), + style = MaterialTheme.typography.labelMedium + ) + }, + selected = selected, + // colors = , + icon = { + Icon( + imageVector = if (selected) item.iconActive else item.iconInactive, + contentDescription = stringResource(item.label) + ) + }, + onClick = { + navController.navigateNavBarElements( + route = item.route, + startDestination = graphStartDestination + ) + } + ) + } + } + } +} + +@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, + ) + } +} From e94e1ebec5f48f116e4953a1d89eeb3233e3bcfd Mon Sep 17 00:00:00 2001 From: Racka Date: Sun, 6 Oct 2024 20:46:30 +0300 Subject: [PATCH 4/8] complete navhost setup and refactor Signed-off-by: Racka --- composeApp/build.gradle.kts | 2 + .../reluct/navigation/NavHelpers.kt | 10 +- .../reluct/navigation/Transitions.kt | 47 ++++ .../navigation/destinations/Dashboard.kt | 4 +- .../reluct/navigation/destinations/Goals.kt | 6 +- .../navigation/destinations/OnBoarding.kt | 2 +- .../destinations/OtherDestination.kt | 6 - .../destinations/OtherGraphDestination.kt | 15 ++ .../navigation/destinations/ScreenTime.kt | 2 +- .../navigation/destinations/Settings.kt | 2 +- .../reluct/navigation/destinations/Tasks.kt | 21 +- .../navigation/navbar/NavBarDestinations.kt | 77 +++---- .../navigation/navbar/ReluctBottomNavBar.kt | 124 +++++++---- .../reluct/navigation/navhost/AppNavHost.kt | 2 + .../navhost/graphs/DashboardNavHost.kt | 206 ++++++++++++++++++ .../navigation/navhost/graphs/GoalsNavHost.kt | 126 +++++++++++ .../navhost/graphs/OtherScreenNavGraph.kt | 159 ++++++++++++++ .../navhost/graphs/ScreenTimeNavHost.kt | 108 +++++++++ .../navigation/navhost/graphs/TasksNavHost.kt | 174 +++++++++++++++ .../navigation/toptabs/DashboardTabBar.kt | 88 -------- .../reluct/navigation/toptabs/GoalsTabBar.kt | 70 ------ .../reluct/navigation/toptabs/ListsOfTabs.kt | 71 ++++++ .../navigation/toptabs/ScreenTimeTabBar.kt | 71 ------ .../reluct/navigation/toptabs/TabBar.kt | 69 ++++++ .../navigation/toptabs/TabDestinations.kt | 69 ++++++ .../reluct/navigation/toptabs/TasksTabBar.kt | 81 ------- .../reluct/navigation/toptabs/TopTabBar.kt | 50 +++++ .../statistics/DashboardStatsScreen.kt | 38 ++++ .../screens/goals/active/ActiveGoalsScreen.kt | 74 +++++++ gradle/libs.versions.toml | 3 + 30 files changed, 1355 insertions(+), 422 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/Transitions.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherGraphDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/DashboardNavHost.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/GoalsNavHost.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/OtherScreenNavGraph.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/ScreenTimeNavHost.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/TasksNavHost.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ListsOfTabs.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TopTabBar.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/dashboard/statistics/DashboardStatsScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/screens/goals/active/ActiveGoalsScreen.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 94559f5..3c2f81f 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) } } diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt index 0a24734..3a1782f 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt @@ -22,8 +22,16 @@ internal object NavHelpers { * 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: String, startDestination: String? = null) { + 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 -> 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 index f5e11e6..bee9211 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Dashboard.kt @@ -3,7 +3,7 @@ package com.apimorlabs.reluct.navigation.destinations import kotlinx.serialization.Serializable @Serializable -object DashboardOverviewDestination +data object DashboardOverviewDestination @Serializable -object DashboardStatsDestination +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 index 4cfb69d..1686c0a 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Goals.kt @@ -4,13 +4,13 @@ import com.apimorlabs.reluct.common.models.util.AppURI import kotlinx.serialization.Serializable @Serializable -object InactiveGoalsDestination +data object InactiveGoalsDestination @Serializable -object ActiveGoalsDestination +data object ActiveGoalsDestination @Serializable -data class AddEditGoalDestination(val goalId: String?, val defaultGoalIndex: Int = -1) +data class AddEditGoalDestination(val goalId: String?, val defaultGoalIndex: Int? = -1) object AddEditGoalLink { const val DEEP_LINK = "${AppURI.BASE_URI}/add_edit_goal" diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt index cc4befa..819ebc5 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt @@ -3,4 +3,4 @@ package com.apimorlabs.reluct.navigation.destinations import kotlinx.serialization.Serializable @Serializable -object OnBoardingDestination +data object OnBoardingDestination diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt deleted file mode 100644 index 7c9eb8e..0000000 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OtherDestination.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.apimorlabs.reluct.navigation.destinations - -import kotlinx.serialization.Serializable - -@Serializable -object OtherDestination 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 index aa8b329..122823a 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/ScreenTime.kt @@ -10,7 +10,7 @@ object ScreenTimeStatsDestination object ScreenTimeLimitsDestination @Serializable -object AppScreenTimeStatsDestination +data class AppScreenTimeStatsDestination(val packageName: String?) object AppScreenTimeStatsLink { const val DEEP_LINK = "${AppURI.BASE_URI}/screen_time_stats" diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt index bdc047c..2d3d196 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt @@ -3,4 +3,4 @@ package com.apimorlabs.reluct.navigation.destinations import kotlinx.serialization.Serializable @Serializable -object SettingsDestination +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 index be57699..2d1e155 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Tasks.kt @@ -4,7 +4,7 @@ import com.apimorlabs.reluct.common.models.util.AppURI import kotlinx.serialization.Serializable @Serializable -object TasksStatisticsDestination +data object TasksStatisticsDestination @Serializable data class TaskDetailsDestination(val taskId: String?) @@ -12,20 +12,21 @@ data class TaskDetailsDestination(val taskId: String?) @Serializable data class AddEditTaskDestination(val taskId: String?) -object TaskDetailsLink { - const val DEEP_LINK = "${AppURI.BASE_URI}/tasks" - fun deepLink(taskId: String) = "$DEEP_LINK?taskId=$taskId" -} +@Serializable +data object CompletedTasksDestination @Serializable -object CompletedTasksDestination +data object PendingTasksDestination @Serializable -object PendingTasksDestination +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" } - -@Serializable -object SearchTasksDestination 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 index 0e171ba..e9b2ddd 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/NavBarDestinations.kt @@ -1,53 +1,30 @@ package com.apimorlabs.reluct.navigation.navbar -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.ui.graphics.vector.ImageVector -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 org.jetbrains.compose.resources.StringResource - -enum class NavBarDestinations( - val route: String, - val iconActive: ImageVector, - val iconInactive: ImageVector, - val label: StringResource -) { - Dashboard( - route = "dashboard_destination", - iconActive = Icons.Rounded.GridView, - iconInactive = Icons.Outlined.GridView, - label = Res.string.dashboard_destination_text - ), - - Tasks( - route = "tasks_destination", - iconActive = Icons.AutoMirrored.Rounded.FactCheck, - iconInactive = Icons.AutoMirrored.Outlined.FactCheck, - label = Res.string.tasks_destination_text - ), - - ScreenTime( - route = "screen_time_destination", - iconActive = Icons.Rounded.Aod, - iconInactive = Icons.Outlined.Aod, - label = Res.string.screen_time_destination_text - ), - - Goals( - route = "goals_destination", - iconActive = Icons.Rounded.TaskAlt, - iconInactive = Icons.Outlined.TaskAlt, - label = Res.string.goals_destination_text - ); +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/ReluctBottomNavBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt index 94a3c0f..715e3da 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt @@ -9,74 +9,124 @@ 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.NavigationBar 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, - graphStartDestination: String, modifier: Modifier = Modifier, ) { - val items = NavBarDestinations.entries + // 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 - Surface( - color = MaterialTheme.colorScheme.surface, - tonalElevation = NavBarElevation.Level2 + FloatingNavigationBar( + modifier = modifier.navigationBarsPadding(), + containerColor = Color.Transparent ) { - FloatingNavigationBar( - modifier = modifier.navigationBarsPadding(), - containerColor = Color.Transparent - ) { - items.forEach { item -> - val selected = currentDestination?.hierarchy?.any { - it.route == item.route - } ?: false + items.forEach { item -> + val selected = currentDestination?.hierarchy?.any { + it.route == item.destination.route::class.qualifiedName + } ?: false - NavigationBarItem( - label = { - Text( - text = stringResource(item.label), - style = MaterialTheme.typography.labelMedium - ) - }, - selected = selected, - // colors = , - icon = { - Icon( - imageVector = if (selected) item.iconActive else item.iconInactive, - contentDescription = stringResource(item.label) - ) - }, - onClick = { - navController.navigateNavBarElements( - route = item.route, - startDestination = graphStartDestination - ) - } - ) - } + 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, null) + } + ) } } } 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..2e354c0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt @@ -0,0 +1,2 @@ +package com.apimorlabs.reluct.navigation.navhost + 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..c066ef7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/graphs/DashboardNavHost.kt @@ -0,0 +1,206 @@ +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/DashboardTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt deleted file mode 100644 index c0313b0..0000000 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/DashboardTabBar.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.apimorlabs.reluct.navigation.toptabs - -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TabRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.apimorlabs.reluct.compose.ui.Res -import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator -import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry -import com.apimorlabs.reluct.compose.ui.dashboard_overview -import com.apimorlabs.reluct.compose.ui.dashboard_statistics -import com.apimorlabs.reluct.compose.ui.theme.ReluctAppTheme -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview - -@Composable -fun DashboardTabBar( - tabPage: State, - onTabSelected: (tabPage: DashboardTabDestination) -> Unit, - modifier: Modifier = Modifier, -) { - TabRow( - modifier = modifier.width(250.dp), - selectedTabIndex = tabPage.value.ordinal, - containerColor = Color.Transparent, - indicator = { tabPositions -> - ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) - }, - divider = { } - ) { - TabEntry( - title = stringResource(Res.string.dashboard_overview), - textColor = getDashboardTabTextColor( - tabPage = DashboardTabDestination.Overview, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(DashboardTabDestination.Overview) - } - ) - TabEntry( - title = stringResource(Res.string.dashboard_statistics), - textColor = getDashboardTabTextColor( - tabPage = DashboardTabDestination.Statistics, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(DashboardTabDestination.Statistics) - } - ) - } -} - -@Preview -@Composable -private fun DashTabPrev() { - ReluctAppTheme { - val page = remember { mutableStateOf(DashboardTabDestination.Overview) } - DashboardTabBar( - tabPage = page, - onTabSelected = {} - ) - } -} - -@Composable -private fun getDashboardTabTextColor( - tabPage: DashboardTabDestination, - selectedTabPage: DashboardTabDestination, -): Color = - if (tabPage == selectedTabPage) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - .copy(alpha = .5f) - } - -// Destinations -enum class DashboardTabDestination { - Overview, - Statistics; -} diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt deleted file mode 100644 index 9fa939e..0000000 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/GoalsTabBar.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.apimorlabs.reluct.navigation.toptabs - -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TabRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.apimorlabs.reluct.compose.ui.Res -import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator -import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry -import com.apimorlabs.reluct.compose.ui.goals_active -import com.apimorlabs.reluct.compose.ui.goals_inactive -import org.jetbrains.compose.resources.stringResource - -@Composable -fun GoalsTabBar( - tabPage: State, - onTabSelected: (tabPage: GoalsTabDestination) -> Unit, - modifier: Modifier = Modifier, -) { - TabRow( - modifier = modifier.width(250.dp), - selectedTabIndex = tabPage.value.ordinal, - containerColor = Color.Transparent, - indicator = { tabPositions -> - ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) - }, - divider = { } - ) { - TabEntry( - title = stringResource(Res.string.goals_active), - textColor = getGoalsTabTextColor( - tabPage = GoalsTabDestination.Active, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(GoalsTabDestination.Active) - } - ) - TabEntry( - title = stringResource(Res.string.goals_inactive), - textColor = getGoalsTabTextColor( - tabPage = GoalsTabDestination.Inactive, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(GoalsTabDestination.Inactive) - } - ) - } -} - -@Composable -private fun getGoalsTabTextColor( - tabPage: GoalsTabDestination, - selectedTabPage: GoalsTabDestination, -): Color = - if (tabPage == selectedTabPage) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - .copy(alpha = .5f) - } - -enum class GoalsTabDestination { - Active, Inactive; -} 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/ScreenTimeTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt deleted file mode 100644 index 5470e25..0000000 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/ScreenTimeTabBar.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.apimorlabs.reluct.navigation.toptabs - -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TabRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.apimorlabs.reluct.compose.ui.Res -import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator -import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry -import com.apimorlabs.reluct.compose.ui.screen_time_limit -import com.apimorlabs.reluct.compose.ui.screen_time_stats -import org.jetbrains.compose.resources.stringResource - -@Composable -fun ScreenTimeTabBar( - tabPage: State, - onTabSelected: (tabPage: ScreenTimeTabDestination) -> Unit, - modifier: Modifier = Modifier, -) { - TabRow( - modifier = modifier.width(250.dp), - selectedTabIndex = tabPage.value.ordinal, - containerColor = Color.Transparent, - indicator = { tabPositions -> - ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) - }, - divider = { } - ) { - TabEntry( - title = stringResource(Res.string.screen_time_stats), - textColor = getScreenTimeTabTextColor( - tabPage = ScreenTimeTabDestination.Statistics, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(ScreenTimeTabDestination.Statistics) - } - ) - - TabEntry( - title = stringResource(Res.string.screen_time_limit), - textColor = getScreenTimeTabTextColor( - tabPage = ScreenTimeTabDestination.Limits, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(ScreenTimeTabDestination.Limits) - } - ) - } -} - -@Composable -private fun getScreenTimeTabTextColor( - tabPage: ScreenTimeTabDestination, - selectedTabPage: ScreenTimeTabDestination, -): Color = - if (tabPage == selectedTabPage) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - .copy(alpha = .5f) - } - -enum class ScreenTimeTabDestination { - Statistics, Limits; -} 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..d9e75aa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt @@ -0,0 +1,69 @@ +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/TasksTabBar.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt deleted file mode 100644 index efdc845..0000000 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TasksTabBar.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.apimorlabs.reluct.navigation.toptabs - -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TabRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.apimorlabs.reluct.compose.ui.Res -import com.apimorlabs.reluct.compose.ui.components.tab.ReluctTabIndicator -import com.apimorlabs.reluct.compose.ui.components.tab.TabEntry -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 org.jetbrains.compose.resources.stringResource - -@Composable -fun TasksTabBar( - tabPage: State, - onTabSelected: (tabPage: TasksTabDestination) -> Unit, - modifier: Modifier = Modifier, -) { - TabRow( - modifier = modifier.width(300.dp), - selectedTabIndex = tabPage.value.ordinal, - containerColor = Color.Transparent, - indicator = { tabPositions -> - ReluctTabIndicator(currentTabPosition = tabPositions[tabPage.value.ordinal]) - }, - divider = { } - ) { - TabEntry( - title = stringResource(Res.string.tasks_pending), - textColor = getTasksTabTextColor( - tabPage = TasksTabDestination.Tasks, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(TasksTabDestination.Tasks) - } - ) - TabEntry( - title = stringResource(Res.string.tasks_done), - textColor = getTasksTabTextColor( - tabPage = TasksTabDestination.Done, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(TasksTabDestination.Done) - } - ) - TabEntry( - title = stringResource(Res.string.tasks_stats), - textColor = getTasksTabTextColor( - tabPage = TasksTabDestination.Statistics, - selectedTabPage = tabPage.value - ), - onClick = { - onTabSelected(TasksTabDestination.Statistics) - } - ) - } -} - -@Composable -private fun getTasksTabTextColor( - tabPage: TasksTabDestination, - selectedTabPage: TasksTabDestination, -): Color = - if (tabPage == selectedTabPage) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.onBackground - .copy(alpha = .5f) - } - -enum class TasksTabDestination { - Tasks, Done, Statistics; -} 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..b856c20 --- /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 -> {} + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 796b454..1f484a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } From ec7dbc4cf04196e310bdc40484b40545fb5595a7 Mon Sep 17 00:00:00 2001 From: Racka Date: Sun, 6 Oct 2024 21:15:53 +0300 Subject: [PATCH 5/8] setup root navhost Signed-off-by: Racka --- .../navigation/destinations/SettingsCheck.kt | 2 +- .../navigation/navbar/ReluctBottomNavBar.kt | 8 +- .../reluct/navigation/navhost/AppNavHost.kt | 266 ++++++++++++++++++ .../screens/onboarding/OnBoardingScreen.kt | 6 +- 4 files changed, 275 insertions(+), 7 deletions(-) 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 index 824c0a3..872d1f1 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt @@ -4,7 +4,7 @@ data class SettingsCheck( val isOnBoardingDone: Boolean, val showChangeLog: Boolean, // val accountCheck: AccountCheck?, - val loginSkipped: Boolean + //val loginSkipped: Boolean ) /* data class AccountCheck( 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 index 715e3da..6bfffa7 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navbar/ReluctBottomNavBar.kt @@ -57,8 +57,9 @@ private data class NavDestination( ) @Composable -fun ReluctBottomNavBar( +fun ReluctBottomNavBar( navController: NavHostController, + startDestination: NavBarDestinations, modifier: Modifier = Modifier, ) { // Title Texts @@ -124,7 +125,10 @@ fun ReluctBottomNavBar( ) }, onClick = { - navController.navigateNavBarElements(route = item.destination.route, null) + navController.navigateNavBarElements( + route = item.destination.route, + startDestination.route + ) } ) } 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 index 2e354c0..037ef62 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt @@ -1,2 +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) { + // 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/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() } ) } From 0adc1295ab6fbb6639dded2b871fc30b4dbc72d5 Mon Sep 17 00:00:00 2001 From: Racka Date: Sun, 6 Oct 2024 22:48:23 +0300 Subject: [PATCH 6/8] link navhost and screen to main app Signed-off-by: Racka --- composeApp/build.gradle.kts | 8 ++ .../src/androidMain/AndroidManifest.xml | 2 +- .../com/apimorlabs/reluct/MainActivity.kt | 86 ++++++++++-- .../com/apimorlabs/reluct/MyApplication.kt | 15 --- .../apimorlabs/reluct/ReluctApplication.kt | 43 ++++++ .../kotlin/com/apimorlabs/reluct/App.kt | 123 +++++++----------- .../navigation/destinations/SettingsCheck.kt | 2 +- .../reluct/navigation/navhost/AppNavHost.kt | 2 +- .../kotlin/com/apimorlabs/reluct/Main.kt | 26 +++- .../source/settings/MultiplatformSettings.kt | 2 + .../settings/MultiplatformSettingsImpl.kt | 3 + .../apimorlabs/reluct/features/di/Features.kt | 3 + .../reluct/features/settings/GetSettings.kt | 14 ++ 13 files changed, 227 insertions(+), 102 deletions(-) delete mode 100644 composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MyApplication.kt create mode 100644 composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/ReluctApplication.kt create mode 100644 features/src/commonMain/kotlin/com/apimorlabs/reluct/features/settings/GetSettings.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 3c2f81f..0e2d0fd 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -22,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. + // TODO: 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..54af0cc 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/destinations/SettingsCheck.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt index 872d1f1..824c0a3 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsCheck.kt @@ -4,7 +4,7 @@ data class SettingsCheck( val isOnBoardingDone: Boolean, val showChangeLog: Boolean, // val accountCheck: AccountCheck?, - //val loginSkipped: Boolean + val loginSkipped: Boolean ) /* data class AccountCheck( 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 index 037ef62..082c8fc 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt @@ -250,7 +250,7 @@ private fun HandleRouteChecks( ) { LaunchedEffect(settingsCheck.value) { settingsCheck.value?.let { check -> - if (check.isOnBoardingDone && !check.showChangeLog) { + if (check.isOnBoardingDone && (!check.showChangeLog || check.loginSkipped)) { // Everything is ok. Go to Dashboard navController.navigate(DashBoardDestinations) { popUpTo(CHECKING_ROUTE) { inclusive = true } 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/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() +} From 8a7252defe1bba1f6dcb25a5e1a44b4761b04298 Mon Sep 17 00:00:00 2001 From: Racka Date: Sun, 6 Oct 2024 22:50:50 +0300 Subject: [PATCH 7/8] fix detekt issues Signed-off-by: Racka --- .../androidMain/kotlin/com/apimorlabs/reluct/MainActivity.kt | 2 +- composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt | 2 +- .../kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt | 2 +- .../destinations/{OnBoarding.kt => OnBoardingDestination.kt} | 0 .../destinations/{Settings.kt => SettingsDestination.kt} | 0 .../com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt | 2 +- .../reluct/navigation/navhost/graphs/DashboardNavHost.kt | 1 - .../com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt | 2 -- .../apimorlabs/reluct/screens/goals/active/ActiveGoalsScreen.kt | 2 +- .../features/screenTime/services/AndroidScreenTimeServices.kt | 2 +- 10 files changed, 6 insertions(+), 9 deletions(-) rename composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/{OnBoarding.kt => OnBoardingDestination.kt} (100%) rename composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/{Settings.kt => SettingsDestination.kt} (100%) diff --git a/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MainActivity.kt index 5e6025b..ebb0ca3 100644 --- a/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/apimorlabs/reluct/MainActivity.kt @@ -51,7 +51,7 @@ class MainActivity : ComponentActivity() { } enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style) // Fix for three-button nav not properly going edge-to-edge. - // TODO: https://issuetracker.google.com/issues/298296168 + // Issue: https://issuetracker.google.com/issues/298296168 window.setFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS, LayoutParams.FLAG_LAYOUT_NO_LIMITS) } diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt index 54af0cc..61d40ae 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/App.kt @@ -50,7 +50,7 @@ private fun ReluctMainCompose(settings: GetSettings) { */ private suspend fun getSettingsCheck(settings: GetSettings): SettingsCheck = withContext(Dispatchers.IO) { - val appVersionCode = 1//?: (BuildConfig.VERSION_CODE) + val appVersionCode = 1 // ?: (BuildConfig.VERSION_CODE) val loginSkipped = settings.loginSkipped.firstOrNull() val onBoardingShown = settings.onBoardingShown.firstOrNull() val savedVersionCode = settings.savedVersionCode.firstOrNull() ?: appVersionCode diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt index 3a1782f..3cef2a4 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/NavHelpers.kt @@ -24,7 +24,7 @@ internal object NavHelpers { * 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?) { + fun NavHostController.navigateNavBarElements(route: T, startDestination: V?) { navigate(route) { popUpTo(graph.findStartDestination().id) { saveState = true diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoardingDestination.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoarding.kt rename to composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/OnBoardingDestination.kt diff --git a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsDestination.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/Settings.kt rename to composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/destinations/SettingsDestination.kt 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 index 082c8fc..a9a193f 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/navhost/AppNavHost.kt @@ -69,7 +69,7 @@ fun AppNavHost(settingsCheck: State, modifier: Modifier = Modifi val mainPadding = PaddingValues( bottom = Dimens.ExtraLargePadding.size + Dimens.MediumPadding.size + - WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() ) // Haze blur effect 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 index c066ef7..dc040e9 100644 --- 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 @@ -63,7 +63,6 @@ internal fun DashboardNavHost( mainScaffoldPadding: PaddingValues, navController: NavHostController = rememberNavController(), ) { - val destinations = rememberDashboardTabs() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.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 index d9e75aa..d002169 100644 --- a/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt +++ b/composeApp/src/commonMain/kotlin/com/apimorlabs/reluct/navigation/toptabs/TabDestinations.kt @@ -52,8 +52,6 @@ sealed class TabDestinations(val route: T) { @Serializable data object ScreenTimeLimits : TabDestinations(ScreenTimeLimitsDestination) - - } @Composable 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 index b856c20..d81d99e 100644 --- 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 @@ -71,4 +71,4 @@ private fun HandleEvents( else -> {} } } -} \ No newline at end of file +} 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 1831b3e..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 @@ -70,7 +70,7 @@ internal class AndroidScreenTimeServices( // Check if the app doesn't violate limits val limitParamChecks = (isFocusModeOn && appLimits.isADistractingAp) || - appLimits.isPaused || appPastLimit + appLimits.isPaused || appPastLimit if (!appLimits.overridden && limitParamChecks) { ScreenTimeServices.BlockState.Blocked(appLimits.appInfo.packageName) } else { From d471659e3b9b9597fe9972ba0c1fb3800b8c5fd1 Mon Sep 17 00:00:00 2001 From: Racka Date: Sun, 6 Oct 2024 23:17:50 +0300 Subject: [PATCH 8/8] update compose version Signed-off-by: Racka --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f484a6..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"