Переглянути джерело

add(state page): add state page

zhaoyadi 1 рік тому
батько
коміт
3d2fba86ea
72 змінених файлів з 1146 додано та 720 видалено
  1. 1 0
      .idea/gradle.xml
  2. 4 3
      app/build.gradle.kts
  3. 25 15
      app/src/main/java/com/zaojiao/app/MainActivity.kt
  4. 0 40
      app/src/main/java/com/zaojiao/app/navigation/NavHost.kt
  5. 16 123
      app/src/main/java/com/zaojiao/app/ui/App.kt
  6. 1 53
      app/src/main/java/com/zaojiao/app/ui/AppState.kt
  7. 1 0
      build.gradle.kts
  8. 0 1
      built/convention/src/main/kotlin/NavigationConventionPlugin.kt
  9. 13 1
      core/auth/build.gradle.kts
  10. 1 1
      core/auth/src/main/AndroidManifest.xml
  11. 0 61
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/LoginActivity.kt
  12. 0 23
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/TokenActivity.kt
  13. 0 61
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/TokenManager.kt
  14. 35 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/AuthRepository.kt
  15. 63 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/LocalAuthData.kt
  16. 28 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/RemoteAuthData.kt
  17. 26 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/di/AuthModule.kt
  18. 0 18
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/di/TokenModule.kt
  19. 29 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthApi.kt
  20. 46 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthClient.kt
  21. 9 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthResult.kt
  22. 2 2
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/model/TokenModel.kt
  23. 34 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginPage.kt
  24. 11 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginViewModel.kt
  25. 31 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenActivity.kt
  26. 15 0
      core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenViewModel.kt
  27. 26 0
      core/common/src/main/kotlin/com/zaojiao/app/core/common/di/CommonModule.kt
  28. 5 0
      core/common/src/main/kotlin/com/zaojiao/app/core/common/extension/ListExtension.kt
  29. 0 40
      core/common/src/main/kotlin/com/zaojiao/app/core/common/pref/TokenPreferences.kt
  30. 1 1
      core/common/src/main/kotlin/com/zaojiao/app/core/common/state/DataState.kt
  31. 0 7
      core/common/src/main/kotlin/com/zaojiao/app/core/common/state/UiState.kt
  32. 3 3
      core/http/build.gradle.kts
  33. 0 14
      core/http/src/main/java/com/zaojiao/app/core/http/common/NetResult.kt
  34. 0 86
      core/http/src/main/java/com/zaojiao/app/core/http/interceptor/TokenInterceptor.kt
  35. 0 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/adapter/ColorAdapter.kt
  36. 12 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetHeaders.kt
  37. 31 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetResult.kt
  38. 0 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/converter/ResultConverterFactory.kt
  39. 4 18
      core/http/src/main/kotlin/com/zaojiao/app/core/http/di/HttpModule.kt
  40. 91 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/AuthInterceptor.kt
  41. 2 5
      core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/ResultInterceptor.kt
  42. 0 0
      core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/VersionInterceptor.kt
  43. 10 0
      core/nav/build.gradle.kts
  44. 4 0
      core/nav/src/main/AndroidManifest.xml
  45. 20 0
      core/nav/src/main/kotlin/com/zaojiao/app/core/nav/LJGNavHost.kt
  46. 108 0
      core/nav/src/main/kotlin/com/zaojiao/app/core/nav/LJGNavigator.kt
  47. 27 0
      core/nav/src/main/kotlin/com/zaojiao/app/core/nav/NavigationCommand.kt
  48. 1 0
      data/domain/build.gradle.kts
  49. 4 5
      data/domain/src/main/kotlin/com/zaojiao/app/data/domain/AccountUseCase.kt
  50. 1 1
      data/local/src/main/kotlin/com/zaojiao/app/data/local/di/LocalDataModule.kt
  51. 4 16
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteBabyData.kt
  52. 7 1
      data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteUserData.kt
  53. 1 1
      data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/BabyRepositoryImpl.kt
  54. 12 5
      data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/UserRepositoryImpl.kt
  55. 5 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Locals.kt
  56. 30 33
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/StatePage.kt
  57. 13 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Themes.kt
  58. 45 0
      feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Views.kt
  59. 3 0
      feat/home/build.gradle.kts
  60. 8 1
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/HomeDestination.kt
  61. 148 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/HomePage.kt
  62. 2 2
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/course/HomeCoursePage.kt
  63. 12 3
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/index/HomeIndexViewModel.kt
  64. 0 21
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeCourseNavigation.kt
  65. 0 26
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeIndexNavigation.kt
  66. 35 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeNavigation.kt
  67. 0 25
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomePersonalNavigation.kt
  68. 37 1
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt
  69. 7 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanState.kt
  70. 31 0
      feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanViewModel.kt
  71. 2 1
      gradle/libs.versions.toml
  72. 3 2
      settings.gradle.kts

+ 1 - 0
.idea/gradle.xml

@@ -30,6 +30,7 @@
             <option value="$PROJECT_DIR$/core/auth" />
             <option value="$PROJECT_DIR$/core/common" />
             <option value="$PROJECT_DIR$/core/http" />
+            <option value="$PROJECT_DIR$/core/nav" />
             <option value="$PROJECT_DIR$/data" />
             <option value="$PROJECT_DIR$/data/domain" />
             <option value="$PROJECT_DIR$/data/local" />

+ 4 - 3
app/build.gradle.kts

@@ -28,12 +28,15 @@ android {
 dependencies {
     implementation(project(":core:common"))
     implementation(project(":core:auth"))
+    implementation(project(":core:nav"))
 
+    implementation(project(":feat:design"))
     implementation(project(":feat:home"))
 
     implementation(project(":data:domain"))
 
-    implementation("androidx.core:core-splashscreen:1.0.0")
+    implementation("androidx.core:core-splashscreen:1.0.1")
+    implementation("androidx.startup:startup-runtime:1.1.1")
     implementation("androidx.activity:activity-compose:1.7.2")
 
     implementation("androidx.appcompat:appcompat:1.6.1")
@@ -43,8 +46,6 @@ dependencies {
 
     implementation("com.google.android.material:material:1.9.0")
 
-    api("com.alipay.sdk:alipaysdk-android:+@aar")
-
     implementation(libs.coil.kt)
     implementation(libs.coil.kt.svg)
 

+ 25 - 15
app/src/main/java/com/zaojiao/app/MainActivity.kt

@@ -1,35 +1,50 @@
 package com.zaojiao.app
 
 import android.content.Intent
-import android.graphics.Color
 import android.os.Build
 import android.os.Bundle
 import android.view.WindowManager
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
+import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.graphics.Color
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
 import com.google.accompanist.systemuicontroller.rememberSystemUiController
-import com.zaojiao.app.core.auth.TokenManager
+import com.zaojiao.app.feat.design.BackgroundTheme
+import com.zaojiao.app.feat.design.LocalBackgroundTheme
+import com.zaojiao.app.feat.design.LocalOnFinishDispatcher
 import com.zaojiao.app.ui.App
 import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
+
+private val LightBackgroundTheme = BackgroundTheme(color = Color.White)
+
+private val DarkBackgroundTheme = BackgroundTheme(color = Color.Black)
 
 @AndroidEntryPoint
 class MainActivity : ComponentActivity() {
-    @Inject
-    lateinit var tokenManager: TokenManager
-
     private lateinit var windowInsetsController: WindowInsetsControllerCompat
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        tokenManager.attach(this)
         configureSystemBar()
         setContent {
             val systemUiController = rememberSystemUiController()
-            MaterialTheme { App() }
+
+            val backgroundTheme = if (isSystemInDarkTheme()) {
+                DarkBackgroundTheme
+            } else {
+                LightBackgroundTheme
+            }
+
+            CompositionLocalProvider(
+                LocalBackgroundTheme provides backgroundTheme,
+                LocalOnFinishDispatcher provides { finish() }
+            ) {
+                MaterialTheme { App() }
+            }
         }
     }
 
@@ -38,8 +53,8 @@ class MainActivity : ComponentActivity() {
         windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
 
         window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
-        window.statusBarColor = Color.TRANSPARENT
-        window.navigationBarColor = Color.TRANSPARENT
+        window.statusBarColor = android.graphics.Color.TRANSPARENT
+        window.navigationBarColor = android.graphics.Color.TRANSPARENT
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
             window.addFlags(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
@@ -48,9 +63,4 @@ class MainActivity : ComponentActivity() {
             show(WindowInsetsCompat.Type.systemBars())
         }
     }
-
-    override fun onDestroy() {
-        super.onDestroy()
-        tokenManager.detach()
-    }
 }

+ 0 - 40
app/src/main/java/com/zaojiao/app/navigation/NavHost.kt

@@ -1,40 +0,0 @@
-package com.zaojiao.app.navigation
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.navigation.compose.NavHost
-import com.zaojiao.app.feat.home.navigation.homeCoursePage
-import com.zaojiao.app.feat.home.navigation.homeIndex
-import com.zaojiao.app.feat.home.navigation.homeIndexPage
-import com.zaojiao.app.feat.home.navigation.homePersonalPage
-import com.zaojiao.app.ui.AppState
-
-/**
- * Top-level navigation graph. Navigation is organized as explained at
- * https://d.android.com/jetpack/compose/nav-adaptive
- *
- * The navigation graph defined in this file defines the different top level routes. Navigation
- * within each route is handled using state and Back Handlers.
- */
-@Composable
-fun NavHost(
-    appState: AppState,
-    modifier: Modifier = Modifier,
-    startDestination: String = homeIndex,
-) {
-    val navController = appState.navController
-    NavHost(
-        navController = navController,
-        startDestination = startDestination,
-        modifier = modifier,
-    ) {
-        homeIndexPage()
-
-        homeCoursePage(
-            onBannerClick = {},
-            onCourseClick = {},
-        )
-
-        homePersonalPage()
-    }
-}

+ 16 - 123
app/src/main/java/com/zaojiao/app/ui/App.kt

@@ -1,138 +1,31 @@
 package com.zaojiao.app.ui
 
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.testTagsAsResourceId
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavDestination
-import androidx.navigation.NavDestination.Companion.hierarchy
-import com.zaojiao.app.navigation.HomeDestination
-import com.zaojiao.app.navigation.NavHost
+import androidx.compose.runtime.LaunchedEffect
+import androidx.navigation.compose.composable
+import com.zaojiao.app.core.auth.ui.LoginPage
+import com.zaojiao.app.core.nav.LJGNavHost
+import com.zaojiao.app.core.nav.LJGNavigator
+import com.zaojiao.app.feat.home.HomePage
 
-@OptIn(
-    ExperimentalMaterial3Api::class,
-    ExperimentalLayoutApi::class,
-    ExperimentalComposeUiApi::class,
-)
 @Composable
 fun App(
     appState: AppState = rememberAppState()
 ) {
-    val snackbarHostState = remember { SnackbarHostState() }
-
-    Scaffold(
-        modifier = Modifier.semantics {
-            testTagsAsResourceId = true
-        },
-        containerColor = Color.Transparent,
-        contentColor = MaterialTheme.colorScheme.onBackground,
-        contentWindowInsets = WindowInsets(0, 0, 0, 0),
-        snackbarHost = { SnackbarHost(snackbarHostState) },
-        bottomBar = {
-            AppBottomBar(
-                destinations = appState.homeDestinations,
-                onNavigateToDestination = appState::navigateToHomeDestination,
-                currentDestination = appState.currentDestination,
-                modifier = Modifier.testTag("NiaBottomBar"),
-            )
-        },
-    ) { padding ->
-        Box(modifier = Modifier
-            .fillMaxSize()
-            .padding(padding)
-            .consumeWindowInsets(padding)) {
-            NavHost(appState = appState)
-        }
+    LaunchedEffect(appState) {
+        LJGNavigator.handleNavigationCommands(appState.navController)
     }
-}
-
-@Composable
-private fun AppBottomBar(
-    destinations: List<HomeDestination>,
-    onNavigateToDestination: (HomeDestination) -> Unit,
-    currentDestination: NavDestination?,
-    modifier: Modifier = Modifier,
-) {
-    MaterialTheme.colorScheme.onSurfaceVariant
 
-    NavigationBar(
-        modifier = modifier,
-        contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
-        tonalElevation = 0.dp,
+    LJGNavHost(
+        navHostController = appState.navController,
+        startDestination = "/"
     ) {
-        destinations.forEach { destination ->
-            val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
-
-            NavigationBarItem(
-                selected = selected,
-                onClick = { onNavigateToDestination(destination) },
-                enabled = true,
-                icon = {
-                    if (selected) {
-                        Icon(
-                            imageVector = destination.selectedIcon,
-                            contentDescription = null,
-                        )
-                    } else {
-                        Icon(
-                            imageVector = destination.unselectedIcon,
-                            contentDescription = null,
-                        )
-                    }
-                },
-                alwaysShowLabel = true,
-                label = { Text(text = destination.iconTitle) },
-                modifier = Modifier.notificationDot(),
-            )
+        composable(route = "/") {
+            HomePage()
         }
-    }
-}
 
-private fun Modifier.notificationDot(): Modifier =
-    composed {
-        val tertiaryColor = MaterialTheme.colorScheme.tertiary
-        drawWithContent {
-            drawContent()
-            drawCircle(
-                tertiaryColor,
-                radius = 5.dp.toPx(),
-                // This is based on the dimensions of the NavigationBar's "indicator pill";
-                // however, its parameters are private, so we must depend on them implicitly
-                // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
-                center = center + Offset(
-                    64.dp.toPx() * .45f,
-                    32.dp.toPx() * -.45f - 6.dp.toPx(),
-                ),
-            )
+        composable(route = "/login/sms") {
+            LoginPage()
         }
     }
-
-private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: HomeDestination) =
-    this?.hierarchy?.any {
-        it.route?.contains(destination.name, true) ?: false
-    } ?: false
+}

+ 1 - 53
app/src/main/java/com/zaojiao/app/ui/AppState.kt

@@ -4,21 +4,8 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.tracing.Trace
-import androidx.navigation.NavDestination
-import androidx.navigation.NavGraph.Companion.findStartDestination
 import androidx.navigation.NavHostController
-import androidx.navigation.compose.currentBackStackEntryAsState
 import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navOptions
-import androidx.tracing.trace
-import com.zaojiao.app.navigation.HomeDestination
-import com.zaojiao.app.feat.home.navigation.homeCourse
-import com.zaojiao.app.feat.home.navigation.homeIndex
-import com.zaojiao.app.feat.home.navigation.homePersonal
-import com.zaojiao.app.feat.home.navigation.navigateToHomeCourse
-import com.zaojiao.app.feat.home.navigation.navigateToHomeIndex
-import com.zaojiao.app.feat.home.navigation.navigateToHomePersonal
 import kotlinx.coroutines.CoroutineScope
 
 @Composable
@@ -41,43 +28,4 @@ fun rememberAppState(
 class AppState(
     val navController: NavHostController,
     val coroutineScope: CoroutineScope,
-) {
-    val currentDestination: NavDestination?
-        @Composable get() = navController
-            .currentBackStackEntryAsState().value?.destination
-
-    val currentHomeDestination: HomeDestination?
-        @Composable get() = when (currentDestination?.route) {
-            homeIndex -> HomeDestination.INDEX
-            homeCourse -> HomeDestination.COURSE
-            homePersonal -> HomeDestination.PERSONAL
-            else -> null
-        }
-
-    val homeDestinations: List<HomeDestination> = HomeDestination.values().asList()
-
-
-    fun navigateToHomeDestination(destination: HomeDestination) {
-        trace("Navigation: ${destination.name}") {
-            val topLevelNavOptions = navOptions {
-                // Pop up to the start destination of the graph to
-                // avoid building up a large stack of destinations
-                // on the back stack as users select items
-                popUpTo(navController.graph.findStartDestination().id) {
-                    saveState = true
-                }
-                // Avoid multiple copies of the same destination when
-                // reselecting the same item
-                launchSingleTop = true
-                // Restore state when reselecting a previously selected item
-                restoreState = true
-            }
-
-            when (destination) {
-                HomeDestination.INDEX -> navController.navigateToHomeIndex(topLevelNavOptions)
-                HomeDestination.COURSE -> navController.navigateToHomeCourse(topLevelNavOptions)
-                HomeDestination.PERSONAL -> navController.navigateToHomePersonal(topLevelNavOptions)
-            }
-        }
-    }
-}
+) {}

+ 1 - 0
build.gradle.kts

@@ -26,6 +26,7 @@ plugins {
     id("com.google.dagger.hilt.android").version("2.44.2").apply(false)
     id("com.google.protobuf").version("0.9.3").apply(false)
     id("org.jetbrains.kotlin.plugin.serialization").version("1.8.21").apply(false)
+    alias(libs.plugins.ksp).apply(false)
 }
 
 subprojects {

+ 0 - 1
built/convention/src/main/kotlin/NavigationConventionPlugin.kt

@@ -14,7 +14,6 @@ class NavigationConventionPlugin : Plugin<Project> {
                 add("implementation", "androidx.navigation:navigation-ui-ktx:2.6.0")
                 add("implementation", "androidx.navigation:navigation-compose:2.6.0")
                 add("implementation", "androidx.hilt:hilt-navigation-compose:1.0.0")
-                add("implementation", "androidx.hilt:hilt-navigation-fragment:1.0.0")
                 add("kapt", "androidx.hilt:hilt-compiler:1.0.0")
             }
         }

+ 13 - 1
core/auth/build.gradle.kts

@@ -1,7 +1,10 @@
 plugins {
     id("d.convention.library")
     id("d.convention.coroutines")
+    id("d.convention.compose")
     id("d.convention.hilt")
+    id("d.convention.lifecycle")
+    id("d.convention.navigation")
 }
 
 android {
@@ -13,9 +16,18 @@ dependencies {
 
     implementation("androidx.appcompat:appcompat:1.6.1")
     implementation("com.google.android.material:material:1.9.0")
+    implementation("androidx.startup:startup-runtime:1.1.1")
 
     implementation("com.squareup.okhttp3:okhttp:4.10.0")
+    implementation("com.squareup.retrofit2:retrofit:2.9.0")
+    implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
+    implementation("com.squareup.moshi:moshi:1.15.0")
+    implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
 
-    implementation("androidx.datastore:datastore:1.0.0")
+    implementation("com.alibaba.fastjson2:fastjson2-kotlin:2.0.35")
+    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.21")
+    implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.21")
+
+    implementation("androidx.datastore:datastore-preferences:1.0.0")
     implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
 }

+ 1 - 1
core/auth/src/main/AndroidManifest.xml

@@ -5,7 +5,7 @@
         <activity android:name=".LoginActivity" />
 
         <activity
-            android:name=".TokenActivity"
+            android:name=".ui.TokenActivity"
             android:exported="true">
             <intent-filter>
                 <data

+ 0 - 61
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/LoginActivity.kt

@@ -1,61 +0,0 @@
-package com.zaojiao.app.core.auth
-
-import android.content.Intent
-import android.os.Bundle
-import android.util.JsonReader
-import android.widget.Button
-import android.widget.EditText
-import androidx.appcompat.app.AppCompatActivity
-import kotlinx.serialization.json.Json
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.OkHttp
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import org.json.JSONObject
-import java.io.IOException
-import java.util.concurrent.TimeUnit
-import kotlin.concurrent.thread
-
-class LoginActivity : AppCompatActivity() {
-    private lateinit var editPhone: EditText
-    private lateinit var editSms: EditText
-    private lateinit var actionLogin: Button
-
-    private val okHttpClient = OkHttpClient.Builder()
-        .connectTimeout(30000, TimeUnit.MILLISECONDS)
-        .readTimeout(30000, TimeUnit.MILLISECONDS)
-        .writeTimeout(30000, TimeUnit.MILLISECONDS)
-        .retryOnConnectionFailure(true)
-        .followRedirects(false)
-        .followSslRedirects(false)
-        .build()
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.layout_login)
-
-        editPhone = findViewById(R.id.edit_phone)
-        editSms = findViewById(R.id.edit_sms)
-        actionLogin = findViewById(R.id.action_login)
-
-        actionLogin.setOnClickListener {
-            val phone = "15141666708"
-            val sms = "5566"
-
-            val request = Request.Builder()
-                .url("https://open.test.luojigou.vip/app/sms/v2/checkCode/app/$phone/$sms")
-                .get()
-                .build()
-            thread {
-                Thread.sleep(3000)
-
-                Intent(this@LoginActivity, TokenActivity::class.java).apply {
-                    startActivity(this)
-                    finish()
-                }
-            }
-        }
-    }
-}

+ 0 - 23
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/TokenActivity.kt

@@ -1,23 +0,0 @@
-package com.zaojiao.app.core.auth
-
-import android.app.Activity
-import android.os.Bundle
-import com.zaojiao.app.core.common.pref.saveAccessToken
-import dagger.hilt.EntryPoint
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-class TokenActivity : Activity() {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        TokenManager.updateToken(
-            accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMzY0ODM1MjU0MDY0ODgxNjY1IiwiZXhwIjoxNjg5MzAxNTEzfQ.ddtv_bU3LbyBzvAqrKCeYM0UCUb_37WPL7nfk25wmsE",
-            refreshToken = "",
-        )
-
-        saveAccessToken("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMzY0ODM1MjU0MDY0ODgxNjY1IiwiZXhwIjoxNjg5MzAxNTEzfQ.ddtv_bU3LbyBzvAqrKCeYM0UCUb_37WPL7nfk25wmsE")
-
-        finish()
-    }
-}

+ 0 - 61
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/TokenManager.kt

@@ -1,61 +0,0 @@
-package com.zaojiao.app.core.auth
-
-import android.app.Activity
-import android.content.Intent
-import java.lang.ref.WeakReference
-import java.util.concurrent.CompletableFuture
-
-object TokenManager {
-    const val ACCESS_TOKEN = "extra_token"
-    const val REFRESH_TOKEN = "extra_token"
-
-    val lock: Any = Any()
-
-    private var activity: WeakReference<Activity?> = WeakReference(null)
-
-    private var tokenFuture: CompletableFuture<String>? = null
-
-    fun attach(activity: Activity) {
-        this.activity = WeakReference(activity)
-    }
-
-    fun detach() {
-        this.activity.clear()
-    }
-
-
-    internal fun putToken(intent: Intent, accessToken: String, refreshToken: String) {
-        intent.putExtra(ACCESS_TOKEN, accessToken)
-        intent.putExtra(REFRESH_TOKEN, refreshToken)
-    }
-
-    @Throws(IllegalAccessException::class)
-    internal fun fromIntent(intent: Intent): TokenState {
-        val accessToken = intent.getStringExtra(ACCESS_TOKEN) ?: throw IllegalAccessException("")
-        val refreshToken = intent.getStringExtra(REFRESH_TOKEN) ?: throw IllegalAccessException("")
-
-        return TokenState(
-            accessToken = accessToken,
-            refreshToken = refreshToken,
-        )
-    }
-
-    fun requestToken(): CompletableFuture<String> {
-        return tokenFuture ?: CompletableFuture<String>().apply {
-            tokenFuture = this
-            activity.get()?.apply {
-                val intent = Intent(this, LoginActivity::class.java)
-                startActivity(intent)
-            }
-            return this
-        }
-    }
-
-    internal fun updateToken(accessToken: String, refreshToken: String) {
-        tokenFuture?.complete(accessToken)
-    }
-
-    fun deleteToken() {
-
-    }
-}

+ 35 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/AuthRepository.kt

@@ -0,0 +1,35 @@
+package com.zaojiao.app.core.auth.data
+
+import com.zaojiao.app.core.auth.model.TokenModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+
+class AuthRepository constructor(
+    private val localAuthData: LocalAuthData,
+    private val remoteTokenData: RemoteAuthData,
+) {
+    val token: Flow<TokenModel?> = localAuthData.token
+
+    suspend fun updateToken(token: TokenModel) {
+        localAuthData.updateToken(token)
+    }
+
+    suspend fun refreshToken(): TokenModel? {
+        val current = token.first()
+        return if (current == null) {
+            null
+        } else {
+            val token = remoteTokenData.refreshToken(current.refreshToken)
+            if (token == null) {
+                localAuthData.clearToken()
+            } else {
+                localAuthData.updateToken(token)
+            }
+            return token
+        }
+    }
+
+    suspend fun clearToken() {
+        localAuthData.clearToken()
+    }
+}

+ 63 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/LocalAuthData.kt

@@ -0,0 +1,63 @@
+package com.zaojiao.app.core.auth.data
+
+import android.content.Context
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStoreFile
+import com.zaojiao.app.core.auth.model.TokenModel
+import com.zaojiao.app.core.common.remote.AppDispatchers
+import com.zaojiao.app.core.common.remote.Dispatcher
+import com.zaojiao.app.core.common.remote.di.ApplicationScope
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class LocalAuthData @Inject constructor(
+    @ApplicationContext context: Context,
+    @Dispatcher(AppDispatchers.IO) ioDispatcher: CoroutineDispatcher,
+    @ApplicationScope scope: CoroutineScope,
+) {
+    private val tokenPreferences = PreferenceDataStoreFactory.create(
+        scope = CoroutineScope(scope.coroutineContext + ioDispatcher),
+    ) {
+        context.preferencesDataStoreFile("token")
+    }
+
+    private val accessTokenKey = stringPreferencesKey("access_token")
+    private val refreshTokenKey = stringPreferencesKey("refreshToken")
+
+    val token = tokenPreferences.data.map {
+        it.run {
+            val accessToken = this[accessTokenKey]
+            val refreshToken = this[refreshTokenKey]
+
+            return@map if (accessToken == null) {
+                null
+            } else {
+                if (refreshToken == null) {
+                    clearToken()
+                    null
+                } else {
+                    TokenModel(
+                        accessToken = accessToken,
+                        refreshToken = refreshToken,
+                    )
+                }
+            }
+        }
+    }
+
+    suspend fun updateToken(token: TokenModel) {
+        tokenPreferences.edit {
+            it[accessTokenKey] = token.accessToken
+            it[refreshTokenKey] = token.refreshToken
+        }
+    }
+
+    suspend fun clearToken() {
+        tokenPreferences.edit { token -> token.clear() }
+    }
+}

+ 28 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/data/RemoteAuthData.kt

@@ -0,0 +1,28 @@
+package com.zaojiao.app.core.auth.data
+
+import com.zaojiao.app.core.auth.http.AuthClient
+import com.zaojiao.app.core.auth.model.TokenModel
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RemoteAuthData @Inject constructor(
+    private val authClient: AuthClient,
+) {
+    suspend fun loginBySms(phone: String, code: String) {
+        authClient.loginBySms(phone, code)
+    }
+
+    suspend fun loginByWechat(code: String) {
+        authClient.loginByWechat(code)
+    }
+
+    suspend fun loginByOneClick(code: String) {
+        authClient.loginByOneClick(code)
+    }
+
+    suspend fun refreshToken(refreshToken: String): TokenModel? {
+        val result = authClient.refreshToken(refreshToken)
+        return result.data
+    }
+}

+ 26 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/di/AuthModule.kt

@@ -0,0 +1,26 @@
+package com.zaojiao.app.core.auth.di
+
+import com.zaojiao.app.core.auth.data.LocalAuthData
+import com.zaojiao.app.core.auth.data.RemoteAuthData
+import com.zaojiao.app.core.auth.data.AuthRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AuthModule {
+    @Provides
+    @Singleton
+    fun provideTokenRepository(
+        localAuthData: LocalAuthData,
+        remoteTokenData: RemoteAuthData,
+    ): AuthRepository {
+        return AuthRepository(
+            localAuthData = localAuthData,
+            remoteTokenData = remoteTokenData,
+        )
+    }
+}

+ 0 - 18
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/di/TokenModule.kt

@@ -1,18 +0,0 @@
-package com.zaojiao.app.core.auth.di
-
-import com.zaojiao.app.core.auth.TokenManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-class TokenModule {
-    @Provides
-    @Singleton
-    fun provideTokenManager(): TokenManager {
-        return TokenManager
-    }
-}

+ 29 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthApi.kt

@@ -0,0 +1,29 @@
+package com.zaojiao.app.core.auth.http
+
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+interface AuthApi {
+    @POST("/app/sms/v2/checkCode/app/{phone}/{code}")
+    suspend fun loginBySms(
+        @Path(value = "phone") phone: String,
+        @Path(value = "code") code: String,
+    ): AuthResult
+
+    @GET("/app/login/v2/{code}")
+    suspend fun loginByWechat(
+        @Path(value = "code") code: String,
+    ): AuthResult
+
+    @POST("/app/login/oneClickLogin")
+    suspend fun loginByOneClick(
+        @Body token: String,
+    ): AuthResult
+
+    @POST("app/login/refreshToken/{refreshToken}")
+    suspend fun refreshToken(
+        @Path(value = "refreshToken") refreshToken: String,
+    ): AuthResult
+}

+ 46 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthClient.kt

@@ -0,0 +1,46 @@
+package com.zaojiao.app.core.auth.http
+
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Singleton
+class AuthClient @Inject constructor(
+    @Named("timeout") val timeout: Long,
+    @Named("endpoint") val endpoint: String,
+) {
+    private val moshi = Moshi.Builder()
+        .add(KotlinJsonAdapterFactory())
+        .build()
+
+    private val okHttpClient = OkHttpClient.Builder()
+        .connectTimeout(timeout, TimeUnit.MILLISECONDS)
+        .readTimeout(timeout, TimeUnit.MILLISECONDS)
+        .writeTimeout(timeout, TimeUnit.MILLISECONDS)
+        .retryOnConnectionFailure(true)
+        .followRedirects(false)
+        .followSslRedirects(false)
+        .build()
+
+    private val retrofit = Retrofit.Builder()
+        .client(okHttpClient)
+        .baseUrl(endpoint)
+        .addConverterFactory(MoshiConverterFactory.create(moshi))
+        .build()
+
+    private val api = retrofit.create(AuthApi::class.java)
+
+    suspend fun loginBySms(phone: String, code: String) = api.loginBySms(phone, code)
+
+    suspend fun loginByWechat(code: String) = api.loginByWechat(code)
+
+    suspend fun loginByOneClick(token: String) = api.loginByOneClick(token)
+
+    suspend fun refreshToken(refreshToken: String) = api.refreshToken(refreshToken)
+}

+ 9 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/http/AuthResult.kt

@@ -0,0 +1,9 @@
+package com.zaojiao.app.core.auth.http
+
+import com.zaojiao.app.core.auth.model.TokenModel
+
+data class AuthResult(
+    val status: Int,
+    val data: TokenModel?,
+    val msg: String?,
+)

+ 2 - 2
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/TokenState.kt → core/auth/src/main/kotlin/com/zaojiao/app/core/auth/model/TokenModel.kt

@@ -1,9 +1,9 @@
-package com.zaojiao.app.core.auth
+package com.zaojiao.app.core.auth.model
 
 import kotlinx.serialization.Serializable
 
 @Serializable
-data class TokenState(
+data class TokenModel(
     val accessToken: String,
     val refreshToken: String,
 )

+ 34 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginPage.kt

@@ -0,0 +1,34 @@
+package com.zaojiao.app.core.auth.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+
+@Composable
+fun LoginPage(
+    loginViewModel: LoginViewModel = hiltViewModel(),
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxSize()
+            .statusBarsPadding(),
+    ) {
+        CloseButton()
+
+    }
+}
+
+@Composable
+fun CloseButton() {
+
+    Icon(
+        imageVector = Icons.Filled.Close,
+        contentDescription = "关闭按钮",
+    )
+}

+ 11 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/LoginViewModel.kt

@@ -0,0 +1,11 @@
+package com.zaojiao.app.core.auth.ui
+
+import androidx.lifecycle.ViewModel
+import com.zaojiao.app.core.auth.data.AuthRepository
+import javax.inject.Inject
+
+class LoginViewModel @Inject constructor(
+    private val authRepository: AuthRepository,
+) : ViewModel() {
+
+}

+ 31 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenActivity.kt

@@ -0,0 +1,31 @@
+package com.zaojiao.app.core.auth.ui
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.zaojiao.app.core.auth.model.TokenModel
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class TokenActivity : AppCompatActivity() {
+
+    @Inject
+    public lateinit var tokenViewModel: TokenViewModel
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val tokenModel = TokenModel(
+            accessToken = "",
+            refreshToken = "",
+        )
+
+        runBlocking {
+            tokenViewModel.saveToken(tokenModel)
+        }
+
+        finish()
+    }
+}

+ 15 - 0
core/auth/src/main/kotlin/com/zaojiao/app/core/auth/ui/TokenViewModel.kt

@@ -0,0 +1,15 @@
+package com.zaojiao.app.core.auth.ui
+
+import androidx.lifecycle.ViewModel
+import com.zaojiao.app.core.auth.data.AuthRepository
+import com.zaojiao.app.core.auth.model.TokenModel
+import javax.inject.Inject
+
+class TokenViewModel @Inject constructor(
+    private val authRepository: AuthRepository,
+) : ViewModel() {
+
+    suspend fun saveToken(tokenModel: TokenModel) {
+        authRepository.updateToken(tokenModel)
+    }
+}

+ 26 - 0
core/common/src/main/kotlin/com/zaojiao/app/core/common/di/CommonModule.kt

@@ -0,0 +1,26 @@
+package com.zaojiao.app.core.common.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object CommonModule {
+    @Provides
+    @Singleton
+    @Named("timeout")
+    fun provideTimeout(): Long {
+        return 30 * 1000
+    }
+
+    @Provides
+    @Singleton
+    @Named("endpoint")
+    fun provideEndpoint(): String {
+        return "https://open.test.luojigou.vip"
+    }
+}

+ 5 - 0
core/common/src/main/kotlin/com/zaojiao/app/core/common/extension/ListExtension.kt

@@ -0,0 +1,5 @@
+package com.zaojiao.app.core.common.extension
+
+fun <T> List<T>?.nonNull(): List<T> {
+    return this ?: emptyList()
+}

+ 0 - 40
core/common/src/main/kotlin/com/zaojiao/app/core/common/pref/TokenPreferences.kt

@@ -1,40 +0,0 @@
-package com.zaojiao.app.core.common.pref
-
-import android.content.Context
-import android.content.SharedPreferences
-
-class TokenPreferences {
-}
-
-private const val fileName = "token_prefs"
-
-private const val accessTokenKey = "access_token"
-private const val refreshTokenKey = "refresh_token"
-
-private fun Context.tokenPreferences(): SharedPreferences {
-    return this.getSharedPreferences(fileName, Context.MODE_PRIVATE)
-}
-
-fun Context.getAccessToken(): String? {
-    return tokenPreferences().getString(accessTokenKey, null)
-}
-
- fun Context.getRefreshToken(): String? {
-    return tokenPreferences().getString(refreshTokenKey, null)
-}
-
- fun Context.saveAccessToken(accessToken: String) {
-    tokenPreferences().edit().putString(accessTokenKey, accessToken).apply()
-}
-
- fun Context.saveRefreshToken(refreshToken: String) {
-    tokenPreferences().edit().putString(refreshTokenKey, refreshToken).apply()
-}
-
- fun Context.clearAccessToken() {
-    tokenPreferences().edit().putString(accessTokenKey, null).apply()
-}
-
- fun Context.clearRefreshToken() {
-    tokenPreferences().edit().putString(refreshTokenKey, null).apply()
-}

+ 1 - 1
core/common/src/main/kotlin/com/zaojiao/app/core/common/state/DataState.kt

@@ -1,7 +1,7 @@
 package com.zaojiao.app.core.common.state
 
 sealed interface DataState<T> {
-    data class Success<T>(val data: T) : DataState<T>
+    data class Success<T>(val data: T?) : DataState<T>
 
     data class Failure<T>(val throwable: Throwable) : DataState<T>
 }

+ 0 - 7
core/common/src/main/kotlin/com/zaojiao/app/core/common/state/UiState.kt

@@ -1,7 +0,0 @@
-package com.zaojiao.app.core.common.state
-
-sealed interface UiState {
-    object Loading : UiState
-    object Success : UiState
-    object Failure : UiState
-}

+ 3 - 3
core/http/build.gradle.kts

@@ -19,7 +19,7 @@ dependencies {
 
     api("com.squareup.retrofit2:retrofit:2.9.0")
     api("com.squareup.retrofit2:converter-moshi:2.9.0")
-    api("com.squareup.moshi:moshi:1.8.0")
-    api("com.squareup.moshi:moshi-kotlin:1.8.0")
-    api("com.squareup.moshi:moshi-adapters:1.8.0")
+    api("com.squareup.moshi:moshi:1.15.0")
+    api("com.squareup.moshi:moshi-kotlin:1.15.0")
+    api("com.squareup.moshi:moshi-adapters:1.14.0")
 }

+ 0 - 14
core/http/src/main/java/com/zaojiao/app/core/http/common/NetResult.kt

@@ -1,14 +0,0 @@
-package com.zaojiao.app.core.http.common
-
-import com.squareup.moshi.Json
-
-data class NetResult<out T>(
-    val status: Int,
-    val data: T?,
-    @Json(name = "msg")
-    val message: String?,
-)
-
-fun NetResult<*>.isSuccess(): Boolean {
-    return status == 200
-}

+ 0 - 86
core/http/src/main/java/com/zaojiao/app/core/http/interceptor/TokenInterceptor.kt

@@ -1,86 +0,0 @@
-package com.zaojiao.app.core.http.interceptor
-
-import android.content.Context
-import android.util.Log
-import com.squareup.moshi.JsonAdapter
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.Types
-import com.zaojiao.app.core.auth.TokenManager
-import com.zaojiao.app.core.common.pref.clearAccessToken
-import com.zaojiao.app.core.common.pref.getAccessToken
-import com.zaojiao.app.core.common.pref.getRefreshToken
-import com.zaojiao.app.core.common.pref.saveAccessToken
-import com.zaojiao.app.core.http.common.NetResult
-import com.zaojiao.app.core.http.common.isSuccess
-import dagger.hilt.android.qualifiers.ApplicationContext
-import okhttp3.Interceptor
-import okhttp3.Response
-import javax.inject.Inject
-
-class TokenInterceptor @Inject constructor(
-    private val moshi: Moshi,
-    private val tokenManager: TokenManager,
-    @ApplicationContext private val context: Context,
-) : Interceptor {
-    private var jsonAdapter: JsonAdapter<NetResult<String>>
-
-    private var accessToken: String? = context.getAccessToken()
-    private var refreshToken: String? = context.getRefreshToken()
-
-    private var lock: Boolean = false
-
-    init {
-        val types = Types.newParameterizedType(
-            NetResult::class.java,
-            String::class.java,
-        )
-        jsonAdapter = moshi.adapter(types)
-    }
-
-    override fun intercept(chain: Interceptor.Chain): Response {
-        val newRequest = chain.request().newBuilder()
-        accessToken?.let { newRequest.addHeader("token", it) }
-        val response = chain.proceed(newRequest.build())
-
-        when (response.code) {
-            401 -> {
-                synchronized(tokenManager.lock) {
-                    if (response.request.header("token") == accessToken) {
-                        requestToken()
-                    }
-
-                    accessToken?.let {
-                        val retryRequest = chain.request().newBuilder()
-                        retryRequest.addHeader("token", it)
-                        return chain.proceed(retryRequest.build())
-                    }
-                }
-            }
-
-            403 -> {
-                synchronized(tokenManager.lock) {
-                    if (response.request.header("token") == accessToken) {
-                        //                    refreshToken()
-                        requestToken()
-                    }
-
-                    accessToken?.let {
-                        val retryRequest = chain.request().newBuilder()
-                        retryRequest.addHeader("token", it)
-                        return chain.proceed(retryRequest.build())
-                    }
-                }
-            }
-        }
-
-        return response
-    }
-
-    private fun requestToken() {
-        accessToken = tokenManager.requestToken().get()
-    }
-
-    private fun refreshToken() {
-
-    }
-}

+ 0 - 0
core/http/src/main/java/com/zaojiao/app/core/http/adapter/ColorAdapter.kt → core/http/src/main/kotlin/com/zaojiao/app/core/http/adapter/ColorAdapter.kt


+ 12 - 0
core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetHeaders.kt

@@ -0,0 +1,12 @@
+package com.zaojiao.app.core.http.common
+
+object NetHeaders {
+    internal const val KEY = "@"
+
+    internal const val TOKEN_TYPE_NO = "no-token"
+
+    internal const val TOKEN_TYPE_NULL = "null-token"
+
+    const val NO_TOKEN = "$KEY: $TOKEN_TYPE_NO"
+    const val NULL_TOKEN = "$KEY: $TOKEN_TYPE_NULL"
+}

+ 31 - 0
core/http/src/main/kotlin/com/zaojiao/app/core/http/common/NetResult.kt

@@ -0,0 +1,31 @@
+package com.zaojiao.app.core.http.common
+
+import com.squareup.moshi.Json
+import com.zaojiao.app.core.common.state.DataState
+
+data class NetResult<out T>(
+    val status: Int,
+    val data: T?,
+    @Json(name = "msg")
+    val message: String?,
+)
+
+fun NetResult<*>.isSuccess(): Boolean {
+    return status == 200
+}
+
+suspend fun <T> tryRequest(action: suspend () -> DataState<T>): DataState<T> {
+    return try {
+        action.invoke()
+    } catch (e: Throwable) {
+        DataState.Failure(e)
+    }
+}
+
+fun <T> NetResult<T>.toDataState(): DataState<T> {
+    return if (this.isSuccess()) {
+        DataState.Success(this.data)
+    } else {
+        DataState.Failure(RuntimeException(this.message))
+    }
+}

+ 0 - 0
core/http/src/main/java/com/zaojiao/app/core/http/converter/ResultConverterFactory.kt → core/http/src/main/kotlin/com/zaojiao/app/core/http/converter/ResultConverterFactory.kt


+ 4 - 18
core/http/src/main/java/com/zaojiao/app/core/http/di/HttpModule.kt → core/http/src/main/kotlin/com/zaojiao/app/core/http/di/HttpModule.kt

@@ -3,16 +3,13 @@ package com.zaojiao.app.core.http.di
 import android.content.Context
 import coil.ImageLoader
 import coil.decode.SvgDecoder
-import com.squareup.moshi.JsonAdapter
 import com.squareup.moshi.Moshi
-import com.squareup.moshi.Types
 import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
 import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
 import com.zaojiao.app.core.http.adapter.HexColorAdapter
-import com.zaojiao.app.core.http.common.NetResult
 import com.zaojiao.app.core.http.converter.ResultConverterFactory
+import com.zaojiao.app.core.http.interceptor.AuthInterceptor
 import com.zaojiao.app.core.http.interceptor.ResultInterceptor
-import com.zaojiao.app.core.http.interceptor.TokenInterceptor
 import com.zaojiao.app.core.http.interceptor.VersionInterceptor
 import dagger.Module
 import dagger.Provides
@@ -31,19 +28,7 @@ import javax.inject.Singleton
 @Module
 @InstallIn(SingletonComponent::class)
 object HttpModule {
-    @Provides
-    @Singleton
-    @Named("timeout")
-    fun provideTimeout(): Long {
-        return 30 * 1000
-    }
 
-    @Provides
-    @Singleton
-    @Named("endpoint")
-    fun provideEndpoint(): String {
-        return "https://open.test.luojigou.vip"
-    }
 
     @Provides
     @Singleton
@@ -69,7 +54,7 @@ object HttpModule {
     fun provideOkHttpClient(
         @Named("timeout") timeout: Long,
         loggingInterceptor: HttpLoggingInterceptor,
-        tokenInterceptor: TokenInterceptor,
+        authInterceptor: AuthInterceptor,
         versionInterceptor: VersionInterceptor,
         resultInterceptor: ResultInterceptor,
     ): OkHttpClient {
@@ -82,7 +67,8 @@ object HttpModule {
             .followSslRedirects(false)
             .addInterceptor(loggingInterceptor)
             .addInterceptor(versionInterceptor)
-            .addInterceptor(tokenInterceptor)
+//            .addInterceptor(tokenInterceptor)
+            .addInterceptor(authInterceptor)
             .addInterceptor(resultInterceptor)
             .build()
     }

+ 91 - 0
core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/AuthInterceptor.kt

@@ -0,0 +1,91 @@
+package com.zaojiao.app.core.http.interceptor
+
+import android.util.Log
+import com.zaojiao.app.core.auth.data.AuthRepository
+import com.zaojiao.app.core.common.remote.di.ApplicationScope
+import com.zaojiao.app.core.http.common.NetHeaders
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import javax.inject.Inject
+
+class AuthInterceptor @Inject constructor(
+    private val authRepository: AuthRepository,
+    @ApplicationScope val coroutineScope: CoroutineScope,
+) : Interceptor {
+    companion object {
+        const val CODE_UNAUTHORIZED = 401
+        const val CODE_FORBIDDEN = 403
+    }
+
+    private val mutex = Mutex()
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val request = chain.request().also { debug(1, it.toString()) }
+
+        if (NetHeaders.TOKEN_TYPE_NO in request.headers(NetHeaders.KEY)) {
+            return chain.processWithoutToken(request)
+        }
+
+        val token = runBlocking(Dispatchers.IO) {
+            authRepository.token.first()?.accessToken?.also {
+                debug(2, it)
+            }
+        }
+
+        val response = if (token == null) {
+            chain.processWithoutToken(request)
+        } else {
+            chain.processWithToken(request, token)
+        }
+
+        if (response.code != CODE_FORBIDDEN) {
+            return response
+        }
+
+        debug(3, response.toString())
+
+        val newToken: String? = runBlocking(Dispatchers.IO) {
+            mutex.withLock {
+                val maybeUpdatedToken = authRepository.token.first()
+
+                when {
+                    maybeUpdatedToken == null -> null
+                    maybeUpdatedToken.accessToken != token -> maybeUpdatedToken.accessToken
+                    else -> {
+                        authRepository.refreshToken()?.accessToken
+                    }
+                }
+            }
+        }.also {
+            debug(4, it ?: "new token is null")
+        }
+
+        return if (newToken != null) chain.processWithToken(request, newToken) else response
+    }
+
+    private fun Interceptor.Chain.processWithToken(request: Request, token: String): Response {
+        return request.newBuilder()
+            .removeHeader(NetHeaders.KEY)
+            .addHeader("token", token)
+            .build()
+            .let(this::proceed)
+    }
+
+    private fun Interceptor.Chain.processWithoutToken(request: Request): Response {
+        return request.newBuilder()
+            .removeHeader(NetHeaders.KEY)
+            .build()
+            .let(this::proceed)
+    }
+
+    private fun debug(num: Int, message: String) {
+        Log.d("AuthInterceptor", "[$num]: $message")
+    }
+}

+ 2 - 5
core/http/src/main/java/com/zaojiao/app/core/http/interceptor/ResultInterceptor.kt → core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/ResultInterceptor.kt

@@ -1,10 +1,7 @@
 package com.zaojiao.app.core.http.interceptor
 
-import com.squareup.moshi.Json
 import com.squareup.moshi.JsonAdapter
 import com.squareup.moshi.Moshi
-import com.squareup.moshi.Types
-import com.zaojiao.app.core.http.common.NetResult
 import okhttp3.Interceptor
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.Protocol
@@ -12,7 +9,7 @@ import okhttp3.Response
 import okhttp3.ResponseBody.Companion.toResponseBody
 import javax.inject.Inject
 
-private data class JsonResult(
+data class JsonResult(
     val status: Int,
     val msg: String?,
 )
@@ -21,7 +18,7 @@ class ResultInterceptor @Inject constructor(
     private val moshi: Moshi,
 ) : Interceptor {
     private var resultAdapter: JsonAdapter<JsonResult> =
-        moshi.adapter(Types.newParameterizedType(JsonResult::class.java))
+        moshi.adapter(JsonResult::class.java)
 
     override fun intercept(chain: Interceptor.Chain): Response {
         val request = chain.request()

+ 0 - 0
core/http/src/main/java/com/zaojiao/app/core/http/interceptor/VersionInterceptor.kt → core/http/src/main/kotlin/com/zaojiao/app/core/http/interceptor/VersionInterceptor.kt


+ 10 - 0
core/nav/build.gradle.kts

@@ -0,0 +1,10 @@
+plugins {
+    id("d.convention.library")
+    id("d.convention.compose")
+    id("d.convention.navigation")
+    id("d.convention.hilt")
+}
+
+android {
+    namespace = "com.zaojiao.app.core.nav"
+}

+ 4 - 0
core/nav/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>

+ 20 - 0
core/nav/src/main/kotlin/com/zaojiao/app/core/nav/LJGNavHost.kt

@@ -0,0 +1,20 @@
+package com.zaojiao.app.core.nav
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+
+@Composable
+fun LJGNavHost(
+    navHostController: NavHostController,
+    startDestination: String,
+    builder: NavGraphBuilder.() -> Unit
+) {
+    NavHost(
+        navController = navHostController,
+        startDestination = startDestination,
+    ) {
+        builder.invoke(this)
+    }
+}

+ 108 - 0
core/nav/src/main/kotlin/com/zaojiao/app/core/nav/LJGNavigator.kt

@@ -0,0 +1,108 @@
+package com.zaojiao.app.core.nav
+
+import androidx.navigation.NavController
+import androidx.navigation.NavOptionsBuilder
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onSubscription
+import java.util.concurrent.CompletableFuture
+
+object LJGNavigator {
+    private val navigationCommands =
+        MutableSharedFlow<NavigationCommand>(extraBufferCapacity = Int.MAX_VALUE)
+
+    private val navControllerFlow = MutableStateFlow<NavController?>(null)
+
+    private val navResultMap: HashMap<Int, CompletableFuture<*>> = HashMap()
+
+    suspend fun handleNavigationCommands(navController: NavController) {
+        navigationCommands
+            .onSubscription { navControllerFlow.value = navController }
+            .onCompletion { navControllerFlow.value = null }
+            .collect { navController.handleComposeNavigationCommand(it) }
+    }
+
+    fun navigate(
+        route: String,
+        optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null,
+        key: Int? = null,
+    ) {
+        if (key != null) {
+            val future = CompletableFuture<Any>()
+            navResultMap[key] = future
+        }
+
+        navigationCommands.tryEmit(
+            NavigationCommand.NavigateToRoute(
+                route = route,
+            )
+        )
+    }
+
+    fun navigateForResult() {
+
+    }
+
+    fun navigateUp() {
+        navigationCommands.tryEmit(NavigationCommand.NavigateUp)
+    }
+
+    private fun NavController.handleComposeNavigationCommand(navigationCommand: NavigationCommand) {
+        when (navigationCommand) {
+            is NavigationCommand.NavigateToRoute -> {
+                navigate(navigationCommand.route, navigationCommand.options)
+            }
+
+            NavigationCommand.NavigateUp -> {
+                navigateUp()
+            }
+
+            is NavigationCommand.PopUpToRoute -> popBackStack(
+                navigationCommand.route,
+                navigationCommand.inclusive
+            )
+
+            is NavigationCommand.NavigateUpWithResult<*> -> {
+                navUpWithResult(navigationCommand)
+            }
+
+            NavigationCommand.PopAll -> {
+
+            }
+
+            NavigationCommand.PopBackStack -> {
+                popBackStack()
+            }
+        }
+    }
+
+    private fun NavController.navUpWithResult(
+        navigationCommand: NavigationCommand.NavigateUpWithResult<*>
+    ) {
+        val backStackEntry =
+            navigationCommand.route?.let { getBackStackEntry(it) }
+                ?: previousBackStackEntry
+        backStackEntry?.savedStateHandle?.set(
+            navigationCommand.key,
+            navigationCommand.result
+        )
+
+        navigationCommand.route?.let {
+            popBackStack(it, false)
+        } ?: run {
+            navigateUp()
+        }
+    }
+}
+
+private fun <T, R> T.navigate(
+    route: String,
+    optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null,
+) {
+    LJGNavigator.navigate(
+        route = route,
+        optionsBuilder = optionsBuilder,
+        key = this.hashCode(),
+    )
+}

+ 27 - 0
core/nav/src/main/kotlin/com/zaojiao/app/core/nav/NavigationCommand.kt

@@ -0,0 +1,27 @@
+package com.zaojiao.app.core.nav
+
+import androidx.navigation.NavOptions
+
+sealed class NavigationCommand {
+    object NavigateUp : NavigationCommand()
+
+    object PopAll : NavigationCommand()
+
+    object PopBackStack : NavigationCommand()
+
+    data class NavigateToRoute(
+        val route: String,
+        val options: NavOptions? = null,
+    ) : NavigationCommand()
+
+    data class NavigateUpWithResult<T>(
+        val key: String,
+        val result: T,
+        val route: String? = null,
+    ) : NavigationCommand()
+
+    data class PopUpToRoute(
+        val route: String,
+        val inclusive: Boolean,
+    ) : NavigationCommand()
+}

+ 1 - 0
data/domain/build.gradle.kts

@@ -9,6 +9,7 @@ android {
 
 dependencies {
     implementation(project(":core:common"))
+    implementation(project(":core:auth"))
 
     implementation(project(":data:remote"))
     implementation(project(":data:local"))

+ 4 - 5
data/domain/src/main/kotlin/com/zaojiao/app/data/domain/AccountUseCase.kt

@@ -1,7 +1,6 @@
 package com.zaojiao.app.data.domain
 
 import android.content.Context
-import com.zaojiao.app.core.common.state.UiState
 import com.zaojiao.app.data.repo.BabyRepository
 import com.zaojiao.app.data.repo.LoginRepository
 import com.zaojiao.app.data.repo.LoginState
@@ -20,8 +19,8 @@ class AccountUseCase @Inject constructor(
     private val babyRepository: BabyRepository,
     @ApplicationContext private val context: Context,
 ) {
-    val appState: Flow<UiState> = flow {
-        emit(UiState.Loading)
+    val appState: Flow<LoginState> = flow {
+        emit(LoginState.OUT)
 
         loginRepository.state.onEach {
             try {
@@ -32,9 +31,9 @@ class AccountUseCase @Inject constructor(
                     userRepository.deleteUser()
                     babyRepository.deleteAllBaby()
                 }
-                emit(UiState.Success)
+                emit(LoginState.IN)
             } catch (throwable: Throwable) {
-                emit(UiState.Failure)
+                emit(LoginState.OUT)
             }
         }
     }

+ 1 - 1
data/local/src/main/kotlin/com/zaojiao/app/data/local/di/LocalDataModule.kt

@@ -24,7 +24,7 @@ import javax.inject.Singleton
 
 @Module
 @InstallIn(SingletonComponent::class)
-class LocalDataModule {
+class aLocalDataModule {
     /**
      *  用户信息 的本地存储
      */

+ 4 - 16
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteBabyData.kt

@@ -2,6 +2,8 @@ package com.zaojiao.app.data.remote
 
 import com.zaojiao.app.core.common.state.DataState
 import com.zaojiao.app.core.http.common.isSuccess
+import com.zaojiao.app.core.http.common.toDataState
+import com.zaojiao.app.core.http.common.tryRequest
 import com.zaojiao.app.data.model.BabyModel
 import com.zaojiao.app.data.model.BabyRelationModel
 import com.zaojiao.app.data.remote.api.BabyApi
@@ -13,24 +15,10 @@ class RemoteBabyData @Inject constructor(
     private val babyApi: BabyApi,
 ) {
     suspend fun getBabyList(): DataState<List<BabyModel>> {
-        return try {
-            val response = babyApi.getBabyList()
-            if (response.isSuccess()) {
-                DataState.Success(response.data ?: emptyList())
-            } else {
-                DataState.Failure(RuntimeException(response.message))
-            }
-        } catch (e: Throwable) {
-            DataState.Failure(e)
-        }
+        return tryRequest { babyApi.getBabyList().toDataState() }
     }
 
     suspend fun getBabyRelation(): DataState<List<BabyRelationModel>> {
-        return try {
-            val response = babyApi.getBabyRelation()
-            DataState.Success(response.data ?: emptyList())
-        } catch (e: Throwable) {
-            DataState.Failure(e)
-        }
+        return tryRequest { babyApi.getBabyRelation().toDataState() }
     }
 }

+ 7 - 1
data/remote/src/main/kotlin/com/zaojiao/app/data/remote/RemoteUserData.kt

@@ -2,6 +2,10 @@ package com.zaojiao.app.data.remote
 
 import com.zaojiao.app.core.common.remote.AppDispatchers
 import com.zaojiao.app.core.common.remote.Dispatcher
+import com.zaojiao.app.core.common.state.DataState
+import com.zaojiao.app.core.http.common.toDataState
+import com.zaojiao.app.core.http.common.tryRequest
+import com.zaojiao.app.data.model.UserGetV2Model
 import com.zaojiao.app.data.model.UserModel
 import com.zaojiao.app.data.remote.api.UserApi
 import kotlinx.coroutines.CoroutineDispatcher
@@ -16,5 +20,7 @@ import javax.inject.Singleton
 class RemoteUserData @Inject constructor(
     private val userApi: UserApi,
 ) {
-    suspend fun getUser() = userApi.getUser().data
+    suspend fun getUser(): DataState<UserGetV2Model> {
+        return tryRequest { userApi.getUser().toDataState() }
+    }
 }

+ 1 - 1
data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/BabyRepositoryImpl.kt

@@ -27,7 +27,7 @@ class BabyRepositoryImpl @Inject constructor(
                 }
 
                 is DataState.Success -> {
-                    localBabyData.updateList(data)
+                    localBabyData.updateList(data ?: emptyList())
                 }
             }
         }

+ 12 - 5
data/repo/src/main/kotlin/com/zaojiao/app/data/repo/impl/UserRepositoryImpl.kt

@@ -1,6 +1,7 @@
 package com.zaojiao.app.data.repo.impl
 
 import com.zaojiao.app.core.common.remote.di.ApplicationScope
+import com.zaojiao.app.core.common.state.DataState
 import com.zaojiao.app.data.local.user.LocalUserData
 import com.zaojiao.app.data.model.UserModel
 import com.zaojiao.app.data.remote.RemoteUserData
@@ -31,11 +32,17 @@ class UserRepositoryImpl @Inject constructor(
     override val wechatState: Flow<Boolean> get() = _wechatState
 
     override suspend fun getUser() {
-        remoteUserData.getUser()?.apply {
-            localUserData.updateUser(this.userModel)
-
-            _fansCount.emit(this.fansCount)
-            _followerCount.emit(this.followCount)
+        remoteUserData.getUser().apply {
+            when (this) {
+                is DataState.Failure -> {}
+                is DataState.Success -> {
+                    data?.apply {
+                        localUserData.updateUser(this.userModel)
+                        _fansCount.emit(this.fansCount)
+                        _followerCount.emit(this.followCount)
+                    }
+                }
+            }
         }
     }
 

+ 5 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Locals.kt

@@ -0,0 +1,5 @@
+package com.zaojiao.app.feat.design
+
+import androidx.compose.runtime.compositionLocalOf
+
+val LocalOnFinishDispatcher = compositionLocalOf<(() -> Unit)?> { null }

+ 30 - 33
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/StatePage.kt

@@ -10,12 +10,20 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.sp
 
-enum class PageState { Idle, Running, Success, Error }
+sealed interface UiState<T> {
+    class Loading<T> : UiState<T>
+
+    data class Success<T>(val state: T) : UiState<T>
+
+    sealed interface Failure<T> : UiState<T> {
+        class NetworkError<T> : Failure<T>
+    }
+}
 
 @Composable
-fun StatePage(state: PageState, onSuccess: @Composable () -> Unit) {
-    when (state) {
-        PageState.Idle -> {
+fun <T> StatePage(uiState: UiState<T>, onSuccess: @Composable T.() -> Unit) {
+    when (uiState) {
+        is UiState.Loading -> {
             Box(
                 modifier = Modifier.fillMaxSize(),
             ) {
@@ -30,37 +38,26 @@ fun StatePage(state: PageState, onSuccess: @Composable () -> Unit) {
             }
         }
 
-        PageState.Running -> {
-            Box(
-                modifier = Modifier.fillMaxSize(),
-            ) {
-                Text(
-                    text = "running state",
-                    modifier = Modifier.align(Alignment.Center),
-                    style = TextStyle(
-                        fontSize = 18.sp,
-                        color = Color.Cyan,
-                    )
-                )
-            }
-        }
-
-        PageState.Success -> {
-            onSuccess.invoke()
+        is UiState.Success -> {
+            onSuccess.invoke(uiState.state)
         }
 
-        PageState.Error -> {
-            Box(
-                modifier = Modifier.fillMaxSize(),
-            ) {
-                Text(
-                    text = "error state",
-                    modifier = Modifier.align(Alignment.Center),
-                    style = TextStyle(
-                        fontSize = 18.sp,
-                        color = Color.Red,
-                    )
-                )
+        is UiState.Failure -> {
+            when (uiState) {
+                is UiState.Failure.NetworkError -> {
+                    Box(
+                        modifier = Modifier.fillMaxSize(),
+                    ) {
+                        Text(
+                            text = "error state",
+                            modifier = Modifier.align(Alignment.Center),
+                            style = TextStyle(
+                                fontSize = 18.sp,
+                                color = Color.Red,
+                            )
+                        )
+                    }
+                }
             }
         }
     }

+ 13 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Themes.kt

@@ -0,0 +1,13 @@
+package com.zaojiao.app.feat.design
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+@Immutable
+data class BackgroundTheme(
+    val color: Color = Color.Unspecified,
+)
+
+val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() }
+

+ 45 - 0
feat/design/src/main/kotlin/com/zaojiao/app/feat/design/Views.kt

@@ -0,0 +1,45 @@
+package com.zaojiao.app.feat.design
+
+data class ViewState<A1>(
+    val state: A1,
+)
+
+data class ViewState2<A1, A2>(
+    val state1: A1,
+    val state2: A2,
+)
+
+data class ViewState3<A1, A2, A3>(
+    val state1: A1,
+    val state2: A2,
+    val state3: A3,
+)
+
+data class ViewState4<A1, A2, A3, A4>(
+    val state1: A1,
+    val state2: A2,
+    val state3: A3,
+    val state4: A4,
+)
+
+data class ViewState5<A1, A2, A3, A4, A5>(
+    val state1: A1,
+    val state2: A2,
+    val state3: A3,
+    val state4: A4,
+    val state5: A5,
+)
+
+data class ViewState6<A1, A2, A3, A4, A5, A6>(
+    val state1: A1,
+    val state2: A2,
+    val state3: A3,
+    val state4: A4,
+    val state5: A5,
+    val state6: A6,
+)
+
+sealed class ViewEvent {
+    data class SnackbarEvent(val message: String) : ViewEvent()
+    data class ToastEvent(val message: String) : ViewEvent()
+}

+ 3 - 0
feat/home/build.gradle.kts

@@ -11,7 +11,10 @@ android {
 }
 
 dependencies {
+    implementation(project(":core:common"))
+
     implementation(project(":feat:design"))
+
     implementation(project(":data:repo"))
     implementation(project(":data:model"))
 }

+ 8 - 1
app/src/main/java/com/zaojiao/app/navigation/HomeDestination.kt → feat/home/src/main/kotlin/com/zaojiao/app/feat/home/HomeDestination.kt

@@ -1,10 +1,12 @@
-package com.zaojiao.app.navigation
+package com.zaojiao.app.feat.home
 
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Add
+import androidx.compose.material.icons.outlined.Delete
 import androidx.compose.material.icons.outlined.Home
 import androidx.compose.material.icons.outlined.Person
 import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.Delete
 import androidx.compose.material.icons.rounded.Home
 import androidx.compose.material.icons.rounded.Person
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -24,6 +26,11 @@ enum class HomeDestination(
         unselectedIcon = Icons.Outlined.Add,
         iconTitle = "成长乐园",
     ),
+    PLAN(
+        selectedIcon = Icons.Rounded.Delete,
+        unselectedIcon = Icons.Outlined.Delete,
+        iconTitle = "学习计划",
+    ),
     PERSONAL(
         selectedIcon = Icons.Rounded.Person,
         unselectedIcon = Icons.Outlined.Person,

+ 148 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/HomePage.kt

@@ -0,0 +1,148 @@
+package com.zaojiao.app.feat.home
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavDestination
+import androidx.navigation.NavDestination.Companion.hierarchy
+import com.zaojiao.app.feat.home.course.HomeCourseRoute
+import com.zaojiao.app.feat.home.index.HomeIndexRoute
+import com.zaojiao.app.feat.home.personal.HomePersonalRoute
+import com.zaojiao.app.feat.home.plan.HomePlanRoute
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
+@Composable
+fun HomePage() {
+    val pageState = rememberPagerState()
+    val coroutineScope = rememberCoroutineScope()
+
+    val snackbarHostState = remember { SnackbarHostState() }
+
+    Scaffold(
+        containerColor = Color.Transparent,
+        contentColor = MaterialTheme.colorScheme.onBackground,
+        contentWindowInsets = WindowInsets(0, 0, 0, 0),
+        snackbarHost = { SnackbarHost(snackbarHostState) },
+        bottomBar = {
+            AppBottomBar(
+                destinations = HomeDestination.values().asList(),
+                onNavigateToDestination = {
+                    coroutineScope.launch {
+                        pageState.scrollToPage(it)
+                    }
+                },
+                currentIndex = pageState.currentPage,
+            )
+        },
+    ) { padding ->
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(padding)
+                .consumeWindowInsets(padding),
+        ) {
+            HorizontalPager(
+                state = pageState,
+                pageCount = 4,
+                userScrollEnabled = false,
+            ) {
+                when (it) {
+                    0 -> HomeIndexRoute()
+                    1 -> HomeCourseRoute()
+                    2 -> HomePlanRoute()
+                    3 -> HomePersonalRoute()
+                    else -> TODO()
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun AppBottomBar(
+    destinations: List<HomeDestination>,
+    onNavigateToDestination: (Int) -> Unit,
+    currentIndex: Int,
+    modifier: Modifier = Modifier,
+) {
+    NavigationBar(
+        modifier = modifier,
+        contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+        tonalElevation = 0.dp,
+    ) {
+        destinations.forEachIndexed { index, destination ->
+            val selected = currentIndex == index
+
+            NavigationBarItem(
+                selected = selected,
+                onClick = { onNavigateToDestination(index) },
+                enabled = true,
+                icon = {
+                    if (selected) {
+                        Icon(
+                            imageVector = destination.selectedIcon,
+                            contentDescription = null,
+                        )
+                    } else {
+                        Icon(
+                            imageVector = destination.unselectedIcon,
+                            contentDescription = null,
+                        )
+                    }
+                },
+                alwaysShowLabel = true,
+                label = { Text(text = destination.iconTitle) },
+                modifier = Modifier.notificationDot(),
+            )
+        }
+    }
+}
+
+private fun Modifier.notificationDot(): Modifier =
+    composed {
+        val tertiaryColor = MaterialTheme.colorScheme.tertiary
+        drawWithContent {
+            drawContent()
+            drawCircle(
+                tertiaryColor,
+                radius = 5.dp.toPx(),
+                // This is based on the dimensions of the NavigationBar's "indicator pill";
+                // however, its parameters are private, so we must depend on them implicitly
+                // (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
+                center = center + Offset(
+                    64.dp.toPx() * .45f,
+                    32.dp.toPx() * -.45f - 6.dp.toPx(),
+                ),
+            )
+        }
+    }
+
+private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: HomeDestination) =
+    this?.hierarchy?.any {
+        it.route?.contains(destination.name, true) ?: false
+    } ?: false

+ 2 - 2
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/course/HomeCoursePage.kt

@@ -10,9 +10,9 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.hilt.navigation.compose.hiltViewModel
-import com.zaojiao.app.feat.design.PageState
 import com.zaojiao.app.feat.design.Screen
 import com.zaojiao.app.feat.design.StatePage
+import com.zaojiao.app.feat.design.UiState
 import com.zaojiao.app.feat.design.grid
 
 @Composable
@@ -31,7 +31,7 @@ fun HomeCoursePage() {
     ) {
         val width = Screen.width()
 
-        StatePage(state = PageState.Success) {
+        StatePage(uiState = UiState.Loading<Any>()) {
             LazyColumn(
                 modifier = Modifier.fillMaxWidth()
             ) {

+ 12 - 3
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/index/HomeIndexViewModel.kt

@@ -4,17 +4,21 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.zaojiao.app.data.repo.BabyRepository
 import com.zaojiao.app.data.repo.LoginRepository
+import com.zaojiao.app.data.repo.UserRepository
 import com.zaojiao.app.feat.home.index.state.HomeIndexBabyUiState
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.async
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 @HiltViewModel
 class HomeIndexViewModel @Inject constructor(
     private val loginRepository: LoginRepository,
+    private val userRepository: UserRepository,
     private val babyRepository: BabyRepository,
 ) : ViewModel() {
     val babyUiState: StateFlow<HomeIndexBabyUiState> =
@@ -38,8 +42,14 @@ class HomeIndexViewModel @Inject constructor(
 //                    HomeIndexBabyUiState.None
 //                }
 //            }
-
-            babyRepository.getBaby()
+            viewModelScope.launch {
+                async {
+                    userRepository.getUser()
+                }
+                async {
+                    babyRepository.getBaby()
+                }
+            }
 
             if (baby != null) {
                 HomeIndexBabyUiState(
@@ -50,7 +60,6 @@ class HomeIndexViewModel @Inject constructor(
             } else {
                 HomeIndexBabyUiState.Add
             }
-
         }.stateIn(
             scope = viewModelScope,
             started = SharingStarted.WhileSubscribed(5_000),

+ 0 - 21
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeCourseNavigation.kt

@@ -1,21 +0,0 @@
-package com.zaojiao.app.feat.home.navigation
-
-
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavOptions
-import androidx.navigation.compose.composable
-import com.zaojiao.app.feat.home.course.HomeCourseRoute
-
-const val homeCourse = "home/course"
-
-fun NavController.navigateToHomeCourse(navOptions: NavOptions? = null) {
-    this.navigate(homeCourse, navOptions)
-}
-
-fun NavGraphBuilder.homeCoursePage(
-    onBannerClick: (String) -> Unit,
-    onCourseClick: (String) -> Unit,
-) {
-    composable(route = homeCourse) { HomeCourseRoute() }
-}

+ 0 - 26
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeIndexNavigation.kt

@@ -1,26 +0,0 @@
-package com.zaojiao.app.feat.home.navigation
-
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavOptions
-import androidx.navigation.compose.composable
-import com.zaojiao.app.feat.home.index.HomeIndexPage
-import com.zaojiao.app.feat.home.index.HomeIndexRoute
-
-const val homeIndex = "home/index"
-
-fun NavController.navigateToHomeIndex(navOptions: NavOptions? = null) {
-    this.navigate(homeIndex, navOptions)
-}
-
-fun NavGraphBuilder.homeIndexPage(
-//    onBannerClick: () -> Unit,
-//    onShopClick: () -> Unit,
-//    onGameClick: () -> Unit,
-//    onGoodClick: (String) -> Unit,
-//    onCourseClick: (String) -> Unit,
-) {
-    composable(route = homeIndex) {
-        HomeIndexRoute()
-    }
-}

+ 35 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomeNavigation.kt

@@ -0,0 +1,35 @@
+package com.zaojiao.app.feat.home.navigation
+
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.zaojiao.app.feat.home.course.HomeCourseRoute
+import com.zaojiao.app.feat.home.index.HomeIndexRoute
+import com.zaojiao.app.feat.home.personal.HomePersonalRoute
+
+const val homeIndex = "home/index"
+const val homeCourse = "home/course"
+const val homePersonal = "home/personal"
+
+fun NavController.navigateToHomeIndex(navOptions: NavOptions? = null) {
+    this.navigate(homeIndex, navOptions)
+}
+
+fun NavController.navigateToHomeCourse(navOptions: NavOptions? = null) {
+    this.navigate(homeCourse, navOptions)
+}
+
+fun NavController.navigateToHomePersonal(navOptions: NavOptions? = null) {
+    this.navigate(homePersonal, navOptions)
+}
+
+fun NavGraphBuilder.homeRoute() {
+    composable(route = homeIndex) { HomeIndexRoute() }
+
+    composable(route = homeCourse) { HomeCourseRoute() }
+
+    composable(route = homePersonal) { HomePersonalRoute() }
+}
+

+ 0 - 25
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/navigation/HomePersonalNavigation.kt

@@ -1,25 +0,0 @@
-package com.zaojiao.app.feat.home.navigation
-
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavOptions
-import androidx.navigation.compose.composable
-import com.zaojiao.app.feat.home.personal.HomePersonalRoute
-
-const val homePersonal = "home/personal"
-
-fun NavController.navigateToHomePersonal(navOptions: NavOptions? = null) {
-    this.navigate(homePersonal, navOptions)
-}
-
-fun NavGraphBuilder.homePersonalPage(
-//    onBannerClick: () -> Unit,
-//    onShopClick: () -> Unit,
-//    onGameClick: () -> Unit,
-//    onGoodClick: (String) -> Unit,
-//    onCourseClick: (String) -> Unit,
-) {
-    composable(route = homePersonal) {
-        HomePersonalRoute()
-    }
-}

+ 37 - 1
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanPage.kt

@@ -1,8 +1,44 @@
 package com.zaojiao.app.feat.home.plan
 
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.zaojiao.app.feat.design.StatePage
+import com.zaojiao.app.feat.design.UiState
 
 @Composable
-fun HomePlanPage() {
+internal fun HomePlanRoute(
+    viewModel: HomePlanViewModel = hiltViewModel(),
+) {
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
 
+    HomePlanPage(uiState = uiState)
+}
+
+@Composable
+fun HomePlanPage(
+    uiState: UiState<HomePlanState>,
+) {
+    StatePage(uiState = uiState) {
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(color = Color.Green)
+        ) {
+            todayPlanList.forEach {
+                Text(
+                    text = "abcdefg",
+                )
+            }
+        }
+    }
 }

+ 7 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanState.kt

@@ -0,0 +1,7 @@
+package com.zaojiao.app.feat.home.plan
+
+data class HomePlanState(
+    val todayPlanList: List<String> = emptyList(),
+    val tomorrowPlanList: List<String> = emptyList(),
+    val historyPlanList: List<String> = emptyList(),
+)

+ 31 - 0
feat/home/src/main/kotlin/com/zaojiao/app/feat/home/plan/HomePlanViewModel.kt

@@ -0,0 +1,31 @@
+package com.zaojiao.app.feat.home.plan
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.zaojiao.app.feat.design.UiState
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
+
+class HomePlanViewModel(
+
+) : ViewModel() {
+    val uiState: StateFlow<UiState<HomePlanState>> = flow<UiState<HomePlanState>> {
+        delay(3000)
+        emit(
+            UiState.Success(
+                HomePlanState(
+                    todayPlanList = listOf(
+                        "a", "b", "c"
+                    )
+                )
+            )
+        )
+    }.stateIn(
+        scope = viewModelScope,
+        started = SharingStarted.WhileSubscribed(5_000),
+        initialValue = UiState.Loading(),
+    )
+}

+ 2 - 1
gradle/libs.versions.toml

@@ -20,4 +20,5 @@ ksp-gradle = { group = "com.google.devtools.ksp", name = "com.google.devtools.ks
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
-android-library = { id = "com.android.library", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

+ 3 - 2
settings.gradle.kts

@@ -22,13 +22,14 @@ include(":app")
 
 include(":core:common")
 include(":core:http")
+include(":core:auth")
+include(":core:nav")
 
 include(":data:model")
 include(":data:local")
 include(":data:remote")
 include(":data:repo")
+include(":data:domain")
 
 include(":feat:design")
 include(":feat:home")
-include(":data:domain")
-include(":core:auth")